mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-10-12 11:39:56 +00:00
Compare commits
89
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8201f2263 | ||
|
|
c1b33e68e3 | ||
|
|
1b8bab91b5 | ||
|
|
70a4984305 | ||
|
|
1aff769285 | ||
|
|
ea714c731c | ||
|
|
cb89b963bf | ||
|
|
d8ec57602f | ||
|
|
b9a5b7802c | ||
|
|
18be2709f5 | ||
|
|
fb113ff008 | ||
|
|
f576260594 | ||
|
|
560dc55312 | ||
|
|
a860936072 | ||
|
|
e7b6b5facd | ||
|
|
eaf5317834 | ||
|
|
117168ccce | ||
|
|
76af9ed242 | ||
|
|
90a8fc91d3 | ||
|
|
6bffcc8503 | ||
|
|
3f2e50eebd | ||
|
|
cc8b8cd10d | ||
|
|
febf9019e2 | ||
|
|
2a6be894e1 | ||
|
|
8fa369f9d3 | ||
|
|
31f520af05 | ||
|
|
7c29e2a189 | ||
|
|
f49607e522 | ||
|
|
757b7eec81 | ||
|
|
88a4cf7d90 | ||
|
|
8ac97ed700 | ||
|
|
79d9f41fd8 | ||
|
|
2fe408666e | ||
|
|
317c68a860 | ||
|
|
c7a7323f54 | ||
|
|
044eca5ac4 | ||
|
|
0841067132 | ||
|
|
40ba94ce72 | ||
|
|
c501f4f9cc | ||
|
|
3b348a463c | ||
|
|
7aae6f3cbe | ||
|
|
d60911c780 | ||
|
|
e0b7942e45 | ||
|
|
4f5d52ada4 | ||
|
|
c051ebab74 | ||
|
|
9460551a04 | ||
|
|
6a2e6b040e | ||
|
|
bd06b47a9d | ||
|
|
102487bc4f | ||
|
|
248144f3de | ||
|
|
ecb23c66e0 | ||
|
|
c0566d37a0 | ||
|
|
1a8dfb4cbe | ||
|
|
1a0c4b73de | ||
|
|
e9b5a849ab | ||
|
|
a5fed340e2 | ||
|
|
ce796ec225 | ||
|
|
4660ee9cff | ||
|
|
70bf665938 | ||
|
|
b5f733d2db | ||
|
|
c22c15a2e8 | ||
|
|
db1b2f77cd | ||
|
|
8e2ffcb297 | ||
|
|
0410948b56 | ||
|
|
511d793b92 | ||
|
|
5f750a6783 | ||
|
|
d920a2504b | ||
|
|
55a6157880 | ||
|
|
73d7f84ee3 | ||
|
|
cf8b9c12ff | ||
|
|
9994375c16 | ||
|
|
9d12d76078 | ||
|
|
deb3fc7359 | ||
|
|
80905c0822 | ||
|
|
decf25d7dc | ||
|
|
5e047f3199 | ||
|
|
1ad69cf74f | ||
|
|
696fa652a5 | ||
|
|
6b0da8ac50 | ||
|
|
9195509485 | ||
|
|
96a38505d6 | ||
|
|
753a6c7542 | ||
|
|
ec1b72cb80 | ||
|
|
fda1284ae4 | ||
|
|
8d47f69083 | ||
|
|
3828a9591c | ||
|
|
81385e39bf | ||
|
|
fee897f367 | ||
|
|
f2d832fe20 |
@@ -11,7 +11,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build-${{ matrix.os }}
|
||||
name: build-${{ matrix.os }}-${{ matrix.ghc }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -19,8 +19,13 @@ jobs:
|
||||
include:
|
||||
- os: ubuntu-20.04
|
||||
platform_name: 20_04-x86-64
|
||||
ghc: "8.10.7"
|
||||
- os: ubuntu-20.04
|
||||
platform_name: 20_04-x86-64
|
||||
ghc: "9.6.3"
|
||||
- os: ubuntu-22.04
|
||||
platform_name: 22_04-x86-64
|
||||
ghc: "9.6.3"
|
||||
steps:
|
||||
- name: Clone project
|
||||
uses: actions/checkout@v3
|
||||
@@ -28,7 +33,7 @@ jobs:
|
||||
- name: Setup Haskell
|
||||
uses: haskell-actions/setup@v2
|
||||
with:
|
||||
ghc-version: "9.6.2"
|
||||
ghc-version: ${{ matrix.ghc }}
|
||||
cabal-version: "3.10.1.0"
|
||||
|
||||
- name: Cache dependencies
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- ignore: {name: "Use underscore"}
|
||||
+44
-9
@@ -1,8 +1,43 @@
|
||||
# 5.4.0
|
||||
|
||||
Migrate to GHC 9.6.3
|
||||
|
||||
Agent:
|
||||
- database improvements:
|
||||
- track slow queries.
|
||||
- better performance.
|
||||
- "busy" error handling.
|
||||
- create parent folder when needed.
|
||||
- support closing and re-opening database.
|
||||
- SMP agent improvements
|
||||
- streaming for batched SMP commands.
|
||||
- fix asynchronous JOINing connection.
|
||||
- handle repeating JOINs without failure.
|
||||
- api to get subscribed connections.
|
||||
- return simplex:/ links as invitations.
|
||||
- fix memory leak.
|
||||
- XFTP improvements:
|
||||
- suspend when agent is suspended.
|
||||
- support locally encrypted files.
|
||||
- fixes - create empty file, prevent permanent error treated as temporary.
|
||||
- upgrade HTTP2 library (fixes error handling and flow control).
|
||||
- Remote control protocol (XRCP)
|
||||
|
||||
SMP server:
|
||||
- control port commands for GHC threads introspection.
|
||||
- allow creating new queues without subscriptions (required for iOS).
|
||||
|
||||
XFTP server:
|
||||
- allow 64kb file chunks.
|
||||
|
||||
NTF server:
|
||||
- faster startup.
|
||||
|
||||
# 5.3.0
|
||||
|
||||
Agent:
|
||||
- improve performance, track slow database queries.
|
||||
- support delievery receipts.
|
||||
- support delivery receipts.
|
||||
|
||||
SMP server:
|
||||
- control port
|
||||
@@ -81,7 +116,7 @@ SMP agent:
|
||||
- batch connection deletion
|
||||
- improve asynchronous connection deletion – it may now be completed after the client is restarted as well.
|
||||
- improve subscription logic to retry if initial attempt fails.
|
||||
- end SMP client connection after a number of failed PINGs (defalt is 3).
|
||||
- end SMP client connection after a number of failed PINGs (default is 3).
|
||||
|
||||
# 4.3.0
|
||||
|
||||
@@ -114,7 +149,7 @@ SMP agent:
|
||||
|
||||
Notification server (v1.3.0):
|
||||
|
||||
- check token status when sending verification notificaiton.
|
||||
- check token status when sending verification notification.
|
||||
|
||||
# 4.1.0
|
||||
|
||||
@@ -133,15 +168,15 @@ SMP agent:
|
||||
SMP server:
|
||||
|
||||
- Basic authentication. The server address can now include an optional password that is required to create messaging queues, so the contacts who message you will not be able to receive messages via your server, unless you share with them the address with the password. It is recommended to enable basic authentication on all private servers by adding `create_password` parameter into AUTH section of server INI file (the previously deployed servers do not have this section, you need to add it).
|
||||
- Disable creating new queues completely with `new_queues: off` parameter in AUTH section of INI file - it can be used to simplify migrating the exising connections to another server.
|
||||
- Disable creating new queues completely with `new_queues: off` parameter in AUTH section of INI file - it can be used to simplify migrating the existing connections to another server.
|
||||
- Updated server CLI with changed defaults:
|
||||
- interactive server initialization, use -y flag to initalize the server non-interactively.
|
||||
- interactive server initialization, use -y flag to initialize the server non-interactively.
|
||||
- store log is now enabled by default, so messaging queues and messages are restored when the server is restarted (to restore undelivered messages the server needs to be stopped with SIGINT signal).
|
||||
- a random password is now generated by default during the server initialization.
|
||||
|
||||
SMP agent:
|
||||
|
||||
- API to test SMP servers. It connects to the server, creates and deletes a messaging queue. The new SimpleX Chat client uses this API to allow you to test that you have the correct server address, with the valid certificate fingerprint and pasword, before enabling the new server.
|
||||
- API to test SMP servers. It connects to the server, creates and deletes a messaging queue. The new SimpleX Chat client uses this API to allow you to test that you have the correct server address, with the valid certificate fingerprint and password, before enabling the new server.
|
||||
|
||||
# 3.4.0
|
||||
|
||||
@@ -187,9 +222,9 @@ SMP server and agent:
|
||||
|
||||
SMP server:
|
||||
|
||||
- restore undeliverd messages when the server is restarted.
|
||||
- restore undelivered messages when the server is restarted.
|
||||
- SMP protocol v3 to support push notification:
|
||||
- updated SEND and MSG to add message flags (for notification flag that contros whether the notification is sent and for any future extensions) and to move message meta-data sent to the recipient into the encrypted envelope.
|
||||
- updated SEND and MSG to add message flags (for notification flag that confirm whether the notification is sent and for any future extensions) and to move message meta-data sent to the recipient into the encrypted envelope.
|
||||
- update NKEY and NID to add e2e encryption keys (for the notification meta-data encryption between SMP server and the client), and update NMSG to include this meta-data.
|
||||
- update ACK command to include message ID (to avoid acknowledging unprocessed message).
|
||||
- add NDEL commands to remove notification subscription credentials from SMP queue.
|
||||
@@ -200,7 +235,7 @@ SMP agent:
|
||||
- new protocol for duplex connection handshake reducing traffic and connection time.
|
||||
- support for SMP notifications server and managing device token.
|
||||
- remove redundant FQDN validation from TLS handshake to prepare for access via Tor.
|
||||
- support for fully stopping agent and for termporary suspending agent operations.
|
||||
- support for fully stopping agent and for temporary suspending agent operations.
|
||||
- improve management of duplicate message delivery.
|
||||
|
||||
SMP notifications server v1.0:
|
||||
|
||||
+4
-4
@@ -5,11 +5,11 @@ FROM ubuntu:${TAG} AS build
|
||||
### Build stage
|
||||
|
||||
# Install curl and git and simplexmq dependencies
|
||||
RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev llvm-12 llvm-12-dev libnuma-dev
|
||||
RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev llvm-12 llvm-12-dev libnuma-dev libssl-dev
|
||||
|
||||
# Specify bootstrap Haskell versions
|
||||
ENV BOOTSTRAP_HASKELL_GHC_VERSION=8.10.7
|
||||
ENV BOOTSTRAP_HASKELL_CABAL_VERSION=3.6.2.0
|
||||
ENV BOOTSTRAP_HASKELL_GHC_VERSION=9.6.3
|
||||
ENV BOOTSTRAP_HASKELL_CABAL_VERSION=3.10.1.0
|
||||
|
||||
# Install ghcup
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 sh
|
||||
@@ -19,7 +19,7 @@ ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH"
|
||||
|
||||
# Set both as default
|
||||
RUN ghcup set ghc "${BOOTSTRAP_HASKELL_GHC_VERSION}" && \
|
||||
ghcup set cabal
|
||||
ghcup set cabal "${BOOTSTRAP_HASKELL_CABAL_VERSION}"
|
||||
|
||||
COPY . /project
|
||||
WORKDIR /project
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
module Main where
|
||||
|
||||
import Control.Logger.Simple
|
||||
import Data.ByteArray (ScrubbedBytes)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import qualified Data.Map.Strict as M
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
@@ -19,7 +20,7 @@ cfg = defaultAgentConfig
|
||||
agentDbFile :: String
|
||||
agentDbFile = "smp-agent.db"
|
||||
|
||||
agentDbKey :: String
|
||||
agentDbKey :: ScrubbedBytes
|
||||
agentDbKey = ""
|
||||
|
||||
servers :: InitialAgentServers
|
||||
@@ -38,5 +39,5 @@ main :: IO ()
|
||||
main = do
|
||||
putStrLn $ "SMP agent listening on port " ++ tcpPort (cfg :: AgentConfig)
|
||||
setLogLevel LogInfo -- LogError
|
||||
Right st <- createAgentStore agentDbFile agentDbKey MCConsole
|
||||
Right st <- createAgentStore agentDbFile agentDbKey False MCConsole
|
||||
withGlobalLogging logCfg $ runSMPAgent (transport @TLS) cfg servers st
|
||||
|
||||
+5
-12
@@ -4,23 +4,21 @@ packages: .
|
||||
-- packages: . ../http2
|
||||
-- packages: . ../network-transport
|
||||
|
||||
with-compiler: ghc-9.6.2
|
||||
index-state: 2023-12-12T00:00:00Z
|
||||
|
||||
package cryptostore
|
||||
flags: +use_crypton
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/aeson.git
|
||||
tag: 68330dce8208173c6acf5f62b23acb500ab5d873
|
||||
tag: aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/hs-socks.git
|
||||
tag: a30cc7a79a08d8108316094f8f2f82a0c5e1ac51
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/kazu-yamamoto/http2.git
|
||||
tag: b5a1b7200cf5bc7044af34ba325284271f6dff25
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/direct-sqlcipher.git
|
||||
@@ -30,8 +28,3 @@ source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/sqlcipher-simple.git
|
||||
tag: a46bd361a19376c5211f1058908fc0ae6bf42446
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/network-transport.git
|
||||
tag: 0013798272a683e35ca38d2fdaf480942311fba8
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Streamlined NTRU Prime: sntrup761
|
||||
|
||||
The implementation of sntrup761 is the _exact_ copy from this [Internet draft](https://www.ietf.org/archive/id/draft-josefsson-ntruprime-streamlined-00.html).
|
||||
@@ -0,0 +1,9 @@
|
||||
#include <openssl/sha.h>
|
||||
#include "sha512.h"
|
||||
|
||||
void crypto_hash_sha512 (unsigned char *out,
|
||||
const unsigned char *in,
|
||||
unsigned long long inlen)
|
||||
{
|
||||
SHA512(in, inlen, out);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
void crypto_hash_sha512 (unsigned char *out,
|
||||
const unsigned char *in,
|
||||
unsigned long long inlen);
|
||||
+1035
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Derived from public domain source, written by (in alphabetical order):
|
||||
* - Daniel J. Bernstein
|
||||
* - Chitchanok Chuengsatiansup
|
||||
* - Tanja Lange
|
||||
* - Christine van Vredendaal
|
||||
*/
|
||||
|
||||
#ifndef SNTRUP761_H
|
||||
#define SNTRUP761_H
|
||||
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define SNTRUP761_SECRETKEY_SIZE 1763
|
||||
#define SNTRUP761_PUBLICKEY_SIZE 1158
|
||||
#define SNTRUP761_CIPHERTEXT_SIZE 1039
|
||||
#define SNTRUP761_SIZE 32
|
||||
|
||||
typedef void sntrup761_random_func (void *ctx, size_t length, uint8_t *dst);
|
||||
|
||||
void
|
||||
sntrup761_keypair (uint8_t *pk, uint8_t *sk,
|
||||
void *random_ctx, sntrup761_random_func *random);
|
||||
|
||||
void
|
||||
sntrup761_enc (uint8_t *c, uint8_t *k, const uint8_t *pk,
|
||||
void *random_ctx, sntrup761_random_func *random);
|
||||
|
||||
void
|
||||
sntrup761_dec (uint8_t *k, const uint8_t *c, const uint8_t *sk);
|
||||
|
||||
#endif /* SNTRUP761_H */
|
||||
@@ -0,0 +1,30 @@
|
||||
indentation: 2
|
||||
column-limit: none
|
||||
function-arrows: trailing
|
||||
comma-style: trailing
|
||||
import-export-style: trailing
|
||||
indent-wheres: true
|
||||
record-brace-space: true
|
||||
newlines-between-decls: 1
|
||||
haddock-style: single-line
|
||||
haddock-style-module: null
|
||||
let-style: inline
|
||||
in-style: right-align
|
||||
single-constraint-parens: never
|
||||
unicode: never
|
||||
respectful: true
|
||||
fixities:
|
||||
- infixr 9 .
|
||||
- infixr 8 .:, .:., .=
|
||||
- infixr 6 <>
|
||||
- infixr 5 ++
|
||||
- infixl 4 <$>, <$, $>, <$$>, <$?>
|
||||
- infixl 4 <*>, <*, *>, <**>
|
||||
- infix 4 ==, /=
|
||||
- infixr 3 &&
|
||||
- infixl 3 <|>
|
||||
- infixr 2 ||
|
||||
- infixl 1 >>, >>=
|
||||
- infixr 1 =<<, >=>, <=<
|
||||
- infixr 0 $, $!
|
||||
reexports: []
|
||||
+35
-16
@@ -1,5 +1,5 @@
|
||||
name: simplexmq
|
||||
version: 5.4.0.3
|
||||
version: 5.4.0.7
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: |
|
||||
This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
@@ -20,6 +20,8 @@ category: Chat, Network, Web, System, Cryptography
|
||||
extra-source-files:
|
||||
- README.md
|
||||
- CHANGELOG.md
|
||||
- cbits/sha512.h
|
||||
- cbits/sntrup761.h
|
||||
|
||||
dependencies:
|
||||
- aeson == 2.2.*
|
||||
@@ -30,49 +32,46 @@ dependencies:
|
||||
- attoparsec == 0.14.*
|
||||
- base >= 4.14 && < 5
|
||||
- base64-bytestring >= 1.0 && < 1.3
|
||||
- bytestring == 0.11.*
|
||||
- case-insensitive == 1.2.*
|
||||
- composition == 1.0.*
|
||||
- constraints >= 0.12 && < 0.14
|
||||
- containers == 0.6.*
|
||||
- cryptonite == 0.30.*
|
||||
- cryptostore == 0.2.*
|
||||
- crypton == 0.34.*
|
||||
- crypton-x509 == 1.7.*
|
||||
- crypton-x509-store == 1.6.*
|
||||
- crypton-x509-validation == 1.6.*
|
||||
- cryptostore == 0.3.*
|
||||
- data-default == 0.7.*
|
||||
- direct-sqlcipher == 2.3.*
|
||||
- directory == 1.3.*
|
||||
- filepath == 1.4.*
|
||||
- hourglass == 0.2.*
|
||||
- http-types == 0.12.*
|
||||
- http2 == 4.1.*
|
||||
- generic-random == 1.5.*
|
||||
- http2 >= 4.2.2 && < 4.3
|
||||
- ini == 0.4.1
|
||||
- iproute == 1.7.*
|
||||
- iso8601-time == 0.1.*
|
||||
- memory == 0.18.*
|
||||
- mtl == 2.3.*
|
||||
- mtl >= 2.3.1 && < 3.0
|
||||
- network >= 3.1.2.7 && < 3.2
|
||||
- network-info >= 0.2 && < 0.3
|
||||
- network-transport == 0.5.6
|
||||
- network-udp >= 0.0 && < 0.1
|
||||
- optparse-applicative >= 0.15 && < 0.17
|
||||
- QuickCheck == 2.14.*
|
||||
- process == 1.6.*
|
||||
- random >= 1.1 && < 1.3
|
||||
- simple-logger == 0.1.*
|
||||
- socks == 0.6.*
|
||||
- sqlcipher-simple == 0.4.*
|
||||
- stm == 2.5.*
|
||||
- template-haskell == 2.20.*
|
||||
- temporary == 1.3.*
|
||||
- text == 2.0.*
|
||||
- time == 1.9.*
|
||||
- time-compat == 1.9.*
|
||||
- time == 1.12.*
|
||||
- time-manager == 0.0.*
|
||||
- tls >= 1.6.0 && < 1.7
|
||||
- tls >= 1.7.0 && < 1.8
|
||||
- transformers == 0.6.*
|
||||
- unliftio == 0.2.*
|
||||
- unliftio-core == 0.2.*
|
||||
- websockets == 0.12.*
|
||||
- x509 == 1.7.*
|
||||
- x509-store == 1.6.*
|
||||
- x509-validation == 1.6.*
|
||||
- yaml == 0.11.*
|
||||
|
||||
flags:
|
||||
@@ -80,14 +79,33 @@ flags:
|
||||
description: Enable swift JSON format
|
||||
manual: True
|
||||
default: False
|
||||
use_crypton:
|
||||
description: Use crypton etc. in cryptostore
|
||||
manual: True
|
||||
default: True
|
||||
|
||||
when:
|
||||
- condition: flag(swift)
|
||||
cpp-options:
|
||||
- -DswiftJSON
|
||||
- condition: impl(ghc >= 9.6.2)
|
||||
dependencies:
|
||||
- bytestring == 0.11.*
|
||||
- template-haskell == 2.20.*
|
||||
- text >= 2.0.1 && < 2.2
|
||||
- condition: impl(ghc < 9.6.2)
|
||||
dependencies:
|
||||
- bytestring == 0.10.*
|
||||
- template-haskell == 2.16.*
|
||||
- text >= 1.2.3.0 && < 1.3
|
||||
|
||||
library:
|
||||
source-dirs: src
|
||||
c-sources:
|
||||
- cbits/sha512.c
|
||||
- cbits/sntrup761.c
|
||||
include-dirs: cbits
|
||||
extra-libraries: crypto
|
||||
|
||||
executables:
|
||||
smp-server:
|
||||
@@ -142,6 +160,7 @@ tests:
|
||||
dependencies:
|
||||
- simplexmq
|
||||
- deepseq == 1.4.*
|
||||
- generic-random == 1.5.*
|
||||
- hspec == 2.11.*
|
||||
- hspec-core == 2.11.*
|
||||
- HUnit == 1.6.*
|
||||
|
||||
@@ -364,9 +364,9 @@ The clients can optionally instruct a dedicated push notification server to subs
|
||||
|
||||
[`SEND` command](#send-message) includes the notification flag to instruct SMP server whether to send the notification - this flag is forwarded to the recepient inside encrypted envelope, together with the timestamp and the message body, so even if TLS is compromised this flag cannot be used for traffic correlation.
|
||||
|
||||
## SMP Transmission andtransport block structure
|
||||
## SMP Transmission and transport block structure
|
||||
|
||||
Each transport block (SMP transmission) has a fixed size of 16384 bytes for traffic uniformity.
|
||||
Each transport block has a fixed size of 16384 bytes for traffic uniformity.
|
||||
|
||||
From SMP version 4 each block can contain multiple transmissions, version 3 blocks have 1 transmission.
|
||||
Some parts of SMP transmission are padded to a fixed size; this padding is uniformly added as a word16 encoded in network byte order - see `paddedString` syntax.
|
||||
@@ -387,17 +387,17 @@ Each transmission/block for SMP v3 between the client and the server must have t
|
||||
|
||||
```abnf
|
||||
paddedTransmission = <padded(transmission), 16384>
|
||||
transmission = [signature] SP signed
|
||||
signed = sessionIdentifier SP [corrId] SP [queueId] SP smpCommand
|
||||
transmission = signature signed
|
||||
signed = sessionIdentifier corrId queueId smpCommand
|
||||
; corrId is required in client commands and server responses,
|
||||
; it is empty in server notifications.
|
||||
corrId = 1*32(%x21-7F) ; any characters other than control/whitespace
|
||||
queueId = encoded ; max 32 bytes when decoded (24 bytes is used),
|
||||
corrId = length *OCTET
|
||||
queueId = length *OCTET
|
||||
; empty queue ID is used with "create" command and in some server responses
|
||||
signature = encoded
|
||||
signature = length *OCTET
|
||||
; empty signature can be used with "send" before the queue is secured with secure command
|
||||
; signature is always empty with "ping" and "serverMsg"
|
||||
encoded = <base64 encoded binary>
|
||||
length = 1*1 OCTET
|
||||
```
|
||||
|
||||
`base64` encoding should be used with padding, as defined in section 4 of [RFC 4648][9]
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Re-sync encryption ratchets, queue rotation, message delivery receipts
|
||||
|
||||
This is very unfocussed doc outlining several problems that seem somewhat related, and some possible solution approaches.
|
||||
|
||||
## Problems
|
||||
|
||||
When old database backup is used, the sending client encrypts the messages in a way that the receiving client cannot decrypt them, because it does not store the old keys, and does not try to decrypt ratchet headers using the old keys. When receiving the messages, the client that uses the old database is unable to decrypt them, either the header or the message itself (if more than 500 messages are skipped, or if root ratchet step happened).
|
||||
|
||||
The same symptoms happen during the queue rotation, unfortunately, as we do not keep the history of received messages once they are acknowledged, so the client reports these error to the users.
|
||||
|
||||
While these are two different problems, to the users they manifest in the same way, so it is difficult to differentiate.
|
||||
|
||||
Another ratchet problem that should be solved as well is not deleting unused skipped keys after some time or some number of events, possibly it should be considered at the same time.
|
||||
|
||||
While it is tempting to consider delivery receipts as an isolated problem, it is somewhat coupled, as the requests to re-sync ratchets and to re-deliver skipped messages are effectively delivery receipts.
|
||||
|
||||
A naive solution to delivery receipts when each message is responded to with a confirmation has two downsides:
|
||||
|
||||
- it doubles the traffic.
|
||||
- in the absense of delays, it also leaks some information about the network latency, that can be used to track user's location.
|
||||
|
||||
Both problems can be mitigated by sending delivery receipts with a random delay, to confirm all messages sent, not each one. But this would only reduce the traffic when the sender sends many messages. Alternatively, we could think how to reduce the block size for SMP commands. The challenge is profile pictures and image previews, so to reduce traffic we need to start sending them as XFTP files, potentially with a smaller 64kb chunks (that would also increase preview quality).
|
||||
|
||||
## Solution for queue rotation
|
||||
|
||||
We need to:
|
||||
- store and show the rotation status in the UI to prevent attempts to rotate the queue multiple times.
|
||||
- process multiple message delivery events while there is a redundancy, to avoid errors in terminal.
|
||||
- differentiate between decryption errors during queue rotation - that would require keeping the messages for some time, at least keeping their IDs and hashes together with some additional status. In this case when duplicate or old message arrives, we can check whether this message was already processed, and do not show error if it was.
|
||||
|
||||
## Solution to re-sync ratchets
|
||||
|
||||
When we establish that the receiving client is indeed unable to decrypt messages, we should:
|
||||
- notify the user - it is already happening, but it happens in case of rotation as well.
|
||||
- optionally, notify another client that message was not decrypted - this effectively becomes a delivery receipt. That could be a separate command triggered by the user in the UI.
|
||||
- clients can get into this state at the same time, how do we handle concurrent requests? Possibly, we could design these messages so this is irrelevant whether the message is sent in response or not, and calculate the new ratchet keys once the clients have both the sent and received messages. After that both clients can send "ratchet reset" message:
|
||||
- EKEY - message containing a new set of keys to initialize the ratchets
|
||||
- EREADY - message confirming that the ratchet is ready, already e2e encrypted
|
||||
|
||||
The downside of this approach, is that unlike the initial handshake, where the second ratchet key is sent in e2e encrypted response, this message will be sent only encrypted with queue key. We could change this protocol by adding EREPLY message, and the clients would know to ingnore EKEY with bigger hash of the keys, so that the client who sent keys with smaller hash would be processing response from another client (that is if EKEY is received after EKEY is sent).
|
||||
|
||||
```
|
||||
A B
|
||||
|
||||
EKEY 1 -> <- EKEY 2 (e2e encrypted with per-queue key only)
|
||||
EKEY 2 <- -> EKEY 1
|
||||
1 < 2, wait 2 > 1, reply EREPLY 3 (e2e encrypted with ratchets)
|
||||
ratchet: 1, 3 ratchet: 3, 1
|
||||
```
|
||||
|
||||
Once ratchets are re-synched the clients could additionally request delivery of skipped messages. As ratchets being out of sync is not the only problem when messages can be lost, it may be managed independently from the ratchet re-sync. This problem seems tied with delivery receipts too, as requesting to re-deliver some messages automatically confirms that some other messages were delivered.
|
||||
|
||||
Alternatively, the request for ratchet sync can already contain the ID of the first failed message - that would allow the agent:
|
||||
- stop sending messages until ratchets are in sync.
|
||||
- re-deliver failed messages first.
|
||||
- only then deliver any pending messages.
|
||||
|
||||
This approach would probably result in a better UX (no out of order delivery), but requires tracking pending messages that are not attempted to deliver and to also insert old messages in the delivery queue with the old IDs.
|
||||
@@ -0,0 +1,34 @@
|
||||
# SMP and XFTP delivery relays
|
||||
|
||||
## Problem
|
||||
|
||||
SimpleX network relies on SMP relays to hold sent messages while the recipient is offline. This design provides many advantages over pure p2p designs [1]. The threat model for relays is covered in [2].
|
||||
|
||||
The problem with this design is that the relays are chosen by the recipients, and they as a result can be used to record transport addresses of the senders.
|
||||
|
||||
Similar problem, but reversed, exists with XFTP design - XFTP relays are chosen by the senders and also can be modified to record transport addresses of the recipients.
|
||||
|
||||
The current solution to this problem is to use onion routing to access relays, and many users do it already. The downside is that it requires additional configuration and can be complex to some users.
|
||||
|
||||
## Solution approach
|
||||
|
||||
Add a second type of relays to the network design.
|
||||
|
||||
For SMP protocol, these relays would accept messages from the senders and deliver them to the recipients. If the recipient were to modify the relay used to receive the messages to also records senders' IP addresses, then they would only succeed in recording the IP address of the relay, not the sender.
|
||||
|
||||
Similar relay can be introduced for XFTP protocol.
|
||||
|
||||
The additional advantage of using the relays is the reduced number of network connections that need to be used, and reduced traffic.
|
||||
|
||||
Compared with Tor this approach has:
|
||||
- lower latency
|
||||
- no centralized components or the registry of the servers
|
||||
- still no visibility of the whole network to the relays, as sending relays do not need to randomly choose the next relay, but would forward the messages directly to the destination relay.
|
||||
|
||||
The downside of this approach is that relays will have more visibility of the network than in the current design.
|
||||
|
||||
## Implementation considerations
|
||||
|
||||
1. Should this new relays be included in the existing SMP and XFTP protocols and servers, or should they be designed as separate protocols and servers?
|
||||
|
||||
2. Should the client aim to choose the same host for delivery relay as the destination server (to reduce the traffic), if possible, or should it aim to choose a different host (to reduce metadata available to single host)? The answer to this would affect the answer to the first question.
|
||||
@@ -0,0 +1,234 @@
|
||||
# Protecting IP addresses of the users from their contacts
|
||||
|
||||
## Problem
|
||||
|
||||
SMP protocol relays are chosen and can be controlled by the message recipients. It means that the recipients can find out IP addresses of message senders by modifying SMP relay code (or by using proxies and timing correlation), unless the senders use VPN or some overlay network. Tor is an audequate solution in most cases to mitigate it, but it requires additional technical knowledge to install and configure (even installing Orbot on Android is seen as "complex" by many users), and reduces usability because of higher latency.
|
||||
|
||||
The lack of in-built IP address protection is the main concern of many users, particularly given that most people do not realise that it is lacking by default - without transport protection SimpleX is not perceived as a "whole product".
|
||||
|
||||
Similarly, XFTP protocol relays are chosen by senders, and they can be used to detect file recipients' IP addresses.
|
||||
|
||||
## Possible solutions
|
||||
|
||||
1. Embed Tor in the client.
|
||||
|
||||
Pros:
|
||||
- no changes in the protocols/servers
|
||||
- acceptable threat model for most users
|
||||
- removes complexity of installing and configuring Tor
|
||||
- probably, the most attractive option for Tor users
|
||||
|
||||
Cons:
|
||||
- higher latency and error rate
|
||||
- higher resource usage
|
||||
- requires us updating Tor client regularly
|
||||
- restriction on Tor usage in some networks, so it would require supporting bridges in the app UI
|
||||
- legislative restrictions on Tor usage, so it may require supporting multiple app versions, and won't solve the problem where Tor is not embedded
|
||||
|
||||
2. Thin clients with hosted accounts.
|
||||
|
||||
Host some part of the current client on the server and have another part running on the devices.
|
||||
|
||||
Pros:
|
||||
- substantially reduces the traffic for mobile clients
|
||||
- probably the most attractive option for mass market users who expect to have an account on the server and be able to access it from multiple devices and via web.
|
||||
|
||||
Cons:
|
||||
- a lot of design and development
|
||||
- makes accounts visible to the server
|
||||
- the new protocol design to protect connections graph and message content from the hosting server.
|
||||
- quite different threat model
|
||||
|
||||
Overall, this is not a viable or even appropriate option for the current stage.
|
||||
|
||||
3. SMP / XFTP proxy.
|
||||
|
||||
Introduce SMP and XFTP protocol extenstions to allow message senders and file recipients to delegate the tasks of sending messages and receiving files to the proxies, so that peer-chosen relays can only observe IP addresses of the proxies and not of the users.
|
||||
|
||||
Pros:
|
||||
- no dependency on and lower latency than via Tor
|
||||
- reduces client traffic, both due to retries handled by these proxies and due to the smaller number of connections to the peer-chosen relays. The flip side is that additional commands (and traffic) are needed to create the sessions with relays.
|
||||
- higher transport privacy: protects IP addresses from peers and makes transport correlation harder.
|
||||
- additional function: retrying pending messages, while the queue is not secured yet, without extra traffic for the clients.
|
||||
- improves current threat model by preventing session-based correlation of the traffic by the destination relay (assuming no information sharing with the proxies), as there will be one session between proxy and destination relay, mixing traffic from multiple users.
|
||||
|
||||
Cons:
|
||||
- design and development cost
|
||||
- can undermine delivery stability
|
||||
|
||||
This seems an attractive option, both from technical (reasonable complexity) and positioning (moving SimpleX closer to mix networks, while still avoiding server list visibility) points of view, and it is in the middle between embedding Tor, which would make the product more niche and has downsides and development costs too, and thin clients, which is 10x+ more work, and also creates a different threat model, that is not very attractive for the current stage and users.
|
||||
|
||||
Below considers this design.
|
||||
|
||||
## SMP/XFTP proxy design
|
||||
|
||||
### Design requirements
|
||||
|
||||
1. To avoid increasing the complexity of self-hosting, we should not create additional server types, and existing SMP and XFTP servers should be extended to provide proxy functions.
|
||||
|
||||
2. SMP proxy should not be able to observe queue addresses and their count on the destination relays. This requirement is not needed for XFTP proxies, as each file chunk is downloaded only once, so there is no need to hide its address.
|
||||
|
||||
3. There must be no identifiers and cyphertext in common in outgoing and incoming traffic inside TLS (the current designs have this quality).
|
||||
|
||||
4. Traffic between the client and destination relays must be e2e encrypted, with MITM-by-proxy mitigated, relying on the relay identity (certificate fingerprint), ideally without any additional fingerprint in relay address.
|
||||
|
||||
5. SMP proxy should implement retry logic and hold messages while they are delivered. They also should return relay replies to the client. To avoid any additional traffic the client should just add "sent to proxy" status and only show "sent" once proxy returns the response from the destination relay - there should be no additional response from the proxy confirming acceptance to delivery.
|
||||
|
||||
6. Sending messages to groups have to be batched in the client to avoid multiple requests for destination relay sessions - such requests can be batched to proxy, even though it leaks _some_ metadata - which destination relays are used by a given sender's IP address, it also reduces the overhead from proxies – it could be an option based on the privacy slider.
|
||||
|
||||
6. SMP proxy may also increase utility and privacy of the platform:
|
||||
|
||||
- holding messages and retrying them for the new connections while the receiving queue is not secured yet.
|
||||
- add delays in message delivery to make traffic correlation harder (same can be done in SMP relays).
|
||||
|
||||
### Implementation considerations
|
||||
|
||||
1. Block size. Possibly, there is not enough spare capacity in the current 16kb block to fit additional headers, any necessary metadata and encryption authentication tags, in which case we cannot send SMP and SMP-proxy traffic in the same transport connection (in case the same server plays both roles). In which case we will need to negotiate role during connection handshake and define a different (sub-)protocol for SMP-proxy.
|
||||
|
||||
2. To be decided if we see it as extension of SMP protocol or as another protocol, irrespective of whether it's provided by the same or another server. Given different roles and block size it may be simpler to see it as a separate protocol, and which protocol is provided in the connection is determined during handshake.
|
||||
|
||||
3. We probably should aim to avoid changing agent/client logic and see it instead as transport concern that can be dynamically decided at a point of sending a message, based on the current configuration.
|
||||
|
||||
4. Configuration should probably allow to choose between not using proxies (particularly, during testing, when it would be the default), using proxies only for unknown relays, and using proxies for all relays (extra traffic, but more complex transport correlation). The clients can aim to use proxy from another provider, to reduce the risks of sharing the information.
|
||||
|
||||
### SMP-proxy protocol
|
||||
|
||||
The flow of the messages will be:
|
||||
|
||||
1. Client requests proxy to create session with the relay by sending `server` command with the SMP relay address and optional proxy basic AUTH (below). It should be possible to batch multiple session requests into one block, to reduce traffic.
|
||||
|
||||
2. Proxy connects to SMP relay, negotiating a shared secret in the handshake that will be used to encrypt all sender blocks inside TLS (proxy-relay encryption). SMP relay also returns in handshake its temporary DH key to agree e2e encryption with the client (sender-relay encryption, to protect metadata from proxy).
|
||||
|
||||
3. Proxy replies with `server_id` command including relay session ID to identify it in further requests, relay DH key for e2e encryption with the client - this key is signed with the TLS online private key associated with the certificate (its fingerprint is included in the relay address), and the TLS session ID between proxy and relay (this session ID must be used in transmissions, to mitigate replay attacks as before).
|
||||
|
||||
A possible attack here is that proxy can use this TLS session to replay commands received from the client. Possibly, it could be mitigated with a bloom filter per proxy/SMP relay connection that would reject the repeated DH keys (that need to be used for replay), and also with DH key expiration (this mitigation should allow some acceptable rate of false positives from the bloom filter).
|
||||
|
||||
With 32 bits per key there will be ~1/1,000,000 false positives (see https://en.wikipedia.org/wiki/Bloom_filter), and the filter would use ~32mb per each proxy connection if we reset relay key every 1000000 messages or more frequently. Given that the only commands accepted via relay would be SEND, replaying it would be interpreted by the receiving client as duplicate message delivery, so a small number of replayed messages won't cause any problems. But without mitigation it could be used to flood the receiving SMP relay queues with repeated messages, effectively causing DoS on these queues, even in case when SMP relays require basic auth to create queues.
|
||||
|
||||
Given that the client chooses proxy it has some trust to, maybe this replay attack risk can be accepted.
|
||||
|
||||
4. Now the client sends `forward` to proxy, which it then forwards to SMP relay, applying additional encryption layer.
|
||||
|
||||
5. SMP relay sends `response` to proxy applying additional encryption layer, which it then forwards to the client removing the additional encryption layer.
|
||||
|
||||
Effectively it works as a simplified two-hop onion routing with the first relay (proxy) chosen by the sending client and the second relay chosen by the recipient, not only protecting senders' IP addresses from the recipients' relays, but also preventing recipients relays from correlating senders' traffic to different queues, as TLS session is owned by the proxy now and it mixes the traffic from multiple senders. To correlate traffic to users, proxy and relay would have to combine their information. SMP relays are still able to correlate traffic to receiving users via transport session.
|
||||
|
||||
Sequence diagram for sending the message via SMP proxy:
|
||||
|
||||
```
|
||||
------------- ------------- ------------- -------------
|
||||
| sending | | SMP | | SMP | | receiving |
|
||||
| client | | proxy | | relay | | client |
|
||||
------------- ------------- ------------- -------------
|
||||
| `server` | | |
|
||||
| -------------------------> | create TLS session, get keys | |
|
||||
| | ------------------------------> | |
|
||||
| `server_id` | (if doesn't exist) | |
|
||||
| <------------------------- | | |
|
||||
| | | |
|
||||
| TLS(F:s2r(SEND(e2e(msg)))) | | |
|
||||
| -------------------------> | TLS(F:p2r(s2r(SEND(e2e(msg))))) | |
|
||||
| | ------------------------------> | |
|
||||
| | | |
|
||||
| | TLS(R:p2r(s2r(OK/ERR))) | |
|
||||
| TLS(R:s2r(OK/ERR)) | <------------------------------ | |
|
||||
| <------------------------- | | TLS(MSG(r2c(e2e(msg)))) |
|
||||
| | | -----------------------> |
|
||||
| | | |
|
||||
| | | TLS(ACK) |
|
||||
| | | <----------------------- |
|
||||
| | | |
|
||||
| | | |
|
||||
|
||||
```
|
||||
|
||||
Below diagram shows the encrypttion layers for `forward` and `response` commands:
|
||||
|
||||
- s2r (added) - encryption between client and SMP relay, with relay key returned in server_id command, with MITM by proxy mitigated by verifying the certificate fingerprint included in the relay address.
|
||||
- e2e (exists now) - end-to-end encryption per SMP queue, with double ratchet e2e encryption inside it.
|
||||
- p2r (added) - additional encryption between proxy and SMP relay with key agreed in the handshake, to mitigate traffic correlation inside TLS. This key could also be signed by the same certificate, if we don't want to rely on TLS security.
|
||||
- r2c (exists now) additional encryption between SMP relay and client to prevent traffic correlation inside TLS.
|
||||
|
||||
```
|
||||
----------------- ----------------- -- TLS -- ----------------- -----------------
|
||||
| | -- TLS -- | | -- p2r -- | | -- TLS -- | |
|
||||
| | -- s2r -- | | -- s2r -- | | -- r2c -- | |
|
||||
| sending | -- e2e -- | | -- e2e -- | | -- e2e -- | receiving |
|
||||
| client | MSG | SMP proxy | MSG | SMP relay | MSG | client |
|
||||
| | -- e2e -- | | -- e2e -- | | -- e2e -- | |
|
||||
| | -- s2r -- | | -- s2r -- | | -- r2c -- | |
|
||||
| | -- TLS -- | | -- p2r -- | | -- TLS -- | |
|
||||
----------------- ----------------- -- TLS -- ----------------- -----------------
|
||||
```
|
||||
|
||||
When proxy connects to SMP relay it would indicate in the handshake that it will use proxy protocol and the SMP relay would expect the same `forward` commands and reply with `response`s.
|
||||
|
||||
Below syntax aims to fit in 16kb block using spare capacity in SMP protocol.
|
||||
|
||||
```abnf
|
||||
proxy_block = padded(proxy_transmission, 16384)
|
||||
proxy_transmission = corr_id relay_session_id proxy_command
|
||||
corr_id = length *8 OCTET
|
||||
proxy_command = server / server_id / forward / response / error
|
||||
server = "S" address [relay_basic_auth] ; creates transport session between proxy and relay
|
||||
server_id = "I" relay_session_id tls_session_id signed_relay_key ;
|
||||
; session_id is the TLS session ID between proxy and relay, it has to be included inside encrypted block to prevent replay attacks
|
||||
forward = %s"F" random_dh_pub_key encrypted_block
|
||||
response = %s"R" encrypted_block; response received from the destination SMP relay
|
||||
relay_session_id = length *8 OCTET
|
||||
error = %s"E" error
|
||||
```
|
||||
|
||||
The overhead is: 1+8 (corrId) + 1+8 (relay_session_id) + 1 (command) + 1+32 (random_dh_pub_key) + 2 (original length) + 16 (auth tag for e2e encryption) + 16 (auth tag for proxy to relay encryption) = 86 bytes. The reserve for sent messages in SMP is ~84 bytes, so it should about fit with some reduced bytes somewhere.
|
||||
|
||||
Another possible design is to allow mixing sent messages and normal SMP commands in the same transport connection, but it can make fitting in the block a bit harder, additional overhead would be: 1 (transmission count) + 2 (transmission size) + 1 (empty signature) = 4 bytes.
|
||||
|
||||
The above assumes that the client can only send one message to an SMP relay and then has to wait for response before sending the next message. Missing the response would cause re-delivery (further improvement is possible when proxy detects these redelieveries and not send them to relays but simply reply with the same response).
|
||||
|
||||
### Threat model for SMP proxy and changes to threat model for SMP
|
||||
|
||||
#### SMP proxy
|
||||
|
||||
*can:*
|
||||
|
||||
- learn a user's IP address, as long as Tor is not used.
|
||||
|
||||
- learn when a user with a given IP address is online.
|
||||
|
||||
- know how many messages are sent from a given IP address and to a given destination SMP relay.
|
||||
|
||||
- drop all messages from a given IP address or to a given destination relay.
|
||||
|
||||
- unless SMP relay detects repeated public DH keys of senders, replay messages to a destination relay within a single session, causing either duplicate message delivery (which will be detected and ignored by the receiving clients), or, when receiving client is not connected to SMP relay, exhausting capacity of destination queues used within the session.
|
||||
|
||||
*cannot:*
|
||||
|
||||
- perform queue correlation (matching multiple queues to a single user), unless it has additional information from SMP relay.
|
||||
|
||||
- undetectably add, duplicate, or corrupt individual messages.
|
||||
|
||||
- undetectably drop individual messages, so long as a subsequent message is delivered.
|
||||
|
||||
- learn the contents of messages.
|
||||
|
||||
- learn the destination queues of messages.
|
||||
|
||||
- distinguish noise messages from regular messages except via timing regularities.
|
||||
|
||||
- compromise the user's end-to-end encryption with another user via an active attack.
|
||||
|
||||
- compromise the user's end-to-end encryption with destination relay via an active attack.
|
||||
|
||||
#### SMP relay accessed via SMP proxy
|
||||
|
||||
*still can:*
|
||||
|
||||
- perform recipients' queue correlation (matching multiple queues to a single recipient) via either a re-used transport connection, user's IP Address, or connection timing regularities
|
||||
|
||||
*no longer can:*
|
||||
|
||||
- perform senders' queue correlation (matching multiple queues to a single sender) via either a re-used transport connection, user's IP Address, or connection timing regularities, unless it has additional information from SMP proxy.
|
||||
|
||||
- learn a sender's IP address, track them through other IP addresses they use to access the same queue, and infer information (e.g. employer) based on the IP addresses, even if Tor is not used.
|
||||
|
||||
The rest of the threat model for SMP relays remains the same as in [overview](../protocol/overview-tjr.md#simplex-messaging-protocol-server).
|
||||
@@ -0,0 +1,361 @@
|
||||
# SimpleX Remote Control protocol
|
||||
|
||||
Using profiles in SimpleX Chat mobile app from desktop app with minimal risk to the security/threat model of SimpleX protocols.
|
||||
|
||||
## Problem
|
||||
|
||||
Synchronizing profiles that use double ratchet for e2e encryption is effectively impossible in a way that tolerates partitioning between devices without reducing security of double ratchet.
|
||||
|
||||
We are not considering replacing (or weakening) double ratchet to allow profile synchronization, as some other messengers did (e.g., Session). We are also not considering Signal model, when profile is known to the server and adding devices results in changing security code and no visibility of conversation history, as it would be substantially different from the current model, and also effectively weakens the security, as in practives most users don't revalidate security codes when they change.
|
||||
|
||||
## Solution
|
||||
|
||||
The proposed solution is a new remote access/control protocol, when the application on host device (usually mobile) acts as a server, and application on another device (usually desktop) acts as a controller usually running in the same local network.
|
||||
|
||||
Existing service discovery and remote control protocols known to us are vulnerable to spoofing, spamming and MITM attacks. This design aims to solve these problems.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Strong, cryptographically verified identity of the controller device, with the initial connection requiring out-of-band communication of public keys (QR code or link).
|
||||
- Identity verification of the host device during session handshake.
|
||||
- Protection against malicious "controllers" trying to make host connect to them instead of valid controller on the same network.
|
||||
- Protection against replay attacks, both during discovery and during control session.
|
||||
- Additional encryption layer inside TLS, with post-quantum algorithm in key agreement.
|
||||
- Protect host device from unauthorized access in case of controller compromise.
|
||||
- Post-compromised security - that is, even if long term secrets were copied from the controller, and host device was made to connect to malicious controller device, prevent malicious controller from accessing the host device data.
|
||||
- Allow general high-level interactions common for many applications:
|
||||
- RPC pattern for commands executed by the host application.
|
||||
- Events sent by host to update controller UI.
|
||||
- Uploading and downloading files between host and controller, either to be processed by the host or to be presented in the controller UI.
|
||||
|
||||
This design will allow application-level protocol that is quite close to how SimpleX Chat UI interacts with SimpleX Chat core - there is a similar RPC + events protocol and support for files. The application-level protocol is out of scope of this specification.
|
||||
|
||||
## Protocol phases
|
||||
|
||||
Protocol consists of four phases:
|
||||
- controller session invitation
|
||||
- establishing session TLS connection
|
||||
- session verification and protocol negotiation
|
||||
- session operation
|
||||
|
||||
### Session invitation
|
||||
|
||||
The invitation to the first session between host and controller pair MUST be shared out-of-band, to establish a long term identity keys/certificates of the controller to host device.
|
||||
|
||||
The subsequent sessions can be announced via an application-defined site-local multicast group, e.g. `224.0.0.251` (also used in mDNS/bonjour) and an application-defined port (SimpleX Chat uses 5227).
|
||||
|
||||
The session invitation contains this data:
|
||||
- supported version range for remote control protocol
|
||||
- application-specific information, e.g. device name, application name and supported version range, settings, etc.
|
||||
- session start time in seconds since epoch
|
||||
- if multicast is used, counter of announce packets sent by controller
|
||||
- network address (ipv4 address and port) of the controller
|
||||
- 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. These DH public key and KEM encapsulation key are always sent unencrypted. NaCL Cryptobox is used for encryption.
|
||||
|
||||
Host device decrypts (except the first session) and validates the invitation:
|
||||
- Session signature is valid.
|
||||
- Timestamp is within some window from the current time.
|
||||
- Long-term key signature is valid.
|
||||
- Long-term CA and signature key are the same as in the first session.
|
||||
- Some version in the offered range is supported.
|
||||
|
||||
OOB session invitation is a URI with this syntax:
|
||||
|
||||
```abnf
|
||||
sessionAddressUri = "xrcp://" encodedCAFingerprint "@" host ":" port "#/?" qsParams
|
||||
encodedCAFingerprint = base64url
|
||||
qsParams = param *("&" param)
|
||||
param = versionRangeParam / appInfoParam / sessionTsParam /
|
||||
sessPubKeyParam / idPubKeyParam / kemEncKeyParam / dhPubKeyParam /
|
||||
sessSignatureParam / idSignatureParam
|
||||
versionRangeParam = "v=" (versionParam / (versionParam "-" versionParam))
|
||||
versionParam = 1*DIGIT
|
||||
appInfoParam = "app=" escapedJSON ; optional
|
||||
sessionTsParam = "ts=" 1*DIGIT
|
||||
sessPubKeyParam = "skey=" base64url ; required
|
||||
idPubKeyParam = "idkey=" base64url ; required
|
||||
dhPubKeyParam = "dh=" base64url ; required
|
||||
sessSignatureParam = "ssig=" base64url ; required, signs the URI with this and idSignatureParam param removed
|
||||
idSignatureParam = "idsig=" base64url ; required, signs the URI with this param removed
|
||||
base64url = <base64url encoded binary> ; RFC4648, section 5
|
||||
```
|
||||
|
||||
Multicast session announcement is a binary encoded packet with this syntax:
|
||||
|
||||
```abnf
|
||||
sessionAddressPacket = dhPubKey nonce encrypted(unpaddedSize sessionAddress packetPad)
|
||||
dhPubKey = length x509encoded ; same as announced
|
||||
nonce = length *OCTET
|
||||
sessionAddress = largeLength sessionAddressUri ; as above
|
||||
length = 1*1 OCTET ; for binary data up to 255 bytes
|
||||
largeLength = 2*2 OCTET ; for binary data up to 65535 bytes
|
||||
packetPad = <pad packet size to 1450 bytes> ; possibly, we may need to move KEM agreement one step later,
|
||||
; with encapsulation key in HELLO block and KEM ciphertext in reply to HELLO.
|
||||
```
|
||||
|
||||
### Establishing session TLS connection
|
||||
|
||||
Host connects to controller via TCP session and validates CA credentials during TLS handshake. Controller acts as a TCP server in this connection, to avoid host device listening on a port, which might create an attack vector. During TLS handshake the controller's TCP server MUST present a self-signed two-certificate chain where the fingerprint of the first certificate MUST be the same as in the announcement.
|
||||
|
||||
Host device presents its own client certificate chain with CA representing a long-term identity of the host device.
|
||||
|
||||
### Session verification and protocol negotiation
|
||||
|
||||
Once TLS session is established, both the host and controller device present a "session security code" to the user who must match them (e.g., visually or via QR code scan) and confirm on the host device. The session security code must be a digest of tlsunique channel binding. As it is computed as a digest of the TLS handshake for both the controller and the host, it will validate that the same TLS certificates are used on both sides, and that the same TLS session is established, mitigating the possibility of MITM attack in the connection.
|
||||
|
||||
Once the session is confirmed by the user, the host device sends "hello" block to the controller. ALPN TLS extension is not used to obtain tlsunique prior to sending any packets.
|
||||
|
||||
Block size should be 16384 bytes.
|
||||
|
||||
Host HELLO must contain:
|
||||
- new session DH key - used to compute new shared secret with the controller keys from the announcement.
|
||||
- encrypted part of hello block (JSON object), containing:
|
||||
- chosen protocol version.
|
||||
- host CA TLS certificate fingerprint - part of host long term identity - must match the one presented in TLS handshake and the previous sessions, otherwise the connection is terminated.
|
||||
- KEM encapsulation key - used to compute new shared secret for the session.
|
||||
- additional application specific parameters, e.g host device name, application version, host settings or JSON encoding format.
|
||||
|
||||
Hello block syntax:
|
||||
|
||||
```abnf
|
||||
hostHello = unpaddedSize %s"HELLO " dhPubKey nonce encrypted(unpaddedSize hostHelloJSON helloPad) pad
|
||||
unpaddedSize = largeLength
|
||||
dhPubKey = length x509encoded
|
||||
pad = <pad block size to 16384 bytes>
|
||||
helloPad = <pad hello size to 12888 bytes>
|
||||
largeLength = 2*2 OCTET
|
||||
```
|
||||
|
||||
Controller decrypts (including the first session) and validates the received hello block:
|
||||
- Chosen versions are supported (must be within offered ranges).
|
||||
- CA fingerprint matches the one presented in TLS handshake and the previous sessions - in subsequent sessions TLS connection should be rejected if the fingerprint is different.
|
||||
|
||||
JTD schema for the encrypted part of host HELLO block `hostHelloJSON`:
|
||||
|
||||
```json
|
||||
{
|
||||
"definitions": {
|
||||
"version": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"format": "[0-9]+"
|
||||
}
|
||||
},
|
||||
"base64url": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"format": "base64url"
|
||||
}
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"v": {"ref": "version"},
|
||||
"ca": {"ref": "base64url"},
|
||||
"kem": {"ref": "base64url"}
|
||||
},
|
||||
"optionalProperties": {
|
||||
"app": {"properties": {}, "additionalProperties": true}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
```
|
||||
|
||||
Controller should reply with with `hello` or `error` response:
|
||||
|
||||
```abnf
|
||||
ctrlHello = unpaddedSize %s"HELLO " kemCyphertext nonce encrypted(unpaddedSize ctrlHelloJSON helloPad) pad
|
||||
; ctrlHelloJSON is encrypted with the hybrid secret,
|
||||
; including both previously agreed DH secret and KEM secret from kemCyphertext
|
||||
unpaddedSize = largeLength
|
||||
kemCyphertext = largeLength *OCTET
|
||||
pad = <pad block size to 16384 bytes>
|
||||
helloPad = <pad hello size to 12888 bytes>
|
||||
largeLength = 2*2 OCTET
|
||||
|
||||
ctrlError = unpaddedSize %s"ERROR " nonce encrypted(unpaddedSize ctrlErrorJSON helloPad) pad
|
||||
; ctrlErrorJSON is encrypted using previously agreed DH secret.
|
||||
```
|
||||
|
||||
JTD schema for the encrypted part of controller HELLO block `ctrlHelloJSON`:
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {},
|
||||
"additionalProperties": true
|
||||
}
|
||||
```
|
||||
|
||||
JTD schema for the encrypted part of controller ERROR block `ctrlErrorJSON`:
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"message": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
```
|
||||
|
||||
Once controller replies HELLO to the valid host HELLO block, it should stop accepting new TCP connections.
|
||||
|
||||
|
||||
### Сontroller/host session operation
|
||||
|
||||
The protocol for communication during the session is out of scope of this protocol.
|
||||
|
||||
SimpleX Chat will use HTTP2 encoding, where host device acts as a server and controller acts as a client (these roles are reversed compared with TLS connection).
|
||||
|
||||
Payloads in the protocol must be encrypted using NaCL cryptobox using the hybrid shared secret agreed during session establishment.
|
||||
|
||||
Commands of the controller must be signed after the encryption using the controller's session and long term Ed25519 keys.
|
||||
|
||||
tlsunique channel binding from TLS session MUST be included in commands (included in the signed body).
|
||||
|
||||
The syntax for encrypted command and response body encoding:
|
||||
|
||||
```
|
||||
commandBody = encBody sessSignature idSignature (attachment / noAttachment)
|
||||
responseBody = encBody attachment; should match counter in the command
|
||||
encBody = nonce encLength32 encrypted(tlsunique counter body)
|
||||
attachment = %x01 nonce encLength32 encrypted(attachment)
|
||||
noAttachment = %x00
|
||||
tlsunique = length 1*OCTET
|
||||
counter = 8*8 OCTET ; int64
|
||||
encLength32 = 4*4 OCTET ; uint32, includes authTag
|
||||
```
|
||||
|
||||
If the command or response includes attachment, it's hash must be included in command/response and validated.
|
||||
|
||||
## Key agreement for announcement packet and for session
|
||||
|
||||
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 containt 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 cyphertext 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 secred 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.
|
||||
|
||||
For the session, the shared secred is computed again using the KEM shared secret encapsulated by the controller using the new KEM key from the HOST hello block and DH shared secret computed using the host DH key from HELLO block and the new controller DH key from the announcement.
|
||||
|
||||
To describe it in pseudocode:
|
||||
|
||||
```
|
||||
// session 1
|
||||
hostHelloSecret(1) = dhSecret(1)
|
||||
sessionSecret(1) = sha256(dhSecret(1) || kemSecret(1)) // to encrypt session 1 data, incl. controller hello
|
||||
dhSecret(1) = dh(hostHelloDhKey(1), controllerInvitationDhKey(1))
|
||||
kemCiphertext(1) = enc(kemSecret(1), kemEncKey(1))
|
||||
// kemEncKey is included in host HELLO, kemCiphertext - in controller HELLO
|
||||
kemSecret(1) = dec(kemCiphertext(1), kemDecKey(1))
|
||||
|
||||
// multicast announcement for session n
|
||||
announcementSecret(n) = sha256(dhSecret(n'))
|
||||
dhSecret(n') = dh(hostHelloDhKey(n - 1), controllerDhKey(n))
|
||||
|
||||
// session n
|
||||
hostHelloSecret(n) = dhSecret(n)
|
||||
sessionSecret(n) = sha256(dhSecret(n) || kemSecret(n)) // to encrypt session n data, incl. controller hello
|
||||
dhSecret(n) = dh(hostHelloDhKey(n), controllerDhKey(n))
|
||||
// controllerDhKey(n) is either from invitation or from multicast announcement
|
||||
kemCiphertext(n) = enc(kemSecret(n), kemEncKey(n))
|
||||
kemSecret(n) = dec(kemCiphertext(n), kemDecKey(n))
|
||||
```
|
||||
|
||||
If controller fails to store the new host DH key after receiving HELLO block, the encryption will become out of sync and the host won't be able to decrypt the next announcement. To mitigate it, the host should keep the last session DH key and also previous session DH key to try to decrypt the next announcement computing shared secret using both keys (first the new one, and in case it fails - the previous).
|
||||
|
||||
To decrypt multicast announcement, the host should try to decrypt it using the keys of all known (paired) remote controllers.
|
||||
|
||||
## Other options
|
||||
|
||||
The proposed design has these pros/cons:
|
||||
|
||||
Pros:
|
||||
- mobile host that has sensitive data doesn't act as TLS server.
|
||||
- multicast is optional - all sessions can happen via QR code only.
|
||||
|
||||
Cons:
|
||||
- reversing of client/server roles between TLS and HTTP2.
|
||||
- in the first session mobile host TLS client credentials are verified after TLS connection is accepted.
|
||||
- cannot be used with host that runs in VM.
|
||||
|
||||
The alternative design will use mobile host device as TLS server. The session negotiation process:
|
||||
|
||||
- desktop shares its initial credentials via QR code, only for the first session
|
||||
- mobile sends encrypted multicast with session address, TLS CA fingerprint, DH key in clear text
|
||||
- desktop connects to mobile
|
||||
- session tlsunique presented to users on both devices - either user would have to confirm session on both devices or the mobile would have to send an additional "ready" block.
|
||||
|
||||
Pros:
|
||||
- no reversing server role between TLS and HTTP2
|
||||
- TLS credentials are exchanged before TLS, so invalid credentials can be rejected during the handshake of the first session.
|
||||
- if some other way to pass data from host to controller is added, then it can be used with host running in VM.
|
||||
|
||||
Cons:
|
||||
- multicast is mandatory, as there is no efficient way to communicate from mobile to desktop.
|
||||
- still needs hello or confirmation on both devices
|
||||
- mobile is now acting as TLS server creating additional attack vector
|
||||
|
||||
In both proposed and alternative design mobile host has chat data, acts as HTTP2 server, commands are signed with desktop key presented out-of–band, and both commands and responses are encrypted inside TLS session.
|
||||
|
||||
Other considered options:
|
||||
- SSH - more work integrating it.
|
||||
- use SMP connection to negotiate TLS session - it seems wrong to require Internet connection to negotiate a local network connection between desktop and mobile.
|
||||
- other multicast service discovery protocols - quite insecure.
|
||||
|
||||
## Threat model
|
||||
|
||||
#### A passive network adversary able to monitor the site-local traffic:
|
||||
|
||||
*can:*
|
||||
- observe session times, duration and volume of the transmitted data between host and controller.
|
||||
|
||||
*cannot:*
|
||||
- observe the content of the transmitted data.
|
||||
- substitute the transmitted commands or responses.
|
||||
- replay transmitted commands or events from the hosts.
|
||||
|
||||
#### An active network adversary able to intercept and substitute the site-local traffic:
|
||||
|
||||
*can:*
|
||||
- prevent host and controller devices from establishing the session
|
||||
|
||||
*cannot:*
|
||||
- same as passive adversary, provided that user visually verified session code out-of-band.
|
||||
|
||||
#### An active adversary with the access to the network:
|
||||
|
||||
*can:*
|
||||
- spam controller device.
|
||||
|
||||
*cannot:*
|
||||
- compromise host or controller devices.
|
||||
|
||||
#### An active adversary with the access to the network who also observed OOB announcement:
|
||||
|
||||
*can:*
|
||||
- connect to controller instead of the host.
|
||||
- present incorrect data to the controller.
|
||||
|
||||
*cannot:*
|
||||
- connect to the host or make host connect to itself.
|
||||
|
||||
#### Compromised controller device:
|
||||
|
||||
*can:*
|
||||
- observe the content of the transmitted data.
|
||||
- access any data of the controlled host application, within the capabilities of the provided API.
|
||||
|
||||
*cannot:*
|
||||
- access other data on the host device.
|
||||
- compromise host device.
|
||||
|
||||
#### Compromised host device:
|
||||
|
||||
*can:*
|
||||
- present incorrect data to the controller.
|
||||
- incorrectly interpret controller commands.
|
||||
|
||||
*cannot:*
|
||||
- access controller data, even related to this host device.
|
||||
@@ -19,7 +19,9 @@ if [ ! -f "${confd}/file-server.ini" ]; then
|
||||
# Set quota
|
||||
case "${QUOTA}" in
|
||||
'') printf 'Please specify $QUOTA environment variable.\n'; exit 1 ;;
|
||||
*) set -- "$@" --quota "${QUOTA}" ;;
|
||||
*GB) QUOTA="$(printf ${QUOTA} | tr '[:upper:]' '[:lower:]')"; set -- "$@" --quota "${QUOTA}" ;;
|
||||
*gb) set -- "$@" --quota "${QUOTA}" ;;
|
||||
*) printf 'Wrong format. Format should be: 1gb, 10gb, 100gb.\n'; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Init the certificates and configs
|
||||
|
||||
+190
-111
@@ -1,11 +1,11 @@
|
||||
cabal-version: 1.12
|
||||
|
||||
-- This file has been generated from package.yaml by hpack version 0.35.2.
|
||||
-- This file has been generated from package.yaml by hpack version 0.35.0.
|
||||
--
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplexmq
|
||||
version: 5.4.0.3
|
||||
version: 5.4.0.7
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
<./docs/Simplex-Messaging-Client.html client> and
|
||||
@@ -26,12 +26,19 @@ build-type: Simple
|
||||
extra-source-files:
|
||||
README.md
|
||||
CHANGELOG.md
|
||||
cbits/sha512.h
|
||||
cbits/sntrup761.h
|
||||
|
||||
flag swift
|
||||
description: Enable swift JSON format
|
||||
manual: True
|
||||
default: False
|
||||
|
||||
flag use_crypton
|
||||
description: Use crypton etc. in cryptostore
|
||||
manual: True
|
||||
default: True
|
||||
|
||||
library
|
||||
exposed-modules:
|
||||
Simplex.FileTransfer.Agent
|
||||
@@ -99,6 +106,11 @@ library
|
||||
Simplex.Messaging.Crypto.File
|
||||
Simplex.Messaging.Crypto.Lazy
|
||||
Simplex.Messaging.Crypto.Ratchet
|
||||
Simplex.Messaging.Crypto.SNTRUP761
|
||||
Simplex.Messaging.Crypto.SNTRUP761.Bindings
|
||||
Simplex.Messaging.Crypto.SNTRUP761.Bindings.Defines
|
||||
Simplex.Messaging.Crypto.SNTRUP761.Bindings.FFI
|
||||
Simplex.Messaging.Crypto.SNTRUP761.Bindings.RNG
|
||||
Simplex.Messaging.Encoding
|
||||
Simplex.Messaging.Encoding.String
|
||||
Simplex.Messaging.Notifications.Client
|
||||
@@ -107,6 +119,7 @@ library
|
||||
Simplex.Messaging.Notifications.Server.Env
|
||||
Simplex.Messaging.Notifications.Server.Main
|
||||
Simplex.Messaging.Notifications.Server.Push.APNS
|
||||
Simplex.Messaging.Notifications.Server.Push.APNS.Internal
|
||||
Simplex.Messaging.Notifications.Server.Stats
|
||||
Simplex.Messaging.Notifications.Server.Store
|
||||
Simplex.Messaging.Notifications.Server.StoreLog
|
||||
@@ -130,22 +143,35 @@ library
|
||||
Simplex.Messaging.Transport
|
||||
Simplex.Messaging.Transport.Buffer
|
||||
Simplex.Messaging.Transport.Client
|
||||
Simplex.Messaging.Transport.Credentials
|
||||
Simplex.Messaging.Transport.HTTP2
|
||||
Simplex.Messaging.Transport.HTTP2.Client
|
||||
Simplex.Messaging.Transport.HTTP2.File
|
||||
Simplex.Messaging.Transport.HTTP2.Server
|
||||
Simplex.Messaging.Transport.KeepAlive
|
||||
Simplex.Messaging.Transport.Server
|
||||
Simplex.Messaging.Transport.WebSockets
|
||||
Simplex.Messaging.Util
|
||||
Simplex.Messaging.Version
|
||||
Simplex.RemoteControl.Client
|
||||
Simplex.RemoteControl.Discovery
|
||||
Simplex.RemoteControl.Discovery.Multicast
|
||||
Simplex.RemoteControl.Invitation
|
||||
Simplex.RemoteControl.Types
|
||||
other-modules:
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
src
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns
|
||||
include-dirs:
|
||||
cbits
|
||||
c-sources:
|
||||
cbits/sha512.c
|
||||
cbits/sntrup761.c
|
||||
extra-libraries:
|
||||
crypto
|
||||
build-depends:
|
||||
QuickCheck ==2.14.*
|
||||
, aeson ==2.2.*
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, asn1-encoding ==0.9.*
|
||||
, asn1-types ==0.3.*
|
||||
@@ -153,27 +179,31 @@ library
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.11.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite ==0.30.*
|
||||
, cryptostore ==0.2.*
|
||||
, crypton ==0.34.*
|
||||
, crypton-x509 ==1.7.*
|
||||
, crypton-x509-store ==1.6.*
|
||||
, crypton-x509-validation ==1.6.*
|
||||
, cryptostore ==0.3.*
|
||||
, data-default ==0.7.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, filepath ==1.4.*
|
||||
, generic-random ==1.5.*
|
||||
, hourglass ==0.2.*
|
||||
, http-types ==0.12.*
|
||||
, http2 ==4.1.*
|
||||
, http2 >=4.2.2 && <4.3
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.18.*
|
||||
, mtl ==2.3.*
|
||||
, mtl >=2.3.1 && <3.0
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-info ==0.2.*
|
||||
, network-transport ==0.5.6
|
||||
, network-udp ==0.0.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
@@ -181,24 +211,28 @@ library
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.20.*
|
||||
, temporary ==1.3.*
|
||||
, text ==2.0.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time ==1.12.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, tls >=1.7.0 && <1.8
|
||||
, transformers ==0.6.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
if impl(ghc >= 9.6.2)
|
||||
build-depends:
|
||||
bytestring ==0.11.*
|
||||
, template-haskell ==2.20.*
|
||||
, text >=2.0.1 && <2.2
|
||||
if impl(ghc < 9.6.2)
|
||||
build-depends:
|
||||
bytestring ==0.10.*
|
||||
, template-haskell ==2.16.*
|
||||
, text >=1.2.3.0 && <1.3
|
||||
|
||||
executable ntf-server
|
||||
main-is: Main.hs
|
||||
@@ -208,8 +242,7 @@ executable ntf-server
|
||||
apps/ntf-server
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded -rtsopts
|
||||
build-depends:
|
||||
QuickCheck ==2.14.*
|
||||
, aeson ==2.2.*
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, asn1-encoding ==0.9.*
|
||||
, asn1-types ==0.3.*
|
||||
@@ -217,27 +250,31 @@ executable ntf-server
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.11.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite ==0.30.*
|
||||
, cryptostore ==0.2.*
|
||||
, crypton ==0.34.*
|
||||
, crypton-x509 ==1.7.*
|
||||
, crypton-x509-store ==1.6.*
|
||||
, crypton-x509-validation ==1.6.*
|
||||
, cryptostore ==0.3.*
|
||||
, data-default ==0.7.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, filepath ==1.4.*
|
||||
, generic-random ==1.5.*
|
||||
, hourglass ==0.2.*
|
||||
, http-types ==0.12.*
|
||||
, http2 ==4.1.*
|
||||
, http2 >=4.2.2 && <4.3
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.18.*
|
||||
, mtl ==2.3.*
|
||||
, mtl >=2.3.1 && <3.0
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-info ==0.2.*
|
||||
, network-transport ==0.5.6
|
||||
, network-udp ==0.0.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
@@ -246,24 +283,28 @@ executable ntf-server
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.20.*
|
||||
, temporary ==1.3.*
|
||||
, text ==2.0.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time ==1.12.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, tls >=1.7.0 && <1.8
|
||||
, transformers ==0.6.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
if impl(ghc >= 9.6.2)
|
||||
build-depends:
|
||||
bytestring ==0.11.*
|
||||
, template-haskell ==2.20.*
|
||||
, text >=2.0.1 && <2.2
|
||||
if impl(ghc < 9.6.2)
|
||||
build-depends:
|
||||
bytestring ==0.10.*
|
||||
, template-haskell ==2.16.*
|
||||
, text >=1.2.3.0 && <1.3
|
||||
|
||||
executable smp-agent
|
||||
main-is: Main.hs
|
||||
@@ -273,8 +314,7 @@ executable smp-agent
|
||||
apps/smp-agent
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded -rtsopts
|
||||
build-depends:
|
||||
QuickCheck ==2.14.*
|
||||
, aeson ==2.2.*
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, asn1-encoding ==0.9.*
|
||||
, asn1-types ==0.3.*
|
||||
@@ -282,27 +322,31 @@ executable smp-agent
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.11.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite ==0.30.*
|
||||
, cryptostore ==0.2.*
|
||||
, crypton ==0.34.*
|
||||
, crypton-x509 ==1.7.*
|
||||
, crypton-x509-store ==1.6.*
|
||||
, crypton-x509-validation ==1.6.*
|
||||
, cryptostore ==0.3.*
|
||||
, data-default ==0.7.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, filepath ==1.4.*
|
||||
, generic-random ==1.5.*
|
||||
, hourglass ==0.2.*
|
||||
, http-types ==0.12.*
|
||||
, http2 ==4.1.*
|
||||
, http2 >=4.2.2 && <4.3
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.18.*
|
||||
, mtl ==2.3.*
|
||||
, mtl >=2.3.1 && <3.0
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-info ==0.2.*
|
||||
, network-transport ==0.5.6
|
||||
, network-udp ==0.0.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
@@ -311,24 +355,28 @@ executable smp-agent
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.20.*
|
||||
, temporary ==1.3.*
|
||||
, text ==2.0.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time ==1.12.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, tls >=1.7.0 && <1.8
|
||||
, transformers ==0.6.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
if impl(ghc >= 9.6.2)
|
||||
build-depends:
|
||||
bytestring ==0.11.*
|
||||
, template-haskell ==2.20.*
|
||||
, text >=2.0.1 && <2.2
|
||||
if impl(ghc < 9.6.2)
|
||||
build-depends:
|
||||
bytestring ==0.10.*
|
||||
, template-haskell ==2.16.*
|
||||
, text >=1.2.3.0 && <1.3
|
||||
|
||||
executable smp-server
|
||||
main-is: Main.hs
|
||||
@@ -338,8 +386,7 @@ executable smp-server
|
||||
apps/smp-server
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded -rtsopts
|
||||
build-depends:
|
||||
QuickCheck ==2.14.*
|
||||
, aeson ==2.2.*
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, asn1-encoding ==0.9.*
|
||||
, asn1-types ==0.3.*
|
||||
@@ -347,27 +394,31 @@ executable smp-server
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.11.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite ==0.30.*
|
||||
, cryptostore ==0.2.*
|
||||
, crypton ==0.34.*
|
||||
, crypton-x509 ==1.7.*
|
||||
, crypton-x509-store ==1.6.*
|
||||
, crypton-x509-validation ==1.6.*
|
||||
, cryptostore ==0.3.*
|
||||
, data-default ==0.7.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, filepath ==1.4.*
|
||||
, generic-random ==1.5.*
|
||||
, hourglass ==0.2.*
|
||||
, http-types ==0.12.*
|
||||
, http2 ==4.1.*
|
||||
, http2 >=4.2.2 && <4.3
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.18.*
|
||||
, mtl ==2.3.*
|
||||
, mtl >=2.3.1 && <3.0
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-info ==0.2.*
|
||||
, network-transport ==0.5.6
|
||||
, network-udp ==0.0.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
@@ -376,24 +427,28 @@ executable smp-server
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.20.*
|
||||
, temporary ==1.3.*
|
||||
, text ==2.0.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time ==1.12.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, tls >=1.7.0 && <1.8
|
||||
, transformers ==0.6.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
if impl(ghc >= 9.6.2)
|
||||
build-depends:
|
||||
bytestring ==0.11.*
|
||||
, template-haskell ==2.20.*
|
||||
, text >=2.0.1 && <2.2
|
||||
if impl(ghc < 9.6.2)
|
||||
build-depends:
|
||||
bytestring ==0.10.*
|
||||
, template-haskell ==2.16.*
|
||||
, text >=1.2.3.0 && <1.3
|
||||
|
||||
executable xftp
|
||||
main-is: Main.hs
|
||||
@@ -403,8 +458,7 @@ executable xftp
|
||||
apps/xftp
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded -rtsopts
|
||||
build-depends:
|
||||
QuickCheck ==2.14.*
|
||||
, aeson ==2.2.*
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, asn1-encoding ==0.9.*
|
||||
, asn1-types ==0.3.*
|
||||
@@ -412,27 +466,31 @@ executable xftp
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.11.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite ==0.30.*
|
||||
, cryptostore ==0.2.*
|
||||
, crypton ==0.34.*
|
||||
, crypton-x509 ==1.7.*
|
||||
, crypton-x509-store ==1.6.*
|
||||
, crypton-x509-validation ==1.6.*
|
||||
, cryptostore ==0.3.*
|
||||
, data-default ==0.7.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, filepath ==1.4.*
|
||||
, generic-random ==1.5.*
|
||||
, hourglass ==0.2.*
|
||||
, http-types ==0.12.*
|
||||
, http2 ==4.1.*
|
||||
, http2 >=4.2.2 && <4.3
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.18.*
|
||||
, mtl ==2.3.*
|
||||
, mtl >=2.3.1 && <3.0
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-info ==0.2.*
|
||||
, network-transport ==0.5.6
|
||||
, network-udp ==0.0.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
@@ -441,24 +499,28 @@ executable xftp
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.20.*
|
||||
, temporary ==1.3.*
|
||||
, text ==2.0.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time ==1.12.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, tls >=1.7.0 && <1.8
|
||||
, transformers ==0.6.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
if impl(ghc >= 9.6.2)
|
||||
build-depends:
|
||||
bytestring ==0.11.*
|
||||
, template-haskell ==2.20.*
|
||||
, text >=2.0.1 && <2.2
|
||||
if impl(ghc < 9.6.2)
|
||||
build-depends:
|
||||
bytestring ==0.10.*
|
||||
, template-haskell ==2.16.*
|
||||
, text >=1.2.3.0 && <1.3
|
||||
|
||||
executable xftp-server
|
||||
main-is: Main.hs
|
||||
@@ -468,8 +530,7 @@ executable xftp-server
|
||||
apps/xftp-server
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded -rtsopts
|
||||
build-depends:
|
||||
QuickCheck ==2.14.*
|
||||
, aeson ==2.2.*
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, asn1-encoding ==0.9.*
|
||||
, asn1-types ==0.3.*
|
||||
@@ -477,27 +538,31 @@ executable xftp-server
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.11.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite ==0.30.*
|
||||
, cryptostore ==0.2.*
|
||||
, crypton ==0.34.*
|
||||
, crypton-x509 ==1.7.*
|
||||
, crypton-x509-store ==1.6.*
|
||||
, crypton-x509-validation ==1.6.*
|
||||
, cryptostore ==0.3.*
|
||||
, data-default ==0.7.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, filepath ==1.4.*
|
||||
, generic-random ==1.5.*
|
||||
, hourglass ==0.2.*
|
||||
, http-types ==0.12.*
|
||||
, http2 ==4.1.*
|
||||
, http2 >=4.2.2 && <4.3
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.18.*
|
||||
, mtl ==2.3.*
|
||||
, mtl >=2.3.1 && <3.0
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-info ==0.2.*
|
||||
, network-transport ==0.5.6
|
||||
, network-udp ==0.0.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
@@ -506,24 +571,28 @@ executable xftp-server
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.20.*
|
||||
, temporary ==1.3.*
|
||||
, text ==2.0.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time ==1.12.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, tls >=1.7.0 && <1.8
|
||||
, transformers ==0.6.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
if impl(ghc >= 9.6.2)
|
||||
build-depends:
|
||||
bytestring ==0.11.*
|
||||
, template-haskell ==2.20.*
|
||||
, text >=2.0.1 && <2.2
|
||||
if impl(ghc < 9.6.2)
|
||||
build-depends:
|
||||
bytestring ==0.10.*
|
||||
, template-haskell ==2.16.*
|
||||
, text >=1.2.3.0 && <1.3
|
||||
|
||||
test-suite simplexmq-test
|
||||
type: exitcode-stdio-1.0
|
||||
@@ -549,6 +618,7 @@ test-suite simplexmq-test
|
||||
FileDescriptionTests
|
||||
NtfClient
|
||||
NtfServerTests
|
||||
RemoteControl
|
||||
ServerTests
|
||||
SMPAgentClient
|
||||
SMPClient
|
||||
@@ -571,31 +641,36 @@ test-suite simplexmq-test
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.11.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite ==0.30.*
|
||||
, cryptostore ==0.2.*
|
||||
, crypton ==0.34.*
|
||||
, crypton-x509 ==1.7.*
|
||||
, crypton-x509-store ==1.6.*
|
||||
, crypton-x509-validation ==1.6.*
|
||||
, cryptostore ==0.3.*
|
||||
, data-default ==0.7.*
|
||||
, deepseq ==1.4.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, filepath ==1.4.*
|
||||
, generic-random ==1.5.*
|
||||
, hourglass ==0.2.*
|
||||
, hspec ==2.11.*
|
||||
, hspec-core ==2.11.*
|
||||
, http-types ==0.12.*
|
||||
, http2 ==4.1.*
|
||||
, http2 >=4.2.2 && <4.3
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, main-tester ==0.2.*
|
||||
, memory ==0.18.*
|
||||
, mtl ==2.3.*
|
||||
, mtl >=2.3.1 && <3.0
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-info ==0.2.*
|
||||
, network-transport ==0.5.6
|
||||
, network-udp ==0.0.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
@@ -605,22 +680,26 @@ test-suite simplexmq-test
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.20.*
|
||||
, temporary ==1.3.*
|
||||
, text ==2.0.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time ==1.12.*
|
||||
, time-manager ==0.0.*
|
||||
, timeit ==2.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, tls >=1.7.0 && <1.8
|
||||
, transformers ==0.6.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
if impl(ghc >= 9.6.2)
|
||||
build-depends:
|
||||
bytestring ==0.11.*
|
||||
, template-haskell ==2.20.*
|
||||
, text >=2.0.1 && <2.2
|
||||
if impl(ghc < 9.6.2)
|
||||
build-depends:
|
||||
bytestring ==0.10.*
|
||||
, template-haskell ==2.16.*
|
||||
, text >=1.2.3.0 && <1.3
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedRecordDot #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
|
||||
module Simplex.FileTransfer.Agent
|
||||
@@ -105,7 +103,7 @@ closeXFTPAgent XFTPAgent {xftpRcvWorkers, xftpSndWorkers} = do
|
||||
|
||||
xftpReceiveFile' :: AgentMonad m => AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> m RcvFileId
|
||||
xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks}) cfArgs = do
|
||||
g <- asks idsDrg
|
||||
g <- asks random
|
||||
prefixPath <- getPrefixPath "rcv.xftp"
|
||||
createDirectory prefixPath
|
||||
let relPrefixPath = takeFileName prefixPath
|
||||
@@ -134,9 +132,7 @@ toFSFilePath :: AgentMonad m => FilePath -> m FilePath
|
||||
toFSFilePath f = (</> f) <$> getXFTPWorkPath
|
||||
|
||||
createEmptyFile :: AgentMonad m => FilePath -> m ()
|
||||
createEmptyFile fPath = do
|
||||
h <- openFile fPath AppendMode
|
||||
liftIO $ B.hPut h "" >> hFlush h
|
||||
createEmptyFile fPath = liftIO $ B.writeFile fPath ""
|
||||
|
||||
addXFTPRcvWorker :: AgentMonad m => AgentClient -> Maybe XFTPServer -> m ()
|
||||
addXFTPRcvWorker c = addWorker c xftpRcvWorkers runXFTPRcvWorker runXFTPRcvLocalWorker
|
||||
@@ -286,7 +282,7 @@ notify c entId cmd = atomically $ writeTBQueue (subQ c) ("", entId, APC (sAEntit
|
||||
|
||||
xftpSendFile' :: AgentMonad m => AgentClient -> UserId -> CryptoFile -> Int -> m SndFileId
|
||||
xftpSendFile' c userId file numRecipients = do
|
||||
g <- asks idsDrg
|
||||
g <- asks random
|
||||
prefixPath <- getPrefixPath "snd.xftp"
|
||||
createDirectory prefixPath
|
||||
let relPrefixPath = takeFileName prefixPath
|
||||
@@ -324,8 +320,8 @@ runXFTPSndPrepareWorker c doWork = do
|
||||
if status /= SFSEncrypted -- status is SFSNew or SFSEncrypting
|
||||
then do
|
||||
fsEncPath <- toFSFilePath $ sndFileEncPath ppath
|
||||
when (status == SFSEncrypting) $
|
||||
whenM (doesFileExist fsEncPath) $ removeFile fsEncPath
|
||||
when (status == SFSEncrypting) . whenM (doesFileExist fsEncPath) $
|
||||
removeFile fsEncPath
|
||||
withStore' c $ \db -> updateSndFileStatus db sndFileId SFSEncrypting
|
||||
(digest, chunkSpecsDigests) <- encryptFileForUpload sndFile fsEncPath
|
||||
withStore c $ \db -> do
|
||||
@@ -443,11 +439,11 @@ runXFTPSndWorker c srv doWork = do
|
||||
| length rcvIdsKeys > numRecipients = throwError $ INTERNAL "too many recipients"
|
||||
| length rcvIdsKeys == numRecipients = pure cr
|
||||
| otherwise = do
|
||||
maxRecipients <- asks $ xftpMaxRecipientsPerRequest . config
|
||||
let numRecipients' = min (numRecipients - length rcvIdsKeys) maxRecipients
|
||||
rcvIdsKeys' <- agentXFTPAddRecipients c userId chunkDigest cr numRecipients'
|
||||
cr' <- withStore' c $ \db -> addSndChunkReplicaRecipients db cr $ L.toList rcvIdsKeys'
|
||||
addRecipients ch cr'
|
||||
maxRecipients <- asks $ xftpMaxRecipientsPerRequest . config
|
||||
let numRecipients' = min (numRecipients - length rcvIdsKeys) maxRecipients
|
||||
rcvIdsKeys' <- agentXFTPAddRecipients c userId chunkDigest cr numRecipients'
|
||||
cr' <- withStore' c $ \db -> addSndChunkReplicaRecipients db cr $ L.toList rcvIdsKeys'
|
||||
addRecipients ch cr'
|
||||
sndFileToDescrs :: SndFile -> m (ValidFileDescription 'FSender, [ValidFileDescription 'FRecipient])
|
||||
sndFileToDescrs SndFile {digest = Nothing} = throwError $ INTERNAL "snd file has no digest"
|
||||
sndFileToDescrs SndFile {chunks = []} = throwError $ INTERNAL "snd file has no chunks"
|
||||
@@ -488,7 +484,7 @@ runXFTPSndWorker c srv doWork = do
|
||||
rcvChunks :: [[FileChunk]]
|
||||
rcvChunks = map (sortChunks . M.elems) $ M.elems $ foldl' addRcvChunk M.empty rcvReplicas
|
||||
sortChunks :: [FileChunk] -> [FileChunk]
|
||||
sortChunks = map reverseReplicas . sortOn (\fc -> fc.chunkNo)
|
||||
sortChunks = map reverseReplicas . sortOn (\FileChunk {chunkNo} -> chunkNo)
|
||||
reverseReplicas ch@FileChunk {replicas} = (ch :: FileChunk) {replicas = reverse replicas}
|
||||
addRcvChunk :: Map Int (Map Int FileChunk) -> SentRecipientReplica -> Map Int (Map Int FileChunk)
|
||||
addRcvChunk m SentRecipientReplica {chunkNo, server, rcvNo, replicaId, replicaKey, digest, chunkSize} =
|
||||
@@ -575,7 +571,7 @@ runXFTPDelWorker c srv doWork = do
|
||||
withStore' c $ \db -> updateDeletedSndChunkReplicaDelay db deletedSndChunkReplicaId replicaDelay
|
||||
atomically $ assertAgentForeground c
|
||||
loop
|
||||
retryDone e = delWorkerInternalError c deletedSndChunkReplicaId e
|
||||
retryDone = delWorkerInternalError c deletedSndChunkReplicaId
|
||||
deleteChunkReplica :: DeletedSndChunkReplica -> m ()
|
||||
deleteChunkReplica replica@DeletedSndChunkReplica {userId, deletedSndChunkReplicaId} = do
|
||||
agentXFTPDeleteChunk c userId replica
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedRecordDot #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
@@ -49,6 +48,7 @@ import Simplex.Messaging.Transport (supportedParameters)
|
||||
import Simplex.Messaging.Transport.Client (TransportClientConfig, TransportHost)
|
||||
import Simplex.Messaging.Transport.HTTP2
|
||||
import Simplex.Messaging.Transport.HTTP2.Client
|
||||
import Simplex.Messaging.Transport.HTTP2.File
|
||||
import Simplex.Messaging.Util (bshow, liftEitherError, whenM)
|
||||
import UnliftIO
|
||||
import UnliftIO.Directory
|
||||
@@ -110,7 +110,7 @@ xftpClientServer = B.unpack . strEncode . snd3 . transportSession
|
||||
snd3 (_, s, _) = s
|
||||
|
||||
xftpTransportHost :: XFTPClient -> TransportHost
|
||||
xftpTransportHost c = c.http2Client.client_.host
|
||||
xftpTransportHost XFTPClient {http2Client = HTTP2Client {client_ = HClient {host}}} = host
|
||||
|
||||
xftpSessionTs :: XFTPClient -> UTCTime
|
||||
xftpSessionTs = sessionTs . http2Client
|
||||
@@ -153,7 +153,7 @@ sendXFTPCommand XFTPClient {config, http2Client = http2@HTTP2Client {sessionId}}
|
||||
forM_ chunkSpec_ $ \XFTPChunkSpec {filePath, chunkOffset, chunkSize} ->
|
||||
withFile filePath ReadMode $ \h -> do
|
||||
hSeek h AbsoluteSeek $ fromIntegral chunkOffset
|
||||
sendFile h send $ fromIntegral chunkSize
|
||||
hSendFile h send $ fromIntegral chunkSize
|
||||
done
|
||||
|
||||
createXFTPChunk ::
|
||||
|
||||
@@ -5,11 +5,9 @@
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedRecordDot #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
|
||||
module Simplex.FileTransfer.Client.Main
|
||||
@@ -362,7 +360,7 @@ cliSendFileOpts SendOptions {filePath, outputDir, numRecipients, xftpServers, re
|
||||
rcvChunks :: [[FileChunk]]
|
||||
rcvChunks = map (sortChunks . M.elems) $ M.elems $ foldl' addRcvChunk M.empty rcvReplicas
|
||||
sortChunks :: [FileChunk] -> [FileChunk]
|
||||
sortChunks = map reverseReplicas . sortOn (\c -> c.chunkNo)
|
||||
sortChunks = map reverseReplicas . sortOn (\FileChunk {chunkNo} -> chunkNo)
|
||||
reverseReplicas ch@FileChunk {replicas} = (ch :: FileChunk) {replicas = reverse replicas}
|
||||
addRcvChunk :: Map Int (Map Int FileChunk) -> SentRecipientReplica -> Map Int (Map Int FileChunk)
|
||||
addRcvChunk m SentRecipientReplica {chunkNo, server, rcvNo, replicaId, replicaKey, digest, chunkSize} =
|
||||
@@ -421,7 +419,9 @@ cliReceiveFile ReceiveOptions {fileDescription, filePath, retryCount, tempPath,
|
||||
a <- atomically $ newXFTPAgent defaultXFTPClientAgentConfig
|
||||
liftIO $ printNoNewLine "Downloading file..."
|
||||
downloadedChunks <- newTVarIO []
|
||||
let srv FileChunk {replicas} = (head replicas).server
|
||||
let srv FileChunk {replicas} = case replicas of
|
||||
[] -> error "empty FileChunk.replicas"
|
||||
FileChunkReplica {server} : _ -> server
|
||||
srvChunks = groupAllOn srv chunks
|
||||
chunkPaths <- map snd . sortOn fst . concat <$> pooledForConcurrentlyN 16 srvChunks (mapM $ downloadFileChunk a encPath size downloadedChunks)
|
||||
encDigest <- liftIO $ LC.sha512Hash <$> readChunks chunkPaths
|
||||
@@ -499,10 +499,9 @@ cliFileDescrInfo InfoOptions {fileDescription} = do
|
||||
printParty
|
||||
putStrLn $ "File download size: " <> strEnc size
|
||||
putStrLn "File server(s):"
|
||||
forM_ replicas $ \srvReplicas -> do
|
||||
let srv = (head srvReplicas).server
|
||||
chSizes = map (\FileServerReplica {chunkSize = chSize_} -> unFileSize $ fromMaybe chunkSize chSize_) srvReplicas
|
||||
putStrLn $ strEnc srv <> ": " <> strEnc (FileSize $ sum chSizes)
|
||||
forM_ replicas $ \srvReplicas@(FileServerReplica {server} :| _) -> do
|
||||
let chSizes = fmap (\FileServerReplica {chunkSize = chSize_} -> unFileSize $ fromMaybe chunkSize chSize_) srvReplicas
|
||||
putStrLn $ strEnc server <> ": " <> strEnc (FileSize $ sum chSizes)
|
||||
where
|
||||
printParty :: IO ()
|
||||
printParty = case party of
|
||||
@@ -527,8 +526,8 @@ prepareChunkSizes size' = prepareSizes size'
|
||||
(smallSize, bigSize)
|
||||
| size' > size34 chunkSize3 = (chunkSize2, chunkSize3)
|
||||
| otherwise = (chunkSize1, chunkSize2)
|
||||
-- | size' > size34 chunkSize2 = (chunkSize1, chunkSize2)
|
||||
-- | otherwise = (chunkSize0, chunkSize1)
|
||||
-- | size' > size34 chunkSize2 = (chunkSize1, chunkSize2)
|
||||
-- | otherwise = (chunkSize0, chunkSize1)
|
||||
size34 sz = (fromIntegral sz * 3) `div` 4
|
||||
prepareSizes 0 = []
|
||||
prepareSizes size
|
||||
@@ -571,11 +570,11 @@ withRetry retryCount = withRetry' retryCount . withExceptT (CLIError . show)
|
||||
removeFD :: Bool -> FilePath -> IO ()
|
||||
removeFD yes fd
|
||||
| yes = do
|
||||
removeFile fd
|
||||
putStrLn $ "\nFile description " <> fd <> " is deleted."
|
||||
removeFile fd
|
||||
putStrLn $ "\nFile description " <> fd <> " is deleted."
|
||||
| otherwise = do
|
||||
y <- liftIO . getConfirmation $ "\nFile description " <> fd <> " can't be used again. Delete it"
|
||||
when y $ removeFile fd
|
||||
y <- liftIO . getConfirmation $ "\nFile description " <> fd <> " can't be used again. Delete it"
|
||||
when y $ removeFile fd
|
||||
|
||||
getConfirmation :: String -> IO Bool
|
||||
getConfirmation prompt = do
|
||||
|
||||
@@ -46,12 +46,12 @@ encryptFile srcFile fileHdr key nonce fileSize' encSize encFile = do
|
||||
encryptChunks_ get w (!sb, !len)
|
||||
| len == 0 = pure sb
|
||||
| otherwise = do
|
||||
let chSize = min len 65536
|
||||
ch <- liftIO $ get chSize
|
||||
when (B.length ch /= fromIntegral chSize) $ throwError $ FTCEFileIOError "encrypting file: unexpected EOF"
|
||||
let (ch', sb') = LC.sbEncryptChunk sb ch
|
||||
liftIO $ B.hPut w ch'
|
||||
encryptChunks_ get w (sb', len - chSize)
|
||||
let chSize = min len 65536
|
||||
ch <- liftIO $ get chSize
|
||||
when (B.length ch /= fromIntegral chSize) $ throwError $ FTCEFileIOError "encrypting file: unexpected EOF"
|
||||
let (ch', sb') = LC.sbEncryptChunk sb ch
|
||||
liftIO $ B.hPut w ch'
|
||||
encryptChunks_ get w (sb', len - chSize)
|
||||
|
||||
decryptChunks :: Int64 -> [FilePath] -> C.SbKey -> C.CbNonce -> (String -> ExceptT String IO CryptoFile) -> ExceptT FTCryptoError IO CryptoFile
|
||||
decryptChunks _ [] _ _ _ = throwError $ FTCEInvalidHeader "empty"
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedRecordDot #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
|
||||
module Simplex.FileTransfer.Description
|
||||
@@ -38,7 +36,7 @@ where
|
||||
import Control.Applicative (optional)
|
||||
import Control.Monad ((<=<))
|
||||
import Data.Aeson (FromJSON, ToJSON)
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.TH as J
|
||||
import Data.Attoparsec.ByteString.Char8 (Parser)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bifunctor (first)
|
||||
@@ -46,6 +44,8 @@ import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Int (Int64)
|
||||
import Data.List (foldl', sortOn)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map (Map)
|
||||
import qualified Data.Map as M
|
||||
import Data.Maybe (fromMaybe)
|
||||
@@ -54,14 +54,13 @@ import Data.Word (Word32)
|
||||
import qualified Data.Yaml as Y
|
||||
import Database.SQLite.Simple.FromField (FromField (..))
|
||||
import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import GHC.Generics (Generic)
|
||||
import Simplex.FileTransfer.Chunks
|
||||
import Simplex.FileTransfer.Protocol
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
import Simplex.Messaging.Parsers (defaultJSON, parseAll)
|
||||
import Simplex.Messaging.Protocol (XFTPServer)
|
||||
import Simplex.Messaging.Util (bshow, groupAllOn, (<$?>))
|
||||
import Simplex.Messaging.Util (bshow, (<$?>))
|
||||
|
||||
data FileDescription (p :: FileParty) = FileDescription
|
||||
{ party :: SFileParty p,
|
||||
@@ -150,21 +149,13 @@ data YAMLFileDescription = YAMLFileDescription
|
||||
chunkSize :: String,
|
||||
replicas :: [YAMLServerReplicas]
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
instance ToJSON YAMLFileDescription where
|
||||
toJSON = J.genericToJSON J.defaultOptions
|
||||
toEncoding = J.genericToEncoding J.defaultOptions
|
||||
deriving (Eq, Show)
|
||||
|
||||
data YAMLServerReplicas = YAMLServerReplicas
|
||||
{ server :: XFTPServer,
|
||||
chunks :: [String]
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
instance ToJSON YAMLServerReplicas where
|
||||
toJSON = J.genericToJSON J.defaultOptions
|
||||
toEncoding = J.genericToEncoding J.defaultOptions
|
||||
deriving (Eq, Show)
|
||||
|
||||
data FileServerReplica = FileServerReplica
|
||||
{ chunkNo :: Int,
|
||||
@@ -176,6 +167,13 @@ data FileServerReplica = FileServerReplica
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
newtype FileSize a = FileSize {unFileSize :: a}
|
||||
deriving (Eq, Show)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''YAMLServerReplicas)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''YAMLFileDescription)
|
||||
|
||||
instance FilePartyI p => StrEncoding (ValidFileDescription p) where
|
||||
strEncode (ValidFD fd) = strEncode fd
|
||||
strDecode s = strDecode s >>= (\(AVFD fd) -> checkParty fd)
|
||||
@@ -202,7 +200,7 @@ validateFileDescription fd@FileDescription {size, chunks}
|
||||
| chunksSize chunks /= unFileSize size = Left "chunks total size is different than file size"
|
||||
| otherwise = Right $ ValidFD fd
|
||||
where
|
||||
chunkNos = map (\c -> c.chunkNo) chunks
|
||||
chunkNos = map (\FileChunk {chunkNo} -> chunkNo) chunks
|
||||
chunksSize = fromIntegral . foldl' (\s FileChunk {chunkSize} -> s + unFileSize chunkSize) 0
|
||||
|
||||
encodeFileDescription :: FileDescription p -> YAMLFileDescription
|
||||
@@ -217,9 +215,6 @@ encodeFileDescription FileDescription {party, size, digest, key, nonce, chunkSiz
|
||||
replicas = encodeFileReplicas chunkSize chunks
|
||||
}
|
||||
|
||||
newtype FileSize a = FileSize {unFileSize :: a}
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance (Integral a, Show a) => StrEncoding (FileSize a) where
|
||||
strEncode (FileSize b)
|
||||
| b' /= 0 = bshow b
|
||||
@@ -242,22 +237,22 @@ instance (Integral a, Show a) => StrEncoding (FileSize a) where
|
||||
instance (Integral a, Show a) => IsString (FileSize a) where
|
||||
fromString = either error id . strDecode . B.pack
|
||||
|
||||
instance (FromField a) => FromField (FileSize a) where fromField f = FileSize <$> fromField f
|
||||
instance FromField a => FromField (FileSize a) where fromField f = FileSize <$> fromField f
|
||||
|
||||
instance (ToField a) => ToField (FileSize a) where toField (FileSize s) = toField s
|
||||
instance ToField a => ToField (FileSize a) where toField (FileSize s) = toField s
|
||||
|
||||
groupReplicasByServer :: FileSize Word32 -> [FileChunk] -> [[FileServerReplica]]
|
||||
groupReplicasByServer :: FileSize Word32 -> [FileChunk] -> [NonEmpty FileServerReplica]
|
||||
groupReplicasByServer defChunkSize =
|
||||
groupAllOn (\r -> r.server) . unfoldChunksToReplicas defChunkSize
|
||||
L.groupAllWith (\FileServerReplica {server} -> server) . unfoldChunksToReplicas defChunkSize
|
||||
|
||||
encodeFileReplicas :: FileSize Word32 -> [FileChunk] -> [YAMLServerReplicas]
|
||||
encodeFileReplicas defChunkSize =
|
||||
map encodeServerReplicas . groupReplicasByServer defChunkSize
|
||||
where
|
||||
encodeServerReplicas fs =
|
||||
encodeServerReplicas fs@(FileServerReplica {server} :| _) =
|
||||
YAMLServerReplicas
|
||||
{ server = (head fs).server, -- groupAllOn guarantees that fs is not empty
|
||||
chunks = map (B.unpack . encodeServerReplica) fs
|
||||
{ server,
|
||||
chunks = map (B.unpack . encodeServerReplica) $ L.toList fs
|
||||
}
|
||||
|
||||
encodeServerReplica :: FileServerReplica -> ByteString
|
||||
@@ -311,7 +306,7 @@ foldReplicasToChunks :: FileSize Word32 -> [FileServerReplica] -> Either String
|
||||
foldReplicasToChunks defChunkSize fs = do
|
||||
sd <- foldSizesDigests fs
|
||||
-- TODO validate (check that chunks match) or in separate function
|
||||
sortOn (\c -> c.chunkNo) . map reverseReplicas . M.elems <$> foldChunks sd fs
|
||||
sortOn (\FileChunk {chunkNo} -> chunkNo) . map reverseReplicas . M.elems <$> foldChunks sd fs
|
||||
where
|
||||
foldSizesDigests :: [FileServerReplica] -> Either String (Map Int (FileSize Word32), Map Int FileDigest)
|
||||
foldSizesDigests = foldl' addSizeDigest $ Right (M.empty, M.empty)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
@@ -7,6 +6,7 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
|
||||
@@ -14,8 +14,7 @@
|
||||
module Simplex.FileTransfer.Protocol where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import Data.Aeson (FromJSON, ToJSON)
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.TH as J
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bifunctor (first)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
@@ -25,8 +24,6 @@ import Data.List.NonEmpty (NonEmpty (..))
|
||||
import Data.Maybe (isNothing)
|
||||
import Data.Type.Equality
|
||||
import Data.Word (Word32)
|
||||
import GHC.Generics (Generic)
|
||||
import Generic.Random (genericArbitraryU)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
@@ -54,12 +51,10 @@ import Simplex.Messaging.Protocol
|
||||
tEncode,
|
||||
tEncodeBatch,
|
||||
tParse,
|
||||
_smpP,
|
||||
)
|
||||
import Simplex.Messaging.Transport (SessionId, TransportError (..))
|
||||
import Simplex.Messaging.Util (bshow, (<$?>))
|
||||
import Simplex.Messaging.Version
|
||||
import Test.QuickCheck (Arbitrary (..))
|
||||
|
||||
currentXFTPVersion :: Version
|
||||
currentXFTPVersion = 1
|
||||
@@ -69,14 +64,7 @@ xftpBlockSize = 16384
|
||||
|
||||
-- | File protocol clients
|
||||
data FileParty = FRecipient | FSender
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
instance FromJSON FileParty where
|
||||
parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "F"
|
||||
|
||||
instance ToJSON FileParty where
|
||||
toJSON = J.genericToJSON . enumJSON $ dropPrefix "F"
|
||||
toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "F"
|
||||
deriving (Eq, Show)
|
||||
|
||||
data SFileParty :: FileParty -> Type where
|
||||
SFRecipient :: SFileParty FRecipient
|
||||
@@ -355,11 +343,7 @@ data XFTPErrorType
|
||||
INTERNAL
|
||||
| -- | used internally, never returned by the server (to be removed)
|
||||
DUPLICATE_ -- not part of SMP protocol, used internally
|
||||
deriving (Eq, Generic, Read, Show)
|
||||
|
||||
instance ToJSON XFTPErrorType where
|
||||
toJSON = J.genericToJSON $ sumTypeJSON id
|
||||
toEncoding = J.genericToEncoding $ sumTypeJSON id
|
||||
deriving (Eq, Read, Show)
|
||||
|
||||
instance StrEncoding XFTPErrorType where
|
||||
strEncode = \case
|
||||
@@ -367,8 +351,6 @@ instance StrEncoding XFTPErrorType where
|
||||
e -> bshow e
|
||||
strP = "CMD " *> (CMD <$> parseRead1) <|> parseRead1
|
||||
|
||||
instance Arbitrary XFTPErrorType where arbitrary = genericArbitraryU
|
||||
|
||||
instance Encoding XFTPErrorType where
|
||||
smpEncode = \case
|
||||
BLOCK -> "BLOCK"
|
||||
@@ -432,3 +414,7 @@ xftpDecodeTransmission sessionId t = do
|
||||
case tParse True t' of
|
||||
t'' :| [] -> Right $ tDecodeParseValidate sessionId currentXFTPVersion t''
|
||||
_ -> Left BLOCK
|
||||
|
||||
$(J.deriveJSON (enumJSON $ dropPrefix "F") ''FileParty)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON id) ''XFTPErrorType)
|
||||
|
||||
@@ -169,17 +169,17 @@ processRequest :: HTTP2Request -> M ()
|
||||
processRequest HTTP2Request {sessionId, reqBody = body@HTTP2Body {bodyHead}, sendResponse}
|
||||
| B.length bodyHead /= xftpBlockSize = sendXFTPResponse ("", "", FRErr BLOCK) Nothing
|
||||
| otherwise = do
|
||||
case xftpDecodeTransmission sessionId bodyHead of
|
||||
Right (sig_, signed, (corrId, fId, cmdOrErr)) -> do
|
||||
case cmdOrErr of
|
||||
Right cmd -> do
|
||||
verifyXFTPTransmission sig_ signed fId cmd >>= \case
|
||||
VRVerified req -> uncurry send =<< processXFTPRequest body req
|
||||
VRFailed -> send (FRErr AUTH) Nothing
|
||||
Left e -> send (FRErr e) Nothing
|
||||
where
|
||||
send resp = sendXFTPResponse (corrId, fId, resp)
|
||||
Left e -> sendXFTPResponse ("", "", FRErr e) Nothing
|
||||
case xftpDecodeTransmission sessionId bodyHead of
|
||||
Right (sig_, signed, (corrId, fId, cmdOrErr)) -> do
|
||||
case cmdOrErr of
|
||||
Right cmd -> do
|
||||
verifyXFTPTransmission sig_ signed fId cmd >>= \case
|
||||
VRVerified req -> uncurry send =<< processXFTPRequest body req
|
||||
VRFailed -> send (FRErr AUTH) Nothing
|
||||
Left e -> send (FRErr e) Nothing
|
||||
where
|
||||
send resp = sendXFTPResponse (corrId, fId, resp)
|
||||
Left e -> sendXFTPResponse ("", "", FRErr e) Nothing
|
||||
where
|
||||
sendXFTPResponse :: (CorrId, XFTPFileId, FileResponse) -> Maybe ServerFile -> M ()
|
||||
sendXFTPResponse (corrId, fId, resp) serverFile_ = do
|
||||
|
||||
@@ -26,7 +26,7 @@ import Simplex.FileTransfer.Server.StoreLog
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol (BasicAuth, RcvPublicVerifyKey)
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Transport.Server (loadFingerprint, loadTLSServerParams, TransportServerConfig)
|
||||
import Simplex.Messaging.Transport.Server (TransportServerConfig, loadFingerprint, loadTLSServerParams)
|
||||
import Simplex.Messaging.Util (tshow)
|
||||
import System.IO (IOMode (..))
|
||||
import UnliftIO.STM
|
||||
|
||||
@@ -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 (..), defaultFileExpiration, defFileExpirationHours)
|
||||
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defFileExpirationHours, defaultFileExpiration)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), pattern XFTPServer)
|
||||
@@ -33,7 +33,7 @@ import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
|
||||
import Text.Read (readMaybe)
|
||||
|
||||
xftpServerVersion :: String
|
||||
xftpServerVersion = "1.1.1"
|
||||
xftpServerVersion = "1.1.3"
|
||||
|
||||
xftpServerCLI :: FilePath -> FilePath -> IO ()
|
||||
xftpServerCLI cfgPath logPath = do
|
||||
@@ -143,9 +143,10 @@ xftpServerCLI cfgPath logPath = do
|
||||
allowNewFiles = fromMaybe True $ iniOnOff "AUTH" "new_files" ini,
|
||||
newFileBasicAuth = either error id <$> strDecodeIni "AUTH" "create_password" ini,
|
||||
fileExpiration =
|
||||
Just defaultFileExpiration
|
||||
{ ttl = 3600 * readIniDefault defFileExpirationHours "STORE_LOG" "expire_files_hours" ini
|
||||
},
|
||||
Just
|
||||
defaultFileExpiration
|
||||
{ ttl = 3600 * readIniDefault defFileExpirationHours "STORE_LOG" "expire_files_hours" ini
|
||||
},
|
||||
caCertificateFile = c caCrtFile,
|
||||
privateKeyFile = c serverKeyFile,
|
||||
certificateFile = c serverCrtFile,
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.FileTransfer.Transport
|
||||
( supportedFileServerVRange,
|
||||
XFTPRcvChunkSpec (..),
|
||||
sendFile,
|
||||
ReceiveFileError (..),
|
||||
receiveFile,
|
||||
sendEncFile,
|
||||
receiveEncFile,
|
||||
receiveSbFile,
|
||||
)
|
||||
where
|
||||
|
||||
@@ -19,16 +18,17 @@ import qualified Control.Exception as E
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Class
|
||||
import Data.Bifunctor (first)
|
||||
import qualified Data.ByteArray as BA
|
||||
import Data.ByteString.Builder (Builder, byteString)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Word (Word32)
|
||||
import GHC.IO.Handle.Internals (ioe_EOF)
|
||||
import Simplex.FileTransfer.Protocol (XFTPErrorType (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||
import Simplex.Messaging.Transport.HTTP2.File
|
||||
import Simplex.Messaging.Version
|
||||
import System.IO (Handle, IOMode (..), withFile)
|
||||
|
||||
@@ -42,18 +42,6 @@ data XFTPRcvChunkSpec = XFTPRcvChunkSpec
|
||||
supportedFileServerVRange :: VersionRange
|
||||
supportedFileServerVRange = mkVersionRange 1 1
|
||||
|
||||
fileBlockSize :: Int
|
||||
fileBlockSize = 16 * 1024
|
||||
|
||||
sendFile :: Handle -> (Builder -> IO ()) -> Word32 -> IO ()
|
||||
sendFile h send = go
|
||||
where
|
||||
go 0 = pure ()
|
||||
go sz =
|
||||
getFileChunk h sz >>= \ch -> do
|
||||
send $ byteString ch
|
||||
go $ sz - fromIntegral (B.length ch)
|
||||
|
||||
sendEncFile :: Handle -> (Builder -> IO ()) -> LC.SbState -> Word32 -> IO ()
|
||||
sendEncFile h send = go
|
||||
where
|
||||
@@ -66,46 +54,42 @@ sendEncFile h send = go
|
||||
send (byteString encCh) `E.catch` \(e :: E.SomeException) -> print e >> E.throwIO e
|
||||
go sbState' $ sz - fromIntegral (B.length ch)
|
||||
|
||||
getFileChunk :: Handle -> Word32 -> IO ByteString
|
||||
getFileChunk h sz =
|
||||
B.hGet h fileBlockSize >>= \case
|
||||
"" -> ioe_EOF
|
||||
ch -> pure $ B.take (fromIntegral sz) ch -- sz >= xftpBlockSize
|
||||
|
||||
receiveFile :: (Int -> IO ByteString) -> XFTPRcvChunkSpec -> ExceptT XFTPErrorType IO ()
|
||||
receiveFile getBody = receiveFile_ receive
|
||||
where
|
||||
receive h sz = do
|
||||
ch <- getBody fileBlockSize
|
||||
let chSize = fromIntegral $ B.length ch
|
||||
if
|
||||
| chSize > sz -> pure $ Left SIZE
|
||||
| chSize > 0 -> B.hPut h ch >> receive h (sz - chSize)
|
||||
| sz == 0 -> pure $ Right ()
|
||||
| otherwise -> pure $ Left SIZE
|
||||
receive h sz = hReceiveFile getBody h sz >>= \sz' -> pure $ if sz' == 0 then Right () else Left SIZE
|
||||
|
||||
receiveEncFile :: (Int -> IO ByteString) -> LC.SbState -> XFTPRcvChunkSpec -> ExceptT XFTPErrorType IO ()
|
||||
receiveEncFile getBody = receiveFile_ . receive
|
||||
where
|
||||
receive sbState h sz = do
|
||||
receive sbState h sz = first err <$> receiveSbFile getBody h sbState sz
|
||||
err RFESize = SIZE
|
||||
err RFECrypto = CRYPTO
|
||||
|
||||
data ReceiveFileError = RFESize | RFECrypto
|
||||
|
||||
receiveSbFile :: (Int -> IO ByteString) -> Handle -> LC.SbState -> Word32 -> IO (Either ReceiveFileError ())
|
||||
receiveSbFile getBody h = receive
|
||||
where
|
||||
receive sbState sz = do
|
||||
ch <- getBody fileBlockSize
|
||||
let chSize = fromIntegral $ B.length ch
|
||||
if
|
||||
| chSize > sz + authSz -> pure $ Left SIZE
|
||||
| chSize > 0 -> do
|
||||
| chSize > sz + authSz -> pure $ Left RFESize
|
||||
| chSize > 0 -> do
|
||||
let (ch', rest) = B.splitAt (fromIntegral sz) ch
|
||||
(decCh, sbState') = LC.sbDecryptChunk sbState ch'
|
||||
sz' = sz - fromIntegral (B.length ch')
|
||||
B.hPut h decCh
|
||||
if sz' > 0
|
||||
then receive sbState' h sz'
|
||||
then receive sbState' sz'
|
||||
else do
|
||||
let tag' = B.take C.authTagSize rest
|
||||
tagSz = B.length tag'
|
||||
tag = LC.sbAuth sbState'
|
||||
tag'' <- if tagSz == C.authTagSize then pure tag' else (tag' <>) <$> getBody (C.authTagSize - tagSz)
|
||||
pure $ if BA.constEq tag'' tag then Right () else Left CRYPTO
|
||||
| otherwise -> pure $ Left SIZE
|
||||
pure $ if BA.constEq tag'' tag then Right () else Left RFECrypto
|
||||
| otherwise -> pure $ Left RFESize
|
||||
authSz = fromIntegral C.authTagSize
|
||||
|
||||
receiveFile_ :: (Handle -> Word32 -> IO (Either XFTPErrorType ())) -> XFTPRcvChunkSpec -> ExceptT XFTPErrorType IO ()
|
||||
|
||||
+301
-193
@@ -8,13 +8,11 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedRecordDot #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
|
||||
-- |
|
||||
@@ -43,6 +41,7 @@ module Simplex.Messaging.Agent
|
||||
disconnectAgentClient,
|
||||
resumeAgentClient,
|
||||
withConnLock,
|
||||
withInvLock,
|
||||
createUser,
|
||||
deleteUser,
|
||||
createConnectionAsync,
|
||||
@@ -65,6 +64,7 @@ module Simplex.Messaging.Agent
|
||||
resubscribeConnection,
|
||||
resubscribeConnections,
|
||||
sendMessage,
|
||||
sendMessages,
|
||||
ackMessage,
|
||||
switchConnection,
|
||||
abortConnectionSwitch,
|
||||
@@ -93,6 +93,10 @@ module Simplex.Messaging.Agent
|
||||
xftpSendFile,
|
||||
xftpDeleteSndFileInternal,
|
||||
xftpDeleteSndFileRemote,
|
||||
rcNewHostPairing,
|
||||
rcConnectHost,
|
||||
rcConnectCtrl,
|
||||
rcDiscoverCtrl,
|
||||
foregroundAgent,
|
||||
suspendAgent,
|
||||
execAgentStoreSQL,
|
||||
@@ -111,22 +115,27 @@ import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift (MonadUnliftIO)
|
||||
import Control.Monad.Reader
|
||||
import Crypto.Random (MonadRandom)
|
||||
import qualified Data.Aeson as J
|
||||
import Data.Bifunctor (bimap, first, second)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Composition ((.:), (.:.), (.::))
|
||||
import Data.Foldable (foldl')
|
||||
import Data.Composition ((.:), (.:.), (.::), (.::.))
|
||||
import Data.Either (rights)
|
||||
import Data.Foldable (foldl', toList)
|
||||
import Data.Functor (($>))
|
||||
import Data.Functor.Identity
|
||||
import Data.List (find)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing)
|
||||
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, mapMaybe)
|
||||
import qualified Data.Set as S
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock
|
||||
import Data.Time.Clock.System (systemToUTCTime)
|
||||
import Data.Word (Word16)
|
||||
import Simplex.FileTransfer.Agent (closeXFTPAgent, deleteSndFileInternal, deleteSndFileRemote, startXFTPWorkers, toFSFilePath, xftpDeleteRcvFile', xftpReceiveFile', xftpSendFile')
|
||||
import Simplex.FileTransfer.Description (ValidFileDescription)
|
||||
import Simplex.FileTransfer.Protocol (FileParty (..))
|
||||
@@ -151,11 +160,14 @@ import Simplex.Messaging.Notifications.Protocol (DeviceToken, NtfRegCode (NtfReg
|
||||
import Simplex.Messaging.Notifications.Server.Push.APNS (PNMessageData (..))
|
||||
import Simplex.Messaging.Notifications.Types
|
||||
import Simplex.Messaging.Parsers (parse)
|
||||
import Simplex.Messaging.Protocol (BrokerMsg, EntityId, ErrorType (AUTH), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI (..), SubscriptionMode (..), SMPMsgMeta, SProtocolType (..), SndPublicVerifyKey, UserProtocol, XFTPServerWithAuth)
|
||||
import Simplex.Messaging.Protocol (BrokerMsg, EntityId, ErrorType (AUTH), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI (..), SMPMsgMeta, SProtocolType (..), SndPublicVerifyKey, SubscriptionMode (..), UserProtocol, XFTPServerWithAuth)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Util
|
||||
import Simplex.Messaging.Version
|
||||
import Simplex.RemoteControl.Client
|
||||
import Simplex.RemoteControl.Invitation
|
||||
import Simplex.RemoteControl.Types
|
||||
import UnliftIO.Async (async, race_)
|
||||
import UnliftIO.Concurrent (forkFinally, forkIO, threadDelay)
|
||||
import UnliftIO.STM
|
||||
@@ -269,6 +281,12 @@ resubscribeConnections c = withAgentEnv c . resubscribeConnections' c
|
||||
sendMessage :: AgentErrorMonad m => AgentClient -> ConnId -> MsgFlags -> MsgBody -> m AgentMsgId
|
||||
sendMessage c = withAgentEnv c .:. sendMessage' c
|
||||
|
||||
type MsgReq = (ConnId, MsgFlags, MsgBody)
|
||||
|
||||
-- | Send multiple messages to different connections (SEND command)
|
||||
sendMessages :: MonadUnliftIO m => AgentClient -> [MsgReq] -> m [Either AgentErrorType AgentMsgId]
|
||||
sendMessages c = withAgentEnv c . sendMessages' c
|
||||
|
||||
ackMessage :: AgentErrorMonad m => AgentClient -> ConnId -> AgentMsgId -> Maybe MsgReceiptInfo -> m ()
|
||||
ackMessage c = withAgentEnv c .:. ackMessage' c
|
||||
|
||||
@@ -379,6 +397,37 @@ xftpDeleteSndFileInternal c = withAgentEnv c . deleteSndFileInternal c
|
||||
xftpDeleteSndFileRemote :: AgentErrorMonad m => AgentClient -> UserId -> SndFileId -> ValidFileDescription 'FSender -> m ()
|
||||
xftpDeleteSndFileRemote c = withAgentEnv c .:. deleteSndFileRemote c
|
||||
|
||||
-- | Create new remote host pairing
|
||||
rcNewHostPairing :: MonadIO m => m RCHostPairing
|
||||
rcNewHostPairing = liftIO newRCHostPairing
|
||||
|
||||
-- | start TLS server for remote host with optional multicast
|
||||
rcConnectHost :: AgentErrorMonad m => AgentClient -> RCHostPairing -> J.Value -> Bool -> Maybe RCCtrlAddress -> Maybe Word16 -> m RCHostConnection
|
||||
rcConnectHost c = withAgentEnv c .::. rcConnectHost'
|
||||
|
||||
rcConnectHost' :: AgentMonad m => RCHostPairing -> J.Value -> Bool -> Maybe RCCtrlAddress -> Maybe Word16 -> m RCHostConnection
|
||||
rcConnectHost' pairing ctrlAppInfo multicast rcAddr_ port_ = do
|
||||
drg <- asks random
|
||||
liftError RCP $ connectRCHost drg pairing ctrlAppInfo multicast rcAddr_ port_
|
||||
|
||||
-- | connect to remote controller via URI
|
||||
rcConnectCtrl :: AgentErrorMonad m => AgentClient -> RCVerifiedInvitation -> Maybe RCCtrlPairing -> J.Value -> m RCCtrlConnection
|
||||
rcConnectCtrl c = withAgentEnv c .:. rcConnectCtrl'
|
||||
|
||||
rcConnectCtrl' :: AgentMonad m => RCVerifiedInvitation -> Maybe RCCtrlPairing -> J.Value -> m RCCtrlConnection
|
||||
rcConnectCtrl' verifiedInv pairing_ hostAppInfo = do
|
||||
drg <- asks random
|
||||
liftError RCP $ connectRCCtrl drg verifiedInv pairing_ hostAppInfo
|
||||
|
||||
-- | connect to known remote controller via multicast
|
||||
rcDiscoverCtrl :: AgentErrorMonad m => AgentClient -> NonEmpty RCCtrlPairing -> m (RCCtrlPairing, RCVerifiedInvitation)
|
||||
rcDiscoverCtrl c = withAgentEnv c . rcDiscoverCtrl'
|
||||
|
||||
rcDiscoverCtrl' :: AgentMonad m => NonEmpty RCCtrlPairing -> m (RCCtrlPairing, RCVerifiedInvitation)
|
||||
rcDiscoverCtrl' pairings = do
|
||||
subs <- asks multicastSubscribers
|
||||
liftError RCP $ discoverRCCtrl subs pairings
|
||||
|
||||
-- | Activate operations
|
||||
foregroundAgent :: MonadUnliftIO m => AgentClient -> m ()
|
||||
foregroundAgent c = withAgentEnv c $ foregroundAgent' c
|
||||
@@ -462,8 +511,8 @@ deleteUser' c userId delSMPQueues = do
|
||||
atomically $ TM.delete userId $ smpServers c
|
||||
where
|
||||
delUser =
|
||||
whenM (withStore' c (`deleteUserWithoutConns` userId)) $
|
||||
atomically $ writeTBQueue (subQ c) ("", "", APC SAENone $ DEL_USER userId)
|
||||
whenM (withStore' c (`deleteUserWithoutConns` userId)) . atomically $
|
||||
writeTBQueue (subQ c) ("", "", APC SAENone $ DEL_USER userId)
|
||||
|
||||
newConnAsync :: forall m c. (AgentMonad m, ConnectionModeI c) => AgentClient -> UserId -> ACorrId -> Bool -> SConnectionMode c -> SubscriptionMode -> m ConnId
|
||||
newConnAsync c userId corrId enableNtfs cMode subMode = do
|
||||
@@ -473,7 +522,7 @@ newConnAsync c userId corrId enableNtfs cMode subMode = do
|
||||
|
||||
newConnNoQueues :: AgentMonad m => AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> m ConnId
|
||||
newConnNoQueues c userId connId enableNtfs cMode = do
|
||||
g <- asks idsDrg
|
||||
g <- asks random
|
||||
connAgentVersion <- asks $ maxVersion . smpAgentVRange . config
|
||||
-- connection mode is determined by the accepting agent
|
||||
let cData = ConnData {userId, connId, connAgentVersion, enableNtfs, duplexHandshake = Nothing, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk}
|
||||
@@ -481,16 +530,17 @@ newConnNoQueues c userId connId enableNtfs cMode = do
|
||||
|
||||
joinConnAsync :: AgentMonad m => AgentClient -> UserId -> ACorrId -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> m ConnId
|
||||
joinConnAsync c userId corrId enableNtfs cReqUri@(CRInvitationUri ConnReqUriData {crAgentVRange} _) cInfo subMode = do
|
||||
aVRange <- asks $ smpAgentVRange . config
|
||||
case crAgentVRange `compatibleVersion` aVRange of
|
||||
Just (Compatible connAgentVersion) -> do
|
||||
g <- asks idsDrg
|
||||
let duplexHS = connAgentVersion /= 1
|
||||
cData = ConnData {userId, connId = "", connAgentVersion, enableNtfs, duplexHandshake = Just duplexHS, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk}
|
||||
connId <- withStore c $ \db -> createNewConn db g cData SCMInvitation
|
||||
enqueueCommand c corrId connId Nothing $ AClientCommand $ APC SAEConn $ JOIN enableNtfs (ACR sConnectionMode cReqUri) subMode cInfo
|
||||
pure connId
|
||||
_ -> throwError $ AGENT A_VERSION
|
||||
withInvLock c (strEncode cReqUri) "joinConnAsync" $ do
|
||||
aVRange <- asks $ smpAgentVRange . config
|
||||
case crAgentVRange `compatibleVersion` aVRange of
|
||||
Just (Compatible connAgentVersion) -> do
|
||||
g <- asks random
|
||||
let duplexHS = connAgentVersion /= 1
|
||||
cData = ConnData {userId, connId = "", connAgentVersion, enableNtfs, duplexHandshake = Just duplexHS, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk}
|
||||
connId <- withStore c $ \db -> createNewConn db g cData SCMInvitation
|
||||
enqueueCommand c corrId connId Nothing $ AClientCommand $ APC SAEConn $ JOIN enableNtfs (ACR sConnectionMode cReqUri) subMode cInfo
|
||||
pure connId
|
||||
_ -> throwError $ AGENT A_VERSION
|
||||
joinConnAsync _c _userId _corrId _enableNtfs (CRContactUri _) _subMode _cInfo =
|
||||
throwError $ CMD PROHIBITED
|
||||
|
||||
@@ -554,11 +604,11 @@ switchConnectionAsync' c corrId connId =
|
||||
SomeConn _ (DuplexConnection cData rqs@(rq :| _rqs) sqs)
|
||||
| isJust (switchingRQ rqs) -> throwError $ CMD PROHIBITED
|
||||
| otherwise -> do
|
||||
when (ratchetSyncSendProhibited cData) $ throwError $ CMD PROHIBITED
|
||||
rq1 <- withStore' c $ \db -> setRcvSwitchStatus db rq $ Just RSSwitchStarted
|
||||
enqueueCommand c corrId connId Nothing $ AClientCommand $ APC SAEConn SWCH
|
||||
let rqs' = updatedQs rq1 rqs
|
||||
pure . connectionStats $ DuplexConnection cData rqs' sqs
|
||||
when (ratchetSyncSendProhibited cData) $ throwError $ CMD PROHIBITED
|
||||
rq1 <- withStore' c $ \db -> setRcvSwitchStatus db rq $ Just RSSwitchStarted
|
||||
enqueueCommand c corrId connId Nothing $ AClientCommand $ APC SAEConn SWCH
|
||||
let rqs' = updatedQs rq1 rqs
|
||||
pure . connectionStats $ DuplexConnection cData rqs' sqs
|
||||
_ -> throwError $ CMD PROHIBITED
|
||||
|
||||
newConn :: AgentMonad m => AgentClient -> UserId -> ConnId -> Bool -> SConnectionMode c -> Maybe CRClientData -> SubscriptionMode -> m (ConnId, ConnectionRequestUri c)
|
||||
@@ -581,7 +631,7 @@ newRcvConnSrv c userId connId enableNtfs cMode clientData subMode srv = do
|
||||
when enableNtfs $ do
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ sendNtfSubCommand ns (connId, NSCCreate)
|
||||
let crData = ConnReqUriData simplexChat smpAgentVRange [qUri] clientData
|
||||
let crData = ConnReqUriData CRSSimplex smpAgentVRange [qUri] clientData
|
||||
case cMode of
|
||||
SCMContact -> pure (connId, CRContactUri crData)
|
||||
SCMInvitation -> do
|
||||
@@ -615,24 +665,25 @@ startJoinInvitation userId connId enableNtfs (CRInvitationUri ConnReqUriData {cr
|
||||
_ -> throwError $ AGENT A_VERSION
|
||||
|
||||
joinConnSrv :: AgentMonad m => AgentClient -> UserId -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> SMPServerWithAuth -> m ConnId
|
||||
joinConnSrv c userId connId enableNtfs inv@CRInvitationUri {} cInfo subMode srv = do
|
||||
(aVersion, cData@ConnData {connAgentVersion}, q, rc, e2eSndParams) <- startJoinInvitation userId connId enableNtfs inv
|
||||
g <- asks idsDrg
|
||||
connId' <- withStore c $ \db -> runExceptT $ do
|
||||
connId' <- ExceptT $ createSndConn db g cData q
|
||||
liftIO $ createRatchet db connId' rc
|
||||
pure connId'
|
||||
let sq = (q :: SndQueue) {connId = connId'}
|
||||
cData' = (cData :: ConnData) {connId = connId'}
|
||||
duplexHS = connAgentVersion /= 1
|
||||
tryError (confirmQueue aVersion c cData' sq srv cInfo (Just e2eSndParams) subMode) >>= \case
|
||||
Right _ -> do
|
||||
unless duplexHS . void $ enqueueMessage c cData' sq SMP.noMsgFlags HELLO
|
||||
joinConnSrv c userId connId enableNtfs inv@CRInvitationUri {} cInfo subMode srv =
|
||||
withInvLock c (strEncode inv) "joinConnSrv" $ do
|
||||
(aVersion, cData@ConnData {connAgentVersion}, q, rc, e2eSndParams) <- startJoinInvitation userId connId enableNtfs inv
|
||||
g <- asks random
|
||||
connId' <- withStore c $ \db -> runExceptT $ do
|
||||
connId' <- ExceptT $ createSndConn db g cData q
|
||||
liftIO $ createRatchet db connId' rc
|
||||
pure connId'
|
||||
Left e -> do
|
||||
-- possible improvement: recovery for failure on network timeout, see rfcs/2022-04-20-smp-conf-timeout-recovery.md
|
||||
withStore' c (`deleteConn` connId')
|
||||
throwError e
|
||||
let sq = (q :: SndQueue) {connId = connId'}
|
||||
cData' = (cData :: ConnData) {connId = connId'}
|
||||
duplexHS = connAgentVersion /= 1
|
||||
tryError (confirmQueue aVersion c cData' sq srv cInfo (Just e2eSndParams) subMode) >>= \case
|
||||
Right _ -> do
|
||||
unless duplexHS . void $ enqueueMessage c cData' sq SMP.noMsgFlags HELLO
|
||||
pure connId'
|
||||
Left e -> do
|
||||
-- possible improvement: recovery for failure on network timeout, see rfcs/2022-04-20-smp-conf-timeout-recovery.md
|
||||
withStore' c (`deleteConn` connId')
|
||||
throwError e
|
||||
joinConnSrv c userId connId enableNtfs (CRContactUri ConnReqUriData {crAgentVRange, crSmpQueues = (qUri :| _)}) cInfo subMode srv = do
|
||||
aVRange <- asks $ smpAgentVRange . config
|
||||
clientVRange <- asks $ smpClientVRange . config
|
||||
@@ -808,37 +859,47 @@ getNotificationMessage' c nonce encNtfInfo = do
|
||||
(ntfConnId, rcvNtfDhSecret) <- withStore c (`getNtfRcvQueue` smpQueue)
|
||||
ntfMsgMeta <- (eitherToMaybe . smpDecode <$> agentCbDecrypt rcvNtfDhSecret nmsgNonce encNMsgMeta) `catchAgentError` \_ -> pure Nothing
|
||||
maxMsgs <- asks $ ntfMaxMessages . config
|
||||
(NotificationInfo {ntfConnId, ntfTs, ntfMsgMeta},) <$> getNtfMessages ntfConnId maxMsgs ntfMsgMeta []
|
||||
(NotificationInfo {ntfConnId, ntfTs, ntfMsgMeta},) <$> getNtfMessages ntfConnId ntfMsgMeta maxMsgs
|
||||
_ -> throwError $ CMD PROHIBITED
|
||||
where
|
||||
getNtfMessages ntfConnId maxMs nMeta ms
|
||||
| length ms < maxMs =
|
||||
getConnectionMessage' c ntfConnId >>= \case
|
||||
Just m@SMP.SMPMsgMeta {msgId, msgTs, msgFlags} -> case nMeta of
|
||||
Just SMP.NMsgMeta {msgId = msgId', msgTs = msgTs'}
|
||||
| msgId == msgId' || msgTs > msgTs' -> pure $ reverse (m : ms)
|
||||
| otherwise -> getMsg (m : ms)
|
||||
_
|
||||
| SMP.notification msgFlags -> pure $ reverse (m : ms)
|
||||
| otherwise -> getMsg (m : ms)
|
||||
_ -> pure $ reverse ms
|
||||
| otherwise = pure $ reverse ms
|
||||
getNtfMessages ntfConnId nMeta = getMsg
|
||||
where
|
||||
getMsg = getNtfMessages ntfConnId maxMs nMeta
|
||||
getMsg 0 = pure []
|
||||
getMsg n =
|
||||
getConnectionMessage' c ntfConnId >>= \case
|
||||
Just m
|
||||
| lastMsg m -> pure [m]
|
||||
| otherwise -> (m :) <$> getMsg (n - 1)
|
||||
Nothing -> pure []
|
||||
lastMsg SMP.SMPMsgMeta {msgId, msgTs, msgFlags} = case nMeta of
|
||||
Just SMP.NMsgMeta {msgId = msgId', msgTs = msgTs'} -> msgId == msgId' || msgTs > msgTs'
|
||||
Nothing -> SMP.notification msgFlags
|
||||
|
||||
-- | Send message to the connection (SEND command) in Reader monad
|
||||
sendMessage' :: forall m. AgentMonad m => AgentClient -> ConnId -> MsgFlags -> MsgBody -> m AgentMsgId
|
||||
sendMessage' c connId msgFlags msg = withConnLock c connId "sendMessage" $ do
|
||||
SomeConn _ conn <- withStore c (`getConn` connId)
|
||||
case conn of
|
||||
DuplexConnection cData _ sqs -> enqueueMsgs cData sqs
|
||||
SndConnection cData sq -> enqueueMsgs cData [sq]
|
||||
_ -> throwError $ CONN SIMPLEX
|
||||
sendMessage' c connId msgFlags msg = liftEither . runIdentity =<< sendMessagesB c (Identity (Right (connId, msgFlags, msg)))
|
||||
|
||||
-- | Send multiple messages to different connections (SEND command) in Reader monad
|
||||
sendMessages' :: forall m. AgentMonad' m => AgentClient -> [MsgReq] -> m [Either AgentErrorType AgentMsgId]
|
||||
sendMessages' c = sendMessagesB c . map Right
|
||||
|
||||
sendMessagesB :: forall m t. (AgentMonad' m, Traversable t) => AgentClient -> t (Either AgentErrorType MsgReq) -> m (t (Either AgentErrorType AgentMsgId))
|
||||
sendMessagesB c reqs = withConnLocks c connIds "sendMessages" $ do
|
||||
reqs' <- withStoreBatch c (\db -> fmap (mapE $ \req@(connId, _, _) -> bimap storeError (req,) <$> getConn db connId) reqs)
|
||||
let reqs'' = fmap (>>= prepareConn) reqs'
|
||||
enqueueMessagesB c reqs''
|
||||
where
|
||||
enqueueMsgs :: ConnData -> NonEmpty SndQueue -> m AgentMsgId
|
||||
enqueueMsgs cData sqs = do
|
||||
when (ratchetSyncSendProhibited cData) $ throwError $ CMD PROHIBITED
|
||||
enqueueMessages c cData sqs msgFlags $ A_MSG msg
|
||||
prepareConn :: (MsgReq, SomeConn) -> Either AgentErrorType (ConnData, NonEmpty SndQueue, MsgFlags, AMessage)
|
||||
prepareConn ((_, msgFlags, msg), SomeConn _ conn) = case conn of
|
||||
DuplexConnection cData _ sqs -> prepareMsgs cData sqs
|
||||
SndConnection cData sq -> prepareMsgs cData [sq]
|
||||
_ -> Left $ CONN SIMPLEX
|
||||
where
|
||||
prepareMsgs :: ConnData -> NonEmpty SndQueue -> Either AgentErrorType (ConnData, NonEmpty SndQueue, MsgFlags, AMessage)
|
||||
prepareMsgs cData sqs
|
||||
| ratchetSyncSendProhibited cData = Left $ CMD PROHIBITED
|
||||
| otherwise = Right (cData, sqs, msgFlags, A_MSG msg)
|
||||
connIds = foldl' (\cs -> either (\_ -> cs) (\(connId, _, _) -> S.insert connId cs)) S.empty reqs
|
||||
|
||||
-- / async command processing v v v
|
||||
|
||||
@@ -944,7 +1005,7 @@ runCommandProcessing c@AgentClient {subQ} server_ = do
|
||||
withServer $ \srv -> tryWithLock "ICQSecure" . withDuplexConn $ \(DuplexConnection cData rqs sqs) ->
|
||||
case find (sameQueue (srv, rId)) rqs of
|
||||
Just rq'@RcvQueue {server, sndId, status, dbReplaceQueueId = Just replaceQId} ->
|
||||
case find (\q -> replaceQId == q.dbQueueId) rqs of
|
||||
case find ((replaceQId ==) . dbQId) rqs of
|
||||
Just rq1 -> when (status == Confirmed) $ do
|
||||
secureQueue c rq' senderKey
|
||||
withStore' c $ \db -> setRcvQueueStatus db rq' Secured
|
||||
@@ -962,12 +1023,12 @@ runCommandProcessing c@AgentClient {subQ} server_ = do
|
||||
Just (rq'@RcvQueue {primary}, rq'' : rqs')
|
||||
| primary -> internalErr "ICQDelete: cannot delete primary rcv queue"
|
||||
| otherwise -> do
|
||||
checkRQSwchStatus rq' RSReceivedMessage
|
||||
tryError (deleteQueue c rq') >>= \case
|
||||
Right () -> finalizeSwitch
|
||||
Left e
|
||||
| temporaryOrHostError e -> throwError e
|
||||
| otherwise -> finalizeSwitch >> throwError e
|
||||
checkRQSwchStatus rq' RSReceivedMessage
|
||||
tryError (deleteQueue c rq') >>= \case
|
||||
Right () -> finalizeSwitch
|
||||
Left e
|
||||
| temporaryOrHostError e -> throwError e
|
||||
| otherwise -> finalizeSwitch >> throwError e
|
||||
where
|
||||
finalizeSwitch = do
|
||||
withStore' c $ \db -> deleteConnRcvQueue db rq'
|
||||
@@ -1017,22 +1078,37 @@ enqueueMessages c cData sqs msgFlags aMessage = do
|
||||
enqueueMessages' c cData sqs msgFlags aMessage
|
||||
|
||||
enqueueMessages' :: AgentMonad m => AgentClient -> ConnData -> NonEmpty SndQueue -> MsgFlags -> AMessage -> m AgentMsgId
|
||||
enqueueMessages' c cData (sq :| sqs) msgFlags aMessage = do
|
||||
msgId <- enqueueMessage c cData sq msgFlags aMessage
|
||||
mapM_ (enqueueSavedMessage c cData msgId) $
|
||||
filter (\SndQueue {status} -> status == Secured || status == Active) sqs
|
||||
pure msgId
|
||||
enqueueMessages' c cData sqs msgFlags aMessage =
|
||||
liftEither . runIdentity =<< enqueueMessagesB c (Identity (Right (cData, sqs, msgFlags, aMessage)))
|
||||
|
||||
enqueueMessagesB :: (AgentMonad' m, Traversable t) => AgentClient -> t (Either AgentErrorType (ConnData, NonEmpty SndQueue, MsgFlags, AMessage)) -> m (t (Either AgentErrorType AgentMsgId))
|
||||
enqueueMessagesB c reqs = do
|
||||
reqs' <- enqueueMessageB c reqs
|
||||
enqueueSavedMessageB c $ mapMaybe snd $ rights $ toList reqs'
|
||||
pure $ fst <$$> reqs'
|
||||
|
||||
isActiveSndQ :: SndQueue -> Bool
|
||||
isActiveSndQ SndQueue {status} = status == Secured || status == Active
|
||||
|
||||
enqueueMessage :: forall m. AgentMonad m => AgentClient -> ConnData -> SndQueue -> MsgFlags -> AMessage -> m AgentMsgId
|
||||
enqueueMessage c cData@ConnData {connId} sq msgFlags aMessage = do
|
||||
resumeMsgDelivery c cData sq
|
||||
aVRange <- asks $ smpAgentVRange . config
|
||||
msgId <- storeSentMsg $ maxVersion aVRange
|
||||
queuePendingMsgs c sq [msgId]
|
||||
pure $ unId msgId
|
||||
enqueueMessage c cData sq msgFlags aMessage =
|
||||
liftEither . fmap fst . runIdentity =<< enqueueMessageB c (Identity (Right (cData, [sq], msgFlags, aMessage)))
|
||||
|
||||
-- this function is used only for sending messages in batch, it returns the list of successes to enqueue additional deliveries
|
||||
enqueueMessageB :: forall m t. (AgentMonad' m, Traversable t) => AgentClient -> t (Either AgentErrorType (ConnData, NonEmpty SndQueue, MsgFlags, AMessage)) -> m (t (Either AgentErrorType (AgentMsgId, Maybe (ConnData, [SndQueue], AgentMsgId))))
|
||||
enqueueMessageB c reqs = do
|
||||
void . forME reqs $ \(cData, sq :| _, _, _) ->
|
||||
runExceptT $ resumeMsgDelivery c cData sq
|
||||
aVRange <- asks $ maxVersion . smpAgentVRange . config
|
||||
reqMids <- withStoreBatch c $ \db -> fmap (mapE $ storeSentMsg db aVRange) reqs
|
||||
forME reqMids $ \((cData, sq :| sqs, _, _), mId) -> do
|
||||
let InternalId msgId = mId
|
||||
queuePendingMsgs c sq [mId]
|
||||
let sqs' = filter isActiveSndQ sqs
|
||||
pure $ Right (msgId, if null sqs' then Nothing else Just (cData, sqs', msgId))
|
||||
where
|
||||
storeSentMsg :: Version -> m InternalId
|
||||
storeSentMsg agentVersion = withStore c $ \db -> runExceptT $ do
|
||||
storeSentMsg :: DB.Connection -> Version -> (ConnData, NonEmpty SndQueue, MsgFlags, AMessage) -> IO (Either AgentErrorType ((ConnData, NonEmpty SndQueue, MsgFlags, AMessage), InternalId))
|
||||
storeSentMsg db agentVersion req@(ConnData {connId}, sq :| _, msgFlags, aMessage) = fmap (first storeError) $ runExceptT $ do
|
||||
internalTs <- liftIO getCurrentTime
|
||||
(internalId, internalSndId, prevMsgHash) <- liftIO $ updateSndIds db connId
|
||||
let privHeader = APrivHeader (unSndId internalSndId) prevMsgHash
|
||||
@@ -1045,14 +1121,25 @@ enqueueMessage c cData@ConnData {connId} sq msgFlags aMessage = do
|
||||
msgData = SndMsgData {internalId, internalSndId, internalTs, msgType, msgFlags, msgBody, internalHash, prevMsgHash}
|
||||
liftIO $ createSndMsg db connId msgData
|
||||
liftIO $ createSndMsgDelivery db connId sq internalId
|
||||
pure internalId
|
||||
pure (req, internalId)
|
||||
|
||||
enqueueSavedMessage :: AgentMonad m => AgentClient -> ConnData -> AgentMsgId -> SndQueue -> m ()
|
||||
enqueueSavedMessage c cData@ConnData {connId} msgId sq = do
|
||||
resumeMsgDelivery c cData sq
|
||||
let mId = InternalId msgId
|
||||
queuePendingMsgs c sq [mId]
|
||||
withStore' c $ \db -> createSndMsgDelivery db connId sq mId
|
||||
enqueueSavedMessage :: AgentMonad' m => AgentClient -> ConnData -> AgentMsgId -> SndQueue -> m ()
|
||||
enqueueSavedMessage c cData msgId sq = enqueueSavedMessageB c $ Identity (cData, [sq], msgId)
|
||||
|
||||
enqueueSavedMessageB :: (AgentMonad' m, Foldable t) => AgentClient -> t (ConnData, [SndQueue], AgentMsgId) -> m ()
|
||||
enqueueSavedMessageB c reqs = do
|
||||
-- saving to the database has to be before enqueue to avoid race condition when delivery is read from queue before it is saved
|
||||
void $ withStoreBatch' c $ \db -> concatMap (storeDeliveries db) reqs
|
||||
forM_ reqs $ \(cData, sqs, msgId) ->
|
||||
forM sqs $ \sq -> do
|
||||
void . runExceptT $ resumeMsgDelivery c cData sq
|
||||
let mId = InternalId msgId
|
||||
queuePendingMsgs c sq [mId]
|
||||
where
|
||||
storeDeliveries :: DB.Connection -> (ConnData, [SndQueue], AgentMsgId) -> [IO ()]
|
||||
storeDeliveries db (ConnData {connId}, sqs, msgId) = do
|
||||
let mId = InternalId msgId
|
||||
in map (\sq -> createSndMsgDelivery db connId sq mId) sqs
|
||||
|
||||
resumeMsgDelivery :: forall m. AgentMonad m => AgentClient -> ConnData -> SndQueue -> m ()
|
||||
resumeMsgDelivery c cData@ConnData {connId} sq@SndQueue {server, sndId} = do
|
||||
@@ -1062,20 +1149,23 @@ resumeMsgDelivery c cData@ConnData {connId} sq@SndQueue {server, sndId} = do
|
||||
>>= \a -> atomically (TM.insert qKey a $ smpQueueMsgDeliveries c)
|
||||
unlessM msgsQueued $
|
||||
withStore' c (\db -> getPendingMsgs db connId sq)
|
||||
>>= queuePendingMsgs c sq
|
||||
>>= queuePendingMsgs' c False sq
|
||||
where
|
||||
queueDelivering qKey = atomically $ TM.member qKey (smpQueueMsgDeliveries c)
|
||||
msgsQueued = atomically $ isJust <$> TM.lookupInsert (server, sndId) True (pendingMsgsQueued c)
|
||||
|
||||
queuePendingMsgs :: AgentMonad' m => AgentClient -> SndQueue -> [InternalId] -> m ()
|
||||
queuePendingMsgs c sq msgIds = atomically $ do
|
||||
modifyTVar' (msgDeliveryOp c) $ \s -> s {opsInProgress = opsInProgress s + length msgIds}
|
||||
queuePendingMsgs c = queuePendingMsgs' c True
|
||||
|
||||
queuePendingMsgs' :: AgentMonad' m => AgentClient -> Bool -> SndQueue -> [InternalId] -> m ()
|
||||
queuePendingMsgs' c setDeliveryOps sq msgIds = atomically $ do
|
||||
when setDeliveryOps $ modifyTVar' (msgDeliveryOp c) $ \s -> s {opsInProgress = opsInProgress s + length msgIds}
|
||||
-- s <- readTVar (msgDeliveryOp c)
|
||||
-- unsafeIOToSTM $ putStrLn $ "msgDeliveryOp: " <> show (opsInProgress s)
|
||||
(mq, _) <- getPendingMsgQ c sq
|
||||
mapM_ (writeTQueue mq) msgIds
|
||||
mapM_ (writeTQueue mq . (,setDeliveryOps)) msgIds
|
||||
|
||||
getPendingMsgQ :: AgentClient -> SndQueue -> STM (TQueue InternalId, TMVar ())
|
||||
getPendingMsgQ :: AgentClient -> SndQueue -> STM (TQueue (InternalId, Bool), TMVar ())
|
||||
getPendingMsgQ c SndQueue {server, sndId} = do
|
||||
let qKey = (server, sndId)
|
||||
maybe (newMsgQueue qKey) pure =<< TM.lookup qKey (smpQueueMsgQueues c)
|
||||
@@ -1093,9 +1183,9 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} cData@ConnData {userId, connId, dupl
|
||||
atomically $ endAgentOperation c AOSndNetwork
|
||||
atomically $ throwWhenInactive c
|
||||
atomically $ throwWhenNoDelivery c sq
|
||||
msgId <- atomically $ readTQueue mq
|
||||
(msgId, deliveryOp) <- atomically $ readTQueue mq
|
||||
atomically $ beginAgentOperation c AOSndNetwork
|
||||
atomically $ endAgentOperation c AOMsgDelivery -- this operation begins in queuePendingMsgs
|
||||
when deliveryOp $ atomically $ endAgentOperation c AOMsgDelivery -- this operation begins in queuePendingMsgs
|
||||
let mId = unId msgId
|
||||
tryAgentError (withStore c $ \db -> getPendingMsgData db connId msgId) >>= \case
|
||||
Left e -> notify $ MERR mId e
|
||||
@@ -1123,7 +1213,7 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} cData@ConnData {userId, connId, dupl
|
||||
-- because the queue must be secured by the time the confirmation or the first HELLO is received
|
||||
| duplexHandshake == Just True -> connErr
|
||||
| otherwise ->
|
||||
ifM (msgExpired helloTimeout) connErr (retrySndMsg RIFast)
|
||||
ifM (msgExpired helloTimeout) connErr (retrySndMsg RIFast)
|
||||
where
|
||||
connErr = case rq_ of
|
||||
-- party initiating connection
|
||||
@@ -1143,8 +1233,8 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} cData@ConnData {userId, connId, dupl
|
||||
-- for other operations BROKER HOST is treated as a permanent error (e.g., when connecting to the server),
|
||||
-- the message sending would be retried
|
||||
| temporaryOrHostError e -> do
|
||||
let timeoutSel = if msgType == AM_HELLO_ then helloTimeout else messageTimeout
|
||||
ifM (msgExpired timeoutSel) (notifyDel msgId err) (retrySndMsg RIFast)
|
||||
let timeoutSel = if msgType == AM_HELLO_ then helloTimeout else messageTimeout
|
||||
ifM (msgExpired timeoutSel) (notifyDel msgId err) (retrySndMsg RIFast)
|
||||
| otherwise -> notifyDel msgId err
|
||||
where
|
||||
msgExpired timeoutSel = do
|
||||
@@ -1201,7 +1291,7 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} cData@ConnData {userId, connId, dupl
|
||||
-- this is the same queue where this loop delivers messages to but with updated state
|
||||
Just SndQueue {dbReplaceQueueId = Just replacedId, primary} ->
|
||||
-- second part of this condition is a sanity check because dbReplaceQueueId cannot point to the same queue, see switchConnection'
|
||||
case removeQP (\sq' -> sq'.dbQueueId == replacedId && not (sameQueue addr sq')) sqs of
|
||||
case removeQP (\sq' -> dbQId sq' == replacedId && not (sameQueue addr sq')) sqs of
|
||||
Nothing -> internalErr msgId "sent QTEST: queue not found in connection"
|
||||
Just (sq', sq'' : sqs') -> do
|
||||
checkSQSwchStatus sq' SSSendingQTEST
|
||||
@@ -1286,9 +1376,9 @@ switchConnection' c connId =
|
||||
SomeConn _ conn@(DuplexConnection cData rqs@(rq :| _rqs) _)
|
||||
| isJust (switchingRQ rqs) -> throwError $ CMD PROHIBITED
|
||||
| otherwise -> do
|
||||
when (ratchetSyncSendProhibited cData) $ throwError $ CMD PROHIBITED
|
||||
rq' <- withStore' c $ \db -> setRcvSwitchStatus db rq $ Just RSSwitchStarted
|
||||
switchDuplexConnection c conn rq'
|
||||
when (ratchetSyncSendProhibited cData) $ throwError $ CMD PROHIBITED
|
||||
rq' <- withStore' c $ \db -> setRcvSwitchStatus db rq $ Just RSSwitchStarted
|
||||
switchDuplexConnection c conn rq'
|
||||
_ -> throwError $ CMD PROHIBITED
|
||||
|
||||
switchDuplexConnection :: AgentMonad m => AgentClient -> Connection 'CDuplex -> RcvQueue -> m ConnectionStats
|
||||
@@ -1314,19 +1404,19 @@ abortConnectionSwitch' c connId =
|
||||
SomeConn _ (DuplexConnection cData rqs sqs) -> case switchingRQ rqs of
|
||||
Just rq
|
||||
| canAbortRcvSwitch rq -> do
|
||||
when (ratchetSyncSendProhibited cData) $ throwError $ CMD PROHIBITED
|
||||
-- multiple queues to which the connections switches were possible when repeating switch was allowed
|
||||
let (delRqs, keepRqs) = L.partition (\q -> Just rq.dbQueueId == q.dbReplaceQueueId) rqs
|
||||
case L.nonEmpty keepRqs of
|
||||
Just rqs' -> do
|
||||
rq' <- withStore' c $ \db -> do
|
||||
mapM_ (setRcvQueueDeleted db) delRqs
|
||||
setRcvSwitchStatus db rq Nothing
|
||||
forM_ delRqs $ \RcvQueue {server, rcvId} -> enqueueCommand c "" connId (Just server) $ AInternalCommand $ ICDeleteRcvQueue rcvId
|
||||
let rqs'' = updatedQs rq' rqs'
|
||||
conn' = DuplexConnection cData rqs'' sqs
|
||||
pure $ connectionStats conn'
|
||||
_ -> throwError $ INTERNAL "won't delete all rcv queues in connection"
|
||||
when (ratchetSyncSendProhibited cData) $ throwError $ CMD PROHIBITED
|
||||
-- multiple queues to which the connections switches were possible when repeating switch was allowed
|
||||
let (delRqs, keepRqs) = L.partition ((Just (dbQId rq) ==) . dbReplaceQId) rqs
|
||||
case L.nonEmpty keepRqs of
|
||||
Just rqs' -> do
|
||||
rq' <- withStore' c $ \db -> do
|
||||
mapM_ (setRcvQueueDeleted db) delRqs
|
||||
setRcvSwitchStatus db rq Nothing
|
||||
forM_ delRqs $ \RcvQueue {server, rcvId} -> enqueueCommand c "" connId (Just server) $ AInternalCommand $ ICDeleteRcvQueue rcvId
|
||||
let rqs'' = updatedQs rq' rqs'
|
||||
conn' = DuplexConnection cData rqs'' sqs
|
||||
pure $ connectionStats conn'
|
||||
_ -> throwError $ INTERNAL "won't delete all rcv queues in connection"
|
||||
| otherwise -> throwError $ CMD PROHIBITED
|
||||
_ -> throwError $ CMD PROHIBITED
|
||||
_ -> throwError $ CMD PROHIBITED
|
||||
@@ -1336,16 +1426,16 @@ synchronizeRatchet' c connId force = withConnLock c connId "synchronizeRatchet"
|
||||
withStore c (`getConn` connId) >>= \case
|
||||
SomeConn _ (DuplexConnection cData rqs sqs)
|
||||
| ratchetSyncAllowed cData || force -> do
|
||||
-- check queues are not switching?
|
||||
AgentConfig {e2eEncryptVRange} <- asks config
|
||||
(pk1, pk2, e2eParams@(CR.E2ERatchetParams _ k1 k2)) <- liftIO . CR.generateE2EParams $ maxVersion e2eEncryptVRange
|
||||
void $ enqueueRatchetKeyMsgs c cData sqs e2eParams
|
||||
withStore' c $ \db -> do
|
||||
setConnRatchetSync db connId RSStarted
|
||||
setRatchetX3dhKeys db connId pk1 pk2 k1 k2
|
||||
let cData' = cData {ratchetSyncState = RSStarted} :: ConnData
|
||||
conn' = DuplexConnection cData' rqs sqs
|
||||
pure $ connectionStats conn'
|
||||
-- check queues are not switching?
|
||||
AgentConfig {e2eEncryptVRange} <- asks config
|
||||
(pk1, pk2, e2eParams@(CR.E2ERatchetParams _ k1 k2)) <- liftIO . CR.generateE2EParams $ maxVersion e2eEncryptVRange
|
||||
void $ enqueueRatchetKeyMsgs c cData sqs e2eParams
|
||||
withStore' c $ \db -> do
|
||||
setConnRatchetSync db connId RSStarted
|
||||
setRatchetX3dhKeys db connId pk1 pk2 k1 k2
|
||||
let cData' = cData {ratchetSyncState = RSStarted} :: ConnData
|
||||
conn' = DuplexConnection cData' rqs sqs
|
||||
pure $ connectionStats conn'
|
||||
| otherwise -> throwError $ CMD PROHIBITED
|
||||
_ -> throwError $ CMD PROHIBITED
|
||||
|
||||
@@ -1434,7 +1524,7 @@ deleteConnQueues c ntf rqs = do
|
||||
| temporaryOrHostError e && deleteErrors rq + 1 < maxErrs -> withStore' c (`incRcvDeleteErrors` rq) $> r
|
||||
| otherwise -> withStore' c (`deleteConnRcvQueue` rq) >> notifyRQ rq (Just e) $> Right ()
|
||||
pure (rq, r')
|
||||
notifyRQ rq e_ = notify ("", rq.connId, APC SAEConn $ DEL_RCVQ (qServer rq) (queueId rq) e_)
|
||||
notifyRQ rq e_ = notify ("", qConnId rq, APC SAEConn $ DEL_RCVQ (qServer rq) (queueId rq) e_)
|
||||
notify = when ntf . atomically . writeTBQueue (subQ c)
|
||||
connResults :: [(RcvQueue, Either AgentErrorType ())] -> Map ConnId (Either AgentErrorType ())
|
||||
connResults = M.map snd . foldl' addResult M.empty
|
||||
@@ -1521,23 +1611,23 @@ registerNtfToken' c suppliedDeviceToken suppliedNtfMode =
|
||||
-- possible improvement: add minimal time before repeat registration
|
||||
(Just tknId, Nothing)
|
||||
| savedDeviceToken == suppliedDeviceToken ->
|
||||
when (ntfTknStatus == NTRegistered) (registerToken tkn) $> NTRegistered
|
||||
when (ntfTknStatus == NTRegistered) (registerToken tkn) $> NTRegistered
|
||||
| otherwise -> replaceToken tknId
|
||||
(Just tknId, Just (NTAVerify code))
|
||||
| savedDeviceToken == suppliedDeviceToken ->
|
||||
t tkn (NTActive, Just NTACheck) $ agentNtfVerifyToken c tknId tkn code
|
||||
t tkn (NTActive, Just NTACheck) $ agentNtfVerifyToken c tknId tkn code
|
||||
| otherwise -> replaceToken tknId
|
||||
(Just tknId, Just NTACheck)
|
||||
| savedDeviceToken == suppliedDeviceToken -> do
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ nsUpdateToken ns tkn {ntfMode = suppliedNtfMode}
|
||||
when (ntfTknStatus == NTActive) $ do
|
||||
cron <- asks $ ntfCron . config
|
||||
agentNtfEnableCron c tknId tkn cron
|
||||
when (suppliedNtfMode == NMInstant) $ initializeNtfSubs c
|
||||
when (suppliedNtfMode == NMPeriodic && savedNtfMode == NMInstant) $ deleteNtfSubs c NSCDelete
|
||||
-- possible improvement: get updated token status from the server, or maybe TCRON could return the current status
|
||||
pure ntfTknStatus
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ nsUpdateToken ns tkn {ntfMode = suppliedNtfMode}
|
||||
when (ntfTknStatus == NTActive) $ do
|
||||
cron <- asks $ ntfCron . config
|
||||
agentNtfEnableCron c tknId tkn cron
|
||||
when (suppliedNtfMode == NMInstant) $ initializeNtfSubs c
|
||||
when (suppliedNtfMode == NMPeriodic && savedNtfMode == NMInstant) $ deleteNtfSubs c NSCDelete
|
||||
-- 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
|
||||
@@ -1647,10 +1737,10 @@ toggleConnectionNtfs' c connId enable = do
|
||||
toggle cData
|
||||
| enableNtfs cData == enable = pure ()
|
||||
| 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)
|
||||
withStore' c $ \db -> setConnectionNtfs db connId enable
|
||||
ns <- asks ntfSupervisor
|
||||
let cmd = if enable then NSCCreate else NSCDelete
|
||||
atomically $ sendNtfSubCommand ns (connId, cmd)
|
||||
|
||||
deleteToken_ :: AgentMonad m => AgentClient -> NtfToken -> m ()
|
||||
deleteToken_ c tkn@NtfToken {ntfTokenId, ntfTknStatus} = do
|
||||
@@ -1743,11 +1833,12 @@ getAgentMigrations' :: AgentMonad m => AgentClient -> m [UpMigration]
|
||||
getAgentMigrations' c = map upMigration <$> withStore' c (Migrations.getCurrent . DB.conn)
|
||||
|
||||
debugAgentLocks' :: AgentMonad' m => AgentClient -> m AgentLocks
|
||||
debugAgentLocks' AgentClient {connLocks = cs, reconnectLocks = rs, deleteLock = d} = do
|
||||
debugAgentLocks' AgentClient {connLocks = cs, invLocks = is, reconnectLocks = rs, deleteLock = d} = do
|
||||
connLocks <- getLocks cs
|
||||
invLocks <- getLocks is
|
||||
srvLocks <- getLocks rs
|
||||
delLock <- atomically $ tryReadTMVar d
|
||||
pure AgentLocks {connLocks, srvLocks, delLock}
|
||||
pure AgentLocks {connLocks, invLocks, srvLocks, delLock}
|
||||
where
|
||||
getLocks ls = atomically $ M.mapKeys (B.unpack . strEncode) . M.mapMaybe id <$> (mapM tryReadTMVar =<< readTVar ls)
|
||||
|
||||
@@ -1843,10 +1934,13 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), v, s
|
||||
cData@ConnData {userId, connId, duplexHandshake, connAgentVersion, ratchetSyncState = rss} =
|
||||
withConnLock c connId "processSMP" $ case cmd of
|
||||
SMP.MSG msg@SMP.RcvMessage {msgId = srvMsgId} ->
|
||||
handleNotifyAck $
|
||||
decryptSMPMessage v rq msg >>= \case
|
||||
handleNotifyAck $ do
|
||||
msg' <- decryptSMPMessage v rq msg
|
||||
handleNotifyAck $ case msg' of
|
||||
SMP.ClientRcvMsgBody {msgTs = srvTs, msgFlags, msgBody} -> processClientMsg srvTs msgFlags msgBody
|
||||
SMP.ClientRcvMsgQuota {} -> queueDrained >> ack
|
||||
whenM (atomically $ hasGetLock c rq) $
|
||||
notify (MSGNTF $ SMP.rcvMessageMeta srvMsgId msg')
|
||||
where
|
||||
queueDrained = case conn of
|
||||
DuplexConnection _ _ sqs -> void $ enqueueMessages c cData sqs SMP.noMsgFlags $ QCONT (sndAddress rq)
|
||||
@@ -1879,7 +1973,7 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), v, s
|
||||
case (conn', dbReplaceQueueId) of
|
||||
(DuplexConnection _ rqs _, Just replacedId) -> do
|
||||
when primary . withStore' c $ \db -> setRcvQueuePrimary db connId rq
|
||||
case find (\q -> replacedId == q.dbQueueId) rqs of
|
||||
case find ((replacedId ==) . dbQId) rqs of
|
||||
Just rq'@RcvQueue {server, rcvId} -> do
|
||||
checkRQSwchStatus rq' RSSendingQUSE
|
||||
void $ withStore' c $ \db -> setRcvSwitchStatus db rq' $ Just RSReceivedMessage
|
||||
@@ -1912,11 +2006,11 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), v, s
|
||||
resetRatchetSync :: m (Connection c)
|
||||
resetRatchetSync
|
||||
| rss `notElem` ([RSOk, RSStarted] :: [RatchetSyncState]) = do
|
||||
let cData'' = (toConnData conn') {ratchetSyncState = RSOk} :: ConnData
|
||||
conn'' = updateConnection cData'' conn'
|
||||
notify . RSYNC RSOk Nothing $ connectionStats conn''
|
||||
withStore' c $ \db -> setConnRatchetSync db connId RSOk
|
||||
pure conn''
|
||||
let cData'' = (toConnData conn') {ratchetSyncState = RSOk} :: ConnData
|
||||
conn'' = updateConnection cData'' conn'
|
||||
notify . RSYNC RSOk Nothing $ connectionStats conn''
|
||||
withStore' c $ \db -> setConnRatchetSync db connId RSOk
|
||||
pure conn''
|
||||
| otherwise = pure conn'
|
||||
Right _ -> prohibited >> ack
|
||||
Left e@(AGENT A_DUPLICATE) -> do
|
||||
@@ -1924,11 +2018,11 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), v, s
|
||||
Just RcvMsg {internalId, msgMeta, msgBody = agentMsgBody, userAck}
|
||||
| userAck -> ackDel internalId
|
||||
| otherwise -> do
|
||||
liftEither (parse smpP (AGENT A_MESSAGE) agentMsgBody) >>= \case
|
||||
AgentMessage _ (A_MSG body) -> do
|
||||
logServer "<--" c srv rId "MSG <MSG>"
|
||||
notify $ MSG msgMeta msgFlags body
|
||||
_ -> pure ()
|
||||
liftEither (parse smpP (AGENT A_MESSAGE) agentMsgBody) >>= \case
|
||||
AgentMessage _ (A_MSG body) -> do
|
||||
logServer "<--" c srv rId "MSG <MSG>"
|
||||
notify $ MSG msgMeta msgFlags body
|
||||
_ -> pure ()
|
||||
_ -> checkDuplicateHash e encryptedMsgHash >> ack
|
||||
Left (AGENT (A_CRYPTO e)) -> do
|
||||
exists <- withStore' c $ \db -> checkRcvMsgHashExists db connId encryptedMsgHash
|
||||
@@ -1976,9 +2070,9 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), v, s
|
||||
case msgAVRange `compatibleVersion` aVRange of
|
||||
Just (Compatible av)
|
||||
| av > connAgentVersion -> do
|
||||
withStore' c $ \db -> setConnAgentVersion db connId av
|
||||
let cData'' = cData' {connAgentVersion = av} :: ConnData
|
||||
pure $ updateConnection cData'' conn'
|
||||
withStore' c $ \db -> setConnAgentVersion db connId av
|
||||
let cData'' = cData' {connAgentVersion = av} :: ConnData
|
||||
pure $ updateConnection cData'' conn'
|
||||
| otherwise -> pure conn'
|
||||
Nothing -> pure conn'
|
||||
ack :: m ()
|
||||
@@ -1994,9 +2088,9 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), v, s
|
||||
processEND = \case
|
||||
Just (Right clnt)
|
||||
| sessId == sessionId clnt -> do
|
||||
removeSubscription c connId
|
||||
notify' END
|
||||
pure "END"
|
||||
removeSubscription c connId
|
||||
notify' END
|
||||
pure "END"
|
||||
| otherwise -> ignored
|
||||
_ -> ignored
|
||||
ignored = pure "END from disconnected client - ignored"
|
||||
@@ -2058,7 +2152,7 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), v, s
|
||||
where
|
||||
processConf connInfo senderConf duplexHS = do
|
||||
let newConfirmation = NewConfirmation {connId, senderConf, ratchetState = rc'}
|
||||
g <- asks idsDrg
|
||||
g <- asks random
|
||||
confId <- withStore c $ \db -> do
|
||||
setHandshakeVersion db connId agentVersion duplexHS
|
||||
createConfirmation db g newConfirmation
|
||||
@@ -2154,7 +2248,7 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), v, s
|
||||
case (findQ (qAddress sqInfo) sqs, findQ addr sqs) of
|
||||
(Just _, _) -> qError "QADD: queue address is already used in connection"
|
||||
(_, Just sq@SndQueue {dbQueueId}) -> do
|
||||
let (delSqs, keepSqs) = L.partition (\q -> Just dbQueueId == q.dbReplaceQueueId) sqs
|
||||
let (delSqs, keepSqs) = L.partition ((Just dbQueueId == ) . dbReplaceQId) sqs
|
||||
case L.nonEmpty keepSqs of
|
||||
Just sqs' -> do
|
||||
-- move inside case?
|
||||
@@ -2186,12 +2280,12 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), v, s
|
||||
case findRQ (smpServer, senderId) rqs of
|
||||
Just rq'@RcvQueue {rcvId, e2ePrivKey = dhPrivKey, smpClientVersion = cVer, status = status'}
|
||||
| status' == New || status' == Confirmed -> do
|
||||
checkRQSwchStatus rq RSSendingQADD
|
||||
logServer "<--" c srv rId $ "MSG <QKEY> " <> logSecret senderId
|
||||
let dhSecret = C.dh' dhPublicKey dhPrivKey
|
||||
withStore' c $ \db -> setRcvQueueConfirmedE2E db rq' dhSecret $ min cVer cVer'
|
||||
enqueueCommand c "" connId (Just smpServer) $ AInternalCommand $ ICQSecure rcvId senderKey
|
||||
notify . SWITCH QDRcv SPConfirmed $ connectionStats conn'
|
||||
checkRQSwchStatus rq RSSendingQADD
|
||||
logServer "<--" c srv rId $ "MSG <QKEY> " <> logSecret senderId
|
||||
let dhSecret = C.dh' dhPublicKey dhPrivKey
|
||||
withStore' c $ \db -> setRcvQueueConfirmedE2E db rq' dhSecret $ min cVer cVer'
|
||||
enqueueCommand c "" connId (Just smpServer) $ AInternalCommand $ ICQSecure rcvId senderKey
|
||||
notify . SWITCH QDRcv SPConfirmed $ connectionStats conn'
|
||||
| otherwise -> qError "QKEY: queue already secured"
|
||||
_ -> qError "QKEY: queue address not found in connection"
|
||||
where
|
||||
@@ -2205,7 +2299,7 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), v, s
|
||||
when (ratchetSyncSendProhibited cData') $ throwError $ AGENT (A_QUEUE "ratchet is not synchronized")
|
||||
case findQ addr sqs of
|
||||
Just sq'@SndQueue {dbReplaceQueueId = Just replaceQId} -> do
|
||||
case find (\q -> replaceQId == q.dbQueueId) sqs of
|
||||
case find ((replaceQId ==) . dbQId) sqs of
|
||||
Just sq1 -> do
|
||||
checkSQSwchStatus sq1 SSSendingQKEY
|
||||
logServer "<--" c srv rId $ "MSG <QUSE> " <> logSecret (snd addr)
|
||||
@@ -2227,15 +2321,15 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), v, s
|
||||
ereadyMsg rcPrev (DuplexConnection cData'@ConnData {lastExternalSndId} _ sqs) = do
|
||||
let CR.Ratchet {rcSnd} = rcPrev
|
||||
-- if ratchet was initialized as receiving, it means EREADY wasn't sent on key negotiation
|
||||
when (isNothing rcSnd) $
|
||||
void . enqueueMessages' c cData' sqs SMP.MsgFlags {notification = True} $ EREADY lastExternalSndId
|
||||
when (isNothing rcSnd) . void $
|
||||
enqueueMessages' c cData' sqs SMP.MsgFlags {notification = True} (EREADY lastExternalSndId)
|
||||
|
||||
smpInvitation :: Connection c -> ConnectionRequestUri 'CMInvitation -> ConnInfo -> m ()
|
||||
smpInvitation conn' connReq@(CRInvitationUri crData _) cInfo = do
|
||||
logServer "<--" c srv rId "MSG <KEY>"
|
||||
case conn' of
|
||||
ContactConnection {} -> do
|
||||
g <- asks idsDrg
|
||||
g <- asks random
|
||||
let newInv = NewInvitation {contactConnId = connId, connReq, recipientConnInfo = cInfo}
|
||||
invId <- withStore c $ \db -> createInvitation db g newInv
|
||||
let srvs = L.map qServer $ crSmpQueues crData
|
||||
@@ -2264,12 +2358,27 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), v, s
|
||||
unless exists $ addProcessedRatchetKeyHash db connId rkHashRcv
|
||||
pure exists
|
||||
getSendRatchetKeys :: m (C.PrivateKeyX448, C.PrivateKeyX448, C.PublicKeyX448, C.PublicKeyX448)
|
||||
getSendRatchetKeys
|
||||
| rss == RSStarted = withStore c (`getRatchetX3dhKeys'` connId)
|
||||
| otherwise = do
|
||||
(pk1, pk2, e2eParams@(CR.E2ERatchetParams _ k1 k2)) <- liftIO . CR.generateE2EParams $ version e2eOtherPartyParams
|
||||
void $ enqueueRatchetKeyMsgs c cData' sqs e2eParams
|
||||
pure (pk1, pk2, k1, k2)
|
||||
getSendRatchetKeys = case rss of
|
||||
RSOk -> sendReplyKey -- receiving client
|
||||
RSAllowed -> sendReplyKey
|
||||
RSRequired -> sendReplyKey
|
||||
RSStarted -> withStore c (`getRatchetX3dhKeys'` connId) -- initiating client
|
||||
RSAgreed -> do
|
||||
withStore' c $ \db -> setConnRatchetSync db connId RSRequired
|
||||
notifyRatchetSyncError
|
||||
-- can communicate for other client to reset to RSRequired
|
||||
-- - need to add new AgentMsgEnvelope, AgentMessage, AgentMessageType
|
||||
-- - need to deduplicate on receiving side
|
||||
throwError $ AGENT (A_CRYPTO RATCHET_SYNC)
|
||||
where
|
||||
sendReplyKey = do
|
||||
(pk1, pk2, e2eParams@(CR.E2ERatchetParams _ k1 k2)) <- liftIO . CR.generateE2EParams $ version e2eOtherPartyParams
|
||||
void $ enqueueRatchetKeyMsgs c cData' sqs e2eParams
|
||||
pure (pk1, pk2, k1, k2)
|
||||
notifyRatchetSyncError = do
|
||||
let cData'' = cData' {ratchetSyncState = RSRequired} :: ConnData
|
||||
conn'' = updateConnection cData'' conn'
|
||||
notify $ RSYNC RSRequired (Just RATCHET_SYNC) (connectionStats conn'')
|
||||
notifyAgreed :: m ()
|
||||
notifyAgreed = do
|
||||
let cData'' = cData' {ratchetSyncState = RSAgreed} :: ConnData
|
||||
@@ -2285,11 +2394,11 @@ processSMPTransmission c@AgentClient {smpClients, subQ} (tSess@(_, srv, _), v, s
|
||||
initRatchet :: VersionRange -> (C.PrivateKeyX448, C.PrivateKeyX448, C.PublicKeyX448, C.PublicKeyX448) -> m ()
|
||||
initRatchet e2eEncryptVRange (pk1, pk2, k1, k2)
|
||||
| rkHash k1 k2 <= rkHashRcv = do
|
||||
recreateRatchet $ CR.initRcvRatchet e2eEncryptVRange pk2 $ CR.x3dhRcv pk1 pk2 e2eOtherPartyParams
|
||||
recreateRatchet $ CR.initRcvRatchet e2eEncryptVRange pk2 $ CR.x3dhRcv pk1 pk2 e2eOtherPartyParams
|
||||
| otherwise = do
|
||||
(_, rcDHRs) <- liftIO C.generateKeyPair'
|
||||
recreateRatchet $ CR.initSndRatchet e2eEncryptVRange k2Rcv rcDHRs $ CR.x3dhSnd pk1 pk2 e2eOtherPartyParams
|
||||
void . enqueueMessages' c cData' sqs SMP.MsgFlags {notification = True} $ EREADY lastExternalSndId
|
||||
(_, rcDHRs) <- liftIO C.generateKeyPair'
|
||||
recreateRatchet $ CR.initSndRatchet e2eEncryptVRange k2Rcv rcDHRs $ CR.x3dhSnd pk1 pk2 e2eOtherPartyParams
|
||||
void . enqueueMessages' c cData' sqs SMP.MsgFlags {notification = True} $ EREADY lastExternalSndId
|
||||
|
||||
checkMsgIntegrity :: PrevExternalSndId -> ExternalSndId -> PrevRcvMsgHash -> ByteString -> MsgIntegrity
|
||||
checkMsgIntegrity prevExtSndId extSndId internalPrevMsgHash receivedPrevMsgHash
|
||||
@@ -2347,8 +2456,8 @@ mkAgentConfirmation :: AgentMonad m => Compatible Version -> AgentClient -> Conn
|
||||
mkAgentConfirmation (Compatible agentVersion) c cData sq srv connInfo subMode
|
||||
| agentVersion == 1 = pure $ AgentConnInfo connInfo
|
||||
| otherwise = do
|
||||
qInfo <- createReplyQueue c cData sq subMode srv
|
||||
pure $ AgentConnInfoReply (qInfo :| []) connInfo
|
||||
qInfo <- createReplyQueue c cData sq subMode srv
|
||||
pure $ AgentConnInfoReply (qInfo :| []) connInfo
|
||||
|
||||
enqueueConfirmation :: AgentMonad m => AgentClient -> ConnData -> SndQueue -> ConnInfo -> Maybe (CR.E2ERatchetParams 'C.X448) -> m ()
|
||||
enqueueConfirmation c cData sq connInfo e2eEncryption_ = do
|
||||
@@ -2373,8 +2482,7 @@ storeConfirmation c ConnData {connId, connAgentVersion} sq e2eEncryption_ agentM
|
||||
enqueueRatchetKeyMsgs :: forall m. AgentMonad m => AgentClient -> ConnData -> NonEmpty SndQueue -> CR.E2ERatchetParams 'C.X448 -> m AgentMsgId
|
||||
enqueueRatchetKeyMsgs c cData (sq :| sqs) e2eEncryption = do
|
||||
msgId <- enqueueRatchetKey c cData sq e2eEncryption
|
||||
mapM_ (enqueueSavedMessage c cData msgId) $
|
||||
filter (\SndQueue {status} -> status == Secured || status == Active) sqs
|
||||
mapM_ (enqueueSavedMessage c cData msgId) $ filter isActiveSndQ sqs
|
||||
pure msgId
|
||||
|
||||
enqueueRatchetKey :: forall m. AgentMonad m => AgentClient -> ConnData -> SndQueue -> CR.E2ERatchetParams 'C.X448 -> m AgentMsgId
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{-# LANGUAGE AllowAmbiguousTypes #-}
|
||||
{-# LANGUAGE ConstraintKinds #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
@@ -10,10 +10,10 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedRecordDot #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# LANGUAGE TypeFamilyDependencies #-}
|
||||
@@ -24,6 +24,8 @@ module Simplex.Messaging.Agent.Client
|
||||
ProtocolTestStep (..),
|
||||
newAgentClient,
|
||||
withConnLock,
|
||||
withConnLocks,
|
||||
withInvLock,
|
||||
closeAgentClient,
|
||||
closeProtocolServerClients,
|
||||
closeXFTPServerClient,
|
||||
@@ -71,7 +73,9 @@ module Simplex.Messaging.Agent.Client
|
||||
logSecret,
|
||||
removeSubscription,
|
||||
hasActiveSubscription,
|
||||
hasGetLock,
|
||||
agentClientStore,
|
||||
agentDRG,
|
||||
getAgentSubscriptions,
|
||||
SubscriptionsInfo (..),
|
||||
SubInfo (..),
|
||||
@@ -96,6 +100,8 @@ module Simplex.Messaging.Agent.Client
|
||||
withStore',
|
||||
withStoreCtx,
|
||||
withStoreCtx',
|
||||
withStoreBatch,
|
||||
withStoreBatch',
|
||||
storeError,
|
||||
userServers,
|
||||
pickServer,
|
||||
@@ -115,9 +121,8 @@ import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Monad.Reader
|
||||
import Crypto.Random (getRandomBytes)
|
||||
import Data.Aeson (ToJSON)
|
||||
import qualified Data.Aeson as J
|
||||
import Crypto.Random (ChaChaDRG, getRandomBytes)
|
||||
import qualified Data.Aeson.TH as J
|
||||
import Data.Bifunctor (bimap, first, second)
|
||||
import Data.ByteString.Base64
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
@@ -136,7 +141,6 @@ import Data.Text (Text)
|
||||
import Data.Text.Encoding
|
||||
import Data.Time (UTCTime, defaultTimeLocale, formatTime, getCurrentTime)
|
||||
import Data.Word (Word16)
|
||||
import GHC.Generics (Generic)
|
||||
import Network.Socket (HostName)
|
||||
import Simplex.FileTransfer.Client (XFTPChunkSpec (..), XFTPClient, XFTPClientConfig (..), XFTPClientError)
|
||||
import qualified Simplex.FileTransfer.Client as X
|
||||
@@ -163,7 +167,7 @@ import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Notifications.Client
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Notifications.Types
|
||||
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, parse)
|
||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parse)
|
||||
import Simplex.Messaging.Protocol
|
||||
( AProtocolType (..),
|
||||
BrokerMsg,
|
||||
@@ -234,7 +238,7 @@ data AgentClient = AgentClient
|
||||
pendingSubs :: TRcvQueues,
|
||||
removedSubs :: TMap (UserId, SMPServer, SMP.RecipientId) SMPClientError,
|
||||
pendingMsgsQueued :: TMap SndQAddr Bool,
|
||||
smpQueueMsgQueues :: TMap SndQAddr (TQueue InternalId, TMVar ()),
|
||||
smpQueueMsgQueues :: TMap SndQAddr (TQueue (InternalId, Bool), TMVar ()),
|
||||
smpQueueMsgDeliveries :: TMap SndQAddr (Async ()),
|
||||
connCmdsQueued :: TMap ConnId Bool,
|
||||
asyncCmdQueues :: TMap (Maybe SMPServer) (TQueue AsyncCmdId),
|
||||
@@ -248,6 +252,8 @@ data AgentClient = AgentClient
|
||||
getMsgLocks :: TMap (SMPServer, SMP.RecipientId) (TMVar ()),
|
||||
-- locks to prevent concurrent operations with connection
|
||||
connLocks :: TMap ConnId Lock,
|
||||
-- locks to prevent concurrent operations with connection request invitations
|
||||
invLocks :: TMap ByteString Lock,
|
||||
-- lock to prevent concurrency between periodic and async connection deletions
|
||||
deleteLock :: Lock,
|
||||
-- locks to prevent concurrent reconnections to SMP servers
|
||||
@@ -273,15 +279,18 @@ agentOpSel = \case
|
||||
agentOperations :: [AgentClient -> TVar AgentOpState]
|
||||
agentOperations = [ntfNetworkOp, rcvNetworkOp, msgDeliveryOp, sndNetworkOp, databaseOp]
|
||||
|
||||
data AgentOpState = AgentOpState {opSuspended :: Bool, opsInProgress :: Int}
|
||||
data AgentOpState = AgentOpState {opSuspended :: !Bool, opsInProgress :: !Int}
|
||||
|
||||
data AgentState = ASForeground | ASSuspending | ASSuspended
|
||||
deriving (Eq, Show)
|
||||
|
||||
data AgentLocks = AgentLocks {connLocks :: Map String String, srvLocks :: Map String String, delLock :: Maybe String}
|
||||
deriving (Show, Generic)
|
||||
|
||||
instance ToJSON AgentLocks where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
data AgentLocks = AgentLocks
|
||||
{ connLocks :: Map String String,
|
||||
invLocks :: Map String String,
|
||||
srvLocks :: Map String String,
|
||||
delLock :: Maybe String
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data AgentStatsKey = AgentStatsKey
|
||||
{ userId :: UserId,
|
||||
@@ -324,6 +333,7 @@ newAgentClient InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = do
|
||||
agentState <- newTVar ASForeground
|
||||
getMsgLocks <- TM.empty
|
||||
connLocks <- TM.empty
|
||||
invLocks <- TM.empty
|
||||
deleteLock <- createLock
|
||||
reconnectLocks <- TM.empty
|
||||
reconnections <- newTAsyncs
|
||||
@@ -361,6 +371,7 @@ newAgentClient InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = do
|
||||
agentState,
|
||||
getMsgLocks,
|
||||
connLocks,
|
||||
invLocks,
|
||||
deleteLock,
|
||||
reconnectLocks,
|
||||
reconnections,
|
||||
@@ -373,6 +384,9 @@ newAgentClient InitialAgentServers {smp, ntf, xftp, netCfg} agentEnv = do
|
||||
agentClientStore :: AgentClient -> SQLiteStore
|
||||
agentClientStore AgentClient {agentEnv = Env {store}} = store
|
||||
|
||||
agentDRG :: AgentClient -> TVar ChaChaDRG
|
||||
agentDRG AgentClient {agentEnv = Env {random}} = random
|
||||
|
||||
class (Encoding err, Show err) => ProtocolServerClient err msg | msg -> err where
|
||||
type Client msg = c | c -> msg
|
||||
getProtocolServerClient :: AgentMonad m => AgentClient -> TransportSession msg -> m (Client msg)
|
||||
@@ -433,7 +447,7 @@ getSMPServerClient c@AgentClient {active, smpClients, msgQ} tSess@(userId, srv,
|
||||
TM.delete tSess smpClients
|
||||
qs <- RQ.getDelSessQueues tSess $ activeSubs c
|
||||
mapM_ (`RQ.addQueue` pendingSubs c) qs
|
||||
let cs = S.fromList $ map (\q -> q.connId) qs
|
||||
let cs = S.fromList $ map qConnId qs
|
||||
cs' <- RQ.getConns $ activeSubs c
|
||||
pure (qs, S.toList $ cs `S.difference` cs')
|
||||
|
||||
@@ -644,8 +658,20 @@ withConnLock :: MonadUnliftIO m => AgentClient -> ConnId -> String -> m a -> m a
|
||||
withConnLock _ "" _ = id
|
||||
withConnLock AgentClient {connLocks} connId name = withLockMap_ connLocks connId name
|
||||
|
||||
withInvLock :: MonadUnliftIO m => AgentClient -> ByteString -> String -> m a -> m a
|
||||
withInvLock AgentClient {invLocks} = withLockMap_ invLocks
|
||||
|
||||
withConnLocks :: MonadUnliftIO m => AgentClient -> Set ConnId -> String -> m a -> m a
|
||||
withConnLocks AgentClient {connLocks} = withLocksMap_ connLocks . S.filter (not . B.null)
|
||||
|
||||
withLockMap_ :: (Ord k, MonadUnliftIO m) => TMap k Lock -> k -> String -> m a -> m a
|
||||
withLockMap_ locks key = withGetLock $ TM.lookup key locks >>= maybe newLock pure
|
||||
withLockMap_ = withGetLock . getMapLock
|
||||
|
||||
withLocksMap_ :: (Ord k, MonadUnliftIO m) => TMap k Lock -> Set k -> String -> m a -> m a
|
||||
withLocksMap_ = withGetLocks . getMapLock
|
||||
|
||||
getMapLock :: Ord k => TMap k Lock -> k -> STM Lock
|
||||
getMapLock locks key = TM.lookup key locks >>= maybe newLock pure
|
||||
where
|
||||
newLock = createLock >>= \l -> TM.insert key l locks $> l
|
||||
|
||||
@@ -724,21 +750,13 @@ data ProtocolTestStep
|
||||
| TSDownloadFile
|
||||
| TSCompareFile
|
||||
| TSDeleteFile
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
instance ToJSON ProtocolTestStep where
|
||||
toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "TS"
|
||||
toJSON = J.genericToJSON . enumJSON $ dropPrefix "TS"
|
||||
deriving (Eq, Show)
|
||||
|
||||
data ProtocolTestFailure = ProtocolTestFailure
|
||||
{ testStep :: ProtocolTestStep,
|
||||
testError :: AgentErrorType
|
||||
}
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
instance ToJSON ProtocolTestFailure where
|
||||
toEncoding = J.genericToEncoding J.defaultOptions
|
||||
toJSON = J.genericToJSON J.defaultOptions
|
||||
deriving (Eq, Show)
|
||||
|
||||
runSMPServerTest :: AgentMonad m => AgentClient -> UserId -> SMPServerWithAuth -> m (Maybe ProtocolTestFailure)
|
||||
runSMPServerTest c userId (ProtoServerWithAuth srv auth) = do
|
||||
@@ -821,7 +839,7 @@ mkSMPTransportSession :: (AgentMonad' m, SMPQueueRec q) => AgentClient -> q -> m
|
||||
mkSMPTransportSession c q = mkSMPTSession q <$> getSessionMode c
|
||||
|
||||
mkSMPTSession :: SMPQueueRec q => q -> TransportSessionMode -> SMPTransportSession
|
||||
mkSMPTSession q = mkTSession q.userId (qServer q) q.connId
|
||||
mkSMPTSession q = mkTSession (qUserId q) (qServer q) (qConnId q)
|
||||
|
||||
getSessionMode :: AgentMonad' m => AgentClient -> m TransportSessionMode
|
||||
getSessionMode = fmap sessionMode . readTVarIO . useNetworkConfig
|
||||
@@ -892,15 +910,15 @@ subscribeQueues c qs = do
|
||||
-- only "checked" queues are subscribed
|
||||
(errs <>) <$> sendTSessionBatches "SUB" 90 id (subscribeQueues_ u) c qs'
|
||||
where
|
||||
checkQueue rq@RcvQueue {rcvId, server} = do
|
||||
prohibited <- atomically . TM.member (server, rcvId) $ getMsgLocks c
|
||||
checkQueue rq = do
|
||||
prohibited <- atomically $ hasGetLock c rq
|
||||
pure $ if prohibited then Left (rq, Left $ CMD PROHIBITED) else Right rq
|
||||
subscribeQueues_ :: UnliftIO m -> SMPClient -> NonEmpty RcvQueue -> IO (BatchResponses SMPClientError ())
|
||||
subscribeQueues_ u smp qs' = do
|
||||
rs <- sendBatch subscribeSMPQueues smp qs'
|
||||
mapM_ (uncurry $ processSubResult c) rs
|
||||
when (any temporaryClientError . lefts . map snd $ L.toList rs) $
|
||||
unliftIO u $ reconnectServer c $ transportSession' smp
|
||||
when (any temporaryClientError . lefts . map snd $ L.toList rs) . unliftIO u $
|
||||
reconnectServer c (transportSession' smp)
|
||||
pure rs
|
||||
|
||||
type BatchResponses e r = (NonEmpty (RcvQueue, Either e r))
|
||||
@@ -987,7 +1005,7 @@ sendInvitation c userId (Compatible (SMPQueueInfo v SMPQueueAddress {smpServer,
|
||||
mkInvitation = do
|
||||
let agentEnvelope = AgentInvitation {agentVersion, connReq, connInfo}
|
||||
agentCbEncryptOnce v dhPublicKey . smpEncode $
|
||||
SMP.ClientMessage SMP.PHEmpty $ smpEncode agentEnvelope
|
||||
SMP.ClientMessage SMP.PHEmpty (smpEncode agentEnvelope)
|
||||
|
||||
getQueueMessage :: AgentMonad m => AgentClient -> RcvQueue -> m (Maybe SMPMsgMeta)
|
||||
getQueueMessage c rq@RcvQueue {server, rcvId, rcvPrivateKey} = do
|
||||
@@ -1044,6 +1062,10 @@ sendAck c rq@RcvQueue {rcvId, rcvPrivateKey} msgId = do
|
||||
ackSMPMessage smp rcvPrivateKey rcvId msgId
|
||||
atomically $ releaseGetLock c rq
|
||||
|
||||
hasGetLock :: AgentClient -> RcvQueue -> STM Bool
|
||||
hasGetLock c RcvQueue {server, rcvId} =
|
||||
TM.member (server, rcvId) $ getMsgLocks c
|
||||
|
||||
releaseGetLock :: AgentClient -> RcvQueue -> STM ()
|
||||
releaseGetLock c RcvQueue {server, rcvId} =
|
||||
TM.lookup (server, rcvId) (getMsgLocks c) >>= mapM_ (`tryPutTMVar` ())
|
||||
@@ -1281,6 +1303,18 @@ withStoreCtx_ ctx_ c action = do
|
||||
handleInternal :: String -> E.SomeException -> IO (Either StoreError a)
|
||||
handleInternal ctxStr e = pure . Left . SEInternal . B.pack $ show e <> ctxStr
|
||||
|
||||
withStoreBatch :: (AgentMonad' m, Traversable t) => AgentClient -> (DB.Connection -> t (IO (Either AgentErrorType a))) -> m (t (Either AgentErrorType a))
|
||||
withStoreBatch c actions = do
|
||||
st <- asks store
|
||||
liftIO $ agentOperationBracket c AODatabase (\_ -> pure ()) $
|
||||
withTransaction st $ mapM (`E.catch` handleInternal) . actions
|
||||
where
|
||||
handleInternal :: E.SomeException -> IO (Either AgentErrorType a)
|
||||
handleInternal = pure . Left . INTERNAL . show
|
||||
|
||||
withStoreBatch' :: (AgentMonad' m, Traversable t) => AgentClient -> (DB.Connection -> t (IO a)) -> m (t (Either AgentErrorType a))
|
||||
withStoreBatch' c actions = withStoreBatch c $ \db -> fmap Right <$> actions db
|
||||
|
||||
storeError :: StoreError -> AgentErrorType
|
||||
storeError = \case
|
||||
SEConnNotFound -> CONN NOT_FOUND
|
||||
@@ -1322,7 +1356,7 @@ userServers c = case protocolTypeI @p of
|
||||
SPSMP -> smpServers c
|
||||
SPXFTP -> xftpServers c
|
||||
|
||||
pickServer :: forall p m. (AgentMonad' m) => NonEmpty (ProtoServerWithAuth p) -> m (ProtoServerWithAuth p)
|
||||
pickServer :: forall p m. AgentMonad' m => NonEmpty (ProtoServerWithAuth p) -> m (ProtoServerWithAuth p)
|
||||
pickServer = \case
|
||||
srv :| [] -> pure srv
|
||||
servers -> do
|
||||
@@ -1341,7 +1375,7 @@ withUserServers c userId action =
|
||||
Just srvs -> action srvs
|
||||
_ -> throwError $ INTERNAL "unknown userId - no user servers"
|
||||
|
||||
withNextSrv :: forall p m a. (ProtocolTypeI p, UserProtocol p, AgentMonad m) => AgentClient -> UserId -> TVar [ProtocolServer p] -> [ProtocolServer p] -> ((ProtoServerWithAuth p) -> m a) -> m a
|
||||
withNextSrv :: forall p m a. (ProtocolTypeI p, UserProtocol p, AgentMonad m) => AgentClient -> UserId -> TVar [ProtocolServer p] -> [ProtocolServer p] -> (ProtoServerWithAuth p -> m a) -> m a
|
||||
withNextSrv c userId usedSrvs initUsed action = do
|
||||
used <- readTVarIO usedSrvs
|
||||
srvAuth@(ProtoServerWithAuth srv _) <- getNextServer c userId used
|
||||
@@ -1353,18 +1387,14 @@ withNextSrv c userId usedSrvs initUsed action = do
|
||||
action srvAuth
|
||||
|
||||
data SubInfo = SubInfo {userId :: UserId, server :: Text, rcvId :: Text, subError :: Maybe String}
|
||||
deriving (Show, Generic)
|
||||
|
||||
instance ToJSON SubInfo where toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
||||
deriving (Show)
|
||||
|
||||
data SubscriptionsInfo = SubscriptionsInfo
|
||||
{ activeSubscriptions :: [SubInfo],
|
||||
pendingSubscriptions :: [SubInfo],
|
||||
removedSubscriptions :: [SubInfo]
|
||||
}
|
||||
deriving (Show, Generic)
|
||||
|
||||
instance ToJSON SubscriptionsInfo where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
deriving (Show)
|
||||
|
||||
getAgentSubscriptions :: MonadIO m => AgentClient -> m SubscriptionsInfo
|
||||
getAgentSubscriptions c = do
|
||||
@@ -1376,6 +1406,16 @@ getAgentSubscriptions c = do
|
||||
getSubs sel = map (`subInfo` Nothing) . M.keys <$> readTVarIO (getRcvQueues $ sel c)
|
||||
getRemovedSubs = map (uncurry subInfo . second Just) . M.assocs <$> readTVarIO (removedSubs c)
|
||||
subInfo :: (UserId, SMPServer, SMP.RecipientId) -> Maybe SMPClientError -> SubInfo
|
||||
subInfo (uId, srv, rId) err = SubInfo {userId = uId, server = enc srv, rcvId = enc rId, subError = show <$> err}
|
||||
subInfo (uId, srv, rId) err = SubInfo {userId = uId, server = enc srv, rcvId = enc rId, subError = show <$> err}
|
||||
enc :: StrEncoding a => a -> Text
|
||||
enc = decodeLatin1 . strEncode
|
||||
|
||||
$(J.deriveJSON defaultJSON ''AgentLocks)
|
||||
|
||||
$(J.deriveJSON (enumJSON $ dropPrefix "TS") ''ProtocolTestStep)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''ProtocolTestFailure)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''SubInfo)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''SubscriptionsInfo)
|
||||
|
||||
@@ -34,6 +34,7 @@ import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Monad.Reader
|
||||
import Crypto.Random
|
||||
import Data.ByteArray (ScrubbedBytes)
|
||||
import Data.Int (Int64)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import Data.Map (Map)
|
||||
@@ -163,7 +164,7 @@ defaultAgentConfig =
|
||||
ntfWorkerDelay = 100000, -- microseconds
|
||||
ntfSMPWorkerDelay = 500000, -- microseconds
|
||||
ntfSubCheckInterval = nominalDay,
|
||||
ntfMaxMessages = 4,
|
||||
ntfMaxMessages = 3,
|
||||
-- CA certificate private key is not needed for initialization
|
||||
-- ! we do not generate these
|
||||
caCertificateFile = "/etc/opt/simplex-agent/ca.crt",
|
||||
@@ -178,24 +179,26 @@ defaultAgentConfig =
|
||||
data Env = Env
|
||||
{ config :: AgentConfig,
|
||||
store :: SQLiteStore,
|
||||
idsDrg :: TVar ChaChaDRG,
|
||||
random :: TVar ChaChaDRG,
|
||||
clientCounter :: TVar Int,
|
||||
randomServer :: TVar StdGen,
|
||||
ntfSupervisor :: NtfSupervisor,
|
||||
xftpAgent :: XFTPAgent
|
||||
xftpAgent :: XFTPAgent,
|
||||
multicastSubscribers :: TMVar Int
|
||||
}
|
||||
|
||||
newSMPAgentEnv :: AgentConfig -> SQLiteStore -> IO Env
|
||||
newSMPAgentEnv config@AgentConfig {initialClientId} store = do
|
||||
idsDrg <- newTVarIO =<< liftIO drgNew
|
||||
random <- newTVarIO =<< drgNew
|
||||
clientCounter <- newTVarIO initialClientId
|
||||
randomServer <- newTVarIO =<< liftIO newStdGen
|
||||
ntfSupervisor <- atomically . newNtfSubSupervisor $ tbqSize config
|
||||
xftpAgent <- atomically newXFTPAgent
|
||||
pure Env {config, store, idsDrg, clientCounter, randomServer, ntfSupervisor, xftpAgent}
|
||||
multicastSubscribers <- newTMVarIO 0
|
||||
pure Env {config, store, random, clientCounter, randomServer, ntfSupervisor, xftpAgent, multicastSubscribers}
|
||||
|
||||
createAgentStore :: FilePath -> String -> MigrationConfirmation -> IO (Either MigrationError SQLiteStore)
|
||||
createAgentStore dbFilePath dbKey = createSQLiteStore dbFilePath dbKey Migrations.app
|
||||
createAgentStore :: FilePath -> ScrubbedBytes -> Bool -> MigrationConfirmation -> IO (Either MigrationError SQLiteStore)
|
||||
createAgentStore dbFilePath dbKey keepKey = createSQLiteStore dbFilePath dbKey keepKey Migrations.app
|
||||
|
||||
data NtfSupervisor = NtfSupervisor
|
||||
{ ntfTkn :: TVar (Maybe NtfToken),
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Lock where
|
||||
module Simplex.Messaging.Agent.Lock
|
||||
( Lock,
|
||||
createLock,
|
||||
withLock,
|
||||
withGetLock,
|
||||
withGetLocks,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad (void)
|
||||
import Control.Monad.IO.Unlift
|
||||
import Data.Functor (($>))
|
||||
import Data.Set (Set)
|
||||
import qualified Data.Set as S
|
||||
import UnliftIO.Async (forConcurrently)
|
||||
import qualified UnliftIO.Exception as E
|
||||
import UnliftIO.STM
|
||||
|
||||
@@ -20,9 +30,22 @@ withLock lock name =
|
||||
(atomically $ putTMVar lock name)
|
||||
(void . atomically $ takeTMVar lock)
|
||||
|
||||
withGetLock :: MonadUnliftIO m => STM Lock -> String -> m a -> m a
|
||||
withGetLock getLock name a =
|
||||
withGetLock :: MonadUnliftIO m => (k -> STM Lock) -> k -> String -> m a -> m a
|
||||
withGetLock getLock key name a =
|
||||
E.bracket
|
||||
(atomically $ getLock >>= \l -> putTMVar l name $> l)
|
||||
(atomically $ getPutLock getLock key name)
|
||||
(atomically . takeTMVar)
|
||||
(const a)
|
||||
|
||||
withGetLocks :: MonadUnliftIO m => (k -> STM Lock) -> Set k -> String -> m a -> m a
|
||||
withGetLocks getLock keys name = E.bracket holdLocks releaseLocks . const
|
||||
where
|
||||
holdLocks = forConcurrently (S.toList keys) $ \key -> atomically $ getPutLock getLock key name
|
||||
-- only this withGetLocks would be holding the locks,
|
||||
-- so it's safe to combine all lock releases into one transaction
|
||||
releaseLocks = atomically . mapM_ takeTMVar
|
||||
|
||||
-- getLock and putTMVar can be in one transaction on the assumption that getLock doesn't write in case the lock already exists,
|
||||
-- and in case it is created and added to some shared resource (we use TMap) it also helps avoid contention for the newly created lock.
|
||||
getPutLock :: (k -> STM Lock) -> k -> String -> STM Lock
|
||||
getPutLock getLock key name = getLock key >>= \l -> putTMVar l name $> l
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
|
||||
module Simplex.Messaging.Agent.NtfSubSupervisor
|
||||
@@ -100,14 +99,14 @@ processNtfSub c (connId, cmd) = do
|
||||
Just (action, _)
|
||||
-- subscription was marked for deletion / is being deleted
|
||||
| isDeleteNtfSubAction action -> do
|
||||
if ntfSubStatus == NASNew || ntfSubStatus == NASOff || ntfSubStatus == NASDeleted
|
||||
then resetSubscription
|
||||
else withNtfServer c $ \ntfServer -> do
|
||||
withStore' c $ \db -> supervisorUpdateNtfSub db sub {ntfServer} (NtfSubNTFAction NSACreate)
|
||||
addNtfNTFWorker ntfServer
|
||||
if ntfSubStatus == NASNew || ntfSubStatus == NASOff || ntfSubStatus == NASDeleted
|
||||
then resetSubscription
|
||||
else withNtfServer c $ \ntfServer -> do
|
||||
withStore' c $ \db -> supervisorUpdateNtfSub db sub {ntfServer} (NtfSubNTFAction NSACreate)
|
||||
addNtfNTFWorker ntfServer
|
||||
| otherwise -> case action of
|
||||
NtfSubNTFAction _ -> addNtfNTFWorker subNtfServer
|
||||
NtfSubSMPAction _ -> addNtfSMPWorker smpServer
|
||||
NtfSubNTFAction _ -> addNtfNTFWorker subNtfServer
|
||||
NtfSubSMPAction _ -> addNtfSMPWorker smpServer
|
||||
rotate :: m ()
|
||||
rotate = do
|
||||
withStore' c $ \db -> supervisorUpdateNtfSub db sub (NtfSubNTFAction NSARotate)
|
||||
@@ -291,11 +290,11 @@ rescheduleAction :: AgentMonad' m => TMVar () -> UTCTime -> UTCTime -> m Bool
|
||||
rescheduleAction doWork ts actionTs
|
||||
| actionTs <= ts = pure False
|
||||
| otherwise = do
|
||||
void . atomically $ tryTakeTMVar doWork
|
||||
void . forkIO $ do
|
||||
liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime actionTs ts
|
||||
void . atomically $ tryPutTMVar doWork ()
|
||||
pure True
|
||||
void . atomically $ tryTakeTMVar doWork
|
||||
void . forkIO $ do
|
||||
liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime actionTs ts
|
||||
void . atomically $ tryPutTMVar doWork ()
|
||||
pure True
|
||||
|
||||
retryOnError :: AgentMonad' m => AgentClient -> Text -> m () -> (AgentErrorType -> m ()) -> AgentErrorType -> m ()
|
||||
retryOnError c name loop done e = do
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
@@ -13,6 +12,7 @@
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
@@ -151,7 +151,7 @@ import Control.Monad (unless)
|
||||
import Control.Monad.Except (runExceptT, throwError)
|
||||
import Control.Monad.IO.Class
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.TH as J
|
||||
import Data.Attoparsec.ByteString.Char8 (Parser)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString.Base64
|
||||
@@ -176,8 +176,6 @@ import Data.Typeable ()
|
||||
import Data.Word (Word32)
|
||||
import Database.SQLite.Simple.FromField
|
||||
import Database.SQLite.Simple.ToField
|
||||
import GHC.Generics (Generic)
|
||||
import Generic.Random (genericArbitraryU)
|
||||
import Simplex.FileTransfer.Description
|
||||
import Simplex.FileTransfer.Protocol (FileParty (..), XFTPErrorType)
|
||||
import Simplex.Messaging.Agent.QueryString
|
||||
@@ -195,6 +193,7 @@ import Simplex.Messaging.Protocol
|
||||
MsgId,
|
||||
NMsgMeta,
|
||||
ProtocolServer (..),
|
||||
SMPMsgMeta,
|
||||
SMPServer,
|
||||
SMPServerWithAuth,
|
||||
SndPublicVerifyKey,
|
||||
@@ -213,7 +212,7 @@ import Simplex.Messaging.Transport (Transport (..), TransportError, serializeTra
|
||||
import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts_ (..))
|
||||
import Simplex.Messaging.Util
|
||||
import Simplex.Messaging.Version
|
||||
import Test.QuickCheck (Arbitrary (..))
|
||||
import Simplex.RemoteControl.Types
|
||||
import Text.Read
|
||||
import UnliftIO.Exception (Exception)
|
||||
|
||||
@@ -339,6 +338,7 @@ data ACommand (p :: AParty) (e :: AEntity) where
|
||||
SENT :: AgentMsgId -> ACommand Agent AEConn
|
||||
MERR :: AgentMsgId -> AgentErrorType -> ACommand Agent AEConn
|
||||
MSG :: MsgMeta -> MsgFlags -> MsgBody -> ACommand Agent AEConn
|
||||
MSGNTF :: SMPMsgMeta -> ACommand Agent AEConn
|
||||
ACK :: AgentMsgId -> Maybe MsgReceiptInfo -> ACommand Client AEConn
|
||||
RCVD :: MsgMeta -> NonEmpty MsgReceipt -> ACommand Agent AEConn
|
||||
SWCH :: ACommand Client AEConn
|
||||
@@ -399,6 +399,7 @@ data ACommandTag (p :: AParty) (e :: AEntity) where
|
||||
SENT_ :: ACommandTag Agent AEConn
|
||||
MERR_ :: ACommandTag Agent AEConn
|
||||
MSG_ :: ACommandTag Agent AEConn
|
||||
MSGNTF_ :: ACommandTag Agent AEConn
|
||||
ACK_ :: ACommandTag Client AEConn
|
||||
RCVD_ :: ACommandTag Agent AEConn
|
||||
SWCH_ :: ACommandTag Client AEConn
|
||||
@@ -452,6 +453,7 @@ aCommandTag = \case
|
||||
SENT _ -> SENT_
|
||||
MERR {} -> MERR_
|
||||
MSG {} -> MSG_
|
||||
MSGNTF {} -> MSGNTF_
|
||||
ACK {} -> ACK_
|
||||
RCVD {} -> RCVD_
|
||||
SWCH -> SWCH_
|
||||
@@ -614,13 +616,11 @@ data RcvQueueInfo = RcvQueueInfo
|
||||
rcvSwitchStatus :: Maybe RcvSwitchStatus,
|
||||
canAbortSwitch :: Bool
|
||||
}
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
instance ToJSON RcvQueueInfo where toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding RcvQueueInfo where
|
||||
strEncode RcvQueueInfo {rcvServer, rcvSwitchStatus, canAbortSwitch} =
|
||||
"srv=" <> strEncode rcvServer
|
||||
("srv=" <> strEncode rcvServer)
|
||||
<> maybe "" (\switch -> ";switch=" <> strEncode switch) rcvSwitchStatus
|
||||
<> (";can_abort_switch=" <> strEncode canAbortSwitch)
|
||||
strP = do
|
||||
@@ -633,9 +633,7 @@ data SndQueueInfo = SndQueueInfo
|
||||
{ sndServer :: SMPServer,
|
||||
sndSwitchStatus :: Maybe SndSwitchStatus
|
||||
}
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
instance ToJSON SndQueueInfo where toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding SndQueueInfo where
|
||||
strEncode SndQueueInfo {sndServer, sndSwitchStatus} =
|
||||
@@ -652,11 +650,11 @@ data ConnectionStats = ConnectionStats
|
||||
ratchetSyncState :: RatchetSyncState,
|
||||
ratchetSyncSupported :: Bool
|
||||
}
|
||||
deriving (Eq, Show, Generic)
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding ConnectionStats where
|
||||
strEncode ConnectionStats {connAgentVersion, rcvQueuesInfo, sndQueuesInfo, ratchetSyncState, ratchetSyncSupported} =
|
||||
"agent_version=" <> strEncode connAgentVersion
|
||||
("agent_version=" <> strEncode connAgentVersion)
|
||||
<> (" rcv=" <> strEncodeList rcvQueuesInfo)
|
||||
<> (" snd=" <> strEncodeList sndQueuesInfo)
|
||||
<> (" sync=" <> strEncode ratchetSyncState)
|
||||
@@ -669,8 +667,6 @@ instance StrEncoding ConnectionStats where
|
||||
ratchetSyncSupported <- " sync_supported=" *> strP
|
||||
pure ConnectionStats {connAgentVersion, rcvQueuesInfo, sndQueuesInfo, ratchetSyncState, ratchetSyncSupported}
|
||||
|
||||
instance ToJSON ConnectionStats where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
data NotificationsMode = NMPeriodic | NMInstant
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -688,6 +684,9 @@ instance ToJSON NotificationsMode where
|
||||
toEncoding = strToJEncoding
|
||||
toJSON = strToJSON
|
||||
|
||||
instance FromJSON NotificationsMode where
|
||||
parseJSON = strParseJSON "NotificationsMode"
|
||||
|
||||
instance ToField NotificationsMode where toField = toField . strEncode
|
||||
|
||||
instance FromField NotificationsMode where fromField = blobFieldDecoder $ parseAll strP
|
||||
@@ -1041,7 +1040,7 @@ instance StrEncoding MsgReceiptStatus where
|
||||
MROk -> "ok"
|
||||
MRBadMsgHash -> "badMsgHash"
|
||||
strP =
|
||||
A.takeWhile1 (/= ' ') >>= \ case
|
||||
A.takeWhile1 (/= ' ') >>= \case
|
||||
"ok" -> pure MROk
|
||||
"badMsgHash" -> pure MRBadMsgHash
|
||||
_ -> fail "bad MsgReceiptStatus"
|
||||
@@ -1050,6 +1049,9 @@ instance ToJSON MsgReceiptStatus where
|
||||
toJSON = strToJSON
|
||||
toEncoding = strToJEncoding
|
||||
|
||||
instance FromJSON MsgReceiptStatus where
|
||||
parseJSON = strParseJSON "MsgReceiptStatus"
|
||||
|
||||
type MsgReceiptInfo = ByteString
|
||||
|
||||
type SndQAddr = (SMPServer, SMP.SenderId)
|
||||
@@ -1118,13 +1120,13 @@ instance forall m. ConnectionModeI m => StrEncoding (ConnectionRequestUri m) whe
|
||||
instance StrEncoding AConnectionRequestUri where
|
||||
strEncode (ACR _ cr) = strEncode cr
|
||||
strP = do
|
||||
crScheme <- strP
|
||||
_crScheme :: ConnReqScheme <- strP
|
||||
crMode <- A.char '/' *> crModeP <* optional (A.char '/') <* "#/?"
|
||||
query <- strP
|
||||
crAgentVRange <- queryParam "v" query
|
||||
crSmpQueues <- queryParam "smp" query
|
||||
let crClientData = safeDecodeUtf8 <$> queryParamStr "data" query
|
||||
let crData = ConnReqUriData {crScheme, crAgentVRange, crSmpQueues, crClientData}
|
||||
let crData = ConnReqUriData {crScheme = CRSSimplex, crAgentVRange, crSmpQueues, crClientData}
|
||||
case crMode of
|
||||
CMInvitation -> do
|
||||
crE2eParams <- queryParam "e2e" query
|
||||
@@ -1381,7 +1383,7 @@ type AgentMsgId = Int64
|
||||
|
||||
-- | Result of received message integrity validation.
|
||||
data MsgIntegrity = MsgOk | MsgError {errorInfo :: MsgErrorType}
|
||||
deriving (Eq, Show, Generic)
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding MsgIntegrity where
|
||||
strP = "OK" $> MsgOk <|> "ERR " *> (MsgError <$> strP)
|
||||
@@ -1389,20 +1391,13 @@ instance StrEncoding MsgIntegrity where
|
||||
MsgOk -> "OK"
|
||||
MsgError e -> "ERR " <> strEncode e
|
||||
|
||||
instance ToJSON MsgIntegrity where
|
||||
toJSON = J.genericToJSON $ sumTypeJSON fstToLower
|
||||
toEncoding = J.genericToEncoding $ sumTypeJSON fstToLower
|
||||
|
||||
instance FromJSON MsgIntegrity where
|
||||
parseJSON = J.genericParseJSON $ sumTypeJSON fstToLower
|
||||
|
||||
-- | Error of message integrity validation.
|
||||
data MsgErrorType
|
||||
= MsgSkipped {fromMsgId :: AgentMsgId, toMsgId :: AgentMsgId}
|
||||
| MsgBadId {msgId :: AgentMsgId}
|
||||
| MsgBadHash
|
||||
| MsgDuplicate
|
||||
deriving (Eq, Show, Generic)
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding MsgErrorType where
|
||||
strP =
|
||||
@@ -1417,13 +1412,6 @@ instance StrEncoding MsgErrorType where
|
||||
MsgBadHash -> "HASH"
|
||||
MsgDuplicate -> "DUPLICATE"
|
||||
|
||||
instance ToJSON MsgErrorType where
|
||||
toJSON = J.genericToJSON $ sumTypeJSON fstToLower
|
||||
toEncoding = J.genericToEncoding $ sumTypeJSON fstToLower
|
||||
|
||||
instance FromJSON MsgErrorType where
|
||||
parseJSON = J.genericParseJSON $ sumTypeJSON fstToLower
|
||||
|
||||
-- | Error type used in errors sent to agent clients.
|
||||
data AgentErrorType
|
||||
= -- | command or response error
|
||||
@@ -1436,6 +1424,8 @@ data AgentErrorType
|
||||
NTF {ntfErr :: ErrorType}
|
||||
| -- | XFTP protocol errors forwarded to agent clients
|
||||
XFTP {xftpErr :: XFTPErrorType}
|
||||
| -- | XRCP protocol errors forwarded to agent clients
|
||||
RCP {rcpErr :: RCErrorType}
|
||||
| -- | SMP server errors
|
||||
BROKER {brokerAddress :: String, brokerErr :: BrokerErrorType}
|
||||
| -- | errors of other agents
|
||||
@@ -1444,11 +1434,7 @@ data AgentErrorType
|
||||
INTERNAL {internalErr :: String}
|
||||
| -- | agent inactive
|
||||
INACTIVE
|
||||
deriving (Eq, Generic, Show, Exception)
|
||||
|
||||
instance ToJSON AgentErrorType where
|
||||
toJSON = J.genericToJSON $ sumTypeJSON id
|
||||
toEncoding = J.genericToEncoding $ sumTypeJSON id
|
||||
deriving (Eq, Show, Exception)
|
||||
|
||||
-- | SMP agent protocol command or response error.
|
||||
data CommandErrorType
|
||||
@@ -1462,11 +1448,7 @@ data CommandErrorType
|
||||
SIZE
|
||||
| -- | message does not fit in SMP block
|
||||
LARGE
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
|
||||
instance ToJSON CommandErrorType where
|
||||
toJSON = J.genericToJSON $ sumTypeJSON id
|
||||
toEncoding = J.genericToEncoding $ sumTypeJSON id
|
||||
deriving (Eq, Read, Show, Exception)
|
||||
|
||||
-- | Connection error.
|
||||
data ConnectionErrorType
|
||||
@@ -1480,11 +1462,7 @@ data ConnectionErrorType
|
||||
NOT_ACCEPTED
|
||||
| -- | connection not available on reply confirmation/HELLO after timeout
|
||||
NOT_AVAILABLE
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
|
||||
instance ToJSON ConnectionErrorType where
|
||||
toJSON = J.genericToJSON $ sumTypeJSON id
|
||||
toEncoding = J.genericToEncoding $ sumTypeJSON id
|
||||
deriving (Eq, Read, Show, Exception)
|
||||
|
||||
-- | SMP server errors.
|
||||
data BrokerErrorType
|
||||
@@ -1500,11 +1478,7 @@ data BrokerErrorType
|
||||
TRANSPORT {transportErr :: TransportError}
|
||||
| -- | command response timeout
|
||||
TIMEOUT
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
|
||||
instance ToJSON BrokerErrorType where
|
||||
toJSON = J.genericToJSON $ sumTypeJSON id
|
||||
toEncoding = J.genericToEncoding $ sumTypeJSON id
|
||||
deriving (Eq, Read, Show, Exception)
|
||||
|
||||
-- | Errors of another SMP agent.
|
||||
data SMPAgentError
|
||||
@@ -1521,7 +1495,7 @@ data SMPAgentError
|
||||
A_DUPLICATE
|
||||
| -- | error in the message to add/delete/etc queue in connection
|
||||
A_QUEUE {queueErr :: String}
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
deriving (Eq, Read, Show, Exception)
|
||||
|
||||
data AgentCryptoError
|
||||
= -- | AES decryption error
|
||||
@@ -1534,11 +1508,9 @@ data AgentCryptoError
|
||||
RATCHET_EARLIER Word32
|
||||
| -- | too many skipped messages
|
||||
RATCHET_SKIPPED Word32
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
|
||||
instance ToJSON AgentCryptoError where
|
||||
toJSON = J.genericToJSON $ sumTypeJSON id
|
||||
toEncoding = J.genericToEncoding $ sumTypeJSON id
|
||||
| -- | ratchet synchronization error
|
||||
RATCHET_SYNC
|
||||
deriving (Eq, Read, Show, Exception)
|
||||
|
||||
instance StrEncoding AgentCryptoError where
|
||||
strP =
|
||||
@@ -1547,16 +1519,14 @@ instance StrEncoding AgentCryptoError where
|
||||
<|> "RATCHET_HEADER" $> RATCHET_HEADER
|
||||
<|> "RATCHET_EARLIER " *> (RATCHET_EARLIER <$> strP)
|
||||
<|> "RATCHET_SKIPPED " *> (RATCHET_SKIPPED <$> strP)
|
||||
<|> "RATCHET_SYNC" $> RATCHET_SYNC
|
||||
strEncode = \case
|
||||
DECRYPT_AES -> "DECRYPT_AES"
|
||||
DECRYPT_CB -> "DECRYPT_CB"
|
||||
RATCHET_HEADER -> "RATCHET_HEADER"
|
||||
RATCHET_EARLIER n -> "RATCHET_EARLIER " <> strEncode n
|
||||
RATCHET_SKIPPED n -> "RATCHET_SKIPPED " <> strEncode n
|
||||
|
||||
instance ToJSON SMPAgentError where
|
||||
toJSON = J.genericToJSON $ sumTypeJSON id
|
||||
toEncoding = J.genericToEncoding $ sumTypeJSON id
|
||||
RATCHET_SYNC -> "RATCHET_SYNC"
|
||||
|
||||
instance StrEncoding AgentErrorType where
|
||||
strP =
|
||||
@@ -1565,6 +1535,7 @@ instance StrEncoding AgentErrorType where
|
||||
<|> "SMP " *> (SMP <$> strP)
|
||||
<|> "NTF " *> (NTF <$> strP)
|
||||
<|> "XFTP " *> (XFTP <$> strP)
|
||||
<|> "RCP " *> (RCP <$> strP)
|
||||
<|> "BROKER " *> (BROKER <$> textP <* " RESPONSE " <*> (RESPONSE <$> textP))
|
||||
<|> "BROKER " *> (BROKER <$> textP <* " TRANSPORT " <*> (TRANSPORT <$> transportErrorP))
|
||||
<|> "BROKER " *> (BROKER <$> textP <* A.space <*> parseRead1)
|
||||
@@ -1581,6 +1552,7 @@ instance StrEncoding AgentErrorType where
|
||||
SMP e -> "SMP " <> strEncode e
|
||||
NTF e -> "NTF " <> strEncode e
|
||||
XFTP e -> "XFTP " <> strEncode e
|
||||
RCP e -> "RCP " <> strEncode e
|
||||
BROKER srv (RESPONSE e) -> "BROKER " <> text srv <> " RESPONSE " <> text e
|
||||
BROKER srv (TRANSPORT e) -> "BROKER " <> text srv <> " TRANSPORT " <> serializeTransportError e
|
||||
BROKER srv e -> "BROKER " <> text srv <> " " <> bshow e
|
||||
@@ -1592,18 +1564,6 @@ instance StrEncoding AgentErrorType where
|
||||
where
|
||||
text = encodeUtf8 . T.pack
|
||||
|
||||
instance Arbitrary AgentErrorType where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary CommandErrorType where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary ConnectionErrorType where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary BrokerErrorType where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary SMPAgentError where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary AgentCryptoError where arbitrary = genericArbitraryU
|
||||
|
||||
cryptoErrToSyncState :: AgentCryptoError -> RatchetSyncState
|
||||
cryptoErrToSyncState = \case
|
||||
DECRYPT_AES -> RSAllowed
|
||||
@@ -1611,6 +1571,7 @@ cryptoErrToSyncState = \case
|
||||
RATCHET_HEADER -> RSRequired
|
||||
RATCHET_EARLIER _ -> RSAllowed
|
||||
RATCHET_SKIPPED _ -> RSRequired
|
||||
RATCHET_SYNC -> RSRequired
|
||||
|
||||
-- | SMP agent command and response parser for commands passed via network (only parses binary length)
|
||||
networkCommandP :: Parser ACmd
|
||||
@@ -1647,6 +1608,7 @@ instance StrEncoding ACmdTag where
|
||||
"SENT" -> ct SENT_
|
||||
"MERR" -> ct MERR_
|
||||
"MSG" -> ct MSG_
|
||||
"MSGNTF" -> ct MSGNTF_
|
||||
"ACK" -> t ACK_
|
||||
"RCVD" -> ct RCVD_
|
||||
"SWCH" -> t SWCH_
|
||||
@@ -1702,6 +1664,7 @@ instance (APartyI p, AEntityI e) => StrEncoding (ACommandTag p e) where
|
||||
SENT_ -> "SENT"
|
||||
MERR_ -> "MERR"
|
||||
MSG_ -> "MSG"
|
||||
MSGNTF_ -> "MSGNTF"
|
||||
ACK_ -> "ACK"
|
||||
RCVD_ -> "RCVD"
|
||||
SWCH_ -> "SWCH"
|
||||
@@ -1770,6 +1733,7 @@ commandP binaryP =
|
||||
SENT_ -> s (SENT <$> A.decimal)
|
||||
MERR_ -> s (MERR <$> A.decimal <* A.space <*> strP)
|
||||
MSG_ -> s (MSG <$> strP <* A.space <*> smpP <* A.space <*> binaryP)
|
||||
MSGNTF_ -> s (MSGNTF <$> strP)
|
||||
RCVD_ -> s (RCVD <$> strP <* A.space <*> strP)
|
||||
DEL_RCVQ_ -> s (DEL_RCVQ <$> strP_ <*> strP_ <*> strP)
|
||||
DEL_CONN_ -> pure DEL_CONN
|
||||
@@ -1824,6 +1788,7 @@ serializeCommand = \case
|
||||
SENT mId -> s (SENT_, Str $ bshow mId)
|
||||
MERR mId e -> s (MERR_, Str $ bshow mId, e)
|
||||
MSG msgMeta msgFlags msgBody -> B.unwords [s MSG_, s msgMeta, smpEncode msgFlags, serializeBinary msgBody]
|
||||
MSGNTF smpMsgMeta -> s (MSGNTF_, smpMsgMeta)
|
||||
ACK mId rcptInfo_ -> s (ACK_, Str $ bshow mId) <> maybe "" (B.cons ' ' . serializeBinary) rcptInfo_
|
||||
RCVD msgMeta rcpts -> s (RCVD_, msgMeta, rcpts)
|
||||
SWCH -> s SWCH_
|
||||
@@ -1929,3 +1894,25 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody
|
||||
unless (B.null s) $ throwError $ CMD SIZE
|
||||
pure body
|
||||
Nothing -> return . Left $ CMD SYNTAX
|
||||
|
||||
$(J.deriveJSON defaultJSON ''RcvQueueInfo)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''SndQueueInfo)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''ConnectionStats)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON fstToLower) ''MsgErrorType)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON fstToLower) ''MsgIntegrity)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON id) ''CommandErrorType)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON id) ''ConnectionErrorType)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON id) ''BrokerErrorType)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON id) ''AgentCryptoError)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON id) ''SMPAgentError)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON id) ''AgentErrorType)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.Messaging.Agent.RetryInterval
|
||||
|
||||
@@ -23,7 +23,7 @@ import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore)
|
||||
import Simplex.Messaging.Transport (ATransport (..), TProxy, Transport (..), simplexMQVersion)
|
||||
import Simplex.Messaging.Transport.Server (loadTLSServerParams, runTransportServer, defaultTransportServerConfig)
|
||||
import Simplex.Messaging.Transport.Server (defaultTransportServerConfig, loadTLSServerParams, runTransportServer)
|
||||
import Simplex.Messaging.Util (bshow)
|
||||
import UnliftIO.Async (race_)
|
||||
import qualified UnliftIO.Exception as E
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedRecordDot #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
@@ -27,7 +26,6 @@ import qualified Data.List.NonEmpty as L
|
||||
import Data.Maybe (isJust)
|
||||
import Data.Time (UTCTime)
|
||||
import Data.Type.Equality
|
||||
import GHC.Records (HasField)
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.RetryInterval (RI2State)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
@@ -182,16 +180,34 @@ switchingRQ = find $ isJust . rcvSwchStatus
|
||||
{-# INLINE switchingRQ #-}
|
||||
|
||||
updatedQs :: SMPQueueRec q => q -> NonEmpty q -> NonEmpty q
|
||||
updatedQs q = L.map $ \q' -> if q.dbQueueId == q'.dbQueueId then q else q'
|
||||
updatedQs q = L.map $ \q' -> if dbQId q == dbQId q' then q else q'
|
||||
{-# INLINE updatedQs #-}
|
||||
|
||||
type SMPQueueRec q =
|
||||
( SMPQueue q,
|
||||
HasField "userId" q UserId,
|
||||
HasField "connId" q ConnId,
|
||||
HasField "dbQueueId" q Int64,
|
||||
HasField "dbReplaceQueueId" q (Maybe Int64)
|
||||
)
|
||||
class SMPQueue q => SMPQueueRec q where
|
||||
qUserId :: q -> UserId
|
||||
qConnId :: q -> ConnId
|
||||
dbQId :: q -> Int64
|
||||
dbReplaceQId :: q -> Maybe Int64
|
||||
|
||||
instance SMPQueueRec RcvQueue where
|
||||
qUserId RcvQueue {userId} = userId
|
||||
{-# INLINE qUserId #-}
|
||||
qConnId RcvQueue {connId} = connId
|
||||
{-# INLINE qConnId #-}
|
||||
dbQId RcvQueue {dbQueueId} = dbQueueId
|
||||
{-# INLINE dbQId #-}
|
||||
dbReplaceQId RcvQueue {dbReplaceQueueId} = dbReplaceQueueId
|
||||
{-# INLINE dbReplaceQId #-}
|
||||
|
||||
instance SMPQueueRec SndQueue where
|
||||
qUserId SndQueue {userId} = userId
|
||||
{-# INLINE qUserId #-}
|
||||
qConnId SndQueue {connId} = connId
|
||||
{-# INLINE qConnId #-}
|
||||
dbQId SndQueue {dbQueueId} = dbQueueId
|
||||
{-# INLINE dbQId #-}
|
||||
dbReplaceQId SndQueue {dbReplaceQueueId} = dbReplaceQueueId
|
||||
{-# INLINE dbReplaceQId #-}
|
||||
|
||||
-- * Connection types
|
||||
|
||||
@@ -576,6 +592,8 @@ data StoreError
|
||||
SEServerNotFound
|
||||
| -- | Connection already used.
|
||||
SEConnDuplicate
|
||||
| -- | Confirmed snd queue already exists.
|
||||
SESndQueueExists
|
||||
| -- | Wrong connection type, e.g. "send" connection when "receive" or "duplex" is expected, or vice versa.
|
||||
-- 'upgradeRcvConnToDuplex' and 'upgradeSndConnToDuplex' do not allow duplex connections - they would also return this error.
|
||||
SEBadConnType ConnType
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{-# LANGUAGE ConstraintKinds #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
@@ -10,18 +10,17 @@
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE OverloadedRecordDot #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.SQLite
|
||||
( SQLiteStore (..),
|
||||
@@ -31,7 +30,11 @@ module Simplex.Messaging.Agent.Store.SQLite
|
||||
createSQLiteStore,
|
||||
connectSQLiteStore,
|
||||
closeSQLiteStore,
|
||||
openSQLiteStore,
|
||||
reopenSQLiteStore,
|
||||
sqlString,
|
||||
keyString,
|
||||
storeKey,
|
||||
execSQL,
|
||||
upMigration, -- used in tests
|
||||
|
||||
@@ -217,11 +220,12 @@ where
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Class
|
||||
import Crypto.Random (ChaChaDRG, randomBytesGenerate)
|
||||
import Data.Aeson (ToJSON)
|
||||
import qualified Data.Aeson as J
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import qualified Data.Aeson.TH as J
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bifunctor (second)
|
||||
import Data.ByteArray (ScrubbedBytes)
|
||||
import qualified Data.ByteArray as BA
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString.Base64.URL as U
|
||||
import Data.Char (toLower)
|
||||
@@ -245,7 +249,6 @@ import Database.SQLite.Simple.FromField
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import qualified Database.SQLite3 as SQLite3
|
||||
import GHC.Generics (Generic)
|
||||
import Network.Socket (ServiceName)
|
||||
import Simplex.FileTransfer.Client (XFTPChunkSpec (..))
|
||||
import Simplex.FileTransfer.Description
|
||||
@@ -265,17 +268,17 @@ import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfSubscriptionId, NtfTknStatus (..), NtfTokenId, SMPQueueNtf (..))
|
||||
import Simplex.Messaging.Notifications.Types
|
||||
import Simplex.Messaging.Parsers (blobFieldParser, dropPrefix, fromTextField_, sumTypeJSON)
|
||||
import Simplex.Messaging.Parsers (blobFieldParser, defaultJSON, dropPrefix, fromTextField_, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||
import Simplex.Messaging.Util (bshow, eitherToMaybe, groupOn, ($>>=), (<$$>))
|
||||
import Simplex.Messaging.Util (bshow, eitherToMaybe, groupOn, ifM, safeDecodeUtf8, ($>>=), (<$$>))
|
||||
import Simplex.Messaging.Version
|
||||
import System.Directory (copyFile, createDirectoryIfMissing, doesFileExist)
|
||||
import System.Exit (exitFailure)
|
||||
import System.FilePath (takeDirectory)
|
||||
import System.IO (hFlush, stdout)
|
||||
import UnliftIO.Exception (onException)
|
||||
import UnliftIO.Exception (bracketOnError, onException)
|
||||
import qualified UnliftIO.Exception as E
|
||||
import UnliftIO.STM
|
||||
|
||||
@@ -285,7 +288,7 @@ data MigrationError
|
||||
= MEUpgrade {upMigrations :: [UpMigration]}
|
||||
| MEDowngrade {downMigrations :: [String]}
|
||||
| MigrationError {mtrError :: MTRError}
|
||||
deriving (Eq, Show, Generic)
|
||||
deriving (Eq, Show)
|
||||
|
||||
migrationErrorDescription :: MigrationError -> String
|
||||
migrationErrorDescription = \case
|
||||
@@ -295,20 +298,12 @@ migrationErrorDescription = \case
|
||||
"Database version is newer than the app.\nConfirm to back up and downgrade using these migrations: " <> intercalate ", " dms
|
||||
MigrationError err -> mtrErrorDescription err
|
||||
|
||||
instance ToJSON MigrationError where
|
||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "ME"
|
||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "ME"
|
||||
|
||||
data UpMigration = UpMigration {upName :: String, withDown :: Bool}
|
||||
deriving (Eq, Show, Generic)
|
||||
deriving (Eq, Show)
|
||||
|
||||
upMigration :: Migration -> UpMigration
|
||||
upMigration Migration {name, down} = UpMigration name $ isJust down
|
||||
|
||||
instance ToJSON UpMigration where
|
||||
toJSON = J.genericToJSON J.defaultOptions
|
||||
toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
data MigrationConfirmation = MCYesUp | MCYesUpDown | MCConsole | MCError
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -326,11 +321,11 @@ instance StrEncoding MigrationConfirmation where
|
||||
"error" -> pure MCError
|
||||
_ -> fail "invalid MigrationConfirmation"
|
||||
|
||||
createSQLiteStore :: FilePath -> String -> [Migration] -> MigrationConfirmation -> IO (Either MigrationError SQLiteStore)
|
||||
createSQLiteStore dbFilePath dbKey migrations confirmMigrations = do
|
||||
createSQLiteStore :: FilePath -> ScrubbedBytes -> Bool -> [Migration] -> MigrationConfirmation -> IO (Either MigrationError SQLiteStore)
|
||||
createSQLiteStore dbFilePath dbKey keepKey migrations confirmMigrations = do
|
||||
let dbDir = takeDirectory dbFilePath
|
||||
createDirectoryIfMissing True dbDir
|
||||
st <- connectSQLiteStore dbFilePath dbKey
|
||||
st <- connectSQLiteStore dbFilePath dbKey keepKey
|
||||
r <- migrateSchema st migrations confirmMigrations `onException` closeSQLiteStore st
|
||||
case r of
|
||||
Right () -> pure $ Right st
|
||||
@@ -347,10 +342,10 @@ migrateSchema st migrations confirmMigrations = do
|
||||
Right ms@(MTRUp ums)
|
||||
| dbNew st -> Migrations.run st ms $> Right ()
|
||||
| otherwise -> case confirmMigrations of
|
||||
MCYesUp -> run ms
|
||||
MCYesUpDown -> run ms
|
||||
MCConsole -> confirm err >> run ms
|
||||
MCError -> pure $ Left err
|
||||
MCYesUp -> run ms
|
||||
MCYesUpDown -> run ms
|
||||
MCConsole -> confirm err >> run ms
|
||||
MCError -> pure $ Left err
|
||||
where
|
||||
err = MEUpgrade $ map upMigration ums -- "The app has a newer version than the database.\nConfirm to back up and upgrade using these migrations: " <> intercalate ", " (map name ums)
|
||||
Right ms@(MTRDown dms) -> case confirmMigrations of
|
||||
@@ -376,15 +371,17 @@ confirmOrExit s = do
|
||||
ok <- getLine
|
||||
when (map toLower ok /= "y") exitFailure
|
||||
|
||||
connectSQLiteStore :: FilePath -> String -> IO SQLiteStore
|
||||
connectSQLiteStore dbFilePath dbKey = do
|
||||
connectSQLiteStore :: FilePath -> ScrubbedBytes -> Bool -> IO SQLiteStore
|
||||
connectSQLiteStore dbFilePath key keepKey = do
|
||||
dbNew <- not <$> doesFileExist dbFilePath
|
||||
dbConn <- dbBusyLoop $ connectDB dbFilePath dbKey
|
||||
dbConnVar <- newTMVarIO dbConn
|
||||
dbEncrypted <- newTVarIO . not $ null dbKey
|
||||
pure SQLiteStore {dbFilePath, dbEncrypted, dbConnection = dbConnVar, dbNew}
|
||||
dbConn <- dbBusyLoop (connectDB dbFilePath key)
|
||||
atomically $ do
|
||||
dbConnection <- newTMVar dbConn
|
||||
dbKey <- newTVar $! storeKey key keepKey
|
||||
dbClosed <- newTVar False
|
||||
pure SQLiteStore {dbFilePath, dbKey, dbConnection, dbNew, dbClosed}
|
||||
|
||||
connectDB :: FilePath -> String -> IO DB.Connection
|
||||
connectDB :: FilePath -> ScrubbedBytes -> IO DB.Connection
|
||||
connectDB path key = do
|
||||
db <- DB.open path
|
||||
prepare db `onException` DB.close db
|
||||
@@ -393,7 +390,7 @@ connectDB path key = do
|
||||
where
|
||||
prepare db = do
|
||||
let exec = SQLite3.exec $ SQL.connectionHandle $ DB.conn db
|
||||
unless (null key) . exec $ "PRAGMA key = " <> sqlString key <> ";"
|
||||
unless (BA.null key) . exec $ "PRAGMA key = " <> keyString key <> ";"
|
||||
exec . fromQuery $
|
||||
[sql|
|
||||
PRAGMA busy_timeout = 100;
|
||||
@@ -404,10 +401,42 @@ connectDB path key = do
|
||||
|]
|
||||
|
||||
closeSQLiteStore :: SQLiteStore -> IO ()
|
||||
closeSQLiteStore st = atomically (takeTMVar $ dbConnection st) >>= DB.close
|
||||
closeSQLiteStore st@SQLiteStore {dbClosed} =
|
||||
ifM (readTVarIO dbClosed) (putStrLn "closeSQLiteStore: already closed") $
|
||||
withConnection st $ \conn -> do
|
||||
DB.close conn
|
||||
atomically $ writeTVar dbClosed True
|
||||
|
||||
sqlString :: String -> Text
|
||||
sqlString s = quote <> T.replace quote "''" (T.pack s) <> quote
|
||||
openSQLiteStore :: SQLiteStore -> ScrubbedBytes -> Bool -> IO ()
|
||||
openSQLiteStore st@SQLiteStore {dbClosed} key keepKey =
|
||||
ifM (readTVarIO dbClosed) (openSQLiteStore_ st key keepKey) (putStrLn "openSQLiteStore: already opened")
|
||||
|
||||
openSQLiteStore_ :: SQLiteStore -> ScrubbedBytes -> Bool -> IO ()
|
||||
openSQLiteStore_ SQLiteStore {dbConnection, dbFilePath, dbKey, dbClosed} key keepKey =
|
||||
bracketOnError
|
||||
(atomically $ takeTMVar dbConnection)
|
||||
(atomically . tryPutTMVar dbConnection)
|
||||
$ \DB.Connection {slow} -> do
|
||||
DB.Connection {conn} <- connectDB dbFilePath key
|
||||
atomically $ do
|
||||
putTMVar dbConnection DB.Connection {conn, slow}
|
||||
writeTVar dbClosed False
|
||||
writeTVar dbKey $! storeKey key keepKey
|
||||
|
||||
reopenSQLiteStore :: SQLiteStore -> IO ()
|
||||
reopenSQLiteStore st@SQLiteStore {dbKey, dbClosed} =
|
||||
ifM (readTVarIO dbClosed) open (putStrLn "reopenSQLiteStore: already opened")
|
||||
where
|
||||
open =
|
||||
readTVarIO dbKey >>= \case
|
||||
Just key -> openSQLiteStore_ st key True
|
||||
Nothing -> fail "reopenSQLiteStore: no key"
|
||||
|
||||
keyString :: ScrubbedBytes -> Text
|
||||
keyString = sqlString . safeDecodeUtf8 . BA.convert
|
||||
|
||||
sqlString :: Text -> Text
|
||||
sqlString s = quote <> T.replace quote "''" s <> quote
|
||||
where
|
||||
quote = "'"
|
||||
|
||||
@@ -541,10 +570,23 @@ createRcvConn db gVar cData@ConnData {userId, connAgentVersion, enableNtfs, dupl
|
||||
|
||||
createSndConn :: DB.Connection -> TVar ChaChaDRG -> ConnData -> SndQueue -> IO (Either StoreError ConnId)
|
||||
createSndConn db gVar cData@ConnData {userId, connAgentVersion, enableNtfs, duplexHandshake} q@SndQueue {server} =
|
||||
createConn_ gVar cData $ \connId -> do
|
||||
serverKeyHash_ <- createServer_ db server
|
||||
DB.execute db "INSERT INTO connections (user_id, conn_id, conn_mode, smp_agent_version, enable_ntfs, duplex_handshake) VALUES (?,?,?,?,?,?)" (userId, connId, SCMInvitation, connAgentVersion, enableNtfs, duplexHandshake)
|
||||
void $ insertSndQueue_ db connId q serverKeyHash_
|
||||
-- check confirmed snd queue doesn't already exist, to prevent it being deleted by REPLACE in insertSndQueue_
|
||||
ifM (liftIO $ checkConfirmedSndQueueExists_ db q) (pure $ Left SESndQueueExists) $
|
||||
createConn_ gVar cData $ \connId -> do
|
||||
serverKeyHash_ <- createServer_ db server
|
||||
DB.execute db "INSERT INTO connections (user_id, conn_id, conn_mode, smp_agent_version, enable_ntfs, duplex_handshake) VALUES (?,?,?,?,?,?)" (userId, connId, SCMInvitation, connAgentVersion, enableNtfs, duplexHandshake)
|
||||
void $ insertSndQueue_ db connId q serverKeyHash_
|
||||
|
||||
checkConfirmedSndQueueExists_ :: DB.Connection -> SndQueue -> IO Bool
|
||||
checkConfirmedSndQueueExists_ db SndQueue {server, sndId} = do
|
||||
fromMaybe False
|
||||
<$> maybeFirstRow
|
||||
fromOnly
|
||||
( DB.query
|
||||
db
|
||||
"SELECT 1 FROM snd_queues WHERE host = ? AND port = ? AND snd_id = ? AND status != ? LIMIT 1"
|
||||
(host server, port server, sndId, New)
|
||||
)
|
||||
|
||||
getRcvConn :: DB.Connection -> SMPServer -> SMP.RecipientId -> IO (Either StoreError (RcvQueue, SomeConn))
|
||||
getRcvConn db ProtocolServer {host, port} rcvId = runExceptT $ do
|
||||
@@ -960,7 +1002,7 @@ updatePendingMsgRIState db connId msgId RI2State {slowInterval, fastInterval} =
|
||||
getPendingMsgs :: DB.Connection -> ConnId -> SndQueue -> IO [InternalId]
|
||||
getPendingMsgs db connId SndQueue {dbQueueId} =
|
||||
map fromOnly
|
||||
<$> DB.query db "SELECT internal_id FROM snd_message_deliveries WHERE conn_id = ? AND snd_queue_id = ?" (connId, dbQueueId)
|
||||
<$> DB.query db "SELECT internal_id FROM snd_message_deliveries WHERE conn_id = ? AND snd_queue_id = ? ORDER BY internal_id ASC" (connId, dbQueueId)
|
||||
|
||||
deletePendingMsgs :: DB.Connection -> ConnId -> SndQueue -> IO ()
|
||||
deletePendingMsgs db connId SndQueue {dbQueueId} =
|
||||
@@ -1059,12 +1101,12 @@ countPendingSndDeliveries_ db connId msgId = do
|
||||
|
||||
deleteRcvMsgHashesExpired :: DB.Connection -> NominalDiffTime -> IO ()
|
||||
deleteRcvMsgHashesExpired db ttl = do
|
||||
cutoffTs <- addUTCTime (- ttl) <$> getCurrentTime
|
||||
cutoffTs <- addUTCTime (-ttl) <$> getCurrentTime
|
||||
DB.execute db "DELETE FROM encrypted_rcv_message_hashes WHERE created_at < ?" (Only cutoffTs)
|
||||
|
||||
deleteSndMsgsExpired :: DB.Connection -> NominalDiffTime -> IO ()
|
||||
deleteSndMsgsExpired db ttl = do
|
||||
cutoffTs <- addUTCTime (- ttl) <$> getCurrentTime
|
||||
cutoffTs <- addUTCTime (-ttl) <$> getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
"DELETE FROM messages WHERE internal_ts < ? AND internal_snd_id IS NOT NULL"
|
||||
@@ -1143,7 +1185,7 @@ getSkippedMsgKeys :: DB.Connection -> ConnId -> IO SkippedMsgKeys
|
||||
getSkippedMsgKeys db connId =
|
||||
skipped <$> DB.query db "SELECT header_key, msg_n, msg_key FROM skipped_messages WHERE conn_id = ?" (Only connId)
|
||||
where
|
||||
skipped ms = foldl' addSkippedKey M.empty ms
|
||||
skipped = foldl' addSkippedKey M.empty
|
||||
addSkippedKey smks (hk, msgN, mk) = M.alter (Just . addMsgKey) hk smks
|
||||
where
|
||||
addMsgKey = maybe (M.singleton msgN mk) (M.insert msgN mk)
|
||||
@@ -1714,15 +1756,15 @@ getAnyConn deleted' dbConn connId =
|
||||
Just (cData@ConnData {deleted}, cMode)
|
||||
| deleted /= deleted' -> pure $ Left SEConnNotFound
|
||||
| otherwise -> do
|
||||
rQ <- getRcvQueuesByConnId_ dbConn connId
|
||||
sQ <- getSndQueuesByConnId_ dbConn connId
|
||||
pure $ case (rQ, sQ, cMode) of
|
||||
(Just rqs, Just sqs, CMInvitation) -> Right $ SomeConn SCDuplex (DuplexConnection cData rqs sqs)
|
||||
(Just (rq :| _), Nothing, CMInvitation) -> Right $ SomeConn SCRcv (RcvConnection cData rq)
|
||||
(Nothing, Just (sq :| _), CMInvitation) -> Right $ SomeConn SCSnd (SndConnection cData sq)
|
||||
(Just (rq :| _), Nothing, CMContact) -> Right $ SomeConn SCContact (ContactConnection cData rq)
|
||||
(Nothing, Nothing, _) -> Right $ SomeConn SCNew (NewConnection cData)
|
||||
_ -> Left SEConnNotFound
|
||||
rQ <- getRcvQueuesByConnId_ dbConn connId
|
||||
sQ <- getSndQueuesByConnId_ dbConn connId
|
||||
pure $ case (rQ, sQ, cMode) of
|
||||
(Just rqs, Just sqs, CMInvitation) -> Right $ SomeConn SCDuplex (DuplexConnection cData rqs sqs)
|
||||
(Just (rq :| _), Nothing, CMInvitation) -> Right $ SomeConn SCRcv (RcvConnection cData rq)
|
||||
(Nothing, Just (sq :| _), CMInvitation) -> Right $ SomeConn SCSnd (SndConnection cData sq)
|
||||
(Just (rq :| _), Nothing, CMContact) -> Right $ SomeConn SCContact (ContactConnection cData rq)
|
||||
(Nothing, Nothing, _) -> Right $ SomeConn SCNew (NewConnection cData)
|
||||
_ -> Left SEConnNotFound
|
||||
|
||||
getConns :: DB.Connection -> [ConnId] -> IO [Either StoreError SomeConn]
|
||||
getConns = getAnyConns_ False
|
||||
@@ -1784,7 +1826,7 @@ checkRatchetKeyHashExists db connId hash = do
|
||||
|
||||
deleteRatchetKeyHashesExpired :: DB.Connection -> NominalDiffTime -> IO ()
|
||||
deleteRatchetKeyHashesExpired db ttl = do
|
||||
cutoffTs <- addUTCTime (- ttl) <$> getCurrentTime
|
||||
cutoffTs <- addUTCTime (-ttl) <$> getCurrentTime
|
||||
DB.execute db "DELETE FROM processed_ratchet_key_hashes WHERE created_at < ?" (Only cutoffTs)
|
||||
|
||||
-- | returns all connection queues, the first queue is the primary one
|
||||
@@ -1948,7 +1990,7 @@ insertRcvMsgDetails_ db connId RcvQueue {dbQueueId} RcvMsgData {msgMeta, interna
|
||||
DB.execute db "INSERT INTO encrypted_rcv_message_hashes (conn_id, hash) VALUES (?,?)" (connId, encryptedMsgHash)
|
||||
|
||||
updateHashRcv_ :: DB.Connection -> ConnId -> RcvMsgData -> IO ()
|
||||
updateHashRcv_ dbConn connId RcvMsgData {msgMeta, internalHash, internalRcvId} =
|
||||
updateHashRcv_ dbConn connId RcvMsgData {msgMeta = MsgMeta {sndMsgId}, internalHash, internalRcvId} =
|
||||
DB.executeNamed
|
||||
dbConn
|
||||
-- last_internal_rcv_msg_id equality check prevents race condition in case next id was reserved
|
||||
@@ -1959,7 +2001,7 @@ updateHashRcv_ dbConn connId RcvMsgData {msgMeta, internalHash, internalRcvId} =
|
||||
WHERE conn_id = :conn_id
|
||||
AND last_internal_rcv_msg_id = :last_internal_rcv_msg_id;
|
||||
|]
|
||||
[ ":last_external_snd_msg_id" := msgMeta.sndMsgId,
|
||||
[ ":last_external_snd_msg_id" := sndMsgId,
|
||||
":last_rcv_msg_hash" := internalHash,
|
||||
":conn_id" := connId,
|
||||
":last_internal_rcv_msg_id" := internalRcvId
|
||||
@@ -2064,7 +2106,7 @@ createWithRandomId gVar create = tryCreate 3
|
||||
| otherwise -> pure . Left . SEInternal $ bshow e
|
||||
|
||||
randomId :: TVar ChaChaDRG -> Int -> IO ByteString
|
||||
randomId gVar n = U.encode <$> (atomically . stateTVar gVar $ randomBytesGenerate n)
|
||||
randomId gVar n = atomically $ U.encode <$> C.pseudoRandomBytes n gVar
|
||||
|
||||
ntfSubAndSMPAction :: NtfSubAction -> (Maybe NtfSubNTFAction, Maybe NtfSubSMPAction)
|
||||
ntfSubAndSMPAction (NtfSubNTFAction action) = (Just action, Nothing)
|
||||
@@ -2233,7 +2275,7 @@ deleteRcvFile' db rcvFileId =
|
||||
|
||||
getNextRcvChunkToDownload :: DB.Connection -> XFTPServer -> NominalDiffTime -> IO (Maybe RcvFileChunk)
|
||||
getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = do
|
||||
cutoffTs <- addUTCTime (- ttl) <$> getCurrentTime
|
||||
cutoffTs <- addUTCTime (-ttl) <$> getCurrentTime
|
||||
maybeFirstRow toChunk $
|
||||
DB.query
|
||||
db
|
||||
@@ -2270,7 +2312,7 @@ getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = d
|
||||
|
||||
getNextRcvFileToDecrypt :: DB.Connection -> NominalDiffTime -> IO (Maybe RcvFile)
|
||||
getNextRcvFileToDecrypt db ttl = do
|
||||
cutoffTs <- addUTCTime (- ttl) <$> getCurrentTime
|
||||
cutoffTs <- addUTCTime (-ttl) <$> getCurrentTime
|
||||
fileId_ :: Maybe DBRcvFileId <-
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
@@ -2288,7 +2330,7 @@ getNextRcvFileToDecrypt db ttl = do
|
||||
|
||||
getPendingRcvFilesServers :: DB.Connection -> NominalDiffTime -> IO [XFTPServer]
|
||||
getPendingRcvFilesServers db ttl = do
|
||||
cutoffTs <- addUTCTime (- ttl) <$> getCurrentTime
|
||||
cutoffTs <- addUTCTime (-ttl) <$> getCurrentTime
|
||||
map toXFTPServer
|
||||
<$> DB.query
|
||||
db
|
||||
@@ -2330,7 +2372,7 @@ getCleanupRcvFilesDeleted db =
|
||||
|
||||
getRcvFilesExpired :: DB.Connection -> NominalDiffTime -> IO [(DBRcvFileId, RcvFileId, FilePath)]
|
||||
getRcvFilesExpired db ttl = do
|
||||
cutoffTs <- addUTCTime (- ttl) <$> getCurrentTime
|
||||
cutoffTs <- addUTCTime (-ttl) <$> getCurrentTime
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
@@ -2438,7 +2480,7 @@ getChunkReplicaRecipients_ db replicaId =
|
||||
|
||||
getNextSndFileToPrepare :: DB.Connection -> NominalDiffTime -> IO (Maybe SndFile)
|
||||
getNextSndFileToPrepare db ttl = do
|
||||
cutoffTs <- addUTCTime (- ttl) <$> getCurrentTime
|
||||
cutoffTs <- addUTCTime (-ttl) <$> getCurrentTime
|
||||
fileId_ :: Maybe DBSndFileId <-
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
@@ -2519,7 +2561,7 @@ createSndFileReplica db SndFileChunk {sndChunkId} NewSndChunkReplica {server, re
|
||||
|
||||
getNextSndChunkToUpload :: DB.Connection -> XFTPServer -> NominalDiffTime -> IO (Maybe SndFileChunk)
|
||||
getNextSndChunkToUpload db server@ProtocolServer {host, port, keyHash} ttl = do
|
||||
cutoffTs <- addUTCTime (- ttl) <$> getCurrentTime
|
||||
cutoffTs <- addUTCTime (-ttl) <$> getCurrentTime
|
||||
chunk_ <-
|
||||
maybeFirstRow toChunk $
|
||||
DB.query
|
||||
@@ -2588,7 +2630,7 @@ updateSndChunkReplicaStatus db replicaId status = do
|
||||
|
||||
getPendingSndFilesServers :: DB.Connection -> NominalDiffTime -> IO [XFTPServer]
|
||||
getPendingSndFilesServers db ttl = do
|
||||
cutoffTs <- addUTCTime (- ttl) <$> getCurrentTime
|
||||
cutoffTs <- addUTCTime (-ttl) <$> getCurrentTime
|
||||
map toXFTPServer
|
||||
<$> DB.query
|
||||
db
|
||||
@@ -2627,7 +2669,7 @@ getCleanupSndFilesDeleted db =
|
||||
|
||||
getSndFilesExpired :: DB.Connection -> NominalDiffTime -> IO [(DBSndFileId, SndFileId, Maybe FilePath)]
|
||||
getSndFilesExpired db ttl = do
|
||||
cutoffTs <- addUTCTime (- ttl) <$> getCurrentTime
|
||||
cutoffTs <- addUTCTime (-ttl) <$> getCurrentTime
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
@@ -2667,7 +2709,7 @@ getDeletedSndChunkReplica db deletedSndChunkReplicaId =
|
||||
|
||||
getNextDeletedSndChunkReplica :: DB.Connection -> XFTPServer -> NominalDiffTime -> IO (Maybe DeletedSndChunkReplica)
|
||||
getNextDeletedSndChunkReplica db ProtocolServer {host, port, keyHash} ttl = do
|
||||
cutoffTs <- addUTCTime (- ttl) <$> getCurrentTime
|
||||
cutoffTs <- addUTCTime (-ttl) <$> getCurrentTime
|
||||
replicaId_ :: Maybe Int64 <-
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
@@ -2696,7 +2738,7 @@ deleteDeletedSndChunkReplica db deletedSndChunkReplicaId =
|
||||
|
||||
getPendingDelFilesServers :: DB.Connection -> NominalDiffTime -> IO [XFTPServer]
|
||||
getPendingDelFilesServers db ttl = do
|
||||
cutoffTs <- addUTCTime (- ttl) <$> getCurrentTime
|
||||
cutoffTs <- addUTCTime (-ttl) <$> getCurrentTime
|
||||
map toXFTPServer
|
||||
<$> DB.query
|
||||
db
|
||||
@@ -2711,5 +2753,9 @@ getPendingDelFilesServers db ttl = do
|
||||
|
||||
deleteDeletedSndChunkReplicasExpired :: DB.Connection -> NominalDiffTime -> IO ()
|
||||
deleteDeletedSndChunkReplicasExpired db ttl = do
|
||||
cutoffTs <- addUTCTime (- ttl) <$> getCurrentTime
|
||||
cutoffTs <- addUTCTime (-ttl) <$> getCurrentTime
|
||||
DB.execute db "DELETE FROM deleted_snd_chunk_replicas WHERE created_at < ?" (Only cutoffTs)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''UpMigration)
|
||||
|
||||
$(J.deriveToJSON (sumTypeJSON $ dropPrefix "ME") ''MigrationError)
|
||||
|
||||
@@ -10,10 +10,13 @@ module Simplex.Messaging.Agent.Store.SQLite.Common
|
||||
withTransaction',
|
||||
withTransactionCtx,
|
||||
dbBusyLoop,
|
||||
storeKey,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Data.ByteArray (ScrubbedBytes)
|
||||
import qualified Data.ByteArray as BA
|
||||
import Data.Time.Clock (diffUTCTime, getCurrentTime)
|
||||
import Database.SQLite.Simple (SQLError)
|
||||
import qualified Database.SQLite.Simple as SQL
|
||||
@@ -23,10 +26,14 @@ import UnliftIO.Exception (bracket)
|
||||
import qualified UnliftIO.Exception as E
|
||||
import UnliftIO.STM
|
||||
|
||||
storeKey :: ScrubbedBytes -> Bool -> Maybe ScrubbedBytes
|
||||
storeKey key keepKey = if keepKey || BA.null key then Just key else Nothing
|
||||
|
||||
data SQLiteStore = SQLiteStore
|
||||
{ dbFilePath :: FilePath,
|
||||
dbEncrypted :: TVar Bool,
|
||||
dbKey :: TVar (Maybe ScrubbedBytes),
|
||||
dbConnection :: TMVar DB.Connection,
|
||||
dbClosed :: TVar Bool,
|
||||
dbNew :: Bool
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE StrictData #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.SQLite.DB
|
||||
( Connection (..),
|
||||
@@ -19,13 +20,12 @@ where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad (when)
|
||||
import Data.Aeson (ToJSON (..))
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.TH as J
|
||||
import Data.Int (Int64)
|
||||
import Data.Time (diffUTCTime, getCurrentTime)
|
||||
import Database.SQLite.Simple (FromRow, NamedParam, Query, ToRow)
|
||||
import qualified Database.SQLite.Simple as SQL
|
||||
import GHC.Generics (Generic)
|
||||
import Simplex.Messaging.Parsers (defaultJSON)
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Util (diffToMilliseconds)
|
||||
@@ -40,9 +40,7 @@ data SlowQueryStats = SlowQueryStats
|
||||
timeMax :: Int64,
|
||||
timeAvg :: Int64
|
||||
}
|
||||
deriving (Show, Generic)
|
||||
|
||||
instance ToJSON SlowQueryStats where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
deriving (Show)
|
||||
|
||||
timeIt :: TMap Query SlowQueryStats -> Query -> IO a -> IO a
|
||||
timeIt slow sql a = do
|
||||
@@ -50,7 +48,7 @@ timeIt slow sql a = do
|
||||
r <- a
|
||||
t' <- getCurrentTime
|
||||
let diff = diffToMilliseconds $ diffUTCTime t' t
|
||||
atomically $ when (diff > 50) $ TM.alter (updateQueryStats diff) sql slow
|
||||
atomically $ when (diff > 5) $ TM.alter (updateQueryStats diff) sql slow
|
||||
pure r
|
||||
where
|
||||
updateQueryStats :: Int64 -> Maybe SlowQueryStats -> Maybe SlowQueryStats
|
||||
@@ -99,3 +97,5 @@ query_ Connection {conn, slow} sql = timeIt slow sql $ SQL.query_ conn sql
|
||||
queryNamed :: FromRow r => Connection -> Query -> [NamedParam] -> IO [r]
|
||||
queryNamed Connection {conn, slow} sql = timeIt slow sql . SQL.queryNamed conn sql
|
||||
{-# INLINE queryNamed #-}
|
||||
|
||||
$(J.deriveJSON defaultJSON ''SlowQueryStats)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
@@ -27,8 +26,7 @@ module Simplex.Messaging.Agent.Store.SQLite.Migrations
|
||||
where
|
||||
|
||||
import Control.Monad (forM_, when)
|
||||
import Data.Aeson (ToJSON)
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.TH as J
|
||||
import Data.List (intercalate, sortOn)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import qualified Data.Map as M
|
||||
@@ -40,7 +38,6 @@ import Database.SQLite.Simple (Connection, Only (..), Query (..))
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
import qualified Database.SQLite3 as SQLite3
|
||||
import GHC.Generics (Generic)
|
||||
import Simplex.Messaging.Agent.Protocol (extraSMPServerHosts)
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Common
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220101_initial
|
||||
@@ -169,11 +166,7 @@ data MigrationsToRun = MTRUp [Migration] | MTRDown [DownMigration] | MTRNone
|
||||
data MTRError
|
||||
= MTRENoDown {dbMigrations :: [String]}
|
||||
| MTREDifferent {appMigration :: String, dbMigration :: String}
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
instance ToJSON MTRError where
|
||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "MTRE"
|
||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "MTRE"
|
||||
deriving (Eq, Show)
|
||||
|
||||
mtrErrorDescription :: MTRError -> String
|
||||
mtrErrorDescription = \case
|
||||
@@ -192,3 +185,5 @@ migrationsToRun [] dbMs
|
||||
migrationsToRun (a : as) (d : ds)
|
||||
| name a == name d = migrationsToRun as ds
|
||||
| otherwise = Left $ MTREDifferent (name a) (name d)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON $ dropPrefix "MTRE") ''MTRError)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
module Simplex.Messaging.Agent.TRcvQueues
|
||||
( TRcvQueues (getRcvQueues),
|
||||
empty,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
@@ -10,6 +9,7 @@
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
@@ -86,8 +86,7 @@ import Control.Exception
|
||||
import Control.Monad
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
import Control.Monad.Trans.Except
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.TH as J
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Functor (($>))
|
||||
@@ -97,13 +96,12 @@ import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Time.Clock (UTCTime, getCurrentTime)
|
||||
import GHC.Generics (Generic)
|
||||
import Network.Socket (ServiceName)
|
||||
import Numeric.Natural
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, enumJSON)
|
||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON)
|
||||
import Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
@@ -190,14 +188,7 @@ data HostMode
|
||||
HMOnion
|
||||
| -- | prefer (or require) public hosts
|
||||
HMPublic
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
instance FromJSON HostMode where
|
||||
parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "HM"
|
||||
|
||||
instance ToJSON HostMode where
|
||||
toJSON = J.genericToJSON . enumJSON $ dropPrefix "HM"
|
||||
toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "HM"
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | network configuration for the client
|
||||
data NetworkConfig = NetworkConfig
|
||||
@@ -223,21 +214,10 @@ data NetworkConfig = NetworkConfig
|
||||
smpPingCount :: Int,
|
||||
logTLSErrors :: Bool
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
instance ToJSON NetworkConfig where
|
||||
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
|
||||
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data TransportSessionMode = TSMUser | TSMEntity
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
instance ToJSON TransportSessionMode where
|
||||
toJSON = J.genericToJSON . enumJSON $ dropPrefix "TSM"
|
||||
toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "TSM"
|
||||
|
||||
instance FromJSON TransportSessionMode where
|
||||
parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "TSM"
|
||||
deriving (Eq, Show)
|
||||
|
||||
defaultNetworkConfig :: NetworkConfig
|
||||
defaultNetworkConfig =
|
||||
@@ -246,9 +226,9 @@ defaultNetworkConfig =
|
||||
hostMode = HMOnionViaSocks,
|
||||
requiredHostMode = False,
|
||||
sessionMode = TSMUser,
|
||||
tcpConnectTimeout = 15_000_000,
|
||||
tcpTimeout = 10_000_000,
|
||||
tcpTimeoutPerKb = 20_000, -- 20ms, should be less than 130ms to avoid Int overflow on 32 bit systems
|
||||
tcpConnectTimeout = 20_000_000,
|
||||
tcpTimeout = 15_000_000,
|
||||
tcpTimeoutPerKb = 45_000, -- 45ms, should be less than 130ms to avoid Int overflow on 32 bit systems
|
||||
tcpKeepAlive = Just defaultKeepAliveOpts,
|
||||
smpPingInterval = 600_000_000, -- 10min
|
||||
smpPingCount = 3,
|
||||
@@ -257,7 +237,7 @@ defaultNetworkConfig =
|
||||
|
||||
transportClientConfig :: NetworkConfig -> TransportClientConfig
|
||||
transportClientConfig NetworkConfig {socksProxy, tcpKeepAlive, logTLSErrors} =
|
||||
TransportClientConfig {socksProxy, tcpKeepAlive, logTLSErrors}
|
||||
TransportClientConfig {socksProxy, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing}
|
||||
|
||||
-- | protocol client configuration.
|
||||
data ProtocolClientConfig = ProtocolClientConfig
|
||||
@@ -429,11 +409,11 @@ getProtocolClient transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize,
|
||||
where
|
||||
response entityId
|
||||
| entityId == entId =
|
||||
case respOrErr of
|
||||
Left e -> Left $ PCEResponseError e
|
||||
Right r -> case protocolError r of
|
||||
Just e -> Left $ PCEProtocolError e
|
||||
_ -> Right r
|
||||
case respOrErr of
|
||||
Left e -> Left $ PCEResponseError e
|
||||
Right r -> case protocolError r of
|
||||
Just e -> Left $ PCEProtocolError e
|
||||
_ -> Right r
|
||||
| otherwise = Left . PCEUnexpectedResponse $ bshow respOrErr
|
||||
sendMsg :: Either err msg -> IO ()
|
||||
sendMsg = \case
|
||||
@@ -661,13 +641,13 @@ sendProtocolCommands c@ProtocolClient {batch, blockSize} cs = do
|
||||
validate rs
|
||||
| diff == 0 = pure $ L.fromList rs
|
||||
| diff > 0 = do
|
||||
putStrLn "send error: fewer responses than expected"
|
||||
pure $ L.fromList $ rs <> replicate diff (Response "" $ Left $ PCETransportError TEBadBlock)
|
||||
putStrLn "send error: fewer responses than expected"
|
||||
pure $ L.fromList $ rs <> replicate diff (Response "" $ Left $ PCETransportError TEBadBlock)
|
||||
| otherwise = do
|
||||
putStrLn "send error: more responses than expected"
|
||||
pure $ L.fromList $ take (L.length cs) rs
|
||||
where
|
||||
diff = L.length cs - length rs
|
||||
putStrLn "send error: more responses than expected"
|
||||
pure $ L.fromList $ take (L.length cs) rs
|
||||
where
|
||||
diff = L.length cs - length rs
|
||||
|
||||
streamProtocolCommands :: forall err msg. ProtocolEncoding err (ProtoCommand msg) => ProtocolClient err msg -> NonEmpty (ClientCommand msg) -> ([Response err msg] -> IO ()) -> IO ()
|
||||
streamProtocolCommands c@ProtocolClient {batch, blockSize} cs cb = do
|
||||
@@ -688,8 +668,8 @@ sendBatch c@ProtocolClient {client_ = PClient {sndQ}} b = do
|
||||
(: []) <$> getResponse c r
|
||||
|
||||
data ClientBatch err msg
|
||||
-- ByteString in CBTransmissions does not include count byte, it is added by tEncodeBatch
|
||||
= CBTransmissions ByteString Int [Request err msg]
|
||||
= -- ByteString in CBTransmissions does not include count byte, it is added by tEncodeBatch
|
||||
CBTransmissions ByteString Int [Request err msg]
|
||||
| CBTransmission ByteString (Request err msg)
|
||||
| CBLargeTransmission (Request err msg)
|
||||
|
||||
@@ -713,9 +693,9 @@ batchClientTransmissions batch blkSize
|
||||
encodeBatch :: ByteString -> Int -> [Request err msg] -> NonEmpty (PCTransmission err msg) -> (ClientBatch err msg, Maybe (NonEmpty (PCTransmission err msg)))
|
||||
encodeBatch s n rs ts@((t, r) :| ts_)
|
||||
| B.length s' <= blkSize - 3 && n < 255 =
|
||||
case L.nonEmpty ts_ of
|
||||
Just ts' -> encodeBatch s' n' rs' ts'
|
||||
Nothing -> (CBTransmissions s' n' (reverse rs'), Nothing)
|
||||
case L.nonEmpty ts_ of
|
||||
Just ts' -> encodeBatch s' n' rs' ts'
|
||||
Nothing -> (CBTransmissions s' n' (reverse rs'), Nothing)
|
||||
| n == 0 = (CBLargeTransmission r, L.nonEmpty ts_)
|
||||
| otherwise = (CBTransmissions s n (reverse rs), Just ts)
|
||||
where
|
||||
@@ -765,3 +745,9 @@ mkTransmission ProtocolClient {sessionId, thVersion, client_ = PClient {clientCo
|
||||
r <- Request entId <$> newEmptyTMVar
|
||||
TM.insert corrId r sentCommands
|
||||
pure r
|
||||
|
||||
$(J.deriveJSON (enumJSON $ dropPrefix "HM") ''HostMode)
|
||||
|
||||
$(J.deriveJSON (enumJSON $ dropPrefix "TSM") ''TransportSessionMode)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''NetworkConfig)
|
||||
|
||||
@@ -18,7 +18,7 @@ import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Monad.Trans.Except
|
||||
import Data.Bifunctor (first, bimap)
|
||||
import Data.Bifunctor (bimap, first)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Either (partitionEithers)
|
||||
@@ -36,11 +36,11 @@ import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Client
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (BrokerMsg, ProtocolServer (..), QueueId, SMPServer, NtfPrivateSignKey, NotifierId, RcvPrivateSignKey, RecipientId)
|
||||
import Simplex.Messaging.Protocol (BrokerMsg, NotifierId, NtfPrivateSignKey, ProtocolServer (..), QueueId, RcvPrivateSignKey, RecipientId, SMPServer)
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Util (catchAll_, ($>>=), toChunks)
|
||||
import Simplex.Messaging.Util (catchAll_, toChunks, ($>>=))
|
||||
import System.Timeout (timeout)
|
||||
import UnliftIO (async)
|
||||
import UnliftIO.Exception (Exception)
|
||||
@@ -238,7 +238,7 @@ getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, msgQ} srv =
|
||||
(tempErrs, finalErrs) = partition (temporaryClientError . snd) errs
|
||||
mapM_ (atomically . addSubscription ca srv) oks
|
||||
mapM_ (liftIO . notify . CAResubscribed srv) $ L.nonEmpty $ map fst oks
|
||||
mapM_ (atomically . removePendingSubscription ca srv . fst) finalErrs
|
||||
mapM_ (atomically . removePendingSubscription ca srv . fst) finalErrs
|
||||
mapM_ (liftIO . notify . CASubError srv) $ L.nonEmpty finalErrs
|
||||
mapM_ (throwE . snd) $ listToMaybe tempErrs
|
||||
|
||||
@@ -281,7 +281,7 @@ subscribeQueue ca srv sub = do
|
||||
|
||||
handleErr e = do
|
||||
atomically . when (e /= PCENetworkError && e /= PCEResponseTimeout) $
|
||||
removePendingSubscription ca srv $ fst sub
|
||||
removePendingSubscription ca srv (fst sub)
|
||||
throwE e
|
||||
|
||||
subscribeQueuesSMP :: SMPClientAgent -> SMPServer -> NonEmpty (RecipientId, RcvPrivateSignKey) -> IO (NonEmpty (RecipientId, Either SMPClientError ()))
|
||||
@@ -300,14 +300,15 @@ subscribeQueues_ party ca srv subs = do
|
||||
smpSubscribeQueues :: SMPSubParty -> SMPClientAgent -> SMPClient -> SMPServer -> NonEmpty (QueueId, C.APrivateSignKey) -> IO (NonEmpty (QueueId, Either SMPClientError ()))
|
||||
smpSubscribeQueues party ca smp srv subs = do
|
||||
rs <- L.zip subs <$> subscribe smp (L.map swap subs)
|
||||
atomically $ forM rs $ \(sub, r) -> (fst sub,) <$> case r of
|
||||
Right () -> do
|
||||
addSubscription ca srv $ first (party,) sub
|
||||
pure $ Right ()
|
||||
Left e -> do
|
||||
when (e /= PCENetworkError && e /= PCEResponseTimeout) $
|
||||
removePendingSubscription ca srv $ (party,) $ fst sub
|
||||
pure $ Left e
|
||||
atomically $ forM rs $ \(sub, r) ->
|
||||
(fst sub,) <$> case r of
|
||||
Right () -> do
|
||||
addSubscription ca srv $ first (party,) sub
|
||||
pure $ Right ()
|
||||
Left e -> do
|
||||
when (e /= PCENetworkError && e /= PCEResponseTimeout) $
|
||||
removePendingSubscription ca srv (party, fst sub)
|
||||
pure $ Left e
|
||||
where
|
||||
subscribe = case party of
|
||||
SPRecipient -> subscribeSMPQueues
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
@@ -40,6 +41,8 @@ module Simplex.Messaging.Crypto
|
||||
DhAlgorithm,
|
||||
PrivateKey (..),
|
||||
PublicKey (..),
|
||||
PrivateKeyEd25519,
|
||||
PublicKeyEd25519,
|
||||
PrivateKeyX25519,
|
||||
PublicKeyX25519,
|
||||
PrivateKeyX448,
|
||||
@@ -64,6 +67,8 @@ module Simplex.Messaging.Crypto
|
||||
generateDhKeyPair,
|
||||
privateToX509,
|
||||
publicKey,
|
||||
signatureKeyPair,
|
||||
publicToX509,
|
||||
|
||||
-- * key encoding/decoding
|
||||
encodePubKey,
|
||||
@@ -80,6 +85,7 @@ module Simplex.Messaging.Crypto
|
||||
SignatureAlgorithm,
|
||||
AlgorithmI (..),
|
||||
sign,
|
||||
sign',
|
||||
verify,
|
||||
verify',
|
||||
validSignatureSize,
|
||||
@@ -111,6 +117,8 @@ module Simplex.Messaging.Crypto
|
||||
cbEncrypt,
|
||||
cbEncryptMaxLenBS,
|
||||
cbDecrypt,
|
||||
sbDecrypt_,
|
||||
sbEncrypt_,
|
||||
cbNonce,
|
||||
randomCbNonce,
|
||||
pseudoRandomCbNonce,
|
||||
@@ -135,6 +143,16 @@ module Simplex.Messaging.Crypto
|
||||
pad,
|
||||
unPad,
|
||||
|
||||
-- * X509 Certificates
|
||||
SignedCertificate,
|
||||
Certificate,
|
||||
signCertificate,
|
||||
signX509,
|
||||
certificateFingerprint,
|
||||
signedFingerprint,
|
||||
SignatureAlgorithmX509 (..),
|
||||
SignedObject (..),
|
||||
|
||||
-- * Cryptography error type
|
||||
CryptoError (..),
|
||||
|
||||
@@ -156,7 +174,7 @@ import Crypto.Cipher.AES (AES256)
|
||||
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)
|
||||
import Crypto.Hash (Digest, SHA256 (..), SHA512, hash)
|
||||
import qualified Crypto.MAC.Poly1305 as Poly1305
|
||||
import qualified Crypto.PubKey.Curve25519 as X25519
|
||||
import qualified Crypto.PubKey.Curve448 as X448
|
||||
@@ -183,6 +201,7 @@ import Data.Type.Equality
|
||||
import Data.Typeable (Proxy (Proxy), Typeable)
|
||||
import Data.Word (Word32)
|
||||
import Data.X509
|
||||
import Data.X509.Validation (Fingerprint (..), getFingerprint)
|
||||
import Database.SQLite.Simple.FromField (FromField (..))
|
||||
import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import GHC.TypeLits (ErrorMessage (..), KnownNat, Nat, TypeError, natVal, type (+))
|
||||
@@ -263,6 +282,8 @@ instance Eq APublicKey where
|
||||
|
||||
deriving instance Show APublicKey
|
||||
|
||||
type PublicKeyEd25519 = PublicKey Ed25519
|
||||
|
||||
type PublicKeyX25519 = PublicKey X25519
|
||||
|
||||
type PublicKeyX448 = PublicKey X448
|
||||
@@ -278,6 +299,10 @@ deriving instance Eq (PrivateKey a)
|
||||
|
||||
deriving instance Show (PrivateKey a)
|
||||
|
||||
-- Do not enable, to avoid leaking key data
|
||||
-- instance StrEncoding (PrivateKey Ed25519) where
|
||||
|
||||
-- Used in notification store log
|
||||
instance StrEncoding (PrivateKey X25519) where
|
||||
strEncode = strEncode . encodePrivKey
|
||||
{-# INLINE strEncode #-}
|
||||
@@ -296,6 +321,8 @@ instance Eq APrivateKey where
|
||||
|
||||
deriving instance Show APrivateKey
|
||||
|
||||
type PrivateKeyEd25519 = PrivateKey Ed25519
|
||||
|
||||
type PrivateKeyX25519 = PrivateKey X25519
|
||||
|
||||
type PrivateKeyX448 = PrivateKey X448
|
||||
@@ -538,6 +565,10 @@ publicKey = \case
|
||||
PrivateKeyX25519 _ k -> PublicKeyX25519 k
|
||||
PrivateKeyX448 _ k -> PublicKeyX448 k
|
||||
|
||||
-- | Expand signature private key to a key pair.
|
||||
signatureKeyPair :: APrivateSignKey -> ASignatureKeyPair
|
||||
signatureKeyPair ak@(APrivateSignKey a k) = (APublicVerifyKey a (publicKey k), ak)
|
||||
|
||||
encodePrivKey :: CryptoPrivateKey pk => pk -> ByteString
|
||||
encodePrivKey = toPrivKey $ encodeASNObj . privateToX509
|
||||
|
||||
@@ -649,13 +680,17 @@ class CryptoSignature s where
|
||||
signatureBytes :: s -> ByteString
|
||||
decodeSignature :: ByteString -> Either String s
|
||||
|
||||
instance CryptoSignature (Signature s) => StrEncoding (Signature s) where
|
||||
strEncode = serializeSignature
|
||||
strDecode = decodeSignature
|
||||
|
||||
instance CryptoSignature ASignature where
|
||||
signatureBytes (ASignature _ sig) = signatureBytes sig
|
||||
decodeSignature s
|
||||
| B.length s == Ed25519.signatureSize =
|
||||
ASignature SEd25519 . SignatureEd25519 <$> ed Ed25519.signature s
|
||||
ASignature SEd25519 . SignatureEd25519 <$> ed Ed25519.signature s
|
||||
| B.length s == Ed448.signatureSize =
|
||||
ASignature SEd448 . SignatureEd448 <$> ed Ed448.signature s
|
||||
ASignature SEd448 . SignatureEd448 <$> ed Ed448.signature s
|
||||
| otherwise = Left "bad signature size"
|
||||
where
|
||||
ed alg = first show . CE.eitherCryptoError . alg
|
||||
@@ -804,6 +839,13 @@ instance StrEncoding KeyHash where
|
||||
strEncode = strEncode . unKeyHash
|
||||
strP = KeyHash <$> strP
|
||||
|
||||
instance ToJSON KeyHash where
|
||||
toEncoding = strToJEncoding
|
||||
toJSON = strToJSON
|
||||
|
||||
instance FromJSON KeyHash where
|
||||
parseJSON = strParseJSON "KeyHash"
|
||||
|
||||
instance IsString KeyHash where
|
||||
fromString = parseString $ parseAll strP
|
||||
|
||||
@@ -967,6 +1009,53 @@ sign' (PrivateKeyEd448 pk k) msg = SignatureEd448 $ Ed448.sign pk k msg
|
||||
sign :: APrivateSignKey -> ByteString -> ASignature
|
||||
sign (APrivateSignKey a k) = ASignature a . sign' k
|
||||
|
||||
signCertificate :: APrivateSignKey -> Certificate -> SignedCertificate
|
||||
signCertificate = signX509
|
||||
|
||||
signX509 :: (ASN1Object o, Eq o, Show o) => APrivateSignKey -> o -> SignedExact o
|
||||
signX509 key = fst . objectToSignedExact f
|
||||
where
|
||||
f bytes =
|
||||
( signatureBytes $ sign key bytes,
|
||||
signatureAlgorithmX509 key,
|
||||
()
|
||||
)
|
||||
|
||||
certificateFingerprint :: SignedCertificate -> KeyHash
|
||||
certificateFingerprint = signedFingerprint
|
||||
|
||||
signedFingerprint :: (ASN1Object o, Eq o, Show o) => SignedExact o -> KeyHash
|
||||
signedFingerprint o = KeyHash fp
|
||||
where
|
||||
Fingerprint fp = getFingerprint o HashSHA256
|
||||
|
||||
class SignatureAlgorithmX509 a where
|
||||
signatureAlgorithmX509 :: a -> SignatureALG
|
||||
|
||||
instance SignatureAlgorithm a => SignatureAlgorithmX509 (SAlgorithm a) where
|
||||
signatureAlgorithmX509 = \case
|
||||
SEd25519 -> SignatureALG_IntrinsicHash PubKeyALG_Ed25519
|
||||
SEd448 -> SignatureALG_IntrinsicHash PubKeyALG_Ed448
|
||||
|
||||
instance SignatureAlgorithmX509 APrivateSignKey where
|
||||
signatureAlgorithmX509 (APrivateSignKey a _) = signatureAlgorithmX509 a
|
||||
|
||||
instance SignatureAlgorithmX509 APublicVerifyKey where
|
||||
signatureAlgorithmX509 (APublicVerifyKey a _) = signatureAlgorithmX509 a
|
||||
|
||||
-- | An instance for 'ASignatureKeyPair' / ('PublicKeyType' pk, pk), without touching its type family.
|
||||
instance SignatureAlgorithmX509 pk => SignatureAlgorithmX509 (a, pk) where
|
||||
signatureAlgorithmX509 = signatureAlgorithmX509 . snd
|
||||
|
||||
-- | A wrapper to marshall signed ASN1 objects, like certificates.
|
||||
newtype SignedObject a = SignedObject (SignedExact a)
|
||||
|
||||
instance (Typeable a, Eq a, Show a, ASN1Object a) => FromField (SignedObject a) where
|
||||
fromField = fmap SignedObject . blobFieldDecoder decodeSignedObject
|
||||
|
||||
instance (Eq a, Show a, ASN1Object a) => ToField (SignedObject a) where
|
||||
toField (SignedObject s) = toField $ encodeSignedObject s
|
||||
|
||||
-- | Signature verification.
|
||||
--
|
||||
-- Used by SMP servers to authorize SMP commands and by SMP agents to verify messages.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
module Simplex.Messaging.Crypto.File
|
||||
( CryptoFile (..),
|
||||
@@ -24,19 +23,18 @@ where
|
||||
import Control.Exception
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Data.Aeson (FromJSON, ToJSON)
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.TH as J
|
||||
import qualified Data.ByteArray as BA
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy as LB
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import Data.Maybe (isJust)
|
||||
import GHC.Generics (Generic)
|
||||
import Simplex.Messaging.Client.Agent ()
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.Lazy (LazyByteString)
|
||||
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||
import Simplex.Messaging.Parsers (defaultJSON)
|
||||
import Simplex.Messaging.Util (liftEitherWith)
|
||||
import System.Directory (getFileSize)
|
||||
import UnliftIO (Handle, IOMode (..), liftIO)
|
||||
@@ -45,16 +43,10 @@ import UnliftIO.STM
|
||||
|
||||
-- Possibly encrypted local file
|
||||
data CryptoFile = CryptoFile {filePath :: FilePath, cryptoArgs :: Maybe CryptoFileArgs}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
instance ToJSON CryptoFile where
|
||||
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
||||
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data CryptoFileArgs = CFArgs {fileKey :: C.SbKey, fileNonce :: C.CbNonce}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
instance ToJSON CryptoFileArgs where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
deriving (Eq, Show)
|
||||
|
||||
data CryptoFileHandle = CFHandle Handle (Maybe (TVar LC.SbState))
|
||||
|
||||
@@ -124,3 +116,7 @@ getFileContentsSize :: CryptoFile -> IO Integer
|
||||
getFileContentsSize (CryptoFile path cfArgs) = do
|
||||
size <- getFileSize path
|
||||
pure $ if isJust cfArgs then size - fromIntegral C.authTagSize else size
|
||||
|
||||
$(J.deriveJSON defaultJSON ''CryptoFileArgs)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''CryptoFile)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{-# LANGUAGE BangPatterns #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
@@ -15,13 +14,16 @@ module Simplex.Messaging.Crypto.Lazy
|
||||
sbEncrypt,
|
||||
sbDecrypt,
|
||||
sbEncryptTailTag,
|
||||
kcbEncryptTailTag,
|
||||
sbDecryptTailTag,
|
||||
kcbDecryptTailTag,
|
||||
fastReplicate,
|
||||
secretBox,
|
||||
secretBoxTailTag,
|
||||
SbState,
|
||||
cbInit,
|
||||
sbInit,
|
||||
kcbInit,
|
||||
sbEncryptChunk,
|
||||
sbDecryptChunk,
|
||||
sbEncryptChunkLazy,
|
||||
@@ -49,6 +51,7 @@ 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.SNTRUP761 (KEMHybridSecret (..))
|
||||
import Simplex.Messaging.Encoding
|
||||
|
||||
type LazyByteString = LB.ByteString
|
||||
@@ -90,10 +93,10 @@ unPad = fmap snd . splitLen
|
||||
splitLen :: LazyByteString -> Either CryptoError (Int64, LazyByteString)
|
||||
splitLen padded
|
||||
| LB.length lenStr == 8 = case smpDecode $ LB.toStrict lenStr of
|
||||
Right len
|
||||
| len < 0 -> Left CryptoInvalidMsgError
|
||||
| otherwise -> Right (len, LB.take len rest)
|
||||
Left _ -> Left CryptoInvalidMsgError
|
||||
Right len
|
||||
| len < 0 -> Left CryptoInvalidMsgError
|
||||
| otherwise -> Right (len, LB.take len rest)
|
||||
Left _ -> Left CryptoInvalidMsgError
|
||||
| otherwise = Left CryptoInvalidMsgError
|
||||
where
|
||||
(lenStr, rest) = LB.splitAt 8 padded
|
||||
@@ -112,10 +115,10 @@ sbDecrypt :: SbKey -> CbNonce -> LazyByteString -> Either CryptoError LazyByteSt
|
||||
sbDecrypt (SbKey key) (CbNonce nonce) packet
|
||||
| LB.length tag' < 16 = Left CBDecryptError
|
||||
| otherwise = case secretBox sbDecryptChunk key nonce c of
|
||||
Right (tag :| cs)
|
||||
| BA.constEq (LB.toStrict tag') tag -> unPad $ LB.fromChunks cs
|
||||
| otherwise -> Left CBDecryptError
|
||||
Left e -> Left e
|
||||
Right (tag :| cs)
|
||||
| BA.constEq (LB.toStrict tag') tag -> unPad $ LB.fromChunks cs
|
||||
| otherwise -> Left CBDecryptError
|
||||
Left e -> Left e
|
||||
where
|
||||
(tag', c) = LB.splitAt 16 packet
|
||||
|
||||
@@ -128,13 +131,33 @@ secretBox sbProcess secret nonce msg = run <$> sbInit_ secret nonce
|
||||
|
||||
-- | NaCl @secret_box@ lazy encrypt with a symmetric 256-bit key and 192-bit nonce with appended auth tag (more efficient with large files).
|
||||
sbEncryptTailTag :: SbKey -> CbNonce -> LazyByteString -> Int64 -> Int64 -> Either CryptoError LazyByteString
|
||||
sbEncryptTailTag (SbKey key) (CbNonce nonce) msg len paddedLen =
|
||||
sbEncryptTailTag (SbKey key) = sbEncryptTailTag_ key
|
||||
{-# INLINE sbEncryptTailTag #-}
|
||||
|
||||
-- | NaCl @crypto_box@ lazy encrypt with with a shared hybrid KEM+DH 256-bit secret and 192-bit nonce with appended auth tag (more efficient with large strings/files).
|
||||
kcbEncryptTailTag :: KEMHybridSecret -> CbNonce -> LazyByteString -> Int64 -> Int64 -> Either CryptoError LazyByteString
|
||||
kcbEncryptTailTag (KEMHybridSecret key) = sbEncryptTailTag_ key
|
||||
{-# INLINE kcbEncryptTailTag #-}
|
||||
|
||||
sbEncryptTailTag_ :: ByteArrayAccess key => key -> CbNonce -> LazyByteString -> Int64 -> Int64 -> Either CryptoError LazyByteString
|
||||
sbEncryptTailTag_ key (CbNonce nonce) msg len paddedLen =
|
||||
LB.fromChunks <$> (secretBoxTailTag sbEncryptChunk key nonce =<< pad msg len paddedLen)
|
||||
|
||||
-- | 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)
|
||||
sbDecryptTailTag (SbKey key) (CbNonce nonce) paddedLen packet =
|
||||
sbDecryptTailTag (SbKey key) = sbDecryptTailTag_ key
|
||||
{-# INLINE sbDecryptTailTag #-}
|
||||
|
||||
-- | NaCl @crypto_box@ lazy decrypt with a shared hybrid KEM+DH 256-bit secret and 192-bit nonce with appended auth tag (more efficient with large strings/files).
|
||||
-- paddedLen should NOT include the tag length, it should be the same number that is passed to sbEncrypt / sbEncryptTailTag.
|
||||
kcbDecryptTailTag :: KEMHybridSecret -> CbNonce -> Int64 -> LazyByteString -> Either CryptoError (Bool, LazyByteString)
|
||||
kcbDecryptTailTag (KEMHybridSecret key) = sbDecryptTailTag_ key
|
||||
{-# INLINE kcbDecryptTailTag #-}
|
||||
|
||||
-- paddedLen should NOT include the tag length, it should be the same number that is passed to sbEncrypt / sbEncryptTailTag.
|
||||
sbDecryptTailTag_ :: ByteArrayAccess key => key -> CbNonce -> Int64 -> LazyByteString -> Either CryptoError (Bool, LazyByteString)
|
||||
sbDecryptTailTag_ key (CbNonce nonce) paddedLen packet =
|
||||
case secretBox sbDecryptChunk key nonce c of
|
||||
Right (tag :| cs) ->
|
||||
let valid = LB.length tag' == 16 && BA.constEq (LB.toStrict tag') tag
|
||||
@@ -166,6 +189,10 @@ sbInit :: SbKey -> CbNonce -> Either CryptoError SbState
|
||||
sbInit (SbKey secret) (CbNonce nonce) = sbInit_ secret nonce
|
||||
{-# INLINE sbInit #-}
|
||||
|
||||
kcbInit :: KEMHybridSecret -> CbNonce -> Either CryptoError SbState
|
||||
kcbInit (KEMHybridSecret k) (CbNonce nonce) = sbInit_ k nonce
|
||||
{-# INLINE kcbInit #-}
|
||||
|
||||
sbInit_ :: ByteArrayAccess key => key -> ByteString -> Either CryptoError SbState
|
||||
sbInit_ secret nonce = (state2,) <$> cryptoPassed (Poly1305.initialize rs)
|
||||
where
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
@@ -20,8 +20,9 @@ import Control.Monad.Trans.Except
|
||||
import Crypto.Cipher.AES (AES256)
|
||||
import Crypto.Hash (SHA512)
|
||||
import qualified Crypto.KDF.HKDF as H
|
||||
import Data.Aeson (FromJSON, ToJSON)
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.TH as JQ
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy as LB
|
||||
@@ -33,12 +34,11 @@ import Data.Typeable (Typeable)
|
||||
import Data.Word (Word32)
|
||||
import Database.SQLite.Simple.FromField (FromField (..))
|
||||
import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import GHC.Generics
|
||||
import Simplex.Messaging.Agent.QueryString
|
||||
import Simplex.Messaging.Crypto
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (blobFieldDecoder, parseE, parseE')
|
||||
import Simplex.Messaging.Parsers (blobFieldDecoder, defaultJSON, parseE, parseE')
|
||||
import Simplex.Messaging.Version
|
||||
|
||||
currentE2EEncryptVersion :: Version
|
||||
@@ -112,11 +112,11 @@ x3dh v (sk1, rk1) dh1 dh2 dh3 =
|
||||
(hk, nhk, sk)
|
||||
-- for backwards compatibility with clients using agent version before 3.4.0
|
||||
| v == 1 =
|
||||
let (hk', rest) = B.splitAt 32 dhs
|
||||
in uncurry (hk',,) $ B.splitAt 32 rest
|
||||
let (hk', rest) = B.splitAt 32 dhs
|
||||
in uncurry (hk',,) $ B.splitAt 32 rest
|
||||
| otherwise =
|
||||
let salt = B.replicate 64 '\0'
|
||||
in hkdf3 salt dhs "SimpleXX3DH"
|
||||
let salt = B.replicate 64 '\0'
|
||||
in hkdf3 salt dhs "SimpleXX3DH"
|
||||
|
||||
type RatchetX448 = Ratchet 'X448
|
||||
|
||||
@@ -135,29 +135,20 @@ data Ratchet a = Ratchet
|
||||
rcNHKs :: HeaderKey,
|
||||
rcNHKr :: HeaderKey
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
instance AlgorithmI a => ToJSON (Ratchet a) where
|
||||
toEncoding = J.genericToEncoding J.defaultOptions
|
||||
deriving (Eq, Show)
|
||||
|
||||
data SndRatchet a = SndRatchet
|
||||
{ rcDHRr :: PublicKey a,
|
||||
rcCKs :: RatchetKey,
|
||||
rcHKs :: HeaderKey
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
instance AlgorithmI a => ToJSON (SndRatchet a) where
|
||||
toEncoding = J.genericToEncoding J.defaultOptions
|
||||
deriving (Eq, Show)
|
||||
|
||||
data RcvRatchet = RcvRatchet
|
||||
{ rcCKr :: RatchetKey,
|
||||
rcHKr :: HeaderKey
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
instance ToJSON RcvRatchet where
|
||||
toEncoding = J.genericToEncoding J.defaultOptions
|
||||
deriving (Eq, Show)
|
||||
|
||||
type SkippedMsgKeys = Map HeaderKey SkippedHdrMsgKeys
|
||||
|
||||
@@ -204,10 +195,6 @@ instance ToJSON RatchetKey where
|
||||
instance FromJSON RatchetKey where
|
||||
parseJSON = fmap RatchetKey . strParseJSON "Key"
|
||||
|
||||
instance AlgorithmI a => ToField (Ratchet a) where toField = toField . LB.toStrict . J.encode
|
||||
|
||||
instance (AlgorithmI a, Typeable a) => FromField (Ratchet a) where fromField = blobFieldDecoder J.eitherDecodeStrict'
|
||||
|
||||
instance ToField MessageKey where toField = toField . smpEncode
|
||||
|
||||
instance FromField MessageKey where fromField = blobFieldDecoder smpDecode
|
||||
@@ -428,9 +415,9 @@ rcDecrypt rc@Ratchet {rcRcv, rcAD = Str rcAD} rcMKSkipped msg' = do
|
||||
| rcNr + maxSkip < untilN = Left $ CERatchetTooManySkipped (untilN + 1 - rcNr)
|
||||
| rcNr == untilN = Right (r, M.empty)
|
||||
| otherwise =
|
||||
let (rcCKr', rcNr', mks) = advanceRcvRatchet (untilN - rcNr) rcCKr rcNr M.empty
|
||||
r' = r {rcRcv = Just rr {rcCKr = rcCKr'}, rcNr = rcNr'}
|
||||
in Right (r', M.singleton rcHKr mks)
|
||||
let (rcCKr', rcNr', mks) = advanceRcvRatchet (untilN - rcNr) rcCKr rcNr M.empty
|
||||
r' = r {rcRcv = Just rr {rcCKr = rcCKr'}, rcNr = rcNr'}
|
||||
in Right (r', M.singleton rcHKr mks)
|
||||
advanceRcvRatchet :: Word32 -> RatchetKey -> Word32 -> SkippedHdrMsgKeys -> (RatchetKey, Word32, SkippedHdrMsgKeys)
|
||||
advanceRcvRatchet 0 ck msgNs mks = (ck, msgNs, mks)
|
||||
advanceRcvRatchet n ck msgNs mks =
|
||||
@@ -493,3 +480,23 @@ hkdf3 salt ikm info = (s1, s2, s3)
|
||||
out = H.expand prk info 96
|
||||
(s1, rest) = B.splitAt 32 out
|
||||
(s2, s3) = B.splitAt 32 rest
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''RcvRatchet)
|
||||
|
||||
instance AlgorithmI a => ToJSON (SndRatchet a) where
|
||||
toEncoding = $(JQ.mkToEncoding defaultJSON ''SndRatchet)
|
||||
toJSON = $(JQ.mkToJSON defaultJSON ''SndRatchet)
|
||||
|
||||
instance AlgorithmI a => FromJSON (SndRatchet a) where
|
||||
parseJSON = $(JQ.mkParseJSON defaultJSON ''SndRatchet)
|
||||
|
||||
instance AlgorithmI a => ToJSON (Ratchet a) where
|
||||
toEncoding = $(JQ.mkToEncoding defaultJSON ''Ratchet)
|
||||
toJSON = $(JQ.mkToJSON defaultJSON ''Ratchet)
|
||||
|
||||
instance AlgorithmI a => FromJSON (Ratchet a) where
|
||||
parseJSON = $(JQ.mkParseJSON defaultJSON ''Ratchet)
|
||||
|
||||
instance AlgorithmI a => ToField (Ratchet a) where toField = toField . LB.toStrict . J.encode
|
||||
|
||||
instance (AlgorithmI a, Typeable a) => FromField (Ratchet a) where fromField = blobFieldDecoder J.eitherDecodeStrict'
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
|
||||
module Simplex.Messaging.Crypto.SNTRUP761 where
|
||||
|
||||
import Crypto.Hash (Digest, SHA256, hash)
|
||||
import Data.ByteArray (ScrubbedBytes)
|
||||
import qualified Data.ByteArray as BA
|
||||
import Data.ByteString (ByteString)
|
||||
import Simplex.Messaging.Crypto
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.SNTRUP761.Bindings
|
||||
|
||||
-- Hybrid shared secret for crypto_box is defined as SHA256(DHSecret || KEMSharedKey),
|
||||
-- similar to https://datatracker.ietf.org/doc/draft-josefsson-ntruprime-hybrid/
|
||||
|
||||
newtype KEMHybridSecret = KEMHybridSecret ScrubbedBytes
|
||||
|
||||
-- | NaCl @crypto_box@ decrypt with a shared hybrid DH + KEM secret and 192-bit nonce.
|
||||
kcbDecrypt :: KEMHybridSecret -> CbNonce -> ByteString -> Either CryptoError ByteString
|
||||
kcbDecrypt (KEMHybridSecret k) = sbDecrypt_ k
|
||||
|
||||
-- | NaCl @crypto_box@ encrypt with a shared hybrid DH + KEM secret and 192-bit nonce.
|
||||
kcbEncrypt :: KEMHybridSecret -> CbNonce -> ByteString -> Int -> Either CryptoError ByteString
|
||||
kcbEncrypt (KEMHybridSecret k) = sbEncrypt_ k
|
||||
|
||||
kemHybridSecret :: PublicKeyX25519 -> PrivateKeyX25519 -> KEMSharedKey -> KEMHybridSecret
|
||||
kemHybridSecret k pk (KEMSharedKey kem) =
|
||||
let DhSecretX25519 dh = C.dh' k pk
|
||||
in KEMHybridSecret $ BA.convert (hash $ BA.convert dh <> kem :: Digest SHA256)
|
||||
@@ -0,0 +1,82 @@
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Simplex.Messaging.Crypto.SNTRUP761.Bindings where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import Data.Bifunctor (bimap)
|
||||
import Data.ByteArray (ScrubbedBytes)
|
||||
import qualified Data.ByteArray as BA
|
||||
import Data.ByteString (ByteString)
|
||||
import Database.SQLite.Simple.FromField
|
||||
import Database.SQLite.Simple.ToField
|
||||
import Foreign (nullPtr)
|
||||
import Simplex.Messaging.Crypto.SNTRUP761.Bindings.Defines
|
||||
import Simplex.Messaging.Crypto.SNTRUP761.Bindings.FFI
|
||||
import Simplex.Messaging.Crypto.SNTRUP761.Bindings.RNG (withDRG)
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
|
||||
newtype KEMPublicKey = KEMPublicKey ByteString
|
||||
deriving (Show)
|
||||
|
||||
newtype KEMSecretKey = KEMSecretKey ScrubbedBytes
|
||||
deriving (Show)
|
||||
|
||||
newtype KEMCiphertext = KEMCiphertext ByteString
|
||||
deriving (Show)
|
||||
|
||||
newtype KEMSharedKey = KEMSharedKey ScrubbedBytes
|
||||
deriving (Show)
|
||||
|
||||
type KEMKeyPair = (KEMPublicKey, KEMSecretKey)
|
||||
|
||||
sntrup761Keypair :: TVar ChaChaDRG -> IO KEMKeyPair
|
||||
sntrup761Keypair drg =
|
||||
bimap KEMPublicKey KEMSecretKey
|
||||
<$> BA.allocRet
|
||||
c_SNTRUP761_SECRETKEY_SIZE
|
||||
( \skPtr ->
|
||||
BA.alloc c_SNTRUP761_PUBLICKEY_SIZE $ \pkPtr ->
|
||||
withDRG drg $ c_sntrup761_keypair pkPtr skPtr nullPtr
|
||||
)
|
||||
|
||||
sntrup761Enc :: TVar ChaChaDRG -> KEMPublicKey -> IO (KEMCiphertext, KEMSharedKey)
|
||||
sntrup761Enc drg (KEMPublicKey pk) =
|
||||
BA.withByteArray pk $ \pkPtr ->
|
||||
bimap KEMCiphertext KEMSharedKey
|
||||
<$> BA.allocRet
|
||||
c_SNTRUP761_SIZE
|
||||
( \kPtr ->
|
||||
BA.alloc c_SNTRUP761_CIPHERTEXT_SIZE $ \cPtr ->
|
||||
withDRG drg $ c_sntrup761_enc cPtr kPtr pkPtr nullPtr
|
||||
)
|
||||
|
||||
sntrup761Dec :: KEMCiphertext -> KEMSecretKey -> IO KEMSharedKey
|
||||
sntrup761Dec (KEMCiphertext c) (KEMSecretKey sk) =
|
||||
BA.withByteArray sk $ \skPtr ->
|
||||
BA.withByteArray c $ \cPtr ->
|
||||
KEMSharedKey
|
||||
<$> BA.alloc c_SNTRUP761_SIZE (\kPtr -> c_sntrup761_dec kPtr cPtr skPtr)
|
||||
|
||||
instance StrEncoding KEMPublicKey where
|
||||
strEncode (KEMPublicKey pk) = strEncode (BA.convert pk :: ByteString)
|
||||
strP = KEMPublicKey . BA.convert <$> strP @ByteString
|
||||
|
||||
instance Encoding KEMCiphertext where
|
||||
smpEncode (KEMCiphertext c) = smpEncode . Large $ BA.convert c
|
||||
smpP = KEMCiphertext . BA.convert . unLarge <$> smpP
|
||||
|
||||
instance ToJSON KEMPublicKey where
|
||||
toJSON = strToJSON
|
||||
toEncoding = strToJEncoding
|
||||
|
||||
instance FromJSON KEMPublicKey where
|
||||
parseJSON = strParseJSON "KEMPublicKey"
|
||||
|
||||
instance ToField KEMSharedKey where
|
||||
toField (KEMSharedKey k) = toField (BA.convert k :: ByteString)
|
||||
|
||||
instance FromField KEMSharedKey where
|
||||
fromField f = KEMSharedKey . BA.convert @ByteString <$> fromField f
|
||||
@@ -0,0 +1,15 @@
|
||||
module Simplex.Messaging.Crypto.SNTRUP761.Bindings.Defines where
|
||||
|
||||
#include "sntrup761.h"
|
||||
|
||||
c_SNTRUP761_SECRETKEY_SIZE :: Int
|
||||
c_SNTRUP761_SECRETKEY_SIZE = #{const SNTRUP761_SECRETKEY_SIZE}
|
||||
|
||||
c_SNTRUP761_PUBLICKEY_SIZE :: Int
|
||||
c_SNTRUP761_PUBLICKEY_SIZE = #{const SNTRUP761_PUBLICKEY_SIZE}
|
||||
|
||||
c_SNTRUP761_CIPHERTEXT_SIZE :: Int
|
||||
c_SNTRUP761_CIPHERTEXT_SIZE = #{const SNTRUP761_CIPHERTEXT_SIZE}
|
||||
|
||||
c_SNTRUP761_SIZE :: Int
|
||||
c_SNTRUP761_SIZE = #{const SNTRUP761_SIZE}
|
||||
@@ -0,0 +1,23 @@
|
||||
{-# LANGUAGE ForeignFunctionInterface #-}
|
||||
|
||||
module Simplex.Messaging.Crypto.SNTRUP761.Bindings.FFI
|
||||
( c_sntrup761_keypair,
|
||||
c_sntrup761_enc,
|
||||
c_sntrup761_dec,
|
||||
) where
|
||||
|
||||
import Foreign
|
||||
import Foreign.C
|
||||
import Simplex.Messaging.Crypto.SNTRUP761.Bindings.RNG (RNGContext, RNGFunc)
|
||||
|
||||
-- void sntrup761_keypair (uint8_t *pk, uint8_t *sk, void *random_ctx, sntrup761_random_func *random);
|
||||
foreign import ccall "sntrup761_keypair"
|
||||
c_sntrup761_keypair :: Ptr Word8 -> Ptr Word8 -> Ptr RNGContext -> FunPtr RNGFunc -> IO ()
|
||||
|
||||
-- void sntrup761_enc (uint8_t *c, uint8_t *k, const uint8_t *pk, void *random_ctx, sntrup761_random_func *random);
|
||||
foreign import ccall "sntrup761_enc"
|
||||
c_sntrup761_enc :: Ptr Word8 -> Ptr Word8 -> Ptr Word8 -> Ptr RNGContext -> FunPtr RNGFunc -> IO ()
|
||||
|
||||
-- void sntrup761_dec (uint8_t *k, const uint8_t *c, const uint8_t *sk);
|
||||
foreign import ccall "sntrup761_dec"
|
||||
c_sntrup761_dec :: Ptr Word8 -> Ptr Word8 -> Ptr Word8 -> IO ()
|
||||
@@ -0,0 +1,30 @@
|
||||
module Simplex.Messaging.Crypto.SNTRUP761.Bindings.RNG
|
||||
( withDRG,
|
||||
RNGContext,
|
||||
RNGFunc,
|
||||
) where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Control.Exception (bracket)
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import Data.ByteArray (ByteArrayAccess (copyByteArrayToPtr))
|
||||
import Foreign
|
||||
import Foreign.C
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
|
||||
withDRG :: TVar ChaChaDRG -> (FunPtr RNGFunc -> IO a) -> IO a
|
||||
withDRG drg = bracket (createRNGFunc drg) freeHaskellFunPtr
|
||||
|
||||
createRNGFunc :: TVar ChaChaDRG -> IO (FunPtr RNGFunc)
|
||||
createRNGFunc drg =
|
||||
mkRNGFunc $ \_ctx sz buf -> do
|
||||
bs <- atomically $ C.pseudoRandomBytes (fromIntegral sz) drg
|
||||
copyByteArrayToPtr bs buf
|
||||
|
||||
type RNGContext = ()
|
||||
|
||||
-- typedef void random_func (void *ctx, size_t length, uint8_t *dst);
|
||||
type RNGFunc = Ptr RNGContext -> CSize -> Ptr Word8 -> IO ()
|
||||
|
||||
foreign import ccall "wrapper"
|
||||
mkRNGFunc :: RNGFunc -> IO (FunPtr RNGFunc)
|
||||
@@ -11,6 +11,7 @@ module Simplex.Messaging.Encoding
|
||||
( Encoding (..),
|
||||
Tail (..),
|
||||
Large (..),
|
||||
_smpP,
|
||||
smpEncodeList,
|
||||
smpListP,
|
||||
lenEncode,
|
||||
@@ -109,7 +110,7 @@ lenP = fromIntegral . c2w <$> A.anyChar
|
||||
{-# INLINE lenP #-}
|
||||
|
||||
instance Encoding a => Encoding (Maybe a) where
|
||||
smpEncode s = maybe "0" (("1" <>) . smpEncode) s
|
||||
smpEncode = maybe "0" (("1" <>) . smpEncode)
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP =
|
||||
smpP >>= \case
|
||||
@@ -143,6 +144,9 @@ instance Encoding SystemTime where
|
||||
smpP = MkSystemTime <$> smpP <*> pure 0
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
_smpP :: Encoding a => Parser a
|
||||
_smpP = A.space *> smpP
|
||||
|
||||
-- lists encode/parse as a sequence of items prefixed with list length (as 1 byte)
|
||||
smpEncodeList :: Encoding a => [a] -> ByteString
|
||||
smpEncodeList xs = B.cons (lenEncode $ length xs) . B.concat $ map smpEncode xs
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
module Simplex.Messaging.Notifications.Protocol where
|
||||
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..), (.=))
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..), (.:), (.=))
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.Encoding as JE
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
@@ -408,6 +408,12 @@ instance ToJSON DeviceToken where
|
||||
toEncoding (DeviceToken pp t) = J.pairs $ "pushProvider" .= decodeLatin1 (strEncode pp) <> "token" .= decodeLatin1 t
|
||||
toJSON (DeviceToken pp t) = J.object ["pushProvider" .= decodeLatin1 (strEncode pp), "token" .= decodeLatin1 t]
|
||||
|
||||
instance FromJSON DeviceToken where
|
||||
parseJSON = J.withObject "DeviceToken" $ \o -> do
|
||||
pp <- strDecode . encodeUtf8 <$?> o .: "pushProvider"
|
||||
t <- encodeUtf8 <$> o .: "token"
|
||||
pure $ DeviceToken pp t
|
||||
|
||||
type NtfEntityId = ByteString
|
||||
|
||||
type NtfSubscriptionId = NtfEntityId
|
||||
@@ -510,6 +516,9 @@ instance ToJSON NtfTknStatus where
|
||||
toEncoding = JE.text . decodeLatin1 . smpEncode
|
||||
toJSON = J.String . decodeLatin1 . smpEncode
|
||||
|
||||
instance FromJSON NtfTknStatus where
|
||||
parseJSON = J.withText "NtfTknStatus" $ either fail pure . smpDecode . encodeUtf8
|
||||
|
||||
checkEntity :: forall t e e'. (NtfEntityI e, NtfEntityI e') => t e' -> Either String (t e)
|
||||
checkEntity c = case testEquality (sNtfEntity @e) (sNtfEntity @e') of
|
||||
Just Refl -> Right c
|
||||
|
||||
@@ -16,13 +16,12 @@ import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Reader
|
||||
import Data.Bifunctor (second)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Functor (($>))
|
||||
import Data.Int (Int64)
|
||||
import Data.List (intercalate, sort)
|
||||
import Data.List.NonEmpty (NonEmpty(..))
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
@@ -205,10 +204,10 @@ ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAge
|
||||
Just err -> (subs, oks, err : errs) -- permanent error, log and don't retry subscription
|
||||
Nothing -> (sub : subs, oks, errs) -- temporary error, retry subscription
|
||||
|
||||
-- | Subscribe to queues. The list of results can have a different order.
|
||||
-- \| Subscribe to queues. The list of results can have a different order.
|
||||
subscribeQueues :: SMPServer -> NonEmpty NtfSubData -> IO (NonEmpty (NtfSubData, Either SMPClientError ()))
|
||||
subscribeQueues srv subs =
|
||||
L.map (second snd) . L.zip subs <$> subscribeQueuesNtfs ca srv (L.map sub subs)
|
||||
L.zipWith (\s r -> (s, snd r)) subs <$> subscribeQueuesNtfs ca srv (L.map sub subs)
|
||||
where
|
||||
sub NtfSubData {smpQueue = SMPQueueNtf {notifierId}, notifierKey} = (notifierId, notifierKey)
|
||||
|
||||
@@ -248,10 +247,11 @@ ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAge
|
||||
forM errs (\((_, ntfId), err) -> handleSubError (SMPQueueNtf srv ntfId) err)
|
||||
>>= logSubErrors srv . catMaybes . L.toList
|
||||
|
||||
logSubStatus srv event n = when (n > 0) $
|
||||
logInfo $ "SMP server " <> event <> " " <> showServer' srv <> " (" <> tshow n <> " subscriptions)"
|
||||
logSubStatus srv event n =
|
||||
when (n > 0) . logInfo $
|
||||
"SMP server " <> event <> " " <> showServer' srv <> " (" <> tshow n <> " subscriptions)"
|
||||
|
||||
logSubErrors :: SMPServer -> [NtfSubStatus] -> M ()
|
||||
logSubErrors :: SMPServer -> [NtfSubStatus] -> M ()
|
||||
logSubErrors srv errs = forM_ (L.group $ sort errs) $ \errs' -> do
|
||||
logError $ "SMP subscription errors on server " <> showServer' srv <> ": " <> tshow (L.head errs') <> " (" <> tshow (length errs') <> " errors)"
|
||||
|
||||
@@ -289,14 +289,14 @@ ntfPush s@NtfPushServer {pushQ} = forever $ do
|
||||
case ntf of
|
||||
PNVerification _
|
||||
| status /= NTInvalid && status /= NTExpired ->
|
||||
deliverNotification pp tkn ntf >>= \case
|
||||
Right _ -> do
|
||||
status_ <- atomically $ stateTVar tknStatus $ \case
|
||||
NTActive -> (Nothing, NTActive)
|
||||
NTConfirmed -> (Nothing, NTConfirmed)
|
||||
_ -> (Just NTConfirmed, NTConfirmed)
|
||||
forM_ status_ $ \status' -> withNtfLog $ \sl -> logTokenStatus sl ntfTknId status'
|
||||
_ -> pure ()
|
||||
deliverNotification pp tkn ntf >>= \case
|
||||
Right _ -> do
|
||||
status_ <- atomically $ stateTVar tknStatus $ \case
|
||||
NTActive -> (Nothing, NTActive)
|
||||
NTConfirmed -> (Nothing, NTConfirmed)
|
||||
_ -> (Just NTConfirmed, NTConfirmed)
|
||||
forM_ status_ $ \status' -> withNtfLog $ \sl -> logTokenStatus sl ntfTknId status'
|
||||
_ -> pure ()
|
||||
| otherwise -> logError "bad notification token status"
|
||||
PNCheckMessages -> checkActiveTkn status $ do
|
||||
void $ deliverNotification pp tkn ntf
|
||||
@@ -345,7 +345,8 @@ runNtfClientTransport th@THandle {sessionId} = do
|
||||
raceAny_ ([liftIO $ send th c, client c s ps, receive th c] <> disconnectThread_ c expCfg)
|
||||
`finally` liftIO (clientDisconnected c)
|
||||
where
|
||||
disconnectThread_ c expCfg = maybe [] ((: []) . liftIO . disconnectTransport th c activeAt) expCfg
|
||||
disconnectThread_ c (Just expCfg) = [liftIO $ disconnectTransport th c activeAt expCfg]
|
||||
disconnectThread_ _ _ = []
|
||||
|
||||
clientDisconnected :: NtfServerClient -> IO ()
|
||||
clientDisconnected NtfServerClient {connected} = atomically $ writeTVar connected False
|
||||
@@ -463,16 +464,16 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
|
||||
else pure $ NRErr AUTH
|
||||
TVFY code -- this allows repeated verification for cases when client connection dropped before server response
|
||||
| (status == NTRegistered || status == NTConfirmed || status == NTActive) && tknRegCode == code -> do
|
||||
logDebug "TVFY - token verified"
|
||||
st <- asks store
|
||||
updateTknStatus tkn NTActive
|
||||
tIds <- atomically $ removeInactiveTokenRegistrations st tkn
|
||||
forM_ tIds cancelInvervalNotifications
|
||||
incNtfStat tknVerified
|
||||
pure NROk
|
||||
logDebug "TVFY - token verified"
|
||||
st <- asks store
|
||||
updateTknStatus tkn NTActive
|
||||
tIds <- atomically $ removeInactiveTokenRegistrations st tkn
|
||||
forM_ tIds cancelInvervalNotifications
|
||||
incNtfStat tknVerified
|
||||
pure NROk
|
||||
| otherwise -> do
|
||||
logDebug "TVFY - incorrect code or token status"
|
||||
pure $ NRErr AUTH
|
||||
logDebug "TVFY - incorrect code or token status"
|
||||
pure $ NRErr AUTH
|
||||
TCHK -> do
|
||||
logDebug "TCHK"
|
||||
pure $ NRTkn status
|
||||
@@ -509,16 +510,16 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
|
||||
TCRN int
|
||||
| int < 20 -> pure $ NRErr QUOTA
|
||||
| otherwise -> do
|
||||
logDebug "TCRN"
|
||||
atomically $ writeTVar tknCronInterval int
|
||||
atomically (TM.lookup tknId intervalNotifiers) >>= \case
|
||||
Nothing -> runIntervalNotifier int
|
||||
Just IntervalNotifier {interval, action} ->
|
||||
unless (interval == int) $ do
|
||||
uninterruptibleCancel action
|
||||
runIntervalNotifier int
|
||||
withNtfLog $ \s -> logTokenCron s tknId int
|
||||
pure NROk
|
||||
logDebug "TCRN"
|
||||
atomically $ writeTVar tknCronInterval int
|
||||
atomically (TM.lookup tknId intervalNotifiers) >>= \case
|
||||
Nothing -> runIntervalNotifier int
|
||||
Just IntervalNotifier {interval, action} ->
|
||||
unless (interval == int) $ do
|
||||
uninterruptibleCancel action
|
||||
runIntervalNotifier int
|
||||
withNtfLog $ \s -> logTokenCron s tknId int
|
||||
pure NROk
|
||||
where
|
||||
runIntervalNotifier interval = do
|
||||
action <- async . intervalNotifier $ fromIntegral interval * 1000000 * 60
|
||||
|
||||
@@ -34,7 +34,7 @@ import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport (ATransport)
|
||||
import Simplex.Messaging.Transport.Server (loadFingerprint, loadTLSServerParams, TransportServerConfig)
|
||||
import Simplex.Messaging.Transport.Server (TransportServerConfig, loadFingerprint, loadTLSServerParams)
|
||||
import System.IO (IOMode (..))
|
||||
import System.Mem.Weak (Weak)
|
||||
import UnliftIO.STM
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
module Simplex.Messaging.Notifications.Server.Main where
|
||||
|
||||
import Data.Either (fromRight)
|
||||
import Data.Functor (($>))
|
||||
import Data.Ini (lookupValue, readIniFile)
|
||||
import Data.Maybe (fromMaybe)
|
||||
@@ -30,7 +29,7 @@ import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
|
||||
import Text.Read (readMaybe)
|
||||
|
||||
ntfServerVersion :: String
|
||||
ntfServerVersion = "1.6.2"
|
||||
ntfServerVersion = "1.6.4"
|
||||
|
||||
defaultSMPBatchDelay :: Int
|
||||
defaultSMPBatchDelay = 10000
|
||||
@@ -92,7 +91,7 @@ ntfServerCLI cfgPath logPath =
|
||||
hSetBuffering stdout LineBuffering
|
||||
hSetBuffering stderr LineBuffering
|
||||
fp <- checkSavedFingerprint cfgPath defaultX509Config
|
||||
let host = fromRight "<hostnames>" $ T.unpack <$> lookupValue "TRANSPORT" "host" ini
|
||||
let host = either (const "<hostnames>") T.unpack $ lookupValue "TRANSPORT" "host" ini
|
||||
port = T.unpack $ strictIni "TRANSPORT" "port" ini
|
||||
cfg@NtfServerConfig {transports, storeLogFile} = serverConfig
|
||||
srv = ProtoServerWithAuth (NtfServer [THDomainName host] (if port == "443" then "" else port) (C.KeyHash fp)) Nothing
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
|
||||
|
||||
{-# HLINT ignore "Use newtype instead of data" #-}
|
||||
@@ -23,15 +23,15 @@ import qualified Crypto.Store.PKCS8 as PK
|
||||
import Data.ASN1.BinaryEncoding (DER (..))
|
||||
import Data.ASN1.Encoding
|
||||
import Data.ASN1.Types
|
||||
import Data.Aeson (FromJSON, ToJSON, (.=))
|
||||
import Data.Aeson (ToJSON, (.=))
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.Encoding as JE
|
||||
import qualified Data.Aeson.TH as JQ
|
||||
import Data.Bifunctor (first)
|
||||
import qualified Data.ByteString.Base64.URL as U
|
||||
import Data.ByteString.Builder (lazyByteString)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import Data.Int (Int64)
|
||||
import Data.Map.Strict (Map)
|
||||
import Data.Maybe (isNothing)
|
||||
@@ -40,8 +40,7 @@ import qualified Data.Text as T
|
||||
import Data.Time.Clock.System
|
||||
import qualified Data.X509 as X
|
||||
import qualified Data.X509.CertificateStore as XS
|
||||
import GHC.Generics (Generic)
|
||||
import Network.HTTP.Types (HeaderName, Status)
|
||||
import Network.HTTP.Types (Status)
|
||||
import qualified Network.HTTP.Types as N
|
||||
import Network.HTTP2.Client (Request)
|
||||
import qualified Network.HTTP2.Client as H
|
||||
@@ -49,7 +48,9 @@ import Network.Socket (HostName, ServiceName)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Notifications.Server.Push.APNS.Internal
|
||||
import Simplex.Messaging.Notifications.Server.Store (NtfTknData (..))
|
||||
import Simplex.Messaging.Parsers (defaultJSON)
|
||||
import Simplex.Messaging.Protocol (EncNMsgMeta)
|
||||
import Simplex.Messaging.Transport.HTTP2 (HTTP2Body (..))
|
||||
import Simplex.Messaging.Transport.HTTP2.Client
|
||||
@@ -61,17 +62,13 @@ data JWTHeader = JWTHeader
|
||||
{ alg :: Text, -- key algorithm, ES256 for APNS
|
||||
kid :: Text -- key ID
|
||||
}
|
||||
deriving (Show, Generic)
|
||||
|
||||
instance ToJSON JWTHeader where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
deriving (Show)
|
||||
|
||||
data JWTClaims = JWTClaims
|
||||
{ iss :: Text, -- issuer, team ID for APNS
|
||||
iat :: Int64 -- issue time, seconds from epoch
|
||||
}
|
||||
deriving (Show, Generic)
|
||||
|
||||
instance ToJSON JWTClaims where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
deriving (Show)
|
||||
|
||||
data JWTToken = JWTToken JWTHeader JWTClaims
|
||||
deriving (Show)
|
||||
@@ -83,6 +80,10 @@ mkJWTToken hdr iss = do
|
||||
|
||||
type SignedJWTToken = ByteString
|
||||
|
||||
$(JQ.deriveToJSON defaultJSON ''JWTHeader)
|
||||
|
||||
$(JQ.deriveToJSON defaultJSON ''JWTClaims)
|
||||
|
||||
signedJWTToken :: EC.PrivateKey -> JWTToken -> IO SignedJWTToken
|
||||
signedJWTToken pk (JWTToken hdr claims) = do
|
||||
let hc = jwtEncode hdr <> "." <> jwtEncode claims
|
||||
@@ -122,24 +123,13 @@ instance StrEncoding PNMessageData where
|
||||
pure PNMessageData {smpQueue, ntfTs, nmsgNonce, encNMsgMeta}
|
||||
|
||||
data APNSNotification = APNSNotification {aps :: APNSNotificationBody, notificationData :: Maybe J.Value}
|
||||
deriving (Show, Generic)
|
||||
|
||||
instance ToJSON APNSNotification where
|
||||
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
|
||||
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
||||
deriving (Show)
|
||||
|
||||
data APNSNotificationBody
|
||||
= APNSBackground {contentAvailable :: Int}
|
||||
| APNSMutableContent {mutableContent :: Int, alert :: APNSAlertBody, category :: Maybe Text}
|
||||
| APNSAlert {alert :: APNSAlertBody, badge :: Maybe Int, sound :: Maybe Text, category :: Maybe Text}
|
||||
deriving (Show, Generic)
|
||||
|
||||
apnsJSONOptions :: J.Options
|
||||
apnsJSONOptions = J.defaultOptions {J.omitNothingFields = True, J.sumEncoding = J.UntaggedValue, J.fieldLabelModifier = J.camelTo2 '-'}
|
||||
|
||||
instance ToJSON APNSNotificationBody where
|
||||
toJSON = J.genericToJSON apnsJSONOptions
|
||||
toEncoding = J.genericToEncoding apnsJSONOptions
|
||||
deriving (Show)
|
||||
|
||||
type APNSNotificationData = Map Text Text
|
||||
|
||||
@@ -305,6 +295,10 @@ apnsNotification NtfTknData {tknDhSecret} nonce paddedLen = \case
|
||||
|
||||
-- apnAlert alert = APNSAlert {alert, badge = Nothing, sound = Nothing, category = Nothing}
|
||||
|
||||
$(JQ.deriveToJSON apnsJSONOptions ''APNSNotificationBody)
|
||||
|
||||
$(JQ.deriveToJSON defaultJSON ''APNSNotification)
|
||||
|
||||
apnsRequest :: APNSPushClient -> ByteString -> APNSNotification -> IO Request
|
||||
apnsRequest c tkn ntf@APNSNotification {aps} = do
|
||||
signedJWT <- getApnsJWTToken c
|
||||
@@ -337,7 +331,8 @@ type PushProviderClient = NtfTknData -> PushNotification -> ExceptT PushProvider
|
||||
|
||||
-- this is not a newtype on purpose to have a correct JSON encoding as a record
|
||||
data APNSErrorResponse = APNSErrorResponse {reason :: Text}
|
||||
deriving (Generic, FromJSON)
|
||||
|
||||
$(JQ.deriveFromJSON defaultJSON ''APNSErrorResponse)
|
||||
|
||||
apnsPushProviderClient :: APNSPushClient -> PushProviderClient
|
||||
apnsPushProviderClient c@APNSPushClient {nonceDrg, apnsCfg} tkn@NtfTknData {token = DeviceToken _ tknStr} pn = do
|
||||
@@ -356,15 +351,15 @@ apnsPushProviderClient c@APNSPushClient {nonceDrg, apnsCfg} tkn@NtfTknData {toke
|
||||
result status reason'
|
||||
| status == Just N.ok200 = pure ()
|
||||
| status == Just N.badRequest400 =
|
||||
case reason' of
|
||||
"BadDeviceToken" -> throwError PPTokenInvalid
|
||||
"DeviceTokenNotForTopic" -> throwError PPTokenInvalid
|
||||
"TopicDisallowed" -> throwError PPPermanentError
|
||||
_ -> err status reason'
|
||||
case reason' of
|
||||
"BadDeviceToken" -> throwError PPTokenInvalid
|
||||
"DeviceTokenNotForTopic" -> throwError PPTokenInvalid
|
||||
"TopicDisallowed" -> throwError PPPermanentError
|
||||
_ -> err status reason'
|
||||
| status == Just N.forbidden403 = case reason' of
|
||||
"ExpiredProviderToken" -> throwError PPPermanentError -- there should be no point retrying it as the token was refreshed
|
||||
"InvalidProviderToken" -> throwError PPPermanentError
|
||||
_ -> err status reason'
|
||||
"ExpiredProviderToken" -> throwError PPPermanentError -- there should be no point retrying it as the token was refreshed
|
||||
"InvalidProviderToken" -> throwError PPPermanentError
|
||||
_ -> err status reason'
|
||||
| status == Just N.gone410 = throwError PPTokenInvalid
|
||||
| status == Just N.serviceUnavailable503 = liftIO (disconnectApnsHTTP2Client c) >> throwError PPRetryLater
|
||||
-- Just tooManyRequests429 -> TooManyRequests - too many requests for the same token
|
||||
@@ -372,12 +367,3 @@ apnsPushProviderClient c@APNSPushClient {nonceDrg, apnsCfg} tkn@NtfTknData {toke
|
||||
err :: Maybe Status -> Text -> ExceptT PushProviderError IO ()
|
||||
err s r = throwError $ PPResponseError s r
|
||||
liftHTTPS2 a = ExceptT $ first PPConnection <$> a
|
||||
|
||||
hApnsTopic :: HeaderName
|
||||
hApnsTopic = CI.mk "apns-topic"
|
||||
|
||||
hApnsPushType :: HeaderName
|
||||
hApnsPushType = CI.mk "apns-push-type"
|
||||
|
||||
hApnsPriority :: HeaderName
|
||||
hApnsPriority = CI.mk "apns-priority"
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Simplex.Messaging.Notifications.Server.Push.APNS.Internal where
|
||||
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import Network.HTTP.Types (HeaderName)
|
||||
import Simplex.Messaging.Parsers (defaultJSON)
|
||||
|
||||
hApnsTopic :: HeaderName
|
||||
hApnsTopic = CI.mk "apns-topic"
|
||||
|
||||
hApnsPushType :: HeaderName
|
||||
hApnsPushType = CI.mk "apns-push-type"
|
||||
|
||||
hApnsPriority :: HeaderName
|
||||
hApnsPriority = CI.mk "apns-priority"
|
||||
|
||||
apnsJSONOptions :: J.Options
|
||||
apnsJSONOptions = defaultJSON {J.sumEncoding = J.UntaggedValue, J.fieldLabelModifier = J.camelTo2 '-'}
|
||||
@@ -1,6 +1,5 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedRecordDot #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Simplex.Messaging.Notifications.Server.Stats where
|
||||
@@ -56,31 +55,31 @@ newNtfServerStats ts = do
|
||||
pure NtfServerStats {fromTime, tknCreated, tknVerified, tknDeleted, subCreated, subDeleted, ntfReceived, ntfDelivered, activeTokens, activeSubs}
|
||||
|
||||
getNtfServerStatsData :: NtfServerStats -> STM NtfServerStatsData
|
||||
getNtfServerStatsData s = do
|
||||
_fromTime <- readTVar s.fromTime
|
||||
_tknCreated <- readTVar s.tknCreated
|
||||
_tknVerified <- readTVar s.tknVerified
|
||||
_tknDeleted <- readTVar s.tknDeleted
|
||||
_subCreated <- readTVar s.subCreated
|
||||
_subDeleted <- readTVar s.subDeleted
|
||||
_ntfReceived <- readTVar s.ntfReceived
|
||||
_ntfDelivered <- readTVar s.ntfDelivered
|
||||
_activeTokens <- getPeriodStatsData s.activeTokens
|
||||
_activeSubs <- getPeriodStatsData s.activeSubs
|
||||
getNtfServerStatsData s@NtfServerStats {fromTime} = do
|
||||
_fromTime <- readTVar fromTime
|
||||
_tknCreated <- readTVar $ tknCreated s
|
||||
_tknVerified <- readTVar $ tknVerified s
|
||||
_tknDeleted <- readTVar $ tknDeleted s
|
||||
_subCreated <- readTVar $ subCreated s
|
||||
_subDeleted <- readTVar $ subDeleted s
|
||||
_ntfReceived <- readTVar $ ntfReceived s
|
||||
_ntfDelivered <- readTVar $ ntfDelivered s
|
||||
_activeTokens <- getPeriodStatsData $ activeTokens s
|
||||
_activeSubs <- getPeriodStatsData $ activeSubs s
|
||||
pure NtfServerStatsData {_fromTime, _tknCreated, _tknVerified, _tknDeleted, _subCreated, _subDeleted, _ntfReceived, _ntfDelivered, _activeTokens, _activeSubs}
|
||||
|
||||
setNtfServerStats :: NtfServerStats -> NtfServerStatsData -> STM ()
|
||||
setNtfServerStats s d = do
|
||||
writeTVar s.fromTime $! d._fromTime
|
||||
writeTVar s.tknCreated $! _tknCreated d
|
||||
writeTVar s.tknVerified $! _tknVerified d
|
||||
writeTVar s.tknDeleted $! _tknDeleted d
|
||||
writeTVar s.subCreated $! _subCreated d
|
||||
writeTVar s.subDeleted $! _subDeleted d
|
||||
writeTVar s.ntfReceived $! _ntfReceived d
|
||||
writeTVar s.ntfDelivered $! _ntfDelivered d
|
||||
setPeriodStats s.activeTokens (_activeTokens d)
|
||||
setPeriodStats s.activeSubs (_activeSubs d)
|
||||
setNtfServerStats s@NtfServerStats {fromTime} d@NtfServerStatsData {_fromTime} = do
|
||||
writeTVar fromTime $! _fromTime
|
||||
writeTVar (tknCreated s) $! _tknCreated d
|
||||
writeTVar (tknVerified s) $! _tknVerified d
|
||||
writeTVar (tknDeleted s) $! _tknDeleted d
|
||||
writeTVar (subCreated s) $! _subCreated d
|
||||
writeTVar (subDeleted s) $! _subDeleted d
|
||||
writeTVar (ntfReceived s) $! _ntfReceived d
|
||||
writeTVar (ntfDelivered s) $! _ntfDelivered d
|
||||
setPeriodStats (activeTokens s) (_activeTokens d)
|
||||
setPeriodStats (activeSubs s) (_activeSubs d)
|
||||
|
||||
instance StrEncoding NtfServerStatsData where
|
||||
strEncode NtfServerStatsData {_fromTime, _tknCreated, _tknVerified, _tknDeleted, _subCreated, _subDeleted, _ntfReceived, _ntfDelivered, _activeTokens, _activeSubs} =
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.Messaging.Notifications.Server.Store where
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE StrictData #-}
|
||||
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
|
||||
module Simplex.Messaging.Notifications.Server.StoreLog
|
||||
|
||||
@@ -50,9 +50,9 @@ ntfServerHandshake c kh ntfVRange = do
|
||||
getHandshake th >>= \case
|
||||
NtfClientHandshake {ntfVersion, keyHash}
|
||||
| keyHash /= kh ->
|
||||
throwError $ TEHandshake IDENTITY
|
||||
| ntfVersion `isCompatible` ntfVRange -> do
|
||||
pure (th :: THandle c) {thVersion = ntfVersion}
|
||||
throwError $ TEHandshake IDENTITY
|
||||
| ntfVersion `isCompatible` ntfVRange ->
|
||||
pure (th :: THandle c) {thVersion = ntfVersion}
|
||||
| otherwise -> throwError $ TEHandshake VERSION
|
||||
|
||||
-- | Notifcations server client transport handshake.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
|
||||
module Simplex.Messaging.Parsers where
|
||||
|
||||
@@ -13,6 +14,7 @@ import Data.ByteString.Base64
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Char (isAlphaNum, toLower)
|
||||
import Data.String
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock (UTCTime)
|
||||
@@ -83,7 +85,7 @@ blobFieldDecoder dec = \case
|
||||
Left e -> returnError ConversionFailed f ("couldn't parse field: " ++ e)
|
||||
f -> returnError ConversionFailed f "expecting SQLBlob column type"
|
||||
|
||||
fromTextField_ :: (Typeable a) => (Text -> Maybe a) -> Field -> Ok a
|
||||
fromTextField_ :: Typeable a => (Text -> Maybe a) -> Field -> Ok a
|
||||
fromTextField_ fromText = \case
|
||||
f@(Field (SQLText t) _) ->
|
||||
case fromText t of
|
||||
@@ -107,17 +109,21 @@ enumJSON tagModifier =
|
||||
J.allNullaryToStringTag = True
|
||||
}
|
||||
|
||||
-- used in platform-specific encoding, includes tag for single-field encoding of sum types to allow conversion to tagged objects
|
||||
sumTypeJSON :: (String -> String) -> J.Options
|
||||
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
||||
sumTypeJSON = singleFieldJSON
|
||||
sumTypeJSON = singleFieldJSON_ $ Just SingleFieldJSONTag
|
||||
#else
|
||||
sumTypeJSON = taggedObjectJSON
|
||||
#endif
|
||||
|
||||
pattern SingleFieldJSONTag :: (Eq a, IsString a) => a
|
||||
pattern SingleFieldJSONTag = "_owsf"
|
||||
|
||||
taggedObjectJSON :: (String -> String) -> J.Options
|
||||
taggedObjectJSON tagModifier =
|
||||
J.defaultOptions
|
||||
{ J.sumEncoding = J.TaggedObject "type" "data",
|
||||
{ J.sumEncoding = J.TaggedObject TaggedObjectJSONTag TaggedObjectJSONData,
|
||||
J.tagSingleConstructors = True,
|
||||
J.constructorTagModifier = tagModifier,
|
||||
J.allNullaryToStringTag = False,
|
||||
@@ -125,13 +131,26 @@ taggedObjectJSON tagModifier =
|
||||
J.omitNothingFields = True
|
||||
}
|
||||
|
||||
pattern TaggedObjectJSONTag :: (Eq a, IsString a) => a
|
||||
pattern TaggedObjectJSONTag = "type"
|
||||
|
||||
pattern TaggedObjectJSONData :: (Eq a, IsString a) => a
|
||||
pattern TaggedObjectJSONData = "data"
|
||||
|
||||
-- used in platform independent encoding, doesn't include tag for single-field encoding of sum types
|
||||
singleFieldJSON :: (String -> String) -> J.Options
|
||||
singleFieldJSON tagModifier =
|
||||
singleFieldJSON = singleFieldJSON_ Nothing
|
||||
|
||||
singleFieldJSON_ :: Maybe String -> (String -> String) -> J.Options
|
||||
singleFieldJSON_ objectTag tagModifier =
|
||||
J.defaultOptions
|
||||
{ J.sumEncoding = J.ObjectWithSingleField,
|
||||
{ J.sumEncoding = J.ObjectWithSingleField objectTag,
|
||||
J.tagSingleConstructors = True,
|
||||
J.constructorTagModifier = tagModifier,
|
||||
J.allNullaryToStringTag = False,
|
||||
J.nullaryToObject = True,
|
||||
J.omitNothingFields = True
|
||||
}
|
||||
|
||||
defaultJSON :: J.Options
|
||||
defaultJSON = J.defaultOptions {J.omitNothingFields = True}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{-# LANGUAGE AllowAmbiguousTypes #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
@@ -16,6 +16,7 @@
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE StrictData #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# LANGUAGE TypeFamilyDependencies #-}
|
||||
@@ -117,6 +118,8 @@ module Simplex.Messaging.Protocol
|
||||
userProtocol,
|
||||
rcvMessageMeta,
|
||||
noMsgFlags,
|
||||
messageId,
|
||||
messageTs,
|
||||
|
||||
-- * Parse and serialize
|
||||
ProtocolMsgTag (..),
|
||||
@@ -134,7 +137,7 @@ module Simplex.Messaging.Protocol
|
||||
noAuthSrv,
|
||||
|
||||
-- * TCP transport functions
|
||||
TransportBatch(..),
|
||||
TransportBatch (..),
|
||||
tPut,
|
||||
tPutLog,
|
||||
tGet,
|
||||
@@ -154,7 +157,7 @@ import Control.Applicative (optional, (<|>))
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Monad.Except
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.TH as J
|
||||
import Data.Attoparsec.ByteString.Char8 (Parser, (<?>))
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
@@ -169,9 +172,7 @@ import Data.Maybe (isJust, isNothing)
|
||||
import Data.String
|
||||
import Data.Time.Clock.System (SystemTime (..))
|
||||
import Data.Type.Equality
|
||||
import GHC.Generics (Generic)
|
||||
import GHC.TypeLits (ErrorMessage (..), TypeError, type (+))
|
||||
import Generic.Random (genericArbitraryU)
|
||||
import Network.Socket (HostName, ServiceName)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
@@ -181,7 +182,6 @@ import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts (..))
|
||||
import Simplex.Messaging.Util (bshow, eitherToMaybe, (<$?>))
|
||||
import Simplex.Messaging.Version
|
||||
import Test.QuickCheck (Arbitrary (..))
|
||||
|
||||
currentSMPClientVersion :: Version
|
||||
currentSMPClientVersion = 2
|
||||
@@ -308,17 +308,19 @@ instance StrEncoding SubscriptionMode where
|
||||
SMSubscribe -> "subscribe"
|
||||
SMOnlyCreate -> "only-create"
|
||||
strP =
|
||||
(A.string "subscribe" $> SMSubscribe) <|> (A.string "only-create" $> SMOnlyCreate)
|
||||
<?> "SubscriptionMode"
|
||||
(A.string "subscribe" $> SMSubscribe)
|
||||
<|> (A.string "only-create" $> SMOnlyCreate)
|
||||
<?> "SubscriptionMode"
|
||||
|
||||
instance Encoding SubscriptionMode where
|
||||
smpEncode = \case
|
||||
SMSubscribe -> "S"
|
||||
SMOnlyCreate -> "C"
|
||||
smpP = A.anyChar >>= \case
|
||||
'S' -> pure SMSubscribe
|
||||
'C' -> pure SMOnlyCreate
|
||||
_ -> fail "bad SubscriptionMode"
|
||||
smpP =
|
||||
A.anyChar >>= \case
|
||||
'S' -> pure SMSubscribe
|
||||
'C' -> pure SMOnlyCreate
|
||||
_ -> fail "bad SubscriptionMode"
|
||||
|
||||
data BrokerMsg where
|
||||
-- SMP broker messages (responses, client messages, notifications)
|
||||
@@ -356,6 +358,16 @@ data Message
|
||||
msgTs :: SystemTime
|
||||
}
|
||||
|
||||
messageId :: Message -> MsgId
|
||||
messageId = \case
|
||||
Message {msgId} -> msgId
|
||||
MessageQuota {msgId} -> msgId
|
||||
|
||||
messageTs :: Message -> SystemTime
|
||||
messageTs = \case
|
||||
Message {msgTs} -> msgTs
|
||||
MessageQuota {msgTs} -> msgTs
|
||||
|
||||
instance StrEncoding RcvMessage where
|
||||
strEncode RcvMessage {msgId, msgTs, msgFlags, msgBody = EncRcvMsgBody body} =
|
||||
B.unwords
|
||||
@@ -448,7 +460,14 @@ data SMPMsgMeta = SMPMsgMeta
|
||||
msgTs :: SystemTime,
|
||||
msgFlags :: MsgFlags
|
||||
}
|
||||
deriving (Show)
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding SMPMsgMeta where
|
||||
strEncode SMPMsgMeta {msgId, msgTs, msgFlags} =
|
||||
strEncode (msgId, msgTs, msgFlags)
|
||||
strP = do
|
||||
(msgId, msgTs, msgFlags) <- strP
|
||||
pure SMPMsgMeta {msgId, msgTs, msgFlags}
|
||||
|
||||
rcvMessageMeta :: MsgId -> ClientRcvMsgBody -> SMPMsgMeta
|
||||
rcvMessageMeta msgId = \case
|
||||
@@ -471,9 +490,7 @@ instance Encoding NMsgMeta where
|
||||
|
||||
-- it must be data for correct JSON encoding
|
||||
data MsgFlags = MsgFlags {notification :: Bool}
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
instance ToJSON MsgFlags where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- this encoding should not become bigger than 7 bytes (currently it is 1 byte)
|
||||
instance Encoding MsgFlags where
|
||||
@@ -737,10 +754,16 @@ instance ProtocolTypeI p => ToJSON (SProtocolType p) where
|
||||
toEncoding = strToJEncoding
|
||||
toJSON = strToJSON
|
||||
|
||||
instance ProtocolTypeI p => FromJSON (SProtocolType p) where
|
||||
parseJSON = strParseJSON "SProtocolType"
|
||||
|
||||
instance ToJSON AProtocolType where
|
||||
toEncoding = strToJEncoding
|
||||
toJSON = strToJSON
|
||||
|
||||
instance FromJSON AProtocolType where
|
||||
parseJSON = strParseJSON "AProtocolType"
|
||||
|
||||
checkProtocolType :: forall t p p'. (ProtocolTypeI p, ProtocolTypeI p') => t p' -> Either String (t p)
|
||||
checkProtocolType p = case testEquality (protocolTypeI @p) (protocolTypeI @p') of
|
||||
Just Refl -> Right p
|
||||
@@ -917,6 +940,9 @@ instance ToJSON CorrId where
|
||||
toJSON = strToJSON
|
||||
toEncoding = strToJEncoding
|
||||
|
||||
instance FromJSON CorrId where
|
||||
parseJSON = strParseJSON "CorrId"
|
||||
|
||||
-- | Queue IDs and keys
|
||||
data QueueIdsKeys = QIK
|
||||
{ rcvId :: RecipientId,
|
||||
@@ -987,11 +1013,7 @@ data ErrorType
|
||||
INTERNAL
|
||||
| -- | used internally, never returned by the server (to be removed)
|
||||
DUPLICATE_ -- not part of SMP protocol, used internally
|
||||
deriving (Eq, Generic, Read, Show)
|
||||
|
||||
instance ToJSON ErrorType where
|
||||
toJSON = J.genericToJSON $ sumTypeJSON id
|
||||
toEncoding = J.genericToEncoding $ sumTypeJSON id
|
||||
deriving (Eq, Read, Show)
|
||||
|
||||
instance StrEncoding ErrorType where
|
||||
strEncode = \case
|
||||
@@ -1013,15 +1035,7 @@ data CommandError
|
||||
HAS_AUTH
|
||||
| -- | transmission has no required entity ID (e.g. SMP queue)
|
||||
NO_ENTITY
|
||||
deriving (Eq, Generic, Read, Show)
|
||||
|
||||
instance ToJSON CommandError where
|
||||
toJSON = J.genericToJSON $ sumTypeJSON id
|
||||
toEncoding = J.genericToEncoding $ sumTypeJSON id
|
||||
|
||||
instance Arbitrary ErrorType where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary CommandError where arbitrary = genericArbitraryU
|
||||
deriving (Eq, Read, Show)
|
||||
|
||||
-- | SMP transmission parser.
|
||||
transmissionP :: Parser RawTransmission
|
||||
@@ -1208,9 +1222,6 @@ instance ProtocolEncoding ErrorType BrokerMsg where
|
||||
| B.null queueId -> Left $ CMD NO_ENTITY
|
||||
| otherwise -> Right cmd
|
||||
|
||||
_smpP :: Encoding a => Parser a
|
||||
_smpP = A.space *> smpP
|
||||
|
||||
-- | Parse SMP protocol commands and broker messages
|
||||
parseProtocol :: forall err msg. ProtocolEncoding err msg => Version -> ByteString -> Either err msg
|
||||
parseProtocol v s =
|
||||
@@ -1290,7 +1301,7 @@ tPutLog th s = do
|
||||
pure r
|
||||
|
||||
-- ByteString does not include length byte, it is added by tEncodeBatch
|
||||
data TransportBatch = TBTransmissions Int ByteString | TBTransmission ByteString | TBLargeTransmission
|
||||
data TransportBatch = TBTransmissions Int ByteString | TBTransmission ByteString | TBLargeTransmission
|
||||
|
||||
-- | encodes and batches transmissions into blocks,
|
||||
batchTransmissions :: Bool -> Int -> NonEmpty SentRawTransmission -> [TransportBatch]
|
||||
@@ -1303,22 +1314,22 @@ batchTransmissions batch bSize
|
||||
let (n, s, ts_) = encodeBatch 0 "" ts
|
||||
r = if n == 0 then TBLargeTransmission else TBTransmissions n s
|
||||
rs' = r : rs
|
||||
in case ts_ of
|
||||
Just ts' -> mkBatch rs' ts'
|
||||
_ -> rs'
|
||||
in case ts_ of
|
||||
Just ts' -> mkBatch rs' ts'
|
||||
_ -> rs'
|
||||
mkBatch1 :: ByteString -> TransportBatch
|
||||
mkBatch1 s = if B.length s > bSize - 2 then TBLargeTransmission else TBTransmission s
|
||||
encodeBatch :: Int -> ByteString -> NonEmpty ByteString -> (Int, ByteString, Maybe (NonEmpty ByteString))
|
||||
encodeBatch n s ts@(t :| ts_)
|
||||
| n == 255 = (n, s, Just ts)
|
||||
| otherwise =
|
||||
let s' = s <> smpEncode (Large t)
|
||||
n' = n + 1
|
||||
in if B.length s' > bSize - 3 -- one byte is reserved for the number of messages in the batch
|
||||
then (n,s,) $ if n == 0 then L.nonEmpty ts_ else Just ts
|
||||
else case L.nonEmpty ts_ of
|
||||
Just ts' -> encodeBatch n' s' ts'
|
||||
_ -> (n', s', Nothing)
|
||||
let s' = s <> smpEncode (Large t)
|
||||
n' = n + 1
|
||||
in if B.length s' > bSize - 3 -- one byte is reserved for the number of messages in the batch
|
||||
then (n,s,) $ if n == 0 then L.nonEmpty ts_ else Just ts
|
||||
else case L.nonEmpty ts_ of
|
||||
Just ts' -> encodeBatch n' s' ts'
|
||||
_ -> (n', s', Nothing)
|
||||
|
||||
tEncode :: SentRawTransmission -> ByteString
|
||||
tEncode (sig, t) = smpEncode (C.signatureBytes sig) <> t
|
||||
@@ -1357,8 +1368,8 @@ tDecodeParseValidate :: forall err cmd. ProtocolEncoding err cmd => SessionId ->
|
||||
tDecodeParseValidate sessionId v = \case
|
||||
Right RawTransmission {signature, signed, sessId, corrId, entityId, command}
|
||||
| sessId == sessionId ->
|
||||
let decodedTransmission = (,corrId,entityId,command) <$> C.decodeSignature signature
|
||||
in either (const $ tError corrId) (tParseValidate signed) decodedTransmission
|
||||
let decodedTransmission = (,corrId,entityId,command) <$> C.decodeSignature signature
|
||||
in either (const $ tError corrId) (tParseValidate signed) decodedTransmission
|
||||
| otherwise -> (Nothing, "", (CorrId corrId, "", Left $ fromProtocolError @err @cmd PESession))
|
||||
Left _ -> tError ""
|
||||
where
|
||||
@@ -1369,3 +1380,9 @@ tDecodeParseValidate sessionId v = \case
|
||||
tParseValidate signed t@(sig, corrId, entityId, command) =
|
||||
let cmd = parseProtocol @err @cmd v command >>= checkCredentials t
|
||||
in (sig, signed, (CorrId corrId, entityId, cmd))
|
||||
|
||||
$(J.deriveJSON defaultJSON ''MsgFlags)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON id) ''CommandError)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON id) ''ErrorType)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedRecordDot #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
@@ -49,18 +49,16 @@ import qualified Data.ByteString.Char8 as B
|
||||
import Data.Either (fromRight, partitionEithers)
|
||||
import Data.Functor (($>))
|
||||
import Data.Int (Int64)
|
||||
import Data.List (intercalate, sort)
|
||||
import Data.List (intercalate)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (fromMaybe, isNothing)
|
||||
import Data.Maybe (isNothing)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (decodeLatin1)
|
||||
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 GHC.Conc (listThreads, threadStatus)
|
||||
import GHC.Conc.Sync (threadLabel)
|
||||
import GHC.Stats (getRTSStats)
|
||||
import GHC.TypeLits (KnownNat)
|
||||
import Network.Socket (ServiceName, Socket, socketToHandle)
|
||||
@@ -92,6 +90,12 @@ import UnliftIO.Directory (doesFileExist, renameFile)
|
||||
import UnliftIO.Exception
|
||||
import UnliftIO.IO
|
||||
import UnliftIO.STM
|
||||
#if MIN_VERSION_base(4,18,0)
|
||||
import Data.List (sort)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import GHC.Conc (listThreads, threadStatus)
|
||||
import GHC.Conc.Sync (threadLabel)
|
||||
#endif
|
||||
|
||||
-- | Runs an SMP server using passed configuration.
|
||||
--
|
||||
@@ -116,9 +120,9 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
restoreServerMessages
|
||||
restoreServerStats
|
||||
raceAny_
|
||||
( serverThread s "server subscribedQ" subscribedQ subscribers subscriptions cancelSub :
|
||||
serverThread s "server ntfSubscribedQ" ntfSubscribedQ Env.notifiers ntfSubscriptions (\_ -> pure ()) :
|
||||
map runServer transports <> expireMessagesThread_ cfg <> serverStatsThread_ cfg <> controlPortThread_ cfg
|
||||
( serverThread s "server subscribedQ" subscribedQ subscribers subscriptions cancelSub
|
||||
: serverThread s "server ntfSubscribedQ" ntfSubscribedQ Env.notifiers ntfSubscriptions (\_ -> pure ())
|
||||
: map runServer transports <> expireMessagesThread_ cfg <> serverStatsThread_ cfg <> controlPortThread_ cfg
|
||||
)
|
||||
`finally` withLock (savingLock s) "final" (saveServer False)
|
||||
where
|
||||
@@ -148,7 +152,7 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
updateSubscribers :: STM (Maybe (QueueId, Client))
|
||||
updateSubscribers = do
|
||||
(qId, clnt) <- readTQueue $ subQ s
|
||||
let clientToBeNotified = \c' ->
|
||||
let clientToBeNotified c' =
|
||||
if sameClientSession clnt c'
|
||||
then pure Nothing
|
||||
else do
|
||||
@@ -277,9 +281,9 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
hPutStrLn h $ "Clients: " <> show (length clients)
|
||||
forM_ (M.toList clients) $ \(cid, Client {sessionId, connected, activeAt, subscriptions}) -> do
|
||||
hPutStrLn h . B.unpack $ "Client " <> encode cid <> " $" <> encode sessionId
|
||||
readTVarIO connected >>= hPutStrLn h . (" connected: " <>) . show
|
||||
readTVarIO activeAt >>= hPutStrLn h . (" activeAt: " <>) . B.unpack . strEncode
|
||||
readTVarIO subscriptions >>= hPutStrLn h . (" subscriptions: " <>) . show . M.size
|
||||
readTVarIO connected >>= hPutStrLn h . (" connected: " <>) . show
|
||||
readTVarIO activeAt >>= hPutStrLn h . (" activeAt: " <>) . B.unpack . strEncode
|
||||
readTVarIO subscriptions >>= hPutStrLn h . (" subscriptions: " <>) . show . M.size
|
||||
CPStats -> do
|
||||
ServerStats {fromTime, qCreated, qSecured, qDeleted, msgSent, msgRecv, msgSentNtf, msgRecvNtf, qCount, msgCount} <- unliftIO u $ asks serverStats
|
||||
putStat "fromTime" fromTime
|
||||
@@ -297,12 +301,16 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg} = do
|
||||
putStat label var = readTVarIO var >>= \v -> hPutStrLn h $ label <> ": " <> show v
|
||||
CPStatsRTS -> getRTSStats >>= hPutStrLn h . show
|
||||
CPThreads -> do
|
||||
#if MIN_VERSION_base(4,18,0)
|
||||
threads <- liftIO listThreads
|
||||
hPutStrLn h $ "Threads: " <> show (length threads)
|
||||
forM_ (sort threads) $ \tid -> do
|
||||
label <- threadLabel tid
|
||||
status <- threadStatus tid
|
||||
hPutStrLn h $ show tid <> " (" <> show status <> ") " <> fromMaybe "" label
|
||||
#else
|
||||
hPutStrLn h "Not available on GHC 8.10"
|
||||
#endif
|
||||
CPSave -> withLock (savingLock srv) "control" $ do
|
||||
hPutStrLn h "saving server state..."
|
||||
unliftIO u $ saveServer True
|
||||
@@ -318,7 +326,7 @@ runClientTransport th@THandle {thVersion, sessionId} = do
|
||||
c <- atomically $ newClient q thVersion sessionId ts
|
||||
s <- asks server
|
||||
expCfg <- asks $ inactiveClientExpiration . config
|
||||
labelMyThread . B.unpack $ "client $" <> encode c.sessionId
|
||||
labelMyThread . B.unpack $ "client $" <> encode sessionId
|
||||
raceAny_ ([liftIO $ send th c, client c s, receive th c] <> disconnectThread_ c expCfg)
|
||||
`finally` clientDisconnected c
|
||||
where
|
||||
@@ -441,8 +449,8 @@ dummyKeyEd448 :: C.PublicKey 'C.Ed448
|
||||
dummyKeyEd448 = "MEMwBQYDK2VxAzoA6ibQc9XpkSLtwrf7PLvp81qW/etiumckVFImCMRdftcG/XopbOSaq9qyLhrgJWKOLyNrQPNVvpMA"
|
||||
|
||||
client :: forall m. (MonadUnliftIO m, MonadReader Env m) => Client -> Server -> m ()
|
||||
client clnt@Client {thVersion, subscriptions, ntfSubscriptions, rcvQ, sndQ} Server {subscribedQ, ntfSubscribedQ, notifiers} = do
|
||||
labelMyThread . B.unpack $ "client $" <> encode clnt.sessionId <> " commands"
|
||||
client clnt@Client {thVersion, subscriptions, ntfSubscriptions, rcvQ, sndQ, sessionId} Server {subscribedQ, ntfSubscribedQ, notifiers} = do
|
||||
labelMyThread . B.unpack $ "client $" <> encode sessionId <> " commands"
|
||||
forever $
|
||||
atomically (readTBQueue rcvQ)
|
||||
>>= mapM processCommand
|
||||
@@ -666,27 +674,27 @@ client clnt@Client {thVersion, subscriptions, ntfSubscriptions, rcvQ, sndQ} Serv
|
||||
sendMessage qr msgFlags msgBody
|
||||
| B.length msgBody > maxMessageLength = pure $ err LARGE_MSG
|
||||
| otherwise = case status qr of
|
||||
QueueOff -> return $ err AUTH
|
||||
QueueActive ->
|
||||
case C.maxLenBS msgBody of
|
||||
Left _ -> pure $ err LARGE_MSG
|
||||
Right body -> do
|
||||
msg_ <- time "SEND" $ do
|
||||
q <- getStoreMsgQueue "SEND" $ recipientId qr
|
||||
expireMessages q
|
||||
atomically . writeMsg q =<< mkMessage body
|
||||
case msg_ of
|
||||
Nothing -> pure $ err QUOTA
|
||||
Just msg -> time "SEND ok" $ do
|
||||
stats <- asks serverStats
|
||||
when (notification msgFlags) $ do
|
||||
atomically . trySendNotification msg =<< asks idsDrg
|
||||
atomically $ modifyTVar' (msgSentNtf stats) (+ 1)
|
||||
atomically $ updatePeriodStats (activeQueuesNtf stats) (recipientId qr)
|
||||
atomically $ modifyTVar' (msgSent stats) (+ 1)
|
||||
atomically $ modifyTVar' (msgCount stats) (subtract 1)
|
||||
atomically $ updatePeriodStats (activeQueues stats) (recipientId qr)
|
||||
pure ok
|
||||
QueueOff -> return $ err AUTH
|
||||
QueueActive ->
|
||||
case C.maxLenBS msgBody of
|
||||
Left _ -> pure $ err LARGE_MSG
|
||||
Right body -> do
|
||||
msg_ <- time "SEND" $ do
|
||||
q <- getStoreMsgQueue "SEND" $ recipientId qr
|
||||
expireMessages q
|
||||
atomically . writeMsg q =<< mkMessage body
|
||||
case msg_ of
|
||||
Nothing -> pure $ err QUOTA
|
||||
Just msg -> time "SEND ok" $ do
|
||||
stats <- asks serverStats
|
||||
when (notification msgFlags) $ do
|
||||
atomically . trySendNotification msg =<< asks idsDrg
|
||||
atomically $ modifyTVar' (msgSentNtf stats) (+ 1)
|
||||
atomically $ updatePeriodStats (activeQueuesNtf stats) (recipientId qr)
|
||||
atomically $ modifyTVar' (msgSent stats) (+ 1)
|
||||
atomically $ modifyTVar' (msgCount stats) (subtract 1)
|
||||
atomically $ updatePeriodStats (activeQueues stats) (recipientId qr)
|
||||
pure ok
|
||||
where
|
||||
mkMessage :: C.MaxLenBS MaxMessageLen -> m Message
|
||||
mkMessage body = do
|
||||
@@ -763,11 +771,11 @@ client clnt@Client {thVersion, subscriptions, ntfSubscriptions, rcvQ, sndQ} Serv
|
||||
encrypt msgFlags body =
|
||||
let encBody = EncRcvMsgBody $ C.cbEncryptMaxLenBS (rcvDhSecret qr) (C.cbNonce msgId') body
|
||||
in RcvMessage msgId' msgTs' msgFlags encBody
|
||||
msgId' = msg.msgId
|
||||
msgTs' = msg.msgTs
|
||||
msgId' = messageId msg
|
||||
msgTs' = messageTs msg
|
||||
|
||||
setDelivered :: Sub -> Message -> STM Bool
|
||||
setDelivered s msg = tryPutTMVar (delivered s) $ msg.msgId
|
||||
setDelivered s msg = tryPutTMVar (delivered s) (messageId msg)
|
||||
|
||||
getStoreMsgQueue :: T.Text -> RecipientId -> m MsgQueue
|
||||
getStoreMsgQueue name rId = time (name <> " getMsgQueue") $ do
|
||||
@@ -867,7 +875,7 @@ restoreServerMessages = asks (storeMsgsFile . config) >>= mapM_ restoreMessages
|
||||
| maybe True (systemSeconds msgTs >=) old_ -> isNothing <$> writeMsg q msg
|
||||
| otherwise -> pure False
|
||||
MessageQuota {} -> writeMsg q msg $> False
|
||||
when logFull . logError . decodeLatin1 $ "message queue " <> strEncode rId <> " is full, message not restored: " <> strEncode msg.msgId
|
||||
when logFull . logError . decodeLatin1 $ "message queue " <> strEncode rId <> " is full, message not restored: " <> strEncode (messageId msg)
|
||||
updateMsgV1toV3 QueueRec {rcvDhSecret} RcvMessage {msgId, msgTs, msgFlags, msgBody = EncRcvMsgBody body} = do
|
||||
let nonce = C.cbNonce msgId
|
||||
msgBody <- liftEither . first (msgErr "v1 message decryption") $ C.maxLenBS =<< C.cbDecrypt rcvDhSecret nonce body
|
||||
|
||||
@@ -31,7 +31,7 @@ import Simplex.Messaging.Server.StoreLog
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport (ATransport)
|
||||
import Simplex.Messaging.Transport.Server (loadFingerprint, loadTLSServerParams, TransportServerConfig)
|
||||
import Simplex.Messaging.Transport.Server (TransportServerConfig, loadFingerprint, loadTLSServerParams)
|
||||
import Simplex.Messaging.Version
|
||||
import System.IO (IOMode (..))
|
||||
import System.Mem.Weak (Weak)
|
||||
|
||||
@@ -11,7 +11,6 @@ module Simplex.Messaging.Server.Main where
|
||||
import Control.Monad (void)
|
||||
import Crypto.Random (getRandomBytes)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Either (fromRight)
|
||||
import Data.Functor (($>))
|
||||
import Data.Ini (lookupValue, readIniFile)
|
||||
import Data.Maybe (fromMaybe)
|
||||
@@ -24,7 +23,7 @@ import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (ProtoServerWithAuth), pattern SMPServer)
|
||||
import Simplex.Messaging.Server (runSMPServer)
|
||||
import Simplex.Messaging.Server.CLI
|
||||
import Simplex.Messaging.Server.Env.STM (ServerConfig (..), defaultInactiveClientExpiration, defaultMessageExpiration, defMsgExpirationDays)
|
||||
import Simplex.Messaging.Server.Env.STM (ServerConfig (..), defMsgExpirationDays, defaultInactiveClientExpiration, defaultMessageExpiration)
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Transport (simplexMQVersion, supportedSMPServerVRange)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
@@ -60,15 +59,15 @@ smpServerCLI cfgPath logPath =
|
||||
initializeServer opts
|
||||
| scripted opts = initialize opts
|
||||
| otherwise = do
|
||||
putStrLn "Use `smp-server init -h` for available options."
|
||||
void $ withPrompt "SMP server will be initialized (press Enter)" getLine
|
||||
enableStoreLog <- onOffPrompt "Enable store log to restore queues and messages on server restart" True
|
||||
logStats <- onOffPrompt "Enable logging daily statistics" False
|
||||
putStrLn "Require a password to create new messaging queues?"
|
||||
password <- withPrompt "'r' for random (default), 'n' - no password, or enter password: " serverPassword
|
||||
let host = fromMaybe (ip opts) (fqdn opts)
|
||||
host' <- withPrompt ("Enter server FQDN or IP address for certificate (" <> host <> "): ") getLine
|
||||
initialize opts {enableStoreLog, logStats, fqdn = if null host' then fqdn opts else Just host', password}
|
||||
putStrLn "Use `smp-server init -h` for available options."
|
||||
void $ withPrompt "SMP server will be initialized (press Enter)" getLine
|
||||
enableStoreLog <- onOffPrompt "Enable store log to restore queues and messages on server restart" True
|
||||
logStats <- onOffPrompt "Enable logging daily statistics" False
|
||||
putStrLn "Require a password to create new messaging queues?"
|
||||
password <- withPrompt "'r' for random (default), 'n' - no password, or enter password: " serverPassword
|
||||
let host = fromMaybe (ip opts) (fqdn opts)
|
||||
host' <- withPrompt ("Enter server FQDN or IP address for certificate (" <> host <> "): ") getLine
|
||||
initialize opts {enableStoreLog, logStats, fqdn = if null host' then fqdn opts else Just host', password}
|
||||
where
|
||||
serverPassword =
|
||||
getLine >>= \case
|
||||
@@ -121,8 +120,8 @@ smpServerCLI cfgPath logPath =
|
||||
\# The password will not be shared with the connecting contacts, you must share it only\n\
|
||||
\# with the users who you want to allow creating messaging queues on your server.\n"
|
||||
<> ( case basicAuth of
|
||||
Just auth -> "create_password: " <> T.unpack (safeDecodeUtf8 $ strEncode auth)
|
||||
_ -> "# create_password: password to create new queues (any printable ASCII characters without whitespace, '@', ':' and '/')"
|
||||
Just auth -> "create_password: " <> T.unpack (safeDecodeUtf8 $ strEncode auth)
|
||||
_ -> "# create_password: password to create new queues (any printable ASCII characters without whitespace, '@', ':' and '/')"
|
||||
)
|
||||
<> "\n\n\
|
||||
\[TRANSPORT]\n\
|
||||
@@ -141,7 +140,7 @@ smpServerCLI cfgPath logPath =
|
||||
hSetBuffering stdout LineBuffering
|
||||
hSetBuffering stderr LineBuffering
|
||||
fp <- checkSavedFingerprint cfgPath defaultX509Config
|
||||
let host = fromRight "<hostnames>" $ T.unpack <$> lookupValue "TRANSPORT" "host" ini
|
||||
let host = either (const "<hostnames>") T.unpack $ lookupValue "TRANSPORT" "host" ini
|
||||
port = T.unpack $ strictIni "TRANSPORT" "port" ini
|
||||
cfg@ServerConfig {transports, storeLogFile, newQueueBasicAuth, messageExpiration, inactiveClientExpiration} = serverConfig
|
||||
srv = ProtoServerWithAuth (SMPServer [THDomainName host] (if port == "5223" then "" else port) (C.KeyHash fp)) newQueueBasicAuth
|
||||
@@ -186,9 +185,10 @@ smpServerCLI cfgPath logPath =
|
||||
allowNewQueues = fromMaybe True $ iniOnOff "AUTH" "new_queues" ini,
|
||||
newQueueBasicAuth = either error id <$> strDecodeIni "AUTH" "create_password" ini,
|
||||
messageExpiration =
|
||||
Just defaultMessageExpiration
|
||||
{ ttl = 86400 * readIniDefault defMsgExpirationDays "STORE_LOG" "expire_messages_days" ini
|
||||
},
|
||||
Just
|
||||
defaultMessageExpiration
|
||||
{ ttl = 86400 * readIniDefault defMsgExpirationDays "STORE_LOG" "expire_messages_days" ini
|
||||
},
|
||||
inactiveClientExpiration =
|
||||
settingIsOn "INACTIVE_CLIENTS" "disconnect" ini
|
||||
$> ExpirationConfig
|
||||
|
||||
@@ -64,7 +64,7 @@ flushMsgQueue :: STMMsgStore -> RecipientId -> STM [Message]
|
||||
flushMsgQueue st rId = TM.lookupDelete rId st >>= maybe (pure []) (flushTQueue . msgQueue)
|
||||
|
||||
snapshotMsgQueue :: STMMsgStore -> RecipientId -> STM [Message]
|
||||
snapshotMsgQueue st rId = TM.lookup rId st >>= maybe (pure []) (snapshotTQueue . msgQueue)
|
||||
snapshotMsgQueue st rId = TM.lookup rId st >>= maybe (pure []) (snapshotTQueue . msgQueue)
|
||||
where
|
||||
snapshotTQueue q = do
|
||||
msgs <- flushTQueue q
|
||||
|
||||
@@ -11,7 +11,7 @@ import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Set (Set)
|
||||
import qualified Data.Set as S
|
||||
import Data.Time.Calendar.Month.Compat (pattern MonthDay)
|
||||
import Data.Time.Calendar.Month (pattern MonthDay)
|
||||
import Data.Time.Calendar.OrdinalDate (mondayStartWeek)
|
||||
import Data.Time.Clock (UTCTime (..))
|
||||
import Simplex.Messaging.Encoding.String
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
|
||||
module Simplex.Messaging.Server.StoreLog
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{-# LANGUAGE AllowAmbiguousTypes #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
@@ -10,6 +9,7 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
-- |
|
||||
@@ -63,8 +63,7 @@ where
|
||||
import Control.Applicative ((<|>))
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Trans.Except (throwE)
|
||||
import Data.Aeson (ToJSON)
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.TH as J
|
||||
import Data.Attoparsec.ByteString.Char8 (Parser)
|
||||
import Data.Bifunctor (first)
|
||||
import Data.Bitraversable (bimapM)
|
||||
@@ -74,9 +73,7 @@ import qualified Data.ByteString.Lazy as BL
|
||||
import Data.Default (def)
|
||||
import Data.Functor (($>))
|
||||
import Data.Version (showVersion)
|
||||
import GHC.Generics (Generic)
|
||||
import GHC.IO.Handle.Internals (ioe_EOF)
|
||||
import Generic.Random (genericArbitraryU)
|
||||
import Network.Socket
|
||||
import qualified Network.TLS as T
|
||||
import qualified Network.TLS.Extra as TE
|
||||
@@ -87,7 +84,6 @@ import Simplex.Messaging.Parsers (dropPrefix, parse, parseRead1, sumTypeJSON)
|
||||
import Simplex.Messaging.Transport.Buffer
|
||||
import Simplex.Messaging.Util (bshow, catchAll, catchAll_)
|
||||
import Simplex.Messaging.Version
|
||||
import Test.QuickCheck (Arbitrary (..))
|
||||
import UnliftIO.Exception (Exception)
|
||||
import qualified UnliftIO.Exception as E
|
||||
import UnliftIO.STM
|
||||
@@ -219,7 +215,7 @@ instance Transport TLS where
|
||||
cGet :: TLS -> Int -> IO ByteString
|
||||
cGet TLS {tlsContext, tlsBuffer, tlsTransportConfig = TransportConfig {transportTimeout = t_}} n =
|
||||
getBuffered tlsBuffer n t_ (T.recvData tlsContext)
|
||||
|
||||
|
||||
cPut :: TLS -> ByteString -> IO ()
|
||||
cPut TLS {tlsContext, tlsTransportConfig = TransportConfig {transportTimeout = t_}} s =
|
||||
withTimedErr t_ . T.sendData tlsContext $ BL.fromStrict s
|
||||
@@ -284,11 +280,7 @@ data TransportError
|
||||
TEBadSession
|
||||
| -- | transport handshake error
|
||||
TEHandshake {handshakeErr :: HandshakeError}
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
|
||||
instance ToJSON TransportError where
|
||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "TE"
|
||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "TE"
|
||||
deriving (Eq, Read, Show, Exception)
|
||||
|
||||
-- | Transport handshake error.
|
||||
data HandshakeError
|
||||
@@ -298,15 +290,7 @@ data HandshakeError
|
||||
VERSION
|
||||
| -- | incorrect server identity
|
||||
IDENTITY
|
||||
deriving (Eq, Generic, Read, Show, Exception)
|
||||
|
||||
instance ToJSON HandshakeError where
|
||||
toJSON = J.genericToJSON $ sumTypeJSON id
|
||||
toEncoding = J.genericToEncoding $ sumTypeJSON id
|
||||
|
||||
instance Arbitrary TransportError where arbitrary = genericArbitraryU
|
||||
|
||||
instance Arbitrary HandshakeError where arbitrary = genericArbitraryU
|
||||
deriving (Eq, Read, Show, Exception)
|
||||
|
||||
-- | SMP encrypted transport error parser.
|
||||
transportErrorP :: Parser TransportError
|
||||
@@ -348,9 +332,9 @@ smpServerHandshake c kh smpVRange = do
|
||||
getHandshake th >>= \case
|
||||
ClientHandshake {smpVersion, keyHash}
|
||||
| keyHash /= kh ->
|
||||
throwE $ TEHandshake IDENTITY
|
||||
throwE $ TEHandshake IDENTITY
|
||||
| smpVersion `isCompatible` smpVRange -> do
|
||||
pure $ smpThHandle th smpVersion
|
||||
pure $ smpThHandle th smpVersion
|
||||
| otherwise -> throwE $ TEHandshake VERSION
|
||||
|
||||
-- | Client SMP transport handshake.
|
||||
@@ -379,3 +363,7 @@ getHandshake th = ExceptT $ (parse smpP (TEHandshake PARSE) =<<) <$> tGetBlock t
|
||||
|
||||
smpTHandle :: Transport c => c -> THandle c
|
||||
smpTHandle c = THandle {connection = c, sessionId = tlsUnique c, blockSize = smpBlockSize, thVersion = 0, batch = False}
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON id) ''HandshakeError)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON $ dropPrefix "TE") ''TransportError)
|
||||
|
||||
@@ -6,10 +6,11 @@ module Simplex.Messaging.Transport.Buffer where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import qualified Control.Exception as E
|
||||
import Control.Monad (forM_)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import GHC.IO.Exception (IOErrorType (..), IOException (..), ioException)
|
||||
import System.Timeout (timeout)
|
||||
import GHC.IO.Exception (ioException, IOException (..), IOErrorType (..))
|
||||
|
||||
data TBuffer = TBuffer
|
||||
{ buffer :: TVar ByteString,
|
||||
@@ -28,6 +29,14 @@ withBufferLock TBuffer {getLock} =
|
||||
(atomically $ takeTMVar getLock)
|
||||
(atomically $ putTMVar getLock ())
|
||||
|
||||
-- | Attempt to read some bytes, appending it to the existing buffer
|
||||
peekBuffered :: TBuffer -> Int -> IO ByteString -> IO (ByteString, Maybe ByteString)
|
||||
peekBuffered tb@TBuffer {buffer} t getChunk = withBufferLock tb $ do
|
||||
old <- readTVarIO buffer
|
||||
next_ <- timeout t getChunk
|
||||
forM_ next_ $ \next -> atomically $ writeTVar buffer $! old <> next
|
||||
pure (old, next_)
|
||||
|
||||
getBuffered :: TBuffer -> Int -> Maybe Int -> IO ByteString -> IO ByteString
|
||||
getBuffered tb@TBuffer {buffer} n t_ getChunk = withBufferLock tb $ do
|
||||
b <- readChunks True =<< readTVarIO buffer
|
||||
@@ -41,9 +50,9 @@ getBuffered tb@TBuffer {buffer} n t_ getChunk = withBufferLock tb $ do
|
||||
readChunks firstChunk b
|
||||
| B.length b >= n = pure b
|
||||
| otherwise =
|
||||
get >>= \case
|
||||
"" -> pure b
|
||||
s -> readChunks False $ b <> s
|
||||
get >>= \case
|
||||
"" -> pure b
|
||||
s -> readChunks False $ b <> s
|
||||
where
|
||||
get
|
||||
| firstChunk = getChunk
|
||||
|
||||
@@ -88,6 +88,9 @@ instance ToJSON TransportHost where
|
||||
toEncoding = strToJEncoding
|
||||
toJSON = strToJSON
|
||||
|
||||
instance FromJSON TransportHost where
|
||||
parseJSON = strParseJSON "TransportHost"
|
||||
|
||||
newtype TransportHosts = TransportHosts {thList :: NonEmpty TransportHost}
|
||||
|
||||
instance StrEncoding TransportHosts where
|
||||
@@ -107,12 +110,13 @@ instance IsString (NonEmpty TransportHost) where fromString = parseString strDec
|
||||
data TransportClientConfig = TransportClientConfig
|
||||
{ socksProxy :: Maybe SocksProxy,
|
||||
tcpKeepAlive :: Maybe KeepAliveOpts,
|
||||
logTLSErrors :: Bool
|
||||
logTLSErrors :: Bool,
|
||||
clientCredentials :: Maybe (X.CertificateChain, T.PrivKey)
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
defaultTransportClientConfig :: TransportClientConfig
|
||||
defaultTransportClientConfig = TransportClientConfig Nothing (Just defaultKeepAliveOpts) True
|
||||
defaultTransportClientConfig = TransportClientConfig Nothing (Just defaultKeepAliveOpts) True Nothing
|
||||
|
||||
clientTransportConfig :: TransportClientConfig -> TransportConfig
|
||||
clientTransportConfig TransportClientConfig {logTLSErrors} =
|
||||
@@ -123,9 +127,9 @@ runTransportClient :: (Transport c, MonadUnliftIO m) => TransportClientConfig ->
|
||||
runTransportClient = runTLSTransportClient supportedParameters Nothing
|
||||
|
||||
runTLSTransportClient :: (Transport c, MonadUnliftIO m) => T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> m a) -> m a
|
||||
runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy, tcpKeepAlive} proxyUsername host port keyHash client = do
|
||||
runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy, tcpKeepAlive, clientCredentials} proxyUsername host port keyHash client = do
|
||||
let hostName = B.unpack $ strEncode host
|
||||
clientParams = mkTLSClientParams tlsParams caStore_ hostName port keyHash
|
||||
clientParams = mkTLSClientParams tlsParams caStore_ hostName port keyHash clientCredentials
|
||||
connectTCP = case socksProxy of
|
||||
Just proxy -> connectSocksClient proxy proxyUsername $ hostAddr host
|
||||
_ -> connectTCPClient hostName
|
||||
@@ -202,14 +206,19 @@ instance ToJSON SocksProxy where
|
||||
instance FromJSON SocksProxy where
|
||||
parseJSON = strParseJSON "SocksProxy"
|
||||
|
||||
mkTLSClientParams :: T.Supported -> Maybe XS.CertificateStore -> HostName -> ServiceName -> Maybe C.KeyHash -> T.ClientParams
|
||||
mkTLSClientParams supported caStore_ host port keyHash_ = do
|
||||
let p = B.pack port
|
||||
mkTLSClientParams :: T.Supported -> Maybe XS.CertificateStore -> HostName -> ServiceName -> Maybe C.KeyHash -> Maybe (X.CertificateChain, T.PrivKey) -> T.ClientParams
|
||||
mkTLSClientParams supported caStore_ host port cafp_ clientCreds_ =
|
||||
(T.defaultParamsClient host p)
|
||||
{ T.clientShared = maybe def (\caStore -> def {T.sharedCAStore = caStore}) caStore_,
|
||||
T.clientHooks = maybe def (\keyHash -> def {T.onServerCertificate = \_ _ _ -> validateCertificateChain keyHash host p}) keyHash_,
|
||||
{ T.clientShared = def {T.sharedCAStore = fromMaybe (T.sharedCAStore def) caStore_},
|
||||
T.clientHooks =
|
||||
def
|
||||
{ T.onServerCertificate = maybe def (\cafp _ _ _ -> validateCertificateChain cafp host p) cafp_,
|
||||
T.onCertificateRequest = maybe def (const . pure . Just) clientCreds_
|
||||
},
|
||||
T.clientSupported = supported
|
||||
}
|
||||
where
|
||||
p = B.pack port
|
||||
|
||||
validateCertificateChain :: C.KeyHash -> HostName -> ByteString -> X.CertificateChain -> IO [XV.FailedReason]
|
||||
validateCertificateChain _ _ _ (X.CertificateChain []) = pure [XV.EmptyChain]
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
|
||||
module Simplex.Messaging.Transport.Credentials
|
||||
( tlsCredentials,
|
||||
Credentials,
|
||||
genCredentials,
|
||||
C.signCertificate,
|
||||
)
|
||||
where
|
||||
|
||||
import Data.ASN1.Types (getObjectID)
|
||||
import Data.ASN1.Types.String (ASN1StringEncoding (UTF8))
|
||||
import Data.Hourglass (Hours (..), timeAdd)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import qualified Data.X509 as X509
|
||||
import Data.X509.Validation (Fingerprint (..), getFingerprint)
|
||||
import qualified Network.TLS as TLS
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import qualified Time.System as Hourglass
|
||||
|
||||
-- | Generate a certificate chain to be used with TLS fingerprint-pinning
|
||||
--
|
||||
-- @
|
||||
-- genTlsCredentials = do
|
||||
-- ca <- genCredentials Nothing (-25, 365 * 24) "Root" -- long-lived root cert
|
||||
-- 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)])
|
||||
where
|
||||
Fingerprint rootFP = getFingerprint root X509.HashSHA256
|
||||
leafKey = fst $ L.head credentials
|
||||
root = snd $ L.last credentials
|
||||
certs = map snd $ L.toList credentials
|
||||
|
||||
privateToTls :: C.APrivateSignKey -> TLS.PrivKey
|
||||
privateToTls (C.APrivateSignKey _ k) = case k of
|
||||
C.PrivateKeyEd25519 secret _ -> TLS.PrivKeyEd25519 secret
|
||||
C.PrivateKeyEd448 secret _ -> TLS.PrivKeyEd448 secret
|
||||
|
||||
type Credentials = (C.ASignatureKeyPair, X509.SignedCertificate)
|
||||
|
||||
genCredentials :: Maybe Credentials -> (Hours, Hours) -> Text -> IO Credentials
|
||||
genCredentials parent (before, after) subjectName = do
|
||||
subjectKeys <- C.generateSignatureKeyPair C.SEd25519
|
||||
let (issuerKeys, issuer) = case parent of
|
||||
Nothing -> (subjectKeys, subject) -- self-signed
|
||||
Just (keys, cert) -> (keys, X509.certSubjectDN . X509.signedObject $ X509.getSigned cert)
|
||||
today <- Hourglass.dateCurrent
|
||||
let signed =
|
||||
C.signCertificate
|
||||
(snd issuerKeys)
|
||||
X509.Certificate
|
||||
{ certVersion = 2,
|
||||
certSerial = 1,
|
||||
certSignatureAlg = C.signatureAlgorithmX509 issuerKeys,
|
||||
certIssuerDN = issuer,
|
||||
certValidity = (timeAdd today (-before), timeAdd today after),
|
||||
certSubjectDN = subject,
|
||||
certPubKey = C.toPubKey C.publicToX509 $ fst subjectKeys,
|
||||
certExtensions = X509.Extensions Nothing
|
||||
}
|
||||
pure (subjectKeys, signed)
|
||||
where
|
||||
subject = dn $ X509.ASN1CharacterString {characterEncoding = UTF8, getCharacterStringRawData = encodeUtf8 subjectName}
|
||||
dn dnCommonName = X509.DistinguishedName [(getObjectID X509.DnCommonName, dnCommonName)]
|
||||
@@ -13,6 +13,7 @@ import Network.HPACK (BufferSize)
|
||||
import Network.HTTP2.Client (Config (..), defaultPositionReadMaker, freeSimpleConfig)
|
||||
import qualified Network.HTTP2.Client as HC
|
||||
import qualified Network.HTTP2.Server as HS
|
||||
import Network.Socket (SockAddr (..))
|
||||
import qualified Network.TLS as T
|
||||
import qualified Network.TLS.Extra as TE
|
||||
import Simplex.Messaging.Transport (SessionId, TLS (tlsUniq), Transport (cGet, cPut))
|
||||
@@ -36,7 +37,9 @@ allocHTTP2Config c sz = do
|
||||
confSendAll = cPut c,
|
||||
confReadN = cGet c,
|
||||
confPositionReadMaker = defaultPositionReadMaker,
|
||||
confTimeoutManager = tm
|
||||
confTimeoutManager = tm,
|
||||
confMySockAddr = SockAddrInet 0 0,
|
||||
confPeerSockAddr = SockAddrInet 0 0
|
||||
}
|
||||
|
||||
http2TLSParams :: T.Supported
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.Messaging.Transport.HTTP2.Client where
|
||||
@@ -22,7 +23,7 @@ import qualified Network.TLS as T
|
||||
import Numeric.Natural (Natural)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Transport (SessionId)
|
||||
import Simplex.Messaging.Transport (SessionId, TLS)
|
||||
import Simplex.Messaging.Transport.Client (TransportClientConfig (..), TransportHost (..), runTLSTransportClient)
|
||||
import Simplex.Messaging.Transport.HTTP2
|
||||
import UnliftIO.STM
|
||||
@@ -65,7 +66,7 @@ defaultHTTP2ClientConfig =
|
||||
HTTP2ClientConfig
|
||||
{ qSize = 64,
|
||||
connTimeout = 10000000,
|
||||
transportConfig = TransportClientConfig Nothing Nothing True,
|
||||
transportConfig = TransportClientConfig Nothing Nothing True Nothing,
|
||||
bufferSize = defaultHTTP2BufferSize,
|
||||
bodyHeadSize = 16384,
|
||||
suportedTLSParams = http2TLSParams
|
||||
@@ -78,7 +79,17 @@ getHTTP2Client :: HostName -> ServiceName -> Maybe XS.CertificateStore -> HTTP2C
|
||||
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@HTTP2ClientConfig {transportConfig, bufferSize, bodyHeadSize, connTimeout, suportedTLSParams} disconnected =
|
||||
getVerifiedHTTP2Client proxyUsername 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
|
||||
|
||||
attachHTTP2Client :: HTTP2ClientConfig -> TransportHost -> ServiceName -> IO () -> Int -> TLS -> IO (Either HTTP2ClientError HTTP2Client)
|
||||
attachHTTP2Client config host port disconnected bufferSize tls = getVerifiedHTTP2ClientWith config host port disconnected setup
|
||||
where
|
||||
setup = runHTTP2ClientWith bufferSize host ($ tls)
|
||||
|
||||
getVerifiedHTTP2ClientWith :: HTTP2ClientConfig -> TransportHost -> ServiceName -> IO () -> ((SessionId -> H.Client HTTP2Response) -> IO HTTP2Response) -> IO (Either HTTP2ClientError HTTP2Client)
|
||||
getVerifiedHTTP2ClientWith config host port disconnected setup =
|
||||
(atomically mkHTTPS2Client >>= runClient)
|
||||
`E.catch` \(e :: IOException) -> pure . Left $ HCIOError e
|
||||
where
|
||||
@@ -91,11 +102,8 @@ getVerifiedHTTP2Client proxyUsername host port keyHash caStore config@HTTP2Clien
|
||||
runClient :: HClient -> IO (Either HTTP2ClientError HTTP2Client)
|
||||
runClient c = do
|
||||
cVar <- newEmptyTMVarIO
|
||||
action <-
|
||||
async $
|
||||
runHTTP2Client suportedTLSParams caStore transportConfig bufferSize proxyUsername host port keyHash (client c cVar)
|
||||
`E.finally` atomically (putTMVar cVar $ Left HCNetworkError)
|
||||
c_ <- connTimeout `timeout` atomically (takeTMVar cVar)
|
||||
action <- async $ setup (client c cVar) `E.finally` atomically (putTMVar cVar $ Left HCNetworkError)
|
||||
c_ <- connTimeout config `timeout` atomically (takeTMVar cVar)
|
||||
pure $ case c_ of
|
||||
Just (Right c') -> Right c' {action = Just action}
|
||||
Just (Left e) -> Left e
|
||||
@@ -114,7 +122,7 @@ getVerifiedHTTP2Client proxyUsername host port keyHash caStore config@HTTP2Clien
|
||||
process HTTP2Client {client_ = HClient {reqQ}} sendReq = forever $ do
|
||||
(req, respVar) <- atomically $ readTBQueue reqQ
|
||||
sendReq req $ \r -> do
|
||||
respBody <- getHTTP2Body r bodyHeadSize
|
||||
respBody <- getHTTP2Body r (bodyHeadSize config)
|
||||
let resp = HTTP2Response {response = r, respBody}
|
||||
atomically $ putTMVar respVar resp
|
||||
pure resp
|
||||
@@ -147,8 +155,12 @@ http2RequestTimeout :: HTTP2ClientConfig -> Maybe Int -> Int
|
||||
http2RequestTimeout HTTP2ClientConfig {connTimeout} = maybe connTimeout (connTimeout +)
|
||||
|
||||
runHTTP2Client :: forall a. T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> BufferSize -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (SessionId -> H.Client a) -> IO a
|
||||
runHTTP2Client tlsParams caStore tcConfig bufferSize proxyUsername host port keyHash client =
|
||||
runTLSTransportClient tlsParams caStore tcConfig proxyUsername host port keyHash $ withHTTP2 bufferSize run
|
||||
runHTTP2Client tlsParams caStore tcConfig bufferSize proxyUsername host port keyHash = runHTTP2ClientWith bufferSize host setup
|
||||
where
|
||||
setup = runTLSTransportClient tlsParams caStore tcConfig proxyUsername host port keyHash
|
||||
|
||||
runHTTP2ClientWith :: forall a. BufferSize -> TransportHost -> ((TLS -> IO a) -> IO a) -> (SessionId -> H.Client a) -> IO a
|
||||
runHTTP2ClientWith bufferSize host setup client = setup $ withHTTP2 bufferSize run
|
||||
where
|
||||
run :: H.Config -> SessionId -> IO a
|
||||
run cfg = H.run (ClientConfig "https" (strEncode host) 20) cfg . client
|
||||
run cfg sessId = H.run (ClientConfig "https" (strEncode host) 20) cfg $ client sessId
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
|
||||
module Simplex.Messaging.Transport.HTTP2.File where
|
||||
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as B
|
||||
import Data.ByteString.Builder (Builder, byteString)
|
||||
import Data.Int (Int64)
|
||||
import Data.Word (Word32)
|
||||
import GHC.IO.Handle.Internals (ioe_EOF)
|
||||
import System.IO (Handle)
|
||||
|
||||
fileBlockSize :: Int
|
||||
fileBlockSize = 16384
|
||||
|
||||
hReceiveFile :: (Int -> IO ByteString) -> Handle -> Word32 -> IO Int64
|
||||
hReceiveFile _ _ 0 = pure 0
|
||||
hReceiveFile getBody h size = get $ fromIntegral size
|
||||
where
|
||||
get sz = do
|
||||
ch <- getBody fileBlockSize
|
||||
let chSize = fromIntegral $ B.length ch
|
||||
if
|
||||
| chSize > sz -> pure (chSize - sz)
|
||||
| chSize > 0 -> B.hPut h ch >> get (sz - chSize)
|
||||
| otherwise -> pure (-fromIntegral sz)
|
||||
|
||||
hSendFile :: Handle -> (Builder -> IO ()) -> Word32 -> IO ()
|
||||
hSendFile h send = go
|
||||
where
|
||||
go 0 = pure ()
|
||||
go sz =
|
||||
getFileChunk h sz >>= \ch -> do
|
||||
send $ byteString ch
|
||||
go $ sz - fromIntegral (B.length ch)
|
||||
|
||||
getFileChunk :: Handle -> Word32 -> IO ByteString
|
||||
getFileChunk h sz = do
|
||||
ch <- B.hGet h fileBlockSize
|
||||
if B.null ch
|
||||
then ioe_EOF
|
||||
else pure $ B.take (fromIntegral sz) ch -- sz >= xftpBlockSize
|
||||
@@ -1,5 +1,4 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Simplex.Messaging.Transport.HTTP2.Server where
|
||||
|
||||
@@ -12,7 +11,7 @@ import qualified Network.HTTP2.Server as H
|
||||
import Network.Socket
|
||||
import qualified Network.TLS as T
|
||||
import Numeric.Natural (Natural)
|
||||
import Simplex.Messaging.Transport (SessionId)
|
||||
import Simplex.Messaging.Transport (SessionId, TLS)
|
||||
import Simplex.Messaging.Transport.HTTP2
|
||||
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), loadSupportedTLSServerParams, runTransportServer)
|
||||
|
||||
@@ -60,7 +59,11 @@ closeHTTP2Server :: HTTP2Server -> IO ()
|
||||
closeHTTP2Server = uninterruptibleCancel . action
|
||||
|
||||
runHTTP2Server :: TMVar Bool -> ServiceName -> BufferSize -> T.ServerParams -> TransportServerConfig -> HTTP2ServerFunc -> IO ()
|
||||
runHTTP2Server started port bufferSize serverParams transportConfig http2Server =
|
||||
runTransportServer started port serverParams transportConfig $ withHTTP2 bufferSize run
|
||||
runHTTP2Server started port bufferSize serverParams transportConfig = runHTTP2ServerWith bufferSize setup
|
||||
where
|
||||
setup = runTransportServer started port serverParams transportConfig
|
||||
|
||||
runHTTP2ServerWith :: BufferSize -> ((TLS -> IO ()) -> a) -> (SessionId -> Request -> (Response -> IO ()) -> IO ()) -> a
|
||||
runHTTP2ServerWith bufferSize setup http2Server = setup $ withHTTP2 bufferSize run
|
||||
where
|
||||
run cfg sessId = H.run cfg $ \req _aux sendResp -> http2Server sessId req (`sendResp` [])
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
{-# LANGUAGE CApiFFI #-}
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
module Simplex.Messaging.Transport.KeepAlive where
|
||||
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.TH as J
|
||||
import Foreign.C (CInt (..))
|
||||
import GHC.Generics (Generic)
|
||||
import Network.Socket
|
||||
import Simplex.Messaging.Parsers (defaultJSON)
|
||||
|
||||
data KeepAliveOpts = KeepAliveOpts
|
||||
{ keepIdle :: Int,
|
||||
keepIntvl :: Int,
|
||||
keepCnt :: Int
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
instance ToJSON KeepAliveOpts where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
deriving (Eq, Show)
|
||||
|
||||
defaultKeepAliveOpts :: KeepAliveOpts
|
||||
defaultKeepAliveOpts =
|
||||
@@ -68,3 +65,5 @@ setSocketKeepAlive sock KeepAliveOpts {keepCnt, keepIdle, keepIntvl} = do
|
||||
setSocketOption sock (SockOpt _SOL_TCP _TCP_KEEPIDLE) keepIdle
|
||||
setSocketOption sock (SockOpt _SOL_TCP _TCP_KEEPINTVL) keepIntvl
|
||||
setSocketOption sock (SockOpt _SOL_TCP _TCP_KEEPCNT) keepCnt
|
||||
|
||||
$(J.deriveJSON defaultJSON ''KeepAliveOpts)
|
||||
|
||||
@@ -5,10 +5,13 @@
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.Messaging.Transport.Server
|
||||
( runTransportServer,
|
||||
runTCPServer,
|
||||
TransportServerConfig (..),
|
||||
( TransportServerConfig (..),
|
||||
defaultTransportServerConfig,
|
||||
runTransportServer,
|
||||
runTransportServerSocket,
|
||||
runTCPServer,
|
||||
runTCPServerSocket,
|
||||
startTCPServer,
|
||||
loadSupportedTLSServerParams,
|
||||
loadTLSServerParams,
|
||||
loadFingerprint,
|
||||
@@ -46,10 +49,11 @@ data TransportServerConfig = TransportServerConfig
|
||||
deriving (Eq, Show)
|
||||
|
||||
defaultTransportServerConfig :: TransportServerConfig
|
||||
defaultTransportServerConfig = TransportServerConfig
|
||||
{ logTLSErrors = True,
|
||||
transportTimeout = 40000000
|
||||
}
|
||||
defaultTransportServerConfig =
|
||||
TransportServerConfig
|
||||
{ logTLSErrors = True,
|
||||
transportTimeout = 40000000
|
||||
}
|
||||
|
||||
serverTransportConfig :: TransportServerConfig -> TransportConfig
|
||||
serverTransportConfig TransportServerConfig {logTLSErrors} =
|
||||
@@ -60,11 +64,15 @@ serverTransportConfig TransportServerConfig {logTLSErrors} =
|
||||
--
|
||||
-- All accepted connections are passed to the passed function.
|
||||
runTransportServer :: forall c m. (Transport c, MonadUnliftIO m) => TMVar Bool -> ServiceName -> T.ServerParams -> TransportServerConfig -> (c -> m ()) -> m ()
|
||||
runTransportServer started port serverParams cfg server = do
|
||||
runTransportServer started port = runTransportServerSocket started (startTCPServer started port) (transportName (TProxy :: TProxy c))
|
||||
|
||||
-- | Run a transport server with provided connection setup and handler.
|
||||
runTransportServerSocket :: (MonadUnliftIO m, T.TLSParams p, Transport a) => TMVar Bool -> IO Socket -> String -> p -> TransportServerConfig -> (a -> m ()) -> m ()
|
||||
runTransportServerSocket started getSocket threadLabel serverParams cfg server = do
|
||||
u <- askUnliftIO
|
||||
let tCfg = serverTransportConfig cfg
|
||||
labelMyThread $ "transport server for " <> transportName (TProxy :: TProxy c)
|
||||
liftIO . runTCPServer started port $ \conn ->
|
||||
labelMyThread $ "transport server for " <> threadLabel
|
||||
liftIO . runTCPServerSocket started getSocket $ \conn ->
|
||||
E.bracket
|
||||
(connectTLS Nothing tCfg serverParams conn >>= getServerConnection tCfg)
|
||||
closeConnection
|
||||
@@ -72,11 +80,15 @@ runTransportServer started port serverParams cfg server = do
|
||||
|
||||
-- | Run TCP server without TLS
|
||||
runTCPServer :: TMVar Bool -> ServiceName -> (Socket -> IO ()) -> IO ()
|
||||
runTCPServer started port server = do
|
||||
runTCPServer started port = runTCPServerSocket started $ startTCPServer started port
|
||||
|
||||
-- | Wrap socket provider in a TCP server bracket.
|
||||
runTCPServerSocket :: TMVar Bool -> IO Socket -> (Socket -> IO ()) -> IO ()
|
||||
runTCPServerSocket started getSocket server = do
|
||||
clients <- atomically TM.empty
|
||||
clientId <- newTVarIO 0
|
||||
E.bracket
|
||||
(startTCPServer started port)
|
||||
getSocket
|
||||
(closeServer started clients)
|
||||
$ \sock -> forever . E.bracketOnError (accept sock) (close . fst) $ \(conn, _peer) -> do
|
||||
-- catchAll_ is needed here in case the connection was closed earlier
|
||||
|
||||
@@ -15,9 +15,9 @@ import qualified Network.WebSockets.Stream as S
|
||||
import Simplex.Messaging.Transport
|
||||
( TProxy,
|
||||
Transport (..),
|
||||
TransportConfig (..),
|
||||
TransportError (..),
|
||||
TransportPeer (..),
|
||||
TransportConfig (..),
|
||||
closeTLS,
|
||||
smpBlockSize,
|
||||
withTlsUnique,
|
||||
|
||||
@@ -3,13 +3,11 @@
|
||||
|
||||
module Simplex.Messaging.Util where
|
||||
|
||||
import Control.Concurrent (threadDelay)
|
||||
import qualified Control.Exception as E
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift
|
||||
import Data.Bifunctor (first)
|
||||
import qualified Data.ByteString as BW
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Int (Int64)
|
||||
@@ -20,9 +18,8 @@ import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (decodeUtf8With)
|
||||
import Data.Time (NominalDiffTime)
|
||||
import GHC.Conc
|
||||
import Numeric (showHex)
|
||||
import UnliftIO.Async
|
||||
import GHC.Conc (labelThread, myThreadId, threadDelay)
|
||||
import UnliftIO
|
||||
import qualified UnliftIO.Exception as UE
|
||||
|
||||
raceAny_ :: MonadUnliftIO m => [m a] -> m ()
|
||||
@@ -65,7 +62,7 @@ liftEitherError :: (MonadIO m, MonadError e' m) => (e -> e') -> IO (Either e a)
|
||||
liftEitherError f a = liftIOEither (first f <$> a)
|
||||
{-# INLINE liftEitherError #-}
|
||||
|
||||
liftEitherWith :: (MonadError e' m) => (e -> e') -> Either e a -> m a
|
||||
liftEitherWith :: MonadError e' m => (e -> e') -> Either e a -> m a
|
||||
liftEitherWith f = liftEither . first f
|
||||
{-# INLINE liftEitherWith #-}
|
||||
|
||||
@@ -88,6 +85,26 @@ unlessM b = ifM b $ pure ()
|
||||
($>>=) :: (Monad m, Monad f, Traversable f) => m (f a) -> (a -> m (f b)) -> m (f b)
|
||||
f $>>= g = f >>= fmap join . mapM g
|
||||
|
||||
mapME :: (Monad m, Traversable t) => (a -> m (Either e b)) -> t (Either e a) -> m (t (Either e b))
|
||||
mapME f = mapM (mapE f)
|
||||
{-# INLINE mapME #-}
|
||||
|
||||
mapME_ :: (Monad m, Traversable t) => (a -> m (Either e b)) -> t (Either e a) -> m ()
|
||||
mapME_ f = mapM_ (mapE f)
|
||||
{-# INLINE mapME_ #-}
|
||||
|
||||
mapE :: Monad m => (a -> m (Either e b)) -> Either e a -> m (Either e b)
|
||||
mapE = either (pure . Left)
|
||||
{-# INLINE mapE #-}
|
||||
|
||||
forME :: (Monad m, Traversable t) => t (Either e a) -> (a -> m (Either e b)) -> m (t (Either e b))
|
||||
forME = flip mapME
|
||||
{-# INLINE forME #-}
|
||||
|
||||
forME_ :: (Monad m, Traversable t) => t (Either e a) -> (a -> m (Either e b)) -> m ()
|
||||
forME_ f = void . forME f
|
||||
{-# INLINE forME_ #-}
|
||||
|
||||
catchAll :: IO a -> (E.SomeException -> IO a) -> IO a
|
||||
catchAll = E.catch
|
||||
{-# INLINE catchAll #-}
|
||||
@@ -101,11 +118,11 @@ tryAllErrors err action = tryError action `UE.catch` (pure . Left . err)
|
||||
{-# INLINE tryAllErrors #-}
|
||||
|
||||
catchAllErrors :: (MonadUnliftIO m, MonadError e m) => (E.SomeException -> e) -> m a -> (e -> m a) -> m a
|
||||
catchAllErrors err action handle = tryAllErrors err action >>= either handle pure
|
||||
catchAllErrors err action handler = tryAllErrors err action >>= either handler pure
|
||||
{-# INLINE catchAllErrors #-}
|
||||
|
||||
catchThrow :: (MonadUnliftIO m, MonadError e m) => m a -> (E.SomeException -> e) -> m a
|
||||
catchThrow action err = catchAllErrors err action throwError
|
||||
catchThrow action err = catchAllErrors err action throwError
|
||||
{-# INLINE catchThrow #-}
|
||||
|
||||
allFinally :: (MonadUnliftIO m, MonadError e m) => (E.SomeException -> e) -> m a -> m b -> m a
|
||||
@@ -118,12 +135,12 @@ eitherToMaybe = either (const Nothing) Just
|
||||
|
||||
groupOn :: Eq k => (a -> k) -> [a] -> [[a]]
|
||||
groupOn = groupBy . eqOn
|
||||
-- it is equivalent to groupBy ((==) `on` f),
|
||||
-- but it redefines `on` to avoid duplicate computation for most values.
|
||||
-- source: https://hackage.haskell.org/package/extra-1.7.13/docs/src/Data.List.Extra.html#groupOn
|
||||
-- the on2 in this package is specialized to only use `==` as the function, `eqOn f` is equivalent to `(==) `on` f`
|
||||
where
|
||||
eqOn f = \x -> let fx = f x in \y -> fx == f y
|
||||
-- it is equivalent to groupBy ((==) `on` f),
|
||||
-- but it redefines `on` to avoid duplicate computation for most values.
|
||||
-- source: https://hackage.haskell.org/package/extra-1.7.13/docs/src/Data.List.Extra.html#groupOn
|
||||
-- the on2 in this package is specialized to only use `==` as the function, `eqOn f` is equivalent to `(==) `on` f`
|
||||
eqOn f x = let fx = f x in \y -> fx == f y
|
||||
|
||||
groupAllOn :: Ord k => (a -> k) -> [a] -> [[a]]
|
||||
groupAllOn f = groupOn f . sortOn f
|
||||
@@ -132,13 +149,16 @@ toChunks :: Int -> [a] -> [NonEmpty a]
|
||||
toChunks _ [] = []
|
||||
toChunks n xs =
|
||||
let (ys, xs') = splitAt n xs
|
||||
in maybe id (:) (L.nonEmpty ys) (toChunks n xs')
|
||||
in maybe id (:) (L.nonEmpty ys) (toChunks n xs')
|
||||
|
||||
safeDecodeUtf8 :: ByteString -> Text
|
||||
safeDecodeUtf8 = decodeUtf8With onError
|
||||
where
|
||||
onError _ _ = Just '?'
|
||||
|
||||
timeoutThrow :: (MonadUnliftIO m, MonadError e m) => e -> Int -> m a -> m a
|
||||
timeoutThrow e ms action = timeout ms action >>= maybe (throwError e) pure
|
||||
|
||||
threadDelay' :: Int64 -> IO ()
|
||||
threadDelay' time
|
||||
| time <= 0 = pure ()
|
||||
|
||||
@@ -0,0 +1,448 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
|
||||
module Simplex.RemoteControl.Client
|
||||
( RCHostClient (action),
|
||||
RCHostConnection,
|
||||
newRCHostPairing,
|
||||
connectRCHost,
|
||||
cancelHostClient,
|
||||
RCCtrlClient (action),
|
||||
RCCtrlConnection,
|
||||
connectRCCtrl,
|
||||
discoverRCCtrl,
|
||||
confirmCtrlSession,
|
||||
cancelCtrlClient,
|
||||
RCStepTMVar,
|
||||
rcEncryptBody,
|
||||
rcDecryptBody,
|
||||
xrcpBlockSize,
|
||||
) where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Class
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import qualified Data.Aeson as J
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Lazy as LB
|
||||
import Data.Default (def)
|
||||
import Data.Functor (($>))
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
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.Word (Word16)
|
||||
import qualified Data.X509 as X509
|
||||
import Data.X509.Validation (Fingerprint (..), getFingerprint)
|
||||
import Network.Socket (PortNumber, SockAddr (..), hostAddressToTuple)
|
||||
import qualified Network.TLS as TLS
|
||||
import qualified Network.UDP as UDP
|
||||
import Simplex.Messaging.Agent.Client ()
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.Lazy (LazyByteString)
|
||||
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||
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.Buffer (peekBuffered)
|
||||
import Simplex.Messaging.Transport.Client (TransportClientConfig (..), TransportHost (..), defaultTransportClientConfig, runTransportClient)
|
||||
import Simplex.Messaging.Transport.Credentials (genCredentials, tlsCredentials)
|
||||
import Simplex.Messaging.Util
|
||||
import Simplex.Messaging.Version
|
||||
import Simplex.RemoteControl.Discovery (getLocalAddress, recvAnnounce, startTLSServer, withListener, withSender)
|
||||
import Simplex.RemoteControl.Invitation
|
||||
import Simplex.RemoteControl.Types
|
||||
import UnliftIO
|
||||
import UnliftIO.Concurrent
|
||||
|
||||
currentRCVersion :: Version
|
||||
currentRCVersion = 1
|
||||
|
||||
supportedRCVRange :: VersionRange
|
||||
supportedRCVRange = mkVersionRange 1 currentRCVersion
|
||||
|
||||
xrcpBlockSize :: Int
|
||||
xrcpBlockSize = 16384
|
||||
|
||||
helloBlockSize :: Int
|
||||
helloBlockSize = 12288
|
||||
|
||||
encInvitationSize :: Int
|
||||
encInvitationSize = 900
|
||||
|
||||
newRCHostPairing :: IO RCHostPairing
|
||||
newRCHostPairing = do
|
||||
((_, caKey), caCert) <- genCredentials Nothing (-25, 24 * 999999) "ca"
|
||||
(_, idPrivKey) <- C.generateKeyPair'
|
||||
pure RCHostPairing {caKey, caCert, idPrivKey, knownHost = Nothing}
|
||||
|
||||
data RCHostClient = RCHostClient
|
||||
{ action :: Async (),
|
||||
client_ :: RCHClient_
|
||||
}
|
||||
|
||||
data RCHClient_ = RCHClient_
|
||||
{ startedPort :: TMVar (Maybe PortNumber),
|
||||
announcer :: TMVar (Async (Either RCErrorType ())),
|
||||
hostCAHash :: TMVar C.KeyHash,
|
||||
endSession :: TMVar ()
|
||||
}
|
||||
|
||||
type RCHostConnection = (NonEmpty RCCtrlAddress, RCSignedInvitation, RCHostClient, RCStepTMVar (SessionCode, TLS, RCStepTMVar (RCHostSession, RCHostHello, RCHostPairing)))
|
||||
|
||||
connectRCHost :: TVar ChaChaDRG -> RCHostPairing -> J.Value -> Bool -> Maybe RCCtrlAddress -> Maybe Word16 -> ExceptT RCErrorType IO RCHostConnection
|
||||
connectRCHost drg pairing@RCHostPairing {caKey, caCert, idPrivKey, knownHost} ctrlAppInfo multicast rcAddrPrefs_ port_ = do
|
||||
r <- newEmptyTMVarIO
|
||||
found@(RCCtrlAddress {address} :| _) <- findCtrlAddress
|
||||
c@RCHClient_ {startedPort, announcer} <- liftIO mkClient
|
||||
hostKeys <- liftIO genHostKeys
|
||||
action <- runClient c r hostKeys `putRCError` r
|
||||
-- wait for the port to make invitation
|
||||
portNum <- atomically $ readTMVar startedPort
|
||||
signedInv@RCSignedInvitation {invitation} <- maybe (throwError RCETLSStartFailed) (liftIO . mkInvitation hostKeys address) portNum
|
||||
when multicast $ case knownHost of
|
||||
Nothing -> throwError RCENewController
|
||||
Just KnownHostPairing {hostDhPubKey} -> do
|
||||
ann <- async . liftIO . runExceptT $ announceRC drg 60 idPrivKey hostDhPubKey hostKeys invitation
|
||||
atomically $ putTMVar announcer ann
|
||||
pure (found, signedInv, RCHostClient {action, client_ = c}, r)
|
||||
where
|
||||
findCtrlAddress :: ExceptT RCErrorType IO (NonEmpty RCCtrlAddress)
|
||||
findCtrlAddress = do
|
||||
found' <- liftIO $ getLocalAddress rcAddrPrefs_
|
||||
maybe (throwError RCENoLocalAddress) pure $ L.nonEmpty found'
|
||||
mkClient :: IO RCHClient_
|
||||
mkClient = do
|
||||
startedPort <- newEmptyTMVarIO
|
||||
announcer <- newEmptyTMVarIO
|
||||
endSession <- newEmptyTMVarIO
|
||||
hostCAHash <- newEmptyTMVarIO
|
||||
pure RCHClient_ {startedPort, announcer, hostCAHash, endSession}
|
||||
runClient :: RCHClient_ -> RCStepTMVar (SessionCode, TLS, RCStepTMVar (RCHostSession, RCHostHello, RCHostPairing)) -> RCHostKeys -> ExceptT RCErrorType IO (Async ())
|
||||
runClient RCHClient_ {startedPort, announcer, hostCAHash, endSession} r hostKeys = do
|
||||
tlsCreds <- liftIO $ genTLSCredentials caKey caCert
|
||||
startTLSServer port_ startedPort tlsCreds (tlsHooks r knownHost hostCAHash) $ \tls ->
|
||||
void . runExceptT $ do
|
||||
r' <- newEmptyTMVarIO
|
||||
whenM (atomically $ tryPutTMVar r $ Right (tlsUniq tls, tls, r')) $
|
||||
runSession tls r' `putRCError` r'
|
||||
where
|
||||
runSession tls r' = do
|
||||
logDebug "Incoming TLS connection"
|
||||
hostEncHello <- receiveRCPacket tls
|
||||
logDebug "Received host HELLO"
|
||||
hostCA <- atomically $ takeTMVar hostCAHash
|
||||
(ctrlEncHello, sessionKeys, helloBody, pairing') <- prepareHostSession drg hostCA pairing hostKeys hostEncHello
|
||||
sendRCPacket tls ctrlEncHello
|
||||
logDebug "Sent ctrl HELLO"
|
||||
whenM (atomically $ tryPutTMVar r' $ Right (RCHostSession {tls, sessionKeys}, helloBody, pairing')) $ do
|
||||
atomically (tryReadTMVar announcer) >>= mapM_ uninterruptibleCancel
|
||||
-- can use `RCHostSession` until `endSession` is signalled
|
||||
logDebug "Holding session"
|
||||
atomically $ takeTMVar endSession
|
||||
tlsHooks :: TMVar a -> Maybe KnownHostPairing -> TMVar C.KeyHash -> TLS.ServerHooks
|
||||
tlsHooks r knownHost_ hostCAHash =
|
||||
def
|
||||
{ TLS.onNewHandshake = \_ -> atomically $ isNothing <$> tryReadTMVar r,
|
||||
TLS.onClientCertificate = \(X509.CertificateChain chain) ->
|
||||
case chain of
|
||||
[_leaf, ca] -> do
|
||||
let kh = certFingerprint ca
|
||||
accept = maybe True (\h -> hostFingerprint h == kh) knownHost_
|
||||
if accept
|
||||
then atomically (putTMVar hostCAHash kh) $> TLS.CertificateUsageAccept
|
||||
else pure $ TLS.CertificateUsageReject TLS.CertificateRejectUnknownCA
|
||||
_ ->
|
||||
pure $ TLS.CertificateUsageReject TLS.CertificateRejectUnknownCA
|
||||
}
|
||||
genHostKeys :: IO RCHostKeys
|
||||
genHostKeys = do
|
||||
sessKeys <- C.generateKeyPair'
|
||||
dhKeys <- C.generateKeyPair'
|
||||
pure RCHostKeys {sessKeys, dhKeys}
|
||||
mkInvitation :: RCHostKeys -> TransportHost -> PortNumber -> IO RCSignedInvitation
|
||||
mkInvitation RCHostKeys {sessKeys, dhKeys} host portNum = do
|
||||
ts <- getSystemTime
|
||||
let inv =
|
||||
RCInvitation
|
||||
{ ca = certFingerprint caCert,
|
||||
host,
|
||||
port = fromIntegral portNum,
|
||||
v = supportedRCVRange,
|
||||
app = ctrlAppInfo,
|
||||
ts,
|
||||
skey = fst sessKeys,
|
||||
idkey = C.publicKey idPrivKey,
|
||||
dh = fst dhKeys
|
||||
}
|
||||
pure $ signInvitation (snd sessKeys) idPrivKey inv
|
||||
|
||||
genTLSCredentials :: C.APrivateSignKey -> C.SignedCertificate -> IO TLS.Credentials
|
||||
genTLSCredentials caKey caCert = do
|
||||
let caCreds = (C.signatureKeyPair caKey, caCert)
|
||||
leaf <- genCredentials (Just caCreds) (0, 24 * 999999) "localhost" -- session-signing cert
|
||||
pure . snd $ tlsCredentials (leaf :| [caCreds])
|
||||
|
||||
certFingerprint :: X509.SignedCertificate -> C.KeyHash
|
||||
certFingerprint caCert = C.KeyHash fp
|
||||
where
|
||||
Fingerprint fp = getFingerprint caCert X509.HashSHA256
|
||||
|
||||
cancelHostClient :: RCHostClient -> IO ()
|
||||
cancelHostClient RCHostClient {action, client_ = RCHClient_ {announcer, endSession}} = do
|
||||
atomically $ putTMVar endSession ()
|
||||
atomically (tryTakeTMVar announcer) >>= mapM_ uninterruptibleCancel
|
||||
uninterruptibleCancel action
|
||||
|
||||
prepareHostSession :: TVar ChaChaDRG -> C.KeyHash -> RCHostPairing -> RCHostKeys -> RCHostEncHello -> ExceptT RCErrorType IO (RCCtrlEncHello, HostSessKeys, RCHostHello, RCHostPairing)
|
||||
prepareHostSession
|
||||
drg
|
||||
tlsHostFingerprint
|
||||
pairing@RCHostPairing {idPrivKey, knownHost = knownHost_}
|
||||
RCHostKeys {sessKeys = (_, sessPrivKey), dhKeys = (_, dhPrivKey)}
|
||||
RCHostEncHello {dhPubKey, nonce, encBody} = do
|
||||
let sharedKey = C.dh' dhPubKey dhPrivKey
|
||||
helloBody <- liftEitherWith (const RCEDecrypt) $ C.cbDecrypt sharedKey nonce encBody
|
||||
hostHello@RCHostHello {v, ca, kem = kemPubKey} <- liftEitherWith RCESyntax $ J.eitherDecodeStrict helloBody
|
||||
unless (ca == tlsHostFingerprint) $ throwError RCEIdentity
|
||||
(kemCiphertext, kemSharedKey) <- liftIO $ sntrup761Enc drg kemPubKey
|
||||
let hybridKey = kemHybridSecret dhPubKey dhPrivKey kemSharedKey
|
||||
unless (isCompatible v supportedRCVRange) $ throwError RCEVersion
|
||||
let keys = HostSessKeys {hybridKey, idPrivKey, sessPrivKey}
|
||||
knownHost' <- updateKnownHost ca dhPubKey
|
||||
let ctrlHello = RCCtrlHello {}
|
||||
-- TODO send error response if something fails
|
||||
nonce' <- liftIO . atomically $ C.pseudoRandomCbNonce drg
|
||||
encBody' <- liftEitherWith (const RCEBlockSize) $ kcbEncrypt hybridKey nonce' (LB.toStrict $ J.encode ctrlHello) helloBlockSize
|
||||
let ctrlEncHello = RCCtrlEncHello {kem = kemCiphertext, nonce = nonce', encBody = encBody'}
|
||||
pure (ctrlEncHello, keys, hostHello, pairing {knownHost = Just knownHost'})
|
||||
where
|
||||
updateKnownHost :: C.KeyHash -> C.PublicKeyX25519 -> ExceptT RCErrorType IO KnownHostPairing
|
||||
updateKnownHost ca hostDhPubKey = case knownHost_ of
|
||||
Just h -> do
|
||||
unless (hostFingerprint h == tlsHostFingerprint) . throwError $
|
||||
RCEInternal "TLS host CA is different from host pairing, should be caught in TLS handshake"
|
||||
pure (h :: KnownHostPairing) {hostDhPubKey}
|
||||
Nothing -> pure KnownHostPairing {hostFingerprint = ca, hostDhPubKey}
|
||||
|
||||
data RCCtrlClient = RCCtrlClient
|
||||
{ action :: Async (),
|
||||
client_ :: RCCClient_
|
||||
}
|
||||
|
||||
data RCCClient_ = RCCClient_
|
||||
{ confirmSession :: TMVar Bool,
|
||||
endSession :: TMVar ()
|
||||
}
|
||||
|
||||
type RCCtrlConnection = (RCCtrlClient, RCStepTMVar (SessionCode, TLS, RCStepTMVar (RCCtrlSession, RCCtrlPairing)))
|
||||
|
||||
-- app should determine whether it is a new or known pairing based on CA fingerprint in the invitation
|
||||
connectRCCtrl :: TVar ChaChaDRG -> RCVerifiedInvitation -> Maybe RCCtrlPairing -> J.Value -> ExceptT RCErrorType IO RCCtrlConnection
|
||||
connectRCCtrl drg (RCVerifiedInvitation inv@RCInvitation {ca, idkey}) pairing_ hostAppInfo = do
|
||||
pairing' <- maybe (liftIO newCtrlPairing) updateCtrlPairing pairing_
|
||||
connectRCCtrl_ drg pairing' inv hostAppInfo
|
||||
where
|
||||
newCtrlPairing :: IO RCCtrlPairing
|
||||
newCtrlPairing = do
|
||||
((_, caKey), caCert) <- genCredentials Nothing (0, 24 * 999999) "ca"
|
||||
(_, dhPrivKey) <- C.generateKeyPair'
|
||||
pure RCCtrlPairing {caKey, caCert, ctrlFingerprint = ca, idPubKey = idkey, dhPrivKey, prevDhPrivKey = Nothing}
|
||||
updateCtrlPairing :: RCCtrlPairing -> ExceptT RCErrorType IO RCCtrlPairing
|
||||
updateCtrlPairing pairing@RCCtrlPairing {ctrlFingerprint, idPubKey, dhPrivKey = currDhPrivKey} = do
|
||||
unless (ca == ctrlFingerprint && idPubKey == idkey) $ throwError RCEIdentity
|
||||
(_, dhPrivKey) <- liftIO C.generateKeyPair'
|
||||
pure pairing {dhPrivKey, prevDhPrivKey = Just currDhPrivKey}
|
||||
|
||||
connectRCCtrl_ :: TVar ChaChaDRG -> RCCtrlPairing -> RCInvitation -> J.Value -> ExceptT RCErrorType IO RCCtrlConnection
|
||||
connectRCCtrl_ drg pairing'@RCCtrlPairing {caKey, caCert} inv@RCInvitation {ca, host, port} hostAppInfo = do
|
||||
r <- newEmptyTMVarIO
|
||||
c <- liftIO mkClient
|
||||
action <- async $ runClient c r `putRCError` r
|
||||
pure (RCCtrlClient {action, client_ = c}, r)
|
||||
where
|
||||
mkClient :: IO RCCClient_
|
||||
mkClient = do
|
||||
confirmSession <- newEmptyTMVarIO
|
||||
endSession <- newEmptyTMVarIO
|
||||
pure RCCClient_ {confirmSession, endSession}
|
||||
runClient :: RCCClient_ -> RCStepTMVar (SessionCode, TLS, RCStepTMVar (RCCtrlSession, RCCtrlPairing)) -> ExceptT RCErrorType IO ()
|
||||
runClient RCCClient_ {confirmSession, endSession} r = do
|
||||
clientCredentials <-
|
||||
liftIO (genTLSCredentials caKey caCert) >>= \case
|
||||
TLS.Credentials (creds : _) -> pure $ Just creds
|
||||
_ -> throwError $ RCEInternal "genTLSCredentials must generate credentials"
|
||||
let clientConfig = defaultTransportClientConfig {clientCredentials}
|
||||
runTransportClient clientConfig Nothing host (show port) (Just ca) $ \tls@TLS {tlsBuffer, tlsContext} -> do
|
||||
-- pump socket to detect connection problems
|
||||
liftIO $ peekBuffered tlsBuffer 100000 (TLS.recvData tlsContext) >>= logDebug . tshow -- should normally be ("", Nothing) here
|
||||
logDebug "Got TLS connection"
|
||||
r' <- newEmptyTMVarIO
|
||||
whenM (atomically $ tryPutTMVar r $ Right (tlsUniq tls, tls, r')) $ do
|
||||
logDebug "Waiting for session confirmation"
|
||||
whenM (atomically $ readTMVar confirmSession) $ runSession tls r' `putRCError` r'
|
||||
where
|
||||
runSession tls r' = do
|
||||
(sharedKey, kemPrivKey, hostEncHello) <- prepareHostHello drg pairing' inv hostAppInfo
|
||||
sendRCPacket tls hostEncHello
|
||||
ctrlEncHello <- receiveRCPacket tls
|
||||
logDebug "Received ctrl HELLO"
|
||||
ctrlSessKeys <- prepareCtrlSession pairing' inv sharedKey kemPrivKey ctrlEncHello
|
||||
whenM (atomically $ tryPutTMVar r' $ Right (RCCtrlSession {tls, sessionKeys = ctrlSessKeys}, pairing')) $ do
|
||||
logDebug "Session started"
|
||||
-- release second putTMVar in confirmCtrlSession
|
||||
void . atomically $ takeTMVar confirmSession
|
||||
atomically $ takeTMVar endSession
|
||||
logDebug "Session ended"
|
||||
|
||||
catchRCError :: ExceptT RCErrorType IO a -> (RCErrorType -> ExceptT RCErrorType IO a) -> ExceptT RCErrorType IO a
|
||||
catchRCError = catchAllErrors (RCEException . show)
|
||||
{-# INLINE catchRCError #-}
|
||||
|
||||
putRCError :: ExceptT RCErrorType IO a -> TMVar (Either RCErrorType b) -> ExceptT RCErrorType IO a
|
||||
a `putRCError` r = a `catchRCError` \e -> atomically (tryPutTMVar r $ Left e) >> throwError e
|
||||
|
||||
sendRCPacket :: Encoding a => TLS -> a -> ExceptT RCErrorType IO ()
|
||||
sendRCPacket tls pkt = do
|
||||
b <- liftEitherWith (const RCEBlockSize) $ C.pad (smpEncode pkt) xrcpBlockSize
|
||||
liftIO $ cPut tls b
|
||||
|
||||
receiveRCPacket :: Encoding a => TLS -> ExceptT RCErrorType IO a
|
||||
receiveRCPacket tls = do
|
||||
b <- liftIO $ cGet tls xrcpBlockSize
|
||||
when (B.length b /= xrcpBlockSize) $ throwError RCEBlockSize
|
||||
b' <- liftEitherWith (const RCEBlockSize) $ C.unPad b
|
||||
liftEitherWith RCESyntax $ smpDecode b'
|
||||
|
||||
prepareHostHello :: TVar ChaChaDRG -> RCCtrlPairing -> RCInvitation -> J.Value -> ExceptT RCErrorType IO (C.DhSecretX25519, KEMSecretKey, RCHostEncHello)
|
||||
prepareHostHello
|
||||
drg
|
||||
RCCtrlPairing {caCert, dhPrivKey}
|
||||
RCInvitation {v, dh = dhPubKey}
|
||||
hostAppInfo = do
|
||||
logDebug "Preparing session"
|
||||
case compatibleVersion v supportedRCVRange of
|
||||
Nothing -> throwError RCEVersion
|
||||
Just (Compatible v') -> do
|
||||
nonce <- liftIO . atomically $ C.pseudoRandomCbNonce drg
|
||||
(kemPubKey, kemPrivKey) <- liftIO $ sntrup761Keypair drg
|
||||
let helloBody = RCHostHello {v = v', ca = certFingerprint caCert, app = hostAppInfo, kem = kemPubKey}
|
||||
sharedKey = C.dh' dhPubKey dhPrivKey
|
||||
encBody <- liftEitherWith (const RCEBlockSize) $ C.cbEncrypt sharedKey nonce (LB.toStrict $ J.encode helloBody) helloBlockSize
|
||||
-- let sessKeys = CtrlSessKeys {hybridKey, idPubKey, sessPubKey = skey}
|
||||
let hostEncHello = RCHostEncHello {dhPubKey = C.publicKey dhPrivKey, nonce, encBody}
|
||||
pure (sharedKey, kemPrivKey, hostEncHello)
|
||||
|
||||
prepareCtrlSession :: RCCtrlPairing -> RCInvitation -> C.DhSecretX25519 -> KEMSecretKey -> RCCtrlEncHello -> ExceptT RCErrorType IO CtrlSessKeys
|
||||
prepareCtrlSession
|
||||
RCCtrlPairing {idPubKey, dhPrivKey}
|
||||
RCInvitation {skey, dh = dhPubKey}
|
||||
sharedKey
|
||||
kemPrivKey = \case
|
||||
RCCtrlEncHello {kem = kemCiphertext, nonce, encBody} -> do
|
||||
kemSharedKey <- liftIO $ sntrup761Dec kemCiphertext kemPrivKey
|
||||
let hybridKey = kemHybridSecret dhPubKey dhPrivKey kemSharedKey
|
||||
helloBody <- liftEitherWith (const RCEDecrypt) $ kcbDecrypt hybridKey nonce encBody
|
||||
logDebug "Decrypted ctrl HELLO"
|
||||
RCCtrlHello {} <- liftEitherWith RCESyntax $ J.eitherDecodeStrict helloBody
|
||||
pure CtrlSessKeys {hybridKey, idPubKey, sessPubKey = skey}
|
||||
RCCtrlEncError {nonce, encMessage} -> do
|
||||
message <- liftEitherWith (const RCEDecrypt) $ C.cbDecrypt sharedKey nonce encMessage
|
||||
throwError $ RCECtrlError $ T.unpack $ safeDecodeUtf8 message
|
||||
|
||||
-- * Multicast discovery
|
||||
|
||||
announceRC :: TVar ChaChaDRG -> Int -> C.PrivateKeyEd25519 -> C.PublicKeyX25519 -> RCHostKeys -> RCInvitation -> ExceptT RCErrorType IO ()
|
||||
announceRC drg maxCount idPrivKey knownDhPub RCHostKeys {sessKeys, dhKeys} inv = withSender $ \sender -> do
|
||||
replicateM_ maxCount $ do
|
||||
logDebug "Announcing..."
|
||||
nonce <- atomically $ C.pseudoRandomCbNonce drg
|
||||
encInvitation <- liftEitherWith undefined $ C.cbEncrypt sharedKey nonce sigInvitation encInvitationSize
|
||||
liftIO . UDP.send sender $ smpEncode RCEncInvitation {dhPubKey, nonce, encInvitation}
|
||||
threadDelay 1000000
|
||||
where
|
||||
sigInvitation = strEncode $ signInvitation sPrivKey idPrivKey inv
|
||||
(_sPub, sPrivKey) = sessKeys
|
||||
sharedKey = C.dh' knownDhPub dhPrivKey
|
||||
(dhPubKey, dhPrivKey) = dhKeys
|
||||
|
||||
discoverRCCtrl :: TMVar Int -> NonEmpty RCCtrlPairing -> ExceptT RCErrorType IO (RCCtrlPairing, RCVerifiedInvitation)
|
||||
discoverRCCtrl subscribers pairings =
|
||||
timeoutThrow RCENotDiscovered 30000000 $ withListener subscribers $ \listener ->
|
||||
loop $ do
|
||||
(source, bytes) <- recvAnnounce listener
|
||||
encInvitation <- liftEitherWith (const RCEInvitation) $ smpDecode bytes
|
||||
r@(_, RCVerifiedInvitation RCInvitation {host}) <- findRCCtrlPairing pairings encInvitation
|
||||
case source of
|
||||
SockAddrInet _ ha | THIPv4 (hostAddressToTuple ha) == host -> pure ()
|
||||
_ -> throwError RCEInvitation
|
||||
pure r
|
||||
where
|
||||
loop :: ExceptT RCErrorType IO a -> ExceptT RCErrorType IO a
|
||||
loop action =
|
||||
liftIO (runExceptT action) >>= \case
|
||||
Left err -> logError (tshow err) >> loop action
|
||||
Right res -> pure res
|
||||
|
||||
findRCCtrlPairing :: NonEmpty RCCtrlPairing -> RCEncInvitation -> ExceptT RCErrorType IO (RCCtrlPairing, RCVerifiedInvitation)
|
||||
findRCCtrlPairing pairings RCEncInvitation {dhPubKey, nonce, encInvitation} = do
|
||||
(pairing, signedInvStr) <- liftEither $ decrypt (L.toList pairings)
|
||||
signedInv <- liftEitherWith RCESyntax $ strDecode signedInvStr
|
||||
inv@(RCVerifiedInvitation RCInvitation {dh = invDh}) <- maybe (throwError RCEInvitation) pure $ verifySignedInvitation signedInv
|
||||
unless (invDh == dhPubKey) $ throwError RCEInvitation
|
||||
pure (pairing, inv)
|
||||
where
|
||||
decrypt :: [RCCtrlPairing] -> Either RCErrorType (RCCtrlPairing, ByteString)
|
||||
decrypt [] = Left RCECtrlNotFound
|
||||
decrypt (pairing@RCCtrlPairing {dhPrivKey, prevDhPrivKey} : rest) =
|
||||
let r = decrypt_ dhPrivKey <|> (decrypt_ =<< prevDhPrivKey)
|
||||
in maybe (decrypt rest) (Right . (pairing,)) r
|
||||
decrypt_ :: C.PrivateKeyX25519 -> Maybe ByteString
|
||||
decrypt_ dhPrivKey =
|
||||
let key = C.dh' dhPubKey dhPrivKey
|
||||
in eitherToMaybe $ C.cbDecrypt key nonce encInvitation
|
||||
|
||||
-- * Controller handle operations
|
||||
|
||||
-- application should call this function when TMVar resolves
|
||||
confirmCtrlSession :: RCCtrlClient -> Bool -> IO ()
|
||||
confirmCtrlSession RCCtrlClient {client_ = RCCClient_ {confirmSession}} res = do
|
||||
atomically $ putTMVar confirmSession res
|
||||
-- controler does takeTMVar, freeing the slot
|
||||
-- TODO add timeout
|
||||
atomically $ putTMVar confirmSession res -- wait for Ctrl to take the var
|
||||
|
||||
cancelCtrlClient :: RCCtrlClient -> IO ()
|
||||
cancelCtrlClient RCCtrlClient {action, client_ = RCCClient_ {endSession}} = do
|
||||
atomically $ putTMVar endSession ()
|
||||
uninterruptibleCancel action
|
||||
|
||||
-- * Session encryption
|
||||
|
||||
rcEncryptBody :: TVar ChaChaDRG -> KEMHybridSecret -> LazyByteString -> ExceptT RCErrorType IO (C.CbNonce, LazyByteString)
|
||||
rcEncryptBody drg hybridKey s = do
|
||||
nonce <- atomically $ C.pseudoRandomCbNonce drg
|
||||
let len = LB.length s
|
||||
ct <- liftEitherWith (const RCEEncrypt) $ LC.kcbEncryptTailTag hybridKey nonce s len (len + 8)
|
||||
pure (nonce, ct)
|
||||
|
||||
rcDecryptBody :: KEMHybridSecret -> C.CbNonce -> LazyByteString -> ExceptT RCErrorType IO LazyByteString
|
||||
rcDecryptBody hybridKey nonce ct = do
|
||||
let len = LB.length ct - 16
|
||||
when (len < 0) $ throwError RCEDecrypt
|
||||
(ok, s) <- liftEitherWith (const RCEDecrypt) $ LC.kcbDecryptTailTag hybridKey nonce len ct
|
||||
unless ok $ throwError RCEDecrypt
|
||||
pure s
|
||||
@@ -0,0 +1,138 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
|
||||
module Simplex.RemoteControl.Discovery where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Default (def)
|
||||
import Data.List (delete, find)
|
||||
import Data.Maybe (mapMaybe)
|
||||
import Data.String (IsString)
|
||||
import qualified Data.Text as T
|
||||
import Data.Word (Word16)
|
||||
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 qualified Simplex.Messaging.Transport as Transport
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
import Simplex.Messaging.Transport.Server (defaultTransportServerConfig, runTransportServerSocket, startTCPServer)
|
||||
import Simplex.Messaging.Util (ifM, tshow)
|
||||
import Simplex.RemoteControl.Discovery.Multicast (setMembership)
|
||||
import Simplex.RemoteControl.Types
|
||||
import UnliftIO
|
||||
|
||||
-- | mDNS multicast group
|
||||
pattern MULTICAST_ADDR_V4 :: (IsString a, Eq a) => a
|
||||
pattern MULTICAST_ADDR_V4 = "224.0.0.251"
|
||||
|
||||
pattern ANY_ADDR_V4 :: (IsString a, Eq a) => a
|
||||
pattern ANY_ADDR_V4 = "0.0.0.0"
|
||||
|
||||
pattern DISCOVERY_PORT :: (IsString a, Eq a) => a
|
||||
pattern DISCOVERY_PORT = "5227"
|
||||
|
||||
getLocalAddress :: Maybe RCCtrlAddress -> IO [RCCtrlAddress]
|
||||
getLocalAddress preferred_ =
|
||||
maybe id preferAddress preferred_ . mkLastLocalHost . mapMaybe toCtrlAddr <$> getNetworkInterfaces
|
||||
where
|
||||
toCtrlAddr NetworkInterface {name, ipv4 = IPv4 ha} = case N.hostAddressToTuple ha of
|
||||
(0, 0, 0, 0) -> Nothing -- "no" address
|
||||
(255, 255, 255, 255) -> Nothing -- broadcast
|
||||
(169, 254, _, _) -> Nothing -- link-local
|
||||
ok -> Just RCCtrlAddress {address = THIPv4 ok, interface = T.pack name}
|
||||
|
||||
mkLastLocalHost :: [RCCtrlAddress] -> [RCCtrlAddress]
|
||||
mkLastLocalHost addrs = case find localHost addrs of
|
||||
Nothing -> addrs
|
||||
Just lh -> delete lh addrs <> [lh]
|
||||
where
|
||||
localHost RCCtrlAddress {address = a} = a == THIPv4 (127, 0, 0, 1)
|
||||
|
||||
preferAddress :: RCCtrlAddress -> [RCCtrlAddress] -> [RCCtrlAddress]
|
||||
preferAddress RCCtrlAddress {address, interface} addrs =
|
||||
case find matchAddr addrs <|> find matchIface addrs of
|
||||
Nothing -> addrs
|
||||
Just p -> p : delete p addrs
|
||||
where
|
||||
matchAddr RCCtrlAddress {address = a} = a == address
|
||||
matchIface RCCtrlAddress {interface = i} = i == interface
|
||||
|
||||
startTLSServer :: MonadUnliftIO m => Maybe Word16 -> TMVar (Maybe N.PortNumber) -> TLS.Credentials -> TLS.ServerHooks -> (Transport.TLS -> IO ()) -> m (Async ())
|
||||
startTLSServer port_ startedOnPort credentials hooks server = async . liftIO $ do
|
||||
started <- newEmptyTMVarIO
|
||||
bracketOnError (startTCPServer started $ maybe "0" show port_) (\_e -> setPort Nothing) $ \socket ->
|
||||
ifM
|
||||
(atomically $ readTMVar started)
|
||||
(runServer started socket)
|
||||
(setPort Nothing)
|
||||
where
|
||||
runServer started socket = do
|
||||
port <- N.socketPort socket
|
||||
logInfo $ "System-assigned port: " <> tshow port
|
||||
setPort $ Just port
|
||||
runTransportServerSocket started (pure socket) "RCP TLS" serverParams defaultTransportServerConfig server
|
||||
setPort = void . atomically . tryPutTMVar startedOnPort
|
||||
serverParams =
|
||||
def
|
||||
{ TLS.serverWantClientCert = True,
|
||||
TLS.serverShared = def {TLS.sharedCredentials = credentials},
|
||||
TLS.serverHooks = hooks,
|
||||
TLS.serverSupported = supportedParameters
|
||||
}
|
||||
|
||||
withSender :: MonadUnliftIO m => (UDP.UDPSocket -> m a) -> m a
|
||||
withSender = bracket (liftIO $ UDP.clientSocket MULTICAST_ADDR_V4 DISCOVERY_PORT False) (liftIO . UDP.close)
|
||||
|
||||
withListener :: MonadUnliftIO m => TMVar Int -> (UDP.ListenSocket -> m a) -> m a
|
||||
withListener subscribers = bracket (openListener subscribers) (closeListener subscribers)
|
||||
|
||||
openListener :: MonadIO m => TMVar Int -> m UDP.ListenSocket
|
||||
openListener subscribers = liftIO $ do
|
||||
sock <- UDP.serverSocket (MULTICAST_ADDR_V4, read DISCOVERY_PORT)
|
||||
logDebug $ "Discovery listener socket: " <> tshow sock
|
||||
let raw = UDP.listenSocket sock
|
||||
-- N.setSocketOption raw N.Broadcast 1
|
||||
joinMulticast subscribers raw (listenerHostAddr4 sock)
|
||||
pure sock
|
||||
|
||||
closeListener :: MonadIO m => TMVar Int -> UDP.ListenSocket -> m ()
|
||||
closeListener subscribers sock =
|
||||
liftIO $
|
||||
partMulticast subscribers (UDP.listenSocket sock) (listenerHostAddr4 sock) `finally` UDP.stop sock
|
||||
|
||||
joinMulticast :: TMVar Int -> N.Socket -> N.HostAddress -> IO ()
|
||||
joinMulticast subscribers sock group = do
|
||||
now <- atomically $ takeTMVar subscribers
|
||||
when (now == 0) $ do
|
||||
setMembership sock group True >>= \case
|
||||
Left e -> atomically (putTMVar subscribers now) >> logError ("setMembership failed " <> tshow e)
|
||||
Right () -> atomically $ putTMVar subscribers (now + 1)
|
||||
|
||||
partMulticast :: TMVar Int -> N.Socket -> N.HostAddress -> IO ()
|
||||
partMulticast subscribers sock group = do
|
||||
now <- atomically $ takeTMVar subscribers
|
||||
when (now == 1) $
|
||||
setMembership sock group False >>= \case
|
||||
Left e -> atomically (putTMVar subscribers now) >> logError ("setMembership failed " <> tshow e)
|
||||
Right () -> atomically $ putTMVar subscribers (now - 1)
|
||||
|
||||
listenerHostAddr4 :: UDP.ListenSocket -> N.HostAddress
|
||||
listenerHostAddr4 sock = case UDP.mySockAddr sock of
|
||||
N.SockAddrInet _port host -> host
|
||||
_ -> error "MULTICAST_ADDR_V4 is V4"
|
||||
|
||||
recvAnnounce :: MonadIO m => UDP.ListenSocket -> m (N.SockAddr, ByteString)
|
||||
recvAnnounce sock = liftIO $ do
|
||||
(invite, UDP.ClientSockAddr source _cmsg) <- UDP.recvFrom sock
|
||||
pure (source, invite)
|
||||
@@ -0,0 +1,48 @@
|
||||
module Simplex.RemoteControl.Discovery.Multicast
|
||||
( setMembership
|
||||
) where
|
||||
|
||||
import Foreign (Ptr, allocaBytes, castPtr, pokeByteOff)
|
||||
import Foreign.C.Types (CInt (..))
|
||||
import Network.Socket
|
||||
|
||||
#include <HsNet.h>
|
||||
|
||||
{- | Toggle multicast group membership.
|
||||
|
||||
NB: Group membership is per-host, not per-process. A socket is only used to access system interface for groups.
|
||||
-}
|
||||
setMembership :: Socket -> HostAddress -> Bool -> IO (Either CInt ())
|
||||
setMembership sock group membership = allocaBytes #{size struct ip_mreq} $ \mReqPtr -> do
|
||||
#{poke struct ip_mreq, imr_multiaddr} mReqPtr group
|
||||
#{poke struct ip_mreq, imr_interface} mReqPtr (0 :: HostAddress) -- attempt to contact the group on ANY interface
|
||||
withFdSocket sock $ \fd -> do
|
||||
rc <- c_setsockopt fd c_IPPROTO_IP flag (castPtr mReqPtr) (#{size struct ip_mreq})
|
||||
if rc == 0
|
||||
then pure $ Right ()
|
||||
else pure $ Left rc
|
||||
where
|
||||
flag = if membership then c_IP_ADD_MEMBERSHIP else c_IP_DROP_MEMBERSHIP
|
||||
|
||||
#ifdef mingw32_HOST_OS
|
||||
|
||||
foreign import stdcall unsafe "setsockopt"
|
||||
c_setsockopt :: CInt -> CInt -> CInt -> Ptr CInt -> CInt -> IO CInt
|
||||
|
||||
c_IP_ADD_MEMBERSHIP, c_IP_DROP_MEMBERSHIP :: CInt
|
||||
c_IP_ADD_MEMBERSHIP = 12
|
||||
c_IP_DROP_MEMBERSHIP = 13
|
||||
|
||||
#else
|
||||
|
||||
foreign import ccall unsafe "setsockopt"
|
||||
c_setsockopt :: CInt -> CInt -> CInt -> Ptr CInt -> CInt -> IO CInt
|
||||
|
||||
c_IP_ADD_MEMBERSHIP, c_IP_DROP_MEMBERSHIP :: CInt
|
||||
c_IP_ADD_MEMBERSHIP = #const IP_ADD_MEMBERSHIP
|
||||
c_IP_DROP_MEMBERSHIP = #const IP_DROP_MEMBERSHIP
|
||||
|
||||
#endif
|
||||
|
||||
c_IPPROTO_IP :: CInt
|
||||
c_IPPROTO_IP = #const IPPROTO_IP
|
||||
@@ -0,0 +1,169 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Simplex.RemoteControl.Invitation
|
||||
( RCInvitation (..)
|
||||
, signInvitation
|
||||
, RCSignedInvitation (..)
|
||||
, verifySignedInvitation
|
||||
, RCVerifiedInvitation (..)
|
||||
, RCEncInvitation (..)
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Lazy as LB
|
||||
import Data.Time.Clock.System (SystemTime)
|
||||
import Data.Word (Word16)
|
||||
import Network.HTTP.Types (parseSimpleQuery)
|
||||
import Network.HTTP.Types.URI (SimpleQuery, renderSimpleQuery, urlDecode)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||
import Simplex.Messaging.Version (VersionRange)
|
||||
|
||||
data RCInvitation = RCInvitation
|
||||
{ -- | 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.
|
||||
ca :: C.KeyHash,
|
||||
host :: TransportHost,
|
||||
port :: Word16,
|
||||
-- | Supported version range for remote control protocol
|
||||
v :: VersionRange,
|
||||
-- | Application information
|
||||
app :: J.Value,
|
||||
-- | Session start time in seconds since epoch
|
||||
ts :: SystemTime,
|
||||
-- | 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.
|
||||
skey :: C.PublicKeyEd25519,
|
||||
-- | Long-term Ed25519 public key used to verify the announcement and commands.
|
||||
--
|
||||
-- Is apart of the long term controller identity.
|
||||
idkey :: C.PublicKeyEd25519,
|
||||
-- | Session X25519 DH key
|
||||
dh :: C.PublicKeyX25519
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
instance StrEncoding RCInvitation where
|
||||
strEncode RCInvitation {ca, host, port, v, app, ts, skey, idkey, dh} =
|
||||
mconcat
|
||||
[ "xrcp:/",
|
||||
strEncode ca,
|
||||
"@",
|
||||
strEncode host,
|
||||
":",
|
||||
strEncode port,
|
||||
"#/?",
|
||||
renderSimpleQuery False query
|
||||
]
|
||||
where
|
||||
query =
|
||||
[ ("v", strEncode v),
|
||||
("app", LB.toStrict $ J.encode app),
|
||||
("ts", strEncode ts),
|
||||
("skey", strEncode skey),
|
||||
("idkey", strEncode idkey),
|
||||
("dh", strEncode dh)
|
||||
]
|
||||
|
||||
strP = do
|
||||
_ <- A.string "xrcp:/"
|
||||
ca <- strP
|
||||
_ <- A.char '@'
|
||||
host <- A.takeWhile (/= ':') >>= either fail pure . strDecode . urlDecode True
|
||||
_ <- A.char ':'
|
||||
port <- strP
|
||||
_ <- A.string "#/?"
|
||||
|
||||
q <- parseSimpleQuery <$> A.takeWhile (/= ' ')
|
||||
v <- requiredP q "v" strDecode
|
||||
app <- requiredP q "app" $ J.eitherDecodeStrict . urlDecode True
|
||||
ts <- requiredP q "ts" $ strDecode . urlDecode True
|
||||
skey <- requiredP q "skey" $ parseAll strP
|
||||
idkey <- requiredP q "idkey" $ parseAll strP
|
||||
dh <- requiredP q "dh" $ parseAll strP
|
||||
pure RCInvitation {ca, host, port, v, app, ts, skey, idkey, dh}
|
||||
|
||||
data RCSignedInvitation = RCSignedInvitation
|
||||
{ invitation :: RCInvitation,
|
||||
ssig :: C.Signature 'C.Ed25519,
|
||||
idsig :: C.Signature 'C.Ed25519
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
-- | URL-encoded and signed for showing in QR code
|
||||
instance StrEncoding RCSignedInvitation where
|
||||
strEncode RCSignedInvitation {invitation, ssig, idsig} =
|
||||
mconcat
|
||||
[ strEncode invitation,
|
||||
"&ssig=",
|
||||
strEncode $ C.signatureBytes ssig,
|
||||
"&idsig=",
|
||||
strEncode $ C.signatureBytes idsig
|
||||
]
|
||||
|
||||
strP = do
|
||||
-- TODO this assumes some order or parameters, can be made independent
|
||||
(url, invitation) <- A.match strP
|
||||
sigs <- case B.breakSubstring "&ssig=" url of
|
||||
(_, sigs) | B.null sigs -> fail "missing signatures"
|
||||
(_, sigs) -> pure $ parseSimpleQuery $ B.drop 1 sigs
|
||||
ssig <- requiredP sigs "ssig" $ parseAll strP
|
||||
idsig <- requiredP sigs "idsig" $ parseAll strP
|
||||
pure RCSignedInvitation {invitation, ssig, idsig}
|
||||
|
||||
signInvitation :: C.PrivateKey C.Ed25519 -> C.PrivateKey C.Ed25519 -> RCInvitation -> RCSignedInvitation
|
||||
signInvitation sKey idKey invitation = RCSignedInvitation {invitation, ssig, idsig}
|
||||
where
|
||||
uri = strEncode invitation
|
||||
ssig =
|
||||
case C.sign (C.APrivateSignKey C.SEd25519 sKey) uri of
|
||||
C.ASignature C.SEd25519 s -> s
|
||||
_ -> error "signing with ed25519"
|
||||
inviteUrlSigned = mconcat [uri, "&ssig=", strEncode ssig]
|
||||
idsig =
|
||||
case C.sign (C.APrivateSignKey C.SEd25519 idKey) inviteUrlSigned of
|
||||
C.ASignature C.SEd25519 s -> s
|
||||
_ -> error "signing with ed25519"
|
||||
|
||||
newtype RCVerifiedInvitation = RCVerifiedInvitation RCInvitation
|
||||
deriving (Show)
|
||||
|
||||
verifySignedInvitation :: RCSignedInvitation -> Maybe RCVerifiedInvitation
|
||||
verifySignedInvitation RCSignedInvitation {invitation, ssig, idsig} =
|
||||
if C.verify' skey ssig inviteURL && C.verify' idkey idsig inviteURLS
|
||||
then Just $ RCVerifiedInvitation invitation
|
||||
else Nothing
|
||||
where
|
||||
RCInvitation {skey, idkey} = invitation
|
||||
inviteURL = strEncode invitation
|
||||
inviteURLS = mconcat [inviteURL, "&ssig=", strEncode ssig]
|
||||
|
||||
data RCEncInvitation = RCEncInvitation
|
||||
{ dhPubKey :: C.PublicKeyX25519,
|
||||
nonce :: C.CbNonce,
|
||||
encInvitation :: ByteString
|
||||
}
|
||||
|
||||
instance Encoding RCEncInvitation where
|
||||
smpEncode RCEncInvitation {dhPubKey, nonce, encInvitation} =
|
||||
smpEncode (dhPubKey, nonce, Tail encInvitation)
|
||||
smpP = do
|
||||
(dhPubKey, nonce, Tail encInvitation) <- smpP
|
||||
pure RCEncInvitation {dhPubKey, nonce, encInvitation}
|
||||
|
||||
-- * Utils
|
||||
|
||||
requiredP :: MonadFail m => SimpleQuery -> ByteString -> (ByteString -> Either String a) -> m a
|
||||
requiredP q k f = maybe (fail $ "missing " <> show k) (either fail pure . f) $ lookup k q
|
||||
@@ -0,0 +1,238 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
module Simplex.RemoteControl.Types where
|
||||
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.TH as JQ
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
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.Client (TransportHost)
|
||||
import Simplex.Messaging.Util (safeDecodeUtf8)
|
||||
import Simplex.Messaging.Version (Version, VersionRange, mkVersionRange)
|
||||
import UnliftIO
|
||||
|
||||
data RCErrorType
|
||||
= RCEInternal {internalErr :: String}
|
||||
| RCEIdentity
|
||||
| RCENoLocalAddress
|
||||
| RCENewController
|
||||
| RCENotDiscovered
|
||||
| RCETLSStartFailed
|
||||
| RCEException {exception :: String}
|
||||
| RCECtrlAuth
|
||||
| RCECtrlNotFound
|
||||
| RCECtrlError {ctrlErr :: String}
|
||||
| RCEInvitation
|
||||
| RCEVersion
|
||||
| RCEEncrypt
|
||||
| RCEDecrypt
|
||||
| RCEBlockSize
|
||||
| RCESyntax {syntaxErr :: String}
|
||||
deriving (Eq, Show, Exception)
|
||||
|
||||
instance StrEncoding RCErrorType where
|
||||
strEncode = \case
|
||||
RCEInternal err -> "INTERNAL" <> text err
|
||||
RCEIdentity -> "IDENTITY"
|
||||
RCENoLocalAddress -> "NO_LOCAL_ADDR"
|
||||
RCENewController -> "NEW_CONTROLLER"
|
||||
RCENotDiscovered -> "NOT_DISCOVERED"
|
||||
RCETLSStartFailed -> "CTRL_TLS_START"
|
||||
RCEException err -> "EXCEPTION" <> text err
|
||||
RCECtrlAuth -> "CTRL_AUTH"
|
||||
RCECtrlNotFound -> "CTRL_NOT_FOUND"
|
||||
RCECtrlError err -> "CTRL_ERROR" <> text err
|
||||
RCEInvitation -> "INVITATION"
|
||||
RCEVersion -> "VERSION"
|
||||
RCEEncrypt -> "ENCRYPT"
|
||||
RCEDecrypt -> "DECRYPT"
|
||||
RCEBlockSize -> "BLOCK_SIZE"
|
||||
RCESyntax err -> "SYNTAX" <> text err
|
||||
where
|
||||
text = (" " <>) . encodeUtf8 . T.pack
|
||||
strP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"INTERNAL" -> RCEInternal <$> textP
|
||||
"IDENTITY" -> pure RCEIdentity
|
||||
"NO_LOCAL_ADDR" -> pure RCENoLocalAddress
|
||||
"NEW_CONTROLLER" -> pure RCENewController
|
||||
"NOT_DISCOVERED" -> pure RCENotDiscovered
|
||||
"CTRL_TLS_START" -> pure RCETLSStartFailed
|
||||
"EXCEPTION" -> RCEException <$> textP
|
||||
"CTRL_AUTH" -> pure RCECtrlAuth
|
||||
"CTRL_NOT_FOUND" -> pure RCECtrlNotFound
|
||||
"CTRL_ERROR" -> RCECtrlError <$> textP
|
||||
"INVITATION" -> pure RCEInvitation
|
||||
"VERSION" -> pure RCEVersion
|
||||
"ENCRYPT" -> pure RCEEncrypt
|
||||
"DECRYPT" -> pure RCEDecrypt
|
||||
"BLOCK_SIZE" -> pure RCEBlockSize
|
||||
"SYNTAX" -> RCESyntax <$> textP
|
||||
_ -> fail "bad RCErrorType"
|
||||
where
|
||||
textP = T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeByteString)
|
||||
|
||||
-- * Discovery
|
||||
|
||||
ipProbeVersionRange :: VersionRange
|
||||
ipProbeVersionRange = mkVersionRange 1 1
|
||||
|
||||
data IpProbe = IpProbe
|
||||
{ versionRange :: VersionRange,
|
||||
randomNonce :: ByteString
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
instance Encoding IpProbe where
|
||||
smpEncode IpProbe {versionRange, randomNonce} = smpEncode (versionRange, 'I', randomNonce)
|
||||
|
||||
smpP = IpProbe <$> (smpP <* "I") *> smpP
|
||||
|
||||
-- * Session
|
||||
|
||||
data RCHostHello = RCHostHello
|
||||
{ v :: Version,
|
||||
ca :: C.KeyHash,
|
||||
app :: J.Value,
|
||||
kem :: KEMPublicKey
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''RCHostHello)
|
||||
|
||||
data RCCtrlHello = RCCtrlHello {}
|
||||
deriving (Show)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON {J.nullaryToObject = True} ''RCCtrlHello)
|
||||
|
||||
-- | Long-term part of controller (desktop) connection to host (mobile)
|
||||
data RCHostPairing = RCHostPairing
|
||||
{ caKey :: C.APrivateSignKey,
|
||||
caCert :: C.SignedCertificate,
|
||||
idPrivKey :: C.PrivateKeyEd25519,
|
||||
knownHost :: Maybe KnownHostPairing
|
||||
}
|
||||
|
||||
data KnownHostPairing = KnownHostPairing
|
||||
{ hostFingerprint :: C.KeyHash, -- this is only changed in the first session, long-term identity of connected remote host
|
||||
hostDhPubKey :: C.PublicKeyX25519
|
||||
}
|
||||
|
||||
data RCCtrlAddress = RCCtrlAddress
|
||||
{ address :: TransportHost, -- allows any interface when found exactly
|
||||
interface :: Text
|
||||
}
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | Long-term part of host (mobile) connection to controller (desktop)
|
||||
data RCCtrlPairing = RCCtrlPairing
|
||||
{ caKey :: C.APrivateSignKey,
|
||||
caCert :: C.SignedCertificate,
|
||||
ctrlFingerprint :: C.KeyHash, -- long-term identity of connected remote controller
|
||||
idPubKey :: C.PublicKeyEd25519,
|
||||
dhPrivKey :: C.PrivateKeyX25519,
|
||||
prevDhPrivKey :: Maybe C.PrivateKeyX25519
|
||||
}
|
||||
|
||||
data RCHostKeys = RCHostKeys
|
||||
{ sessKeys :: C.KeyPair 'C.Ed25519,
|
||||
dhKeys :: C.KeyPair 'C.X25519
|
||||
}
|
||||
|
||||
-- Connected session with Host
|
||||
data RCHostSession = RCHostSession
|
||||
{ tls :: TLS,
|
||||
sessionKeys :: HostSessKeys
|
||||
}
|
||||
|
||||
data HostSessKeys = HostSessKeys
|
||||
{ hybridKey :: KEMHybridSecret,
|
||||
idPrivKey :: C.PrivateKeyEd25519,
|
||||
sessPrivKey :: C.PrivateKeyEd25519
|
||||
}
|
||||
|
||||
-- Host: RCCtrlPairing + RCInvitation => (RCCtrlSession, RCCtrlPairing)
|
||||
|
||||
data RCCtrlSession = RCCtrlSession
|
||||
{ tls :: TLS,
|
||||
sessionKeys :: CtrlSessKeys
|
||||
}
|
||||
|
||||
data CtrlSessKeys = CtrlSessKeys
|
||||
{ hybridKey :: KEMHybridSecret,
|
||||
idPubKey :: C.PublicKeyEd25519,
|
||||
sessPubKey :: C.PublicKeyEd25519
|
||||
}
|
||||
|
||||
data RCHostEncHello = RCHostEncHello
|
||||
{ dhPubKey :: C.PublicKeyX25519,
|
||||
nonce :: C.CbNonce,
|
||||
encBody :: ByteString
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
instance Encoding RCHostEncHello where
|
||||
smpEncode RCHostEncHello {dhPubKey, nonce, encBody} =
|
||||
"HELLO " <> smpEncode (dhPubKey, nonce, Tail encBody)
|
||||
smpP = do
|
||||
(dhPubKey, nonce, Tail encBody) <- "HELLO " *> smpP
|
||||
pure RCHostEncHello {dhPubKey, nonce, encBody}
|
||||
|
||||
data RCCtrlEncHello
|
||||
= RCCtrlEncHello {kem :: KEMCiphertext, nonce :: C.CbNonce, 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)
|
||||
RCCtrlEncError {nonce, encMessage} -> "ERROR " <> smpEncode (nonce, Tail encMessage)
|
||||
smpP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"HELLO" -> do
|
||||
(kem, nonce, Tail encBody) <- _smpP
|
||||
pure RCCtrlEncHello {kem, nonce, encBody}
|
||||
"ERROR" -> do
|
||||
(nonce, Tail encMessage) <- _smpP
|
||||
pure RCCtrlEncError {nonce, encMessage}
|
||||
_ -> fail "bad RCCtrlEncHello"
|
||||
|
||||
-- * Utils
|
||||
|
||||
-- | tlsunique channel binding
|
||||
type SessionCode = ByteString
|
||||
|
||||
type RCStepTMVar a = TMVar (Either RCErrorType a)
|
||||
|
||||
type Tasks = TVar [Async ()]
|
||||
|
||||
asyncRegistered :: MonadUnliftIO m => Tasks -> m () -> m ()
|
||||
asyncRegistered tasks action = async action >>= registerAsync tasks
|
||||
|
||||
registerAsync :: MonadIO m => Tasks -> Async () -> m ()
|
||||
registerAsync tasks = atomically . modifyTVar tasks . (:)
|
||||
|
||||
cancelTasks :: MonadIO m => Tasks -> m ()
|
||||
cancelTasks tasks = readTVarIO tasks >>= mapM_ cancel
|
||||
|
||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RCE") ''RCErrorType)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''RCCtrlAddress)
|
||||
-88
@@ -1,88 +0,0 @@
|
||||
# This file was automatically generated by 'stack init'
|
||||
#
|
||||
# Some commonly used options have been documented as comments in this file.
|
||||
# For advanced use and comprehensive documentation of the format, please see:
|
||||
# https://docs.haskellstack.org/en/stable/yaml_configuration/
|
||||
|
||||
# Resolver to choose a 'specific' stackage snapshot or a compiler version.
|
||||
# A snapshot resolver dictates the compiler version and the set of packages
|
||||
# to be used for project dependencies. For example:
|
||||
#
|
||||
# resolver: lts-3.5
|
||||
# resolver: nightly-2015-09-21
|
||||
# resolver: ghc-7.10.2
|
||||
#
|
||||
# The location of a snapshot can be provided as a file or url. Stack assumes
|
||||
# a snapshot provided as a file might change, whereas a url resource does not.
|
||||
#
|
||||
# resolver: ./custom-snapshot.yaml
|
||||
# resolver: https://example.com/snapshots/2018-01-01.yaml
|
||||
resolver: nightly-2023-08-22
|
||||
|
||||
# User packages to be built.
|
||||
# Various formats can be used as shown in the example below.
|
||||
#
|
||||
# packages:
|
||||
# - some-directory
|
||||
# - https://example.com/foo/bar/baz-0.0.2.tar.gz
|
||||
# subdirs:
|
||||
# - auto-update
|
||||
# - wai
|
||||
packages:
|
||||
- .
|
||||
# Dependency packages to be pulled from upstream that are not in the resolver.
|
||||
# These entries can reference officially published versions as well as
|
||||
# forks / in-progress versions pinned to a git hash. For example:
|
||||
#
|
||||
extra-deps:
|
||||
- cryptostore-0.2.1.0@sha256:9896e2984f36a1c8790f057fd5ce3da4cbcaf8aa73eb2d9277916886978c5b19,3881
|
||||
- network-3.1.2.7@sha256:e3d78b13db9512aeb106e44a334ab42b7aa48d26c097299084084cb8be5c5568,4888
|
||||
- simple-logger-0.1.0@sha256:be8ede4bd251a9cac776533bae7fb643369ebd826eb948a9a18df1a8dd252ff8,1079
|
||||
- tls-1.6.0@sha256:7ae39373fd2de27fb80e90f76d22aeeb9a074a0ddd120cbd02c9c52f516a9e55,6987
|
||||
# below dependencies are to update Aeson to 2.0.3
|
||||
- OneTuple-0.3.1@sha256:a848c096c9d29e82ffdd30a9998aa2931cbccb3a1bc137539d80f6174d31603e,2262
|
||||
- attoparsec-0.14.4@sha256:79584bdada8b730cb5138fca8c35c76fbef75fc1d1e01e6b1d815a5ee9843191,5810
|
||||
- hashable-1.4.0.2@sha256:0cddd0229d1aac305ea0404409c0bbfab81f075817bd74b8b2929eff58333e55,5005
|
||||
- semialign-1.2.0.1@sha256:0e179b4d3a8eff79001d374d6c91917c6221696b9620f0a4d86852fc6a9b9501,2836
|
||||
- text-short-0.1.5@sha256:962c6228555debdc46f758d0317dea16e5240d01419b42966674b08a5c3d8fa6,3498
|
||||
- time-compat-1.9.6.1@sha256:42d8f2e08e965e1718917d54ad69e1d06bd4b87d66c41dc7410f59313dba4ed1,5033
|
||||
- github: simplex-chat/aeson
|
||||
commit: 68330dce8208173c6acf5f62b23acb500ab5d873
|
||||
- github: kazu-yamamoto/http2
|
||||
commit: b5a1b7200cf5bc7044af34ba325284271f6dff25
|
||||
# - ../direct-sqlcipher
|
||||
- github: simplex-chat/direct-sqlcipher
|
||||
commit: f814ee68b16a9447fbb467ccc8f29bdd3546bfd9
|
||||
# - ../sqlcipher-simple
|
||||
- github: simplex-chat/sqlcipher-simple
|
||||
commit: a46bd361a19376c5211f1058908fc0ae6bf42446
|
||||
- github: simplex-chat/network-transport
|
||||
commit: 0013798272a683e35ca38d2fdaf480942311fba8
|
||||
# - ../hs-tls/core
|
||||
# - github: simplex-chat/hs-tls
|
||||
# commit: f6cc753611f80af300401cfae63846e9d7c40d9e
|
||||
# subdirs:
|
||||
# - core
|
||||
# Override default flag values for local packages and extra-deps
|
||||
# flags: {}
|
||||
|
||||
# Extra package databases containing global packages
|
||||
# extra-package-dbs: []
|
||||
|
||||
# Control whether we use the GHC we find on the path
|
||||
# system-ghc: true
|
||||
#
|
||||
# Require a specific version of stack, using version ranges
|
||||
# require-stack-version: -any # Default
|
||||
# require-stack-version: ">=2.1"
|
||||
#
|
||||
# Override the architecture used by stack, especially useful on Windows
|
||||
# arch: i386
|
||||
# arch: x86_64
|
||||
#
|
||||
# Extra directories used by stack for building
|
||||
# extra-include-dirs: [/path/to/dir]
|
||||
# extra-lib-dirs: [/path/to/dir]
|
||||
#
|
||||
# Allow a newer minor version of GHC than the snapshot specifies
|
||||
# compiler-check: newer-minor
|
||||
+17
-17
@@ -45,42 +45,42 @@ agentTests (ATransport t) = do
|
||||
describe "Migration tests" migrationTests
|
||||
describe "SMP agent protocol syntax" $ syntaxTests t
|
||||
describe "Establishing duplex connection" $ do
|
||||
it "should connect via one server and one agent" $
|
||||
it "should connect via one server and one agent" $ do
|
||||
smpAgentTest2_1_1 $ testDuplexConnection t
|
||||
it "should connect via one server and one agent (random IDs)" $
|
||||
it "should connect via one server and one agent (random IDs)" $ do
|
||||
smpAgentTest2_1_1 $ testDuplexConnRandomIds t
|
||||
it "should connect via one server and 2 agents" $
|
||||
it "should connect via one server and 2 agents" $ do
|
||||
smpAgentTest2_2_1 $ testDuplexConnection t
|
||||
it "should connect via one server and 2 agents (random IDs)" $
|
||||
it "should connect via one server and 2 agents (random IDs)" $ do
|
||||
smpAgentTest2_2_1 $ testDuplexConnRandomIds t
|
||||
it "should connect via 2 servers and 2 agents" $
|
||||
it "should connect via 2 servers and 2 agents" $ do
|
||||
smpAgentTest2_2_2 $ testDuplexConnection t
|
||||
it "should connect via 2 servers and 2 agents (random IDs)" $
|
||||
it "should connect via 2 servers and 2 agents (random IDs)" $ do
|
||||
smpAgentTest2_2_2 $ testDuplexConnRandomIds t
|
||||
describe "Establishing connections via `contact connection`" $ do
|
||||
it "should connect via contact connection with one server and 3 agents" $
|
||||
it "should connect via contact connection with one server and 3 agents" $ do
|
||||
smpAgentTest3 $ testContactConnection t
|
||||
it "should connect via contact connection with one server and 2 agents (random IDs)" $
|
||||
it "should connect via contact connection with one server and 2 agents (random IDs)" $ do
|
||||
smpAgentTest2_2_1 $ testContactConnRandomIds t
|
||||
it "should support rejecting contact request" $
|
||||
it "should support rejecting contact request" $ do
|
||||
smpAgentTest2_2_1 $ testRejectContactRequest t
|
||||
describe "Connection subscriptions" $ do
|
||||
it "should connect via one server and one agent" $
|
||||
it "should connect via one server and one agent" $ do
|
||||
smpAgentTest3_1_1 $ testSubscription t
|
||||
it "should send notifications to client when server disconnects" $
|
||||
it "should send notifications to client when server disconnects" $ do
|
||||
smpAgentServerTest $ testSubscrNotification t
|
||||
describe "Message delivery and server reconnection" $ do
|
||||
it "should deliver messages after losing server connection and re-connecting" $
|
||||
it "should deliver messages after losing server connection and re-connecting" $ do
|
||||
smpAgentTest2_2_2_needs_server $ testMsgDeliveryServerRestart t
|
||||
it "should connect to the server when server goes up if it initially was down" $
|
||||
it "should connect to the server when server goes up if it initially was down" $ do
|
||||
smpAgentTestN [] $ testServerConnectionAfterError t
|
||||
it "should deliver pending messages after agent restarting" $
|
||||
it "should deliver pending messages after agent restarting" $ do
|
||||
smpAgentTest1_1_1 $ testMsgDeliveryAgentRestart t
|
||||
it "should concurrently deliver messages to connections without blocking" $
|
||||
it "should concurrently deliver messages to connections without blocking" $ do
|
||||
smpAgentTest2_2_1 $ testConcurrentMsgDelivery t
|
||||
it "should deliver messages if one of connections has quota exceeded" $
|
||||
it "should deliver messages if one of connections has quota exceeded" $ do
|
||||
smpAgentTest2_2_1 $ testMsgDeliveryQuotaExceeded t
|
||||
it "should resume delivering messages after exceeding quota once all messages are received" $
|
||||
it "should resume delivering messages after exceeding quota once all messages are received" $ do
|
||||
smpAgentTest2_2_1 $ testResumeDeliveryQuotaExceeded t
|
||||
|
||||
type AEntityTransmission p e = (ACorrId, ConnId, ACommand p e)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
|
||||
module AgentTests.ConnectionRequestTests where
|
||||
@@ -52,7 +51,7 @@ testDhKeyStrUri = urlEncode True testDhKeyStr
|
||||
connReqData :: ConnReqUriData
|
||||
connReqData =
|
||||
ConnReqUriData
|
||||
{ crScheme = simplexChat,
|
||||
{ crScheme = CRSSimplex,
|
||||
crAgentVRange = mkVersionRange 1 1,
|
||||
crSmpQueues = [queueV1],
|
||||
crClientData = Nothing
|
||||
@@ -112,25 +111,25 @@ connectionRequestTests =
|
||||
`shouldBe` Right queueV1
|
||||
it "should serialize connection requests" $ do
|
||||
strEncode connectionRequest
|
||||
`shouldBe` "https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D"
|
||||
<> urlEncode True testDhKeyStrUri
|
||||
<> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
|
||||
`shouldBe` "simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D"
|
||||
<> urlEncode True testDhKeyStrUri
|
||||
<> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
|
||||
strEncode connectionRequestCurrentRange
|
||||
`shouldBe` "https://simplex.chat/invitation#/?v=1-4&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D"
|
||||
<> urlEncode True testDhKeyStrUri
|
||||
<> "%2Csmp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D"
|
||||
<> urlEncode True testDhKeyStrUri
|
||||
<> "&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
|
||||
`shouldBe` "simplex:/invitation#/?v=1-4&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D"
|
||||
<> urlEncode True testDhKeyStrUri
|
||||
<> "%2Csmp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D"
|
||||
<> urlEncode True testDhKeyStrUri
|
||||
<> "&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
|
||||
strEncode connectionRequestClientDataEmpty
|
||||
`shouldBe` "https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D"
|
||||
<> urlEncode True testDhKeyStrUri
|
||||
<> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
|
||||
<> "&data=%7B%7D"
|
||||
`shouldBe` "simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D"
|
||||
<> urlEncode True testDhKeyStrUri
|
||||
<> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
|
||||
<> "&data=%7B%7D"
|
||||
strEncode connectionRequestClientData
|
||||
`shouldBe` "https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D"
|
||||
<> urlEncode True testDhKeyStrUri
|
||||
<> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
|
||||
<> "&data=%7B%22type%22%3A%22group_link%22%2C%20%22group_link_id%22%3A%22abc%22%7D"
|
||||
`shouldBe` "simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1%26dh%3D"
|
||||
<> urlEncode True testDhKeyStrUri
|
||||
<> "&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
|
||||
<> "&data=%7B%22type%22%3A%22group_link%22%2C%20%22group_link_id%22%3A%22abc%22%7D"
|
||||
it "should parse connection requests" $ do
|
||||
strDecode
|
||||
( "https://simplex.chat/invitation#/?smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23"
|
||||
|
||||
@@ -43,6 +43,7 @@ import Data.Int (Int64)
|
||||
import qualified Data.Map as M
|
||||
import Data.Maybe (isNothing)
|
||||
import qualified Data.Set as S
|
||||
import Data.Time.Clock (diffUTCTime, getCurrentTime)
|
||||
import Data.Time.Clock.System (SystemTime (..), getSystemTime)
|
||||
import Data.Type.Equality
|
||||
import SMPAgentClient
|
||||
@@ -142,7 +143,7 @@ prevRange vr = vr {maxVersion = maxVersion vr - 1}
|
||||
runRight_ :: (Eq e, Show e, HasCallStack) => ExceptT e IO () -> Expectation
|
||||
runRight_ action = runExceptT action `shouldReturn` Right ()
|
||||
|
||||
runRight :: HasCallStack => ExceptT AgentErrorType IO a -> IO a
|
||||
runRight :: (Show e, HasCallStack) => ExceptT e IO a -> IO a
|
||||
runRight action =
|
||||
runExceptT action >>= \case
|
||||
Right x -> pure x
|
||||
@@ -310,6 +311,7 @@ functionalAPITests t = do
|
||||
describe "Delivery receipts" $ do
|
||||
it "should send and receive delivery receipt" $ withSmpServer t testDeliveryReceipts
|
||||
it "should send delivery receipt only in connection v3+" $ testDeliveryReceiptsVersion t
|
||||
it "send delivery receipts concurrently with messages" $ testDeliveryReceiptsConcurrent t
|
||||
|
||||
testBasicAuth :: ATransport -> Bool -> (Maybe BasicAuth, Version) -> (Maybe BasicAuth, Version) -> (Maybe BasicAuth, Version) -> IO Int
|
||||
testBasicAuth t allowNewQueues srv@(srvAuth, srvVersion) clnt1 clnt2 = do
|
||||
@@ -1008,34 +1010,35 @@ testOnlyCreatePull :: IO ()
|
||||
testOnlyCreatePull = withAgentClients2 $ \alice bob -> runRight_ $ do
|
||||
(bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMOnlyCreate
|
||||
aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMOnlyCreate
|
||||
getMsg alice bobId
|
||||
Just ("", _, CONF confId _ "bob's connInfo") <- timeout 5_000000 $ get alice
|
||||
Just ("", _, CONF confId _ "bob's connInfo") <- getMsg alice bobId $ timeout 5_000000 $ get alice
|
||||
allowConnection alice bobId confId "alice's connInfo"
|
||||
liftIO $ threadDelay 1_000000
|
||||
getMsg bob aliceId
|
||||
get bob ##> ("", aliceId, INFO "alice's connInfo")
|
||||
getMsg bob aliceId $
|
||||
get bob ##> ("", aliceId, INFO "alice's connInfo")
|
||||
liftIO $ threadDelay 1_000000
|
||||
getMsg alice bobId
|
||||
getMsg alice bobId $ pure ()
|
||||
get alice ##> ("", bobId, CON)
|
||||
getMsg bob aliceId
|
||||
get bob ##> ("", aliceId, CON)
|
||||
getMsg bob aliceId $
|
||||
get bob ##> ("", aliceId, CON)
|
||||
-- exchange messages
|
||||
4 <- sendMessage alice bobId SMP.noMsgFlags "hello"
|
||||
get alice ##> ("", bobId, SENT 4)
|
||||
getMsg bob aliceId
|
||||
get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False
|
||||
getMsg bob aliceId $
|
||||
get bob =##> \case ("", c, Msg "hello") -> c == aliceId; _ -> False
|
||||
ackMessage bob aliceId 4 Nothing
|
||||
5 <- sendMessage bob aliceId SMP.noMsgFlags "hello too"
|
||||
get bob ##> ("", aliceId, SENT 5)
|
||||
getMsg alice bobId
|
||||
get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False
|
||||
getMsg alice bobId $
|
||||
get alice =##> \case ("", c, Msg "hello too") -> c == bobId; _ -> False
|
||||
ackMessage alice bobId 5 Nothing
|
||||
where
|
||||
getMsg :: AgentClient -> ConnId -> ExceptT AgentErrorType IO ()
|
||||
getMsg c cId = do
|
||||
getMsg :: AgentClient -> ConnId -> ExceptT AgentErrorType IO a -> ExceptT AgentErrorType IO a
|
||||
getMsg c cId action = do
|
||||
liftIO $ noMessages c "nothing should be delivered before GET"
|
||||
Just _ <- getConnectionMessage c cId
|
||||
pure ()
|
||||
r <- action
|
||||
get c =##> \case ("", cId', MSGNTF _) -> cId == cId'; _ -> False
|
||||
pure r
|
||||
|
||||
makeConnection :: AgentClient -> AgentClient -> ExceptT AgentErrorType IO (ConnId, ConnId)
|
||||
makeConnection alice bob = makeConnectionForUsers alice 1 bob 1
|
||||
@@ -1159,7 +1162,7 @@ testBatchedSubscriptions nCreate nDel t = do
|
||||
a <- getSMPAgentClient' agentCfg initAgentServers2 testDB
|
||||
b <- getSMPAgentClient' agentCfg initAgentServers2 testDB2
|
||||
conns <- runServers $ do
|
||||
conns <- forM [1 .. nCreate :: Int] . const $ makeConnection a b
|
||||
conns <- replicateM (nCreate :: Int) $ makeConnection a b
|
||||
forM_ conns $ \(aId, bId) -> exchangeGreetings a bId b aId
|
||||
let (aIds', bIds') = unzip $ take nDel conns
|
||||
delete a bIds'
|
||||
@@ -1928,6 +1931,57 @@ testDeliveryReceiptsVersion t = do
|
||||
disconnectAgentClient a'
|
||||
disconnectAgentClient b'
|
||||
|
||||
testDeliveryReceiptsConcurrent :: HasCallStack => ATransport -> IO ()
|
||||
testDeliveryReceiptsConcurrent t =
|
||||
withSmpServerConfigOn t cfg {msgQueueQuota = 128} testPort $ \_ -> do
|
||||
withAgentClients2 $ \a b -> do
|
||||
(aId, bId) <- runRight $ makeConnection a b
|
||||
t1 <- liftIO getCurrentTime
|
||||
concurrently_ (runClient "a" a bId) (runClient "b" b aId)
|
||||
t2 <- liftIO getCurrentTime
|
||||
diffUTCTime t2 t1 `shouldSatisfy` (< 15)
|
||||
liftIO $ noMessages a "nothing else should be delivered to alice"
|
||||
liftIO $ noMessages b "nothing else should be delivered to bob"
|
||||
where
|
||||
runClient :: String -> AgentClient -> ConnId -> IO ()
|
||||
runClient _cName client connId = do
|
||||
concurrently_ send receive
|
||||
where
|
||||
numMsgs = 100
|
||||
send = runRight_ $
|
||||
replicateM_ numMsgs $ do
|
||||
-- liftIO $ print $ cName <> ": sendMessage"
|
||||
void $ sendMessage client connId SMP.noMsgFlags "hello"
|
||||
receive =
|
||||
runRight_ $
|
||||
-- for each sent message: 1 SENT, 1 RCVD, 1 OK for acknowledging RCVD
|
||||
-- for each received message: 1 MSG, 1 OK for acknowledging MSG
|
||||
receiveLoop (numMsgs * 5)
|
||||
receiveLoop :: Int -> ExceptT AgentErrorType IO ()
|
||||
receiveLoop 0 = pure ()
|
||||
receiveLoop n = do
|
||||
r <- getWithTimeout
|
||||
case r of
|
||||
(_, _, SENT _) -> do
|
||||
-- liftIO $ print $ cName <> ": SENT"
|
||||
pure ()
|
||||
(_, _, MSG MsgMeta {recipient = (msgId, _), integrity = MsgOk} _ _) -> do
|
||||
-- liftIO $ print $ cName <> ": MSG " <> show msgId
|
||||
ackMessageAsync client (B.pack . show $ n) connId msgId (Just "")
|
||||
(_, _, RCVD MsgMeta {recipient = (msgId, _), integrity = MsgOk} _) -> do
|
||||
-- liftIO $ print $ cName <> ": RCVD " <> show msgId
|
||||
ackMessageAsync client (B.pack . show $ n) connId msgId Nothing
|
||||
(_, _, OK) -> do
|
||||
-- liftIO $ print $ cName <> ": OK"
|
||||
pure ()
|
||||
r' -> error $ "unexpected event: " <> show r'
|
||||
receiveLoop (n - 1)
|
||||
getWithTimeout :: ExceptT AgentErrorType IO (AEntityTransmission 'AEConn)
|
||||
getWithTimeout = do
|
||||
1000000 `timeout` get client >>= \case
|
||||
Just r -> pure r
|
||||
_ -> error "timeout"
|
||||
|
||||
testTwoUsers :: HasCallStack => IO ()
|
||||
testTwoUsers = withAgentClients2 $ \a b -> do
|
||||
let nc = netCfg initAgentServers
|
||||
@@ -1996,7 +2050,7 @@ testTwoUsers = withAgentClients2 $ \a b -> do
|
||||
|
||||
getSMPAgentClient' :: AgentConfig -> InitialAgentServers -> FilePath -> IO AgentClient
|
||||
getSMPAgentClient' cfg' initServers dbPath = do
|
||||
Right st <- liftIO $ createAgentStore dbPath "" MCError
|
||||
Right st <- liftIO $ createAgentStore dbPath "" False MCError
|
||||
getSMPAgentClient cfg' initServers st
|
||||
|
||||
testServerMultipleIdentities :: HasCallStack => IO ()
|
||||
|
||||
@@ -178,16 +178,16 @@ testMigration ::
|
||||
testMigration (initMs, initTables) (finalMs, confirmModes, tablesOrError) = forM_ confirmModes $ \confirmMode -> do
|
||||
r <- randomIO :: IO Word32
|
||||
let dpPath = testDB <> show r
|
||||
Right st <- createSQLiteStore dpPath "" initMs MCError
|
||||
Right st <- createSQLiteStore dpPath "" False initMs MCError
|
||||
st `shouldHaveTables` initTables
|
||||
closeSQLiteStore st
|
||||
case tablesOrError of
|
||||
Right tables -> do
|
||||
Right st' <- createSQLiteStore dpPath "" finalMs confirmMode
|
||||
Right st' <- createSQLiteStore dpPath "" False finalMs confirmMode
|
||||
st' `shouldHaveTables` tables
|
||||
closeSQLiteStore st'
|
||||
Left e -> do
|
||||
Left e' <- createSQLiteStore dpPath "" finalMs confirmMode
|
||||
Left e' <- createSQLiteStore dpPath "" False finalMs confirmMode
|
||||
e `shouldBe` e'
|
||||
removeFile dpPath
|
||||
where
|
||||
|
||||
@@ -504,7 +504,7 @@ testNotificationsSMPRestartBatch n t APNSMockServer {apnsQ} = do
|
||||
a <- getSMPAgentClient' agentCfg initAgentServers2 testDB
|
||||
b <- getSMPAgentClient' agentCfg initAgentServers2 testDB2
|
||||
conns <- runServers $ do
|
||||
conns <- forM [1 .. n :: Int] . const $ makeConnection a b
|
||||
conns <- replicateM (n :: Int) $ makeConnection a b
|
||||
_ <- registerTestToken a "abcd" NMInstant apnsQ
|
||||
liftIO $ threadDelay 1500000
|
||||
forM_ conns $ \(aliceId, bobId) -> do
|
||||
|
||||
@@ -6,16 +6,19 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
|
||||
module AgentTests.SQLiteTests (storeTests) where
|
||||
|
||||
import Control.Concurrent.Async (concurrently_)
|
||||
import Control.Concurrent.STM
|
||||
import Control.Exception (SomeException)
|
||||
import Control.Monad (replicateM_)
|
||||
import Crypto.Random (drgNew)
|
||||
import Data.ByteArray (ScrubbedBytes)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import Data.List (isInfixOf)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Time
|
||||
@@ -47,15 +50,18 @@ withStore2 = before connect2 . after (removeStore . fst)
|
||||
connect2 :: IO (SQLiteStore, SQLiteStore)
|
||||
connect2 = do
|
||||
s1 <- createStore
|
||||
s2 <- connectSQLiteStore (dbFilePath s1) ""
|
||||
s2 <- connectSQLiteStore (dbFilePath s1) "" False
|
||||
pure (s1, s2)
|
||||
|
||||
createStore :: IO SQLiteStore
|
||||
createStore = do
|
||||
createStore = createEncryptedStore "" False
|
||||
|
||||
createEncryptedStore :: ScrubbedBytes -> Bool -> IO SQLiteStore
|
||||
createEncryptedStore key keepKey = do
|
||||
-- Randomize DB file name to avoid SQLite IO errors supposedly caused by asynchronous
|
||||
-- IO operations on multiple similarly named files; error seems to be environment specific
|
||||
r <- randomIO :: IO Word32
|
||||
Right st <- createSQLiteStore (testDB <> show r) "" Migrations.app MCError
|
||||
Right st <- createSQLiteStore (testDB <> show r) key keepKey Migrations.app MCError
|
||||
pure st
|
||||
|
||||
removeStore :: SQLiteStore -> IO ()
|
||||
@@ -104,6 +110,10 @@ storeTests = do
|
||||
testCreateRcvMsg
|
||||
testCreateSndMsg
|
||||
testCreateRcvAndSndMsgs
|
||||
describe "open/close store" $ do
|
||||
it "should close and re-open" testCloseReopenStore
|
||||
it "should close and re-open encrypted store" testCloseReopenEncryptedStore
|
||||
it "should close and re-open encrypted store (keep key)" testReopenEncryptedStoreKeepKey
|
||||
|
||||
testConcurrentWrites :: SpecWith (SQLiteStore, SQLiteStore)
|
||||
testConcurrentWrites =
|
||||
@@ -504,3 +514,54 @@ testCreateRcvAndSndMsgs =
|
||||
testCreateRcvMsg_ db 2 "rcv_hash_2" connId rcvQueue1 $ mkRcvMsgData (InternalId 4) (InternalRcvId 3) 3 "3" "rcv_hash_3"
|
||||
testCreateSndMsg_ db "snd_hash_1" connId $ mkSndMsgData (InternalId 5) (InternalSndId 2) "snd_hash_2"
|
||||
testCreateSndMsg_ db "snd_hash_2" connId $ mkSndMsgData (InternalId 6) (InternalSndId 3) "snd_hash_3"
|
||||
|
||||
testCloseReopenStore :: IO ()
|
||||
testCloseReopenStore = do
|
||||
st <- createStore
|
||||
hasMigrations st
|
||||
closeSQLiteStore st
|
||||
closeSQLiteStore st
|
||||
errorGettingMigrations st
|
||||
openSQLiteStore st "" False
|
||||
openSQLiteStore st "" False
|
||||
hasMigrations st
|
||||
closeSQLiteStore st
|
||||
errorGettingMigrations st
|
||||
reopenSQLiteStore st
|
||||
hasMigrations st
|
||||
|
||||
testCloseReopenEncryptedStore :: IO ()
|
||||
testCloseReopenEncryptedStore = do
|
||||
let key = "test_key"
|
||||
st <- createEncryptedStore key False
|
||||
hasMigrations st
|
||||
closeSQLiteStore st
|
||||
closeSQLiteStore st
|
||||
errorGettingMigrations st
|
||||
reopenSQLiteStore st `shouldThrow` \(e :: SomeException) -> "reopenSQLiteStore: no key" `isInfixOf` show e
|
||||
openSQLiteStore st key True
|
||||
openSQLiteStore st key True
|
||||
hasMigrations st
|
||||
closeSQLiteStore st
|
||||
errorGettingMigrations st
|
||||
reopenSQLiteStore st
|
||||
hasMigrations st
|
||||
|
||||
testReopenEncryptedStoreKeepKey :: IO ()
|
||||
testReopenEncryptedStoreKeepKey = do
|
||||
let key = "test_key"
|
||||
st <- createEncryptedStore key True
|
||||
hasMigrations st
|
||||
closeSQLiteStore st
|
||||
errorGettingMigrations st
|
||||
reopenSQLiteStore st
|
||||
hasMigrations st
|
||||
|
||||
getMigrations :: SQLiteStore -> IO Bool
|
||||
getMigrations st = not . null <$> withTransaction st (Migrations.getCurrent . DB.conn)
|
||||
|
||||
hasMigrations :: SQLiteStore -> Expectation
|
||||
hasMigrations st = getMigrations st `shouldReturn` True
|
||||
|
||||
errorGettingMigrations :: SQLiteStore -> Expectation
|
||||
errorGettingMigrations st = getMigrations st `shouldThrow` \(e :: SomeException) -> "ErrorMisuse" `isInfixOf` show e
|
||||
|
||||
@@ -33,14 +33,14 @@ testVerifySchemaDump :: IO ()
|
||||
testVerifySchemaDump = do
|
||||
savedSchema <- ifM (doesFileExist appSchema) (readFile appSchema) (pure "")
|
||||
savedSchema `deepseq` pure ()
|
||||
void $ createSQLiteStore testDB "" Migrations.app MCConsole
|
||||
void $ createSQLiteStore testDB "" False Migrations.app MCConsole
|
||||
getSchema testDB appSchema `shouldReturn` savedSchema
|
||||
removeFile testDB
|
||||
|
||||
testSchemaMigrations :: IO ()
|
||||
testSchemaMigrations = do
|
||||
let noDownMigrations = dropWhileEnd (\Migration {down} -> isJust down) Migrations.app
|
||||
Right st <- createSQLiteStore testDB "" noDownMigrations MCError
|
||||
Right st <- createSQLiteStore testDB "" False noDownMigrations MCError
|
||||
mapM_ (testDownMigration st) $ drop (length noDownMigrations) Migrations.app
|
||||
closeSQLiteStore st
|
||||
removeFile testDB
|
||||
|
||||
@@ -4,7 +4,7 @@ module CoreTests.BatchingTests (batchingTests) where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad
|
||||
import Crypto.Random (MonadRandom(..))
|
||||
import Crypto.Random (MonadRandom (..))
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.List.NonEmpty as L
|
||||
@@ -12,7 +12,7 @@ import Simplex.Messaging.Client
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Version (VersionRange(..))
|
||||
import Simplex.Messaging.Version (VersionRange (..))
|
||||
import Test.Hspec
|
||||
|
||||
batchingTests :: Spec
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user