Compare commits

..
130 changed files with 3243 additions and 7893 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ jobs:
mv $(cabal list-bin xftp) xftp-ubuntu-${{ matrix.platform_name}}
- name: Build changelog
if: startsWith(github.ref, 'refs/tags/v')
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-22.04'
id: build_changelog
uses: mikepenz/release-changelog-builder-action@v1
with:
-123
View File
@@ -1,126 +1,3 @@
# 6.1.1
SMP:
- stop server faster (#1371)
- add STORE error (#1372)
# 6.1.0
Version 6.1.0.7
SMP server and client:
- transport block encryption (#1317).
Agent:
- batch and optimize iOS notifications processing (#1308, #1311, #1313, #1316, #1330, #1331, #1333, #1337, #1346).
- allow receiving multiple messages from single iOS notification (#1355, #1362).
- prepare connection to accept to avoid race condition with events (#1365).
- transport isolation mode "Session" (default) to use new SOCKS credentials when client restarts or SOCKS proxy configuration changes (#1321).
Ntf server:
- control port (#1354).
- enable pings on ntf subscriptions, to resubscribe on reconnection (#1353).
SMP server:
- support multiple server ports (#1319).
- support serving HTTPS and SMP transport on the same port (#1326, #1327).
- persist iOS notifications to avoid losing them when Ntf server is offline (#1336, #1339, #1350).
- fix lost notification subscriptions (#1347).
- reject SKEY with different key earlier, at verification step (#1366).
- pass server information via CLI during server initialization (#1356).
- show version on server page (#1341).
- explicit graceful shutdown on SIGINT (#1360).
XRCP (remote access protocol):
- use SHA3-256 in hybrid key agreement (#1302).
- session encryption with forward secrecy (#1328).
# 6.0.5
SMP agent:
- support generic SOCKS proxy (without isolate-by-auth).
- reduce max message sizes
# 6.0.4
SMP server:
- better performance/memory: fewer map updates on re-subscriptions (#1297), split and reduce STM transactions (#1294)
- send DELD when subscribed queue is deleted (#1312)
- add created/updated/used date to queues to manage expiration (#1306)
XFTP server: truncate file creation time to 1 hour (#1310)
Servers:
- bind control port only to 127.0.0.1 for better security in case of firewall misconfiguration (#1280)
- reduce memory used for period stats (#1298)
Agent: process last notification from list (#1307)
- report receive file error with redirected file ID, when redirect is present (#1304)
- special error when deleted user record is not in database (#1303)
- fix race when sending a message to the deleted connection (#1296)
- support for multiple messages in a single notification
Ntf server:
- only use SOCKS proxy for servers without public address (#1314)
# 6.0.3
Agent:
- fix possible stuck queue rotation (#1290).
SMP server:
- batch END responses when subscribed client switches to reduce server and client traffic.
- reduce STM transactions for better performance.
- add stats for END events and for SUB/DEL event batches.
- remove "expensive" stats to save memory.
# 6.0.2
SMP agent:
- fix stuck connection commands when a server is not responding.
- store query errors, reduce slow query threshold to 1ms.
Notification server:
- reduce PING interval to 1 minute.
- fix subscriptions disabled on race condition (only mark subscriptions with END status when received via the active connection).
# 6.0.1
SMP agent:
- support changing user of the new connection.
- do not start delivery workers when there are no messages to deliver.
- enable notifications for all connections.
- combine database transactions when subscribing.
SMP server:
- safe compacting of store log.
- fix possible race when creating client that might lead to memory leak.
Dependencies: upgrade tls to 1.9
# 6.0.0
Version 6.0.0.8
Agent:
- enabled fast handshake support.
- batch-send multiple messages in each connection.
- resume subscriptions as soon as agent moves to foreground or as network connection resumes.
- "known" servers to determine whether to use SMP proxy.
- retry on SMP proxy NO_SESSION error.
- fixes to notification subscriptions.
- persistent server statistics.
- better concurrency.
SMP server:
- reduce threads usage.
- additional statistics.
- improve disabling inactive clients.
- additional control port commands for monitoring.
Notification server:
- support onion-only SMP servers.
# 5.8.2
Agent:
+2 -9
View File
@@ -149,15 +149,8 @@ On Linux, you can deploy smp and xftp server using Docker. This will download im
You can install and setup servers automatically using our script:
```sh
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/install.sh -o simplex-server-install.sh &&\
if echo '53fcdb4ceab324316e2c4cda7e84dbbb344f32550a65975a7895425e5a1be757 simplex-server-install.sh' | sha256sum -c; then
chmod +x ./simplex-server-install.sh
./simplex-server-install.sh
rm ./simplex-server-install.sh
else
echo "SHA-256 checksum is incorrect!"
rm ./simplex-server-install.sh
fi
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/install.sh -o simplex-server-install.sh \
&& if echo 'b8cf2be103f21f9461d9a500bcd3db06ab7d01d68871b07f4bd245195cbead1d simplex-server-install.sh' | sha256sum -c; then chmod +x ./simplex-server-install.sh && ./simplex-server-install.sh; rm ./simplex-server-install.sh; else echo "SHA-256 checksum is incorrect!" && rm ./simplex-server-install.sh; fi
```
### Build from source
+1 -1
View File
@@ -15,7 +15,7 @@ logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
main :: IO ()
main = do
setLogLevel LogInfo
setLogLevel LogDebug -- change to LogError in production
cfgPath <- getEnvPath "NTF_SERVER_CFG_PATH" defaultCfgPath
logPath <- getEnvPath "NTF_SERVER_LOG_PATH" defaultLogPath
withGlobalLogging logCfg $ ntfServerCLI cfgPath logPath
+1 -1
View File
@@ -19,4 +19,4 @@ main = do
setLogLevel LogDebug
cfgPath <- getEnvPath "SMP_SERVER_CFG_PATH" defaultCfgPath
logPath <- getEnvPath "SMP_SERVER_LOG_PATH" defaultLogPath
withGlobalLogging logCfg $ smpServerCLI_ Static.generateSite Static.serveStaticFiles Static.attachStaticFiles cfgPath logPath
withGlobalLogging logCfg $ smpServerCLI_ Static.generateSite Static.serveStaticFiles cfgPath logPath
-10
View File
@@ -221,10 +221,6 @@
Public information
</h2>
<table id="public-info">
<tr class="text-grey-black dark:text-white text-base">
<td>Server version:</td>
<td>${version}</td>
</tr>
<tr class="text-grey-black dark:text-white text-base">
<td>Source code:</td>
<td><a href="${sourceCode}" target="_blank">${sourceCode}</a></td>
@@ -295,12 +291,6 @@
<td>${hostingEntity} (${hostingCountry})</td>
</tr>
</x-hosting>
<x-hostingType>
<tr class="text-grey-black dark:text-white text-base">
<td>Hosting type:</td>
<td>${hostingType}</td>
</tr>
</x-hostingType>
<x-serverCountry>
<tr class="text-grey-black dark:text-white text-base">
<td>Server country:</td>
+7 -52
View File
@@ -7,30 +7,22 @@ module Static where
import Control.Logger.Simple
import Control.Monad
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Char (toUpper)
import Data.IORef (readIORef)
import qualified Data.ByteString as B
import Data.Maybe (fromMaybe)
import Data.String (fromString)
import Data.Text.Encoding (encodeUtf8)
import Network.Socket (getPeerName)
import Network.Wai (Application)
import qualified Network.Wai.Application.Static as S
import qualified Network.Wai.Handler.Warp as W
import qualified Network.Wai.Handler.Warp.Internal as WI
import qualified Network.Wai.Handler.WarpTLS as WT
import Network.Wai.Application.Static as S
import Network.Wai.Handler.Warp as W
import qualified Network.Wai.Handler.WarpTLS as W
import Simplex.Messaging.Encoding.String (strEncode)
import Simplex.Messaging.Server (AttachHTTP)
import Simplex.Messaging.Server.Information
import Simplex.Messaging.Server.Main (EmbeddedWebParams (..), WebHttpsParams (..))
import Simplex.Messaging.Transport (simplexMQVersion)
import Simplex.Messaging.Transport.Client (TransportHost (..))
import Simplex.Messaging.Util (tshow)
import Static.Embedded as E
import System.Directory (createDirectoryIfMissing)
import System.FilePath
import UnliftIO.Concurrent (forkFinally)
import UnliftIO.Exception (bracket, finally)
serveStaticFiles :: EmbeddedWebParams -> IO ()
serveStaticFiles EmbeddedWebParams {webStaticPath, webHttpPort, webHttpsParams} = do
@@ -39,44 +31,9 @@ serveStaticFiles EmbeddedWebParams {webStaticPath, webHttpPort, webHttpsParams}
W.runSettings (mkSettings port) (S.staticApp $ S.defaultFileServerSettings webStaticPath)
forM_ webHttpsParams $ \WebHttpsParams {port, cert, key} -> flip forkFinally (\e -> logError $ "HTTPS server crashed: " <> tshow e) $ do
logInfo $ "Serving static site on port " <> tshow port <> " (TLS)"
WT.runTLS (WT.tlsSettings cert key) (mkSettings port) app
W.runTLS (W.tlsSettings cert key) (mkSettings port) (S.staticApp $ S.defaultFileServerSettings webStaticPath)
where
app = staticFiles webStaticPath
mkSettings port = W.setPort port warpSettings
-- | Prepare context and prepare HTTP handler for TLS connections that already passed TLS.handshake and ALPN check.
attachStaticFiles :: FilePath -> (AttachHTTP -> IO ()) -> IO ()
attachStaticFiles path action =
-- Initialize global internal state for http server.
WI.withII warpSettings $ \ii -> do
action $ \socket cxt -> do
-- Initialize internal per-connection resources.
addr <- getPeerName socket
withConnection addr cxt $ \(conn, transport) ->
withTimeout ii conn $ \th ->
-- Run Warp connection handler to process HTTP requests for static files.
WI.serveConnection conn ii th addr transport warpSettings app
where
app = staticFiles path
-- from warp-tls
withConnection socket cxt = bracket (WT.attachConn socket cxt) (terminate . fst)
-- from warp
withTimeout ii conn =
bracket
(WI.registerKillThread (WI.timeoutManager ii) (WI.connClose conn))
WI.cancel
-- shared clean up
terminate conn = WI.connClose conn `finally` (readIORef (WI.connWriteBuffer conn) >>= WI.bufFree)
warpSettings :: W.Settings
warpSettings = W.setGracefulShutdownTimeout (Just 1) W.defaultSettings
staticFiles :: FilePath -> Application
staticFiles root = S.staticApp settings
where
settings = (S.defaultFileServerSettings root)
{ S.ssListing = Nothing
}
mkSettings port = setPort port defaultSettings
generateSite :: ServerInformation -> Maybe TransportHost -> FilePath -> IO ()
generateSite si onionHost sitePath = do
@@ -121,7 +78,6 @@ serverInformation ServerInformation {config, information} onionHost = render E.i
where
basic =
[ ("sourceCode", Just . encodeUtf8 $ sourceCode spi),
("version", Just $ B.pack simplexMQVersion),
("website", encodeUtf8 <$> website spi)
]
conds ServerConditions {conditions, amendments} =
@@ -153,8 +109,7 @@ serverInformation ServerInformation {config, information} onionHost = render E.i
("hostingCountry", encodeUtf8 <$> country)
]
server =
[ ("serverCountry", encodeUtf8 <$> serverCountry spi),
("hostingType", (\s -> maybe s (\(c, rest) -> toUpper c `B.cons` rest) $ B.uncons s) . strEncode <$> hostingType spi)
[ ("serverCountry", fmap encodeUtf8 $ serverCountry =<< information)
]
-- Copy-pasted from simplex-chat Simplex.Chat.Types.Preferences
-14
View File
@@ -28,17 +28,3 @@ source-repository-package
type: git
location: https://github.com/simplex-chat/sqlcipher-simple.git
tag: a46bd361a19376c5211f1058908fc0ae6bf42446
-- waiting for published warp-tls-3.4.7
source-repository-package
type: git
location: https://github.com/yesodweb/wai.git
tag: ec5e017d896a78e787a5acea62b37a4e677dec2e
subdir: warp-tls
-- backported fork due http-5.0
source-repository-package
type: git
location: https://github.com/simplex-chat/wai.git
tag: 2f6e5aa5f05ba9140ac99e195ee647b4f7d926b0
subdir: warp
+21 -105
View File
@@ -2,6 +2,10 @@
set -eu
# Links to scripts/configs
bin="https://github.com/simplex-chat/simplexmq/releases/latest/download"
bin_smp="$bin/smp-server-ubuntu-20_04-x86-64"
bin_xftp="$bin/xftp-server-ubuntu-20_04-x86-64"
scripts="https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/scripts/main"
scripts_systemd_smp="$scripts/smp-server.service"
scripts_systemd_xftp="$scripts/xftp-server.service"
@@ -22,8 +26,6 @@ path_conf_var="/var/opt"
path_conf_smp="$path_conf_etc/simplex $path_conf_var/simplex"
path_conf_xftp="$path_conf_etc/simplex-xftp $path_conf_var/simplex-xftp /srv/xftp"
path_conf_info="$path_conf_etc/simplex-info"
path_systemd="/etc/systemd/system"
path_systemd_smp="$path_systemd/smp-server.service"
path_systemd_xftp="$path_systemd/xftp-server.service"
@@ -54,7 +56,7 @@ ${GRN}1.${NC} Install latest binaries from GitHub releases:
${GRN}2.${NC} Create server directories:
- smp: ${YLW}${path_conf_smp}${NC}
- xftp: ${YLW}${path_conf_xftp}${NC}
${GRN}3.${NC} Setup user for server:
${GRN}3.${NC} Setup user for each server:
- xmp: ${YLW}${user_smp}${NC}
- xftp: ${YLW}${user_xftp}${NC}
${GRN}4.${NC} Create systemd services:
@@ -63,12 +65,7 @@ ${GRN}4.${NC} Create systemd services:
${GRN}5.${NC} Install stopscript (systemd), update and uninstallation script:
- all: ${YLW}${path_bin_update}${NC}, ${YLW}${path_bin_uninstall}${NC}, ${YLW}${path_bin_stopscript}${NC}
Press:
- ${GRN}1${NC} to install smp server
- ${GRN}2${NC} to install xftp server
- ${RED}Ctrl+C${NC} to cancel installation
Selection: "
Press ${GRN}ENTER${NC} to continue or ${RED}Ctrl+C${NC} to cancel installation"
end="Installtion is complete!
@@ -79,79 +76,27 @@ Please checkout our server guides:
To uninstall with full clean-up, simply run: ${YLW}sudo /usr/local/bin/simplex-servers-uninstall${NC}
"
set_version() {
ver="${VER:-latest}"
case "$ver" in
latest)
bin="https://github.com/simplex-chat/simplexmq/releases/latest/download"
remote_version="$(curl --proto '=https' --tlsv1.2 -sSf -L https://api.github.com/repos/simplex-chat/simplexmq/releases/latest | grep -i "tag_name" | awk -F \" '{print $4}')"
;;
*)
bin="https://github.com/simplex-chat/simplexmq/releases/download/${ver}"
remote_version="${ver}"
;;
esac
}
os_test() {
. /etc/os-release
case "$VERSION_ID" in
20.04|22.04) : ;;
24.04) VERSION_ID='22.04' ;;
*) printf "${RED}Unsupported Ubuntu version!${NC}\nPlease file Github issue with request to support Ubuntu %s: https://github.com/simplex-chat/simplexmq/issues/new\n" "$VERSION_ID" && exit 1 ;;
esac
version="$(printf '%s' "$VERSION_ID" | tr '.' '_')"
arch="$(uname -p)"
case "$arch" in
x86_64) arch="$(printf '%s' "$arch" | tr '_' '-')" ;;
*) printf "${RED}Unsupported architecture!${NC}\nPlease file Github issue with request to support %s architecture: https://github.com/simplex-chat/simplexmq/issues/new" "$arch" && exit 1 ;;
esac
bin_smp="$bin/smp-server-ubuntu-${version}-${arch}"
bin_xftp="$bin/xftp-server-ubuntu-${version}-${arch}"
}
setup_bins() {
eval "bin=\$bin_${1}"
eval "path=\$path_bin_${1}"
curl --proto '=https' --tlsv1.2 -sSf -L "$bin" -o "$path" && chmod +x "$path"
unset bin path
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_smp" -o "$path_bin_smp" && chmod +x "$path_bin_smp"
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_xftp" -o "$path_bin_xftp" && chmod +x "$path_bin_xftp"
}
setup_users() {
eval "user=\$user_${1}"
useradd -M "$user" 2> /dev/null || true
unset user
useradd -M "$user_smp" 2> /dev/null || true
useradd -M "$user_xftp" 2> /dev/null || true
}
setup_dirs() {
# Unquoted varibles, so field splitting can occur
eval "path_conf=\$path_conf_${1}"
eval "user=\$user_${1}"
mkdir -p $path_conf
mkdir -p $path_conf_info
printf "local_version_%s='%s'\n" "$1" "$remote_version" >> "$path_conf_info/release"
chown -R "$user":"$user" $path_conf
unset path_conf user
mkdir -p $path_conf_smp
chown "$user_smp":"$user_smp" $path_conf_smp
mkdir -p $path_conf_xftp
chown "$user_xftp":"$user_xftp" $path_conf_xftp
}
setup_systemd() {
eval "scripts_systemd=\$scripts_systemd_${1}"
eval "path_systemd=\$path_systemd_${1}"
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd" -o "$path_systemd"
unset scripts_systemd path_systemd
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd_smp" -o "$path_systemd_smp"
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd_xftp" -o "$path_systemd_xftp"
}
setup_scripts() {
@@ -165,61 +110,32 @@ checks() {
printf "This script is intended to be run with root privileges. Please re-run script using sudo."
exit 1
fi
set_version
os_test
mkdir -p $path_conf_info
}
main() {
checks
printf "%b\n%b" "${BLU}$logo${NC}" "$welcome"
printf "%b\n%b\n" "${BLU}$logo${NC}" "$welcome"
read ans
case "$ans" in
1) setup='smp' ;;
2) setup='xftp' ;;
*) printf 'Installation aborted.\n' && exit 0 ;;
esac
printf "Installing binaries..."
for i in $setup; do
setup_bins "$i"
done
setup_bins
printf "${GRN} Done!${NC}\n"
printf "Creating users..."
for i in $setup; do
setup_users "$i"
done
setup_users
printf "${GRN} Done!${NC}\n"
printf "Creating directories..."
for i in $setup; do
setup_dirs "$i"
done
setup_dirs
printf "${GRN} Done!${NC}\n"
printf "Creating systemd services..."
for i in $setup; do
setup_systemd "$i"
done
setup_systemd
printf "${GRN} Done!${NC}\n"
printf "Installing stopscript, update and uninstallation script..."
setup_scripts
printf "${GRN} Done!${NC}\n"
printf "%b" "$end"
+7 -19
View File
@@ -1,5 +1,5 @@
name: simplexmq
version: 6.1.1.0
version: 6.0.0.1
synopsis: SimpleXMQ message broker
description: |
This package includes <./docs/Simplex-Messaging-Server.html server>,
@@ -47,7 +47,6 @@ dependencies:
- direct-sqlcipher == 2.3.*
- directory == 1.3.*
- filepath == 1.4.*
- hashable == 1.4.*
- hourglass == 0.2.*
- http-types == 0.12.*
- http2 >= 4.2.2 && < 4.3
@@ -70,7 +69,7 @@ dependencies:
- temporary == 1.3.*
- time == 1.12.*
- time-manager == 0.0.*
- tls >= 1.9.0 && < 1.10
- tls >= 1.7.0 && < 1.8
- transformers == 0.6.*
- unliftio == 0.2.*
- unliftio-core == 0.2.*
@@ -123,10 +122,9 @@ executables:
dependencies:
- file-embed
- simplexmq
- wai
- wai-app-static
- warp ==3.3.30 # the last one before http2-5.0
- warp-tls ==3.4.7 # extra internals exposed
- warp
- warp-tls
ghc-options:
- -threaded
- -rtsopts
@@ -160,29 +158,19 @@ executables:
tests:
simplexmq-test:
source-dirs:
- tests
- apps/smp-server/web
source-dirs: tests
main: Test.hs
dependencies:
- simplexmq
- deepseq == 1.4.*
- file-embed
- generic-random == 1.5.*
- hspec == 2.11.*
- hspec-core == 2.11.*
- http-client
- http-client-tls
- HUnit == 1.6.*
- main-tester == 0.2.*
- QuickCheck == 2.14.*
- silently == 1.2.*
- simplexmq
- main-tester == 0.2.*
- timeit == 2.0.*
- unordered-containers
- wai
- wai-app-static
- warp
- warp-tls
ghc-options:
- -threaded
- -rtsopts
+3 -5
View File
@@ -559,13 +559,11 @@ In current implementation of XFTP protocol in SimpleX Chat clients don't use FAC
- perform traffic correlation attacks against senders and recipients and correlate senders and recipients within the monitored set, frustrated by the number of users on the servers.
- observe how much traffic is being sent, and make guesses as to its purpose.
- observe how much traffic is being sent, and make guesses as to its purpose
- in case of a compromised transport protocol, correlate file senders and receivers.
*cannot, even in case of a compromised transport protocol:*
*cannot, in case of a non-compromised transport protocol:*
- perform traffic correlation attacks.
- perform traffic correlation attacks with any increase in efficiency over a non-compromised transport protocol
#### XFTP server
+9 -29
View File
@@ -67,7 +67,7 @@ The session invitation contains this data:
- CA TLS certificate fingerprint of the controller - this is part of long term identity of the controller established during the first session, and repeated in the subsequent session announcements.
- Session Ed25519 public key used to verify the announcement and commands - this mitigates the compromise of the long term signature key, as the controller will have to sign each command with this key first.
- Long-term Ed25519 public key used to verify the announcement and commands - this is part of the long term controller identity.
- Session X25519 DH key to agree session encryption (both for multicast announcement and for commands and responses in TLS), as described in https://datatracker.ietf.org/doc/draft-josefsson-ntruprime-hybrid/. The new keys are used for each session, and if client key is already available (from the previous session), the computed shared secret will be used to encrypt the announcement multicast packet. The out-of-band invitation is unencrypted. DH public key and KEM encapsulation key are sent unencrypted. NaCL crypto_box is used for encryption.
- Session X25519 DH key and SNTRUP761 KEM encapsulation key to agree session encryption (both for multicast announcement and for commands and responses in TLS), as described in https://datatracker.ietf.org/doc/draft-josefsson-ntruprime-hybrid/. The new keys are used for each session, and if client key is already available (from the previous session), the computed shared secret will be used to encrypt the announcement multicast packet. The out-of-band invitation is unencrypted. DH public key and KEM encapsulation key are sent unencrypted. NaCL crypto_box is used for encryption.
Host application decrypts (except the first session) and validates the invitation:
- Session signature is valid.
@@ -184,7 +184,7 @@ The controller decrypts (including the first session) and validates the received
The controller should reply with with `ctrlHello` or `ctrlError` response:
```abnf
ctrlHello = %s"HELLO " kemCiphertext encrypted(unpaddedSize ctrlHelloJSON helloPad) pad
ctrlHello = %s"HELLO " kemCiphertext nonce encrypted(unpaddedSize ctrlHelloJSON helloPad) pad
; ctrlHelloJSON is encrypted with the hybrid secret,
; including both previously agreed DH secret and KEM secret from kemCiphertext
unpaddedSize = largeLength
@@ -206,8 +206,6 @@ JTD schema for the encrypted part of controller HELLO block `ctrlHelloJSON`:
}
```
Controller `hello` block and all subsequent protocol messages are encrypted with the chain keys derived from the hybrid key (see key exchange below) - that is why conntroller hello block does not include nonce. That provides forward secrecy within the XRCP session. Receiving this `hello` block allows host to compute the same hybrid keys and to derive the same chain keys.
Once the controller replies HELLO to the valid host HELLO block, it should stop accepting new TCP connections.
### Controller/host session operation
@@ -225,12 +223,10 @@ tlsunique channel binding from TLS session MUST be included in commands (include
The syntax for encrypted command and response body encoding:
```abnf
commandBody = counter encBody sessSignature idSignature [attachment]
responseBody = counter encBody [attachment] ; counter must match command
; counter is placed outside of encrypted body to allow correlating encryption keys
; with the chain keys (each command and response are encrypted by different keys)
encBody = encLength32 encrypted(tlsunique body)
attachment = %x01 encLength32 encrypted(attachment)
commandBody = encBody sessSignature idSignature [attachment]
responseBody = encBody [attachment] ; counter must match command
encBody = nonce encLength32 encrypted(tlsunique counter body)
attachment = %x01 nonce encLength32 encrypted(attachment)
noAttachment = %x00
tlsunique = length 1*OCTET
counter = 8*8 OCTET ; int64
@@ -243,7 +239,7 @@ If the command or response includes attachment, its hash must be included in com
Initial announcement is shared out-of-band (URI with xrcp scheme), and it is not encrypted.
This announcement contains only DH keys, as KEM key is too large to include in QR code, which are used to agree encryption key for host HELLO block. The host HELLO block will contain DH key in plaintext part and KEM encapsulation (public) key in encrypted part, that will be used to determine the shared secret (using SHA3-256 over concatenated DH shared secret and KEM encapsulated secret) to derive keys for controller HELLO response (that contains KEM ciphertext in plaintext part) and subsequent session commands and responses.
This announcement contains only DH keys, as KEM key is too large to include in QR code, which are used to agree encryption key for host HELLO block. The host HELLO block will contain DH key in plaintext part and KEM encapsulation (public) key in encrypted part, that will be used to determine the shared secret (using SHA256 over concatenated DH shared secret and KEM encapsulated secret) both for controller HELLO response (that contains KEM ciphertext in plaintext part) and subsequent session commands and responses.
During the next session the announcement is sent via encrypted multicast block. The shared key for this announcement and for host HELLO block is determined using the KEM shared secret from the previous session and DH shared secret computed using the host DH key from the previous session and the new controller DH key from the announcement.
@@ -254,7 +250,7 @@ In pseudo-code:
```
// session 1
hostHelloSecret(1) = dhSecret(1)
sessionSecret(1) = sha3-256(dhSecret(1) || kemSecret(1)) // to encrypt session 1 data, incl. controller hello
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
@@ -266,7 +262,7 @@ dhSecret(n') = dh(hostHelloDhKey(n - 1), controllerDhKey(n))
// session n
hostHelloSecret(n) = dhSecret(n)
sessionSecret(n) = sha3-256(dhSecret(n) || kemSecret(n)) // to encrypt session n data, incl. controller hello
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))
@@ -277,22 +273,6 @@ If controller fails to store the new host DH key after receiving HELLO block, th
To decrypt a multicast announcement, the host should try to decrypt it using the keys of all known (paired) remote controllers.
Once kemSecret is agreed for the session, it is used to derive two chain keys, to receive and to send messages:
```
host: sndKey, rcvKey = HKDF(kemSecret, "SimpleXSbChainInit", 64)
controller: rcvKey, sndKey = HKDF(kemSecret, "SimpleXSbChainInit", 64)
```
where HKDF is based on SHA512, with empty salt.
Actual keys and nonces to encrypt and decrypt messages are derived from these chain keys:
```
to send: (sndKey', sk, nonce) = HKDF(sndKey, "SimpleXSbChain", 88)
to receive: (rcvKey', sk, nonce) = HKDF(rcvKey, "SimpleXSbChain", 88)
```
## Threat model
#### A passive network adversary able to monitor the site-local traffic:
-124
View File
@@ -1,124 +0,0 @@
# Short invitation links
## Problem
Long links look scary and unsafe for many users. While this is a perceived problem, rather than a real one, it hurts adoption.
What is worse, long links do not fit in profile descriptions of other social networks where people might want to advertize their contact addresses.
The current link size limitation is also the reason for not including PQ KEM keys into invitation links and addresses, postponing the moment when PQ-resistant encryption kicks in - if we include PQ KEM key into the link, the QR code will not be scannable.
Additionally, if we store short links, they can also include chat preferences and public profile data.
## Solution
MITM-resistant link shortening.
Instead of generating the random address that would resolve into the link - doing so would create the possibility of MITM by the server hosting this link - we can use private key as the link ID that will be passed to the accepting party, and the hash of the public key as ID for the server - the accepting party would present this key itself as ID and it will also be used for server to client encryption (see Protocol below). HKDF will be used to derive symmetric key from private key and used in secret_box together with random nonce (to allow replacing data with the same key but with a different nonce - nonce will be sent to the server too). secret_box construction is authenticated encryption, so it would protect from MITM.
The proposed syntax:
```abnf
shortConnectionRequest = connectionScheme "/" connReqType "#/" smpServer "/" linkHash
connReqType = %s"invitation" / %s"contact"
connectionScheme = (%s"https://" clientAppServer) / %s"simplex:"
clientAppServer = hostname [ ":" port ]
; client app server, e.g. simplex.chat
smpServer = serverIdentity "@" srvHosts [":" port] ; no smp:// prefix, no escaping
srvHosts = <hostname> ["," srvHosts] ; RFC1123, RFC5891
linkHash = <base64url encoded SHA256 or SHA512 hash of the original link>
```
If SMP server supports pages, its name can be used as clientAppServer, without repeating it after #, for a shorter link.
Example link:
```
https://simplex.chat/contact/#0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im/abcdefghij0123456789abcdefghij0123456789abc=
```
This link has the length of ~136 characters (256 bits), which is shorter than the full contact address (~310 characters) and much shorter than invitation links (~528 characters) even without post-quantum keys added to them.
This size can be further reduced by
- use server domain in the link.
- do not include onion address, as the connection happens via proxy anyway, if it's untrusted server.
- not pinning server TLS certificate - the downside here is that while the attack that compromises TLS will not be able to substitute the link (because it's hash will not match), it will be able to intercept and to block it.
- using shorter hash, e.g. SHA128 - reducing the collision resistance.
If the server is known, the client could use it's hash and onion address, otherwise it could trust the proxy to use any existing session with the same hostname or to accept the risk of interception - given that there is no risk of substitution.
With the first two of these "improvements" the link could be ~122 characters:
```
https://smp8.simplex.im/contact/#0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU@/abcdefghij0123456789abcdefghij0123456789abc
```
If onion address is preserved the link will be ~184 characters (won't fit in Twitter 160 characters bio):
```
https://smp8.simplex.im/contact/#0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU@beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion/abcdefghij0123456789abcdefghij0123456789abc
```
If we implement it, the request to resolve the link would be made via proxied SMP command (to avoid the direct connection between the client and the recipient's server).
Pros:
- a bit shorter link.
- possibility to include post-quantum keys into the full link keeping the same shortened link size.
- possibility to include chat profile of contact or group, and preferences, for a much better connection experience, and to show this information when the link sent in the conversation (clients can resolve them automatically, without connecting - it can be resolved by the sending clients).
- server will not have access to the link.
Cons:
- protocol complexity.
- observers can access the link content, so for 1-time invitation we should only include permissions and not profile.
Pros are a huge improvement of UX of connecting both within and from outside of the app (e.g., link can be resolved even before creating chat profile, as part of the onboarding).
## Protocol
To support short links, the SMP servers would provide a simple key-value store enabled by three additional commands: `WRT`, `CLR` and `READ`
`WRT` command is used to store and to update values in the store. The size of the value is limited by the same size as sent messages (or, possibly, smaller - as connection information size used in confirmation messages) - the clients would use this fixed size irrespective of the content. `WRT` command will be sent with the data blob ID in the transaction entityId field, public authorization key used to authorize `WRT` and `CLR` commands (subsequent WRT commands to the existing key must use the same key), and the data blob.
`CLR` command must use with the same entity ID and must be authorized by the same key.
`READ` command must use the ID which hash would be equal of the ID used to create the data blob, and this ID would also be used as public authorization
## Algorithm to store and to retrieve data blob.
**Store data blob**
- the data blob owner generates X25519 key pair: `(k, pk)`.
- private key `pk` will be included in the short link shared with the other party (only base64url encoded key bytes, not X509 encoding).
- `HKDF(pk)` will be used to encrypt the link data with secret_box before storing it on the server.
- the hash of public key `sha256(k)` will be used as ID by the owner to store and to remove the data blob (`WRT` and `CLR` commands).
**Retrieve data blob**
- the sender uses the public key `k` derived from the private key `pk` included in the link as entity ID to retrieve data blob (the server will compute the ID used by the owner as `sha256(k)` and will be able to look it up). This provides the quality that the traffic of the parties has no shared IDs inside TLS. It also means that unlike message queue creation, the ID to retrieve the blob was never sent to the blob creator, and also is not known to the server in advance (the second part is only an observation, in itself it does not increase security, as server has access to an encrypted blob anyway).
- note that the sender does not authorize the request to retrieve the blob, as it would not increase security unless a different key is used to authorize, and adding a key would increase link size.
- server session keys with the sender will be `(sk, spk)`, where `sk` is public key shared with the sender during session handshake, and `spk` is the private key known only to the server.
- this public key `k` will also be combined with server session key `spk` using `dh(k, spk)` to encrypt the response, so that there is no ciphertext in common in sent and received traffic for these blobs. Correlation ID will be used as a nonce for this encryption.
- having received the blob, the client can now decrypt it using secret_box with `HKDF(pk)`.
Using the same key as ID for the request, and also to additionally encrypt the response allows to use a single key in the link, without increasing the link size.
## Threat model
**Compromised SMP server**
can:
- delete link data.
- hide link selectively from some requests.
cannot:
- undetectably replace link data.
- access unencrypted link data, whether it was or was not accessed by the accepting party.
- observe IP addresses of the users accessing link data.
**Passive observer who observed short link**:
can:
- access original unencrypted link data
cannot:
- replace or delete the link data
-18
View File
@@ -1,18 +0,0 @@
# iOS notifications stability
## Problem
iOS notifications may fail to deliver for several reasons, but there are two important reasons that we could address:
- when notification server is not subscribed to SMP server(s), the notifications can be dropped - it can happen because either notification server restarts or becuase SMP server restarted and some messages are received before notification server resubscribed. We lose approximately 3% of notifications because of this reason.
- when user device is offline or has low power condition, Apple does not deliver notification, but puts them to storage. If while the notification is in storage a new one arrives it would overwrite the previous notification. If it was the message to the same message queue, the client will download messages anyway, up to a limit, but if the message was to another queue, it will not be delivered until the app is opened. Apple delivers about 88% of notifications that should be delivered (not accounting for uninstalled apps), the rest is replaced with the newer notifications.
## Solution
The first problem can be solved by preserving notifications for a limited time (say 1 hour) in case there is no subscription to notification from notification server. At the very least, they can be preserved in SMP server memory but can also be stored to a file on restart, similar to messages, and be delivered when notification server resubscribes. It is sufficient to store one notification per messaging queue.
The second problem is both more damaging and more complex to solve. The solution could be to always deliver several last notifications to different queues in one packet (Apple allows up to ~4-5kb notification size, and we are sending packets of fixed size 512 bytes, so we could fit up to 8-10 of them in each notification).
Every time a client receives such batch of notifications if can:
- check if that notification was already received in the previous batch.
- if it was received, it would be ignored, otherwise it would be processed.
- process them one by one, started from the most recent one while the time allows.
-179
View File
@@ -1,179 +0,0 @@
# SMP server message storage
## Problem
Currently SMP servers store all queues in server memory. As the traffic grows, so does the number of undelivered messages. What is worse, Haskell is not avoiding heap fragmentation when messages are allocated and then de-allocated - undelivered messages use ByteString and GC cannot move them around, as they use pinned memory.
## Possible solutions
### Solution 1: solve only GC fragmentation problem
Move from ByteString to some other primitive to store messages in memory long term, e.g. ShortByteString, or manage allocation/de-allocation of stored messages manually in some other way.
Pros: the simplest solution that avoids substantial re-engineering of the server.
Cons:
- not a long term solution, as memory growth still has limits.
- may be ineffective, as it introduces additional copying of bytes.
### Solution 2: move message storage to hard drive
Use files or RocksDB to store messages.
Pros:
- much lower memory usage.
- no message loss in case of abnormal server termination (important until clients have delivery redundancy).
- this is a long term solution, and at some point it might need to be done anyway.
Cons:
- substantial re-engineering costs and risks.
- metadata privacy. Currently we only save undelivered messages when server is restarted, with this approach all messages will be stored for some time. this argument is limited, as hosting providers of VMs can make memory snapshots too, on the other hand they are harder to analyze than files. On another hand, with this approach messages will be stored for a shorter time.
#### RocksDB and other key-value stores
The downside of any key-value stores is that they don't seem to have efficient primitives for sequential delivery. While sequential delivery can be modelled with linked lists, they would require 1 key insert (on send), 3 key updates (1 update to update queue data on send, 1 update of the last message to point to the next, 1 update on delivery or message expiration) and 1 key deletion (on delivery or message expiration) for each delivered message.
This might result in substantial write amplification and compacting costs.
In general, tree structures that are efficient for quick lookups and updates, given approximately fixed value size, are inefficient for modelling queues.
#### Files
The upside of files is that they are well suited for sequential delivery and don't result in the same churn, with careful design, as trees do.
The downside of filesystem is that it does not scale well with the large number of files in a folder, so queues will have to be spread across multiple folders, following tree-like structure.
I could not find an available library that efficiently models sequential delivery in highly concurrent environment.
A possible design could be the following.
##### Queue folder and files
Each message queue is stored in its own folder (see below on folder locations). Folder would contain these files:
- `messages.abcd.log` - the file that is used to read messages from, sequentially
- `messages.efgh.log` - the optional file that is used to write messages, in case it is different from read file.
- `queue.log` - append-only file where the last line represents the current queue state
- `queue.timestamp.log` - previous states of queue.log file
Each line in "queue.log" file has this syntax
```abnf
queueLogLine =
%s"read_file=" base64
%s"read_msg=" digits
%s"read_byte=" digits
%s"write_file=" base64
%s"write_msg=" digits
```
When queue is first requested by the server:
```c
if queue folder exists:
read queue state from last line of queue.log
if queue.log contained more than one line: // compaction
copy queue.log to queue.timestamp.log
write one line queue state to queue.log
else:
create queue folder
create messages.abcd.log (abcd is some random string)
read_msg = 0
read_byte = 0
create queue.log with one line: "read_file=abcd read_msg=0 read_byte=0 write_files=abcd write_msg=0"
open read_file in ReadMode and seek to read_byte position
nextReadByte = read_byte
nextReadMsg = read_msg
open write_file in AppendMode
```
When message is added to the queue (assumes that queue state is loaded to server memory, if not the previous section will be done first):
```c
if write_msg > max_queue_messages:
return quota error
else if write_msg = max_queue_messages:
add quota_exceeded message to write_file
update queue state: write_msg += 1
append updated queue state to queue.log
else ]
// It is required that `max_queue_messages < max_file_messages`,
// so that we never need more than one additional write file.
if write_msg >= max_file_messages: // queue file rotation
create messages.efgh.log // efgh is some random string
update queue state: write_file=efgh write_msg=0 // read file remains the same as it was
append updated queue state to queue.log
copy queue.log to queue.timestamp.log
// `old` needs to be defined to limit the number and storage duration,
// preserving not more than N files, and not more than M days files, "and then some"
// (that is if the queue has high churn, we have file from M days before in any case, for any debugging).
delete `old` `queue.timestamp.log` files
write one line queue state to queue.log // compaction
add message to write_file
update queue state: write_msg += 1
append updated queue state to queue.log
```
The above algorithm assumes `max_queue_messages < than max_file_messages`, so that we never need more than one write file.
When message is delivered, it is simply read from the read queue, queue state does not change yet:
```c
if nextReadMsg > read_msg:
deliver cached message, no need to read it again
else
read message from read_file handle
nextReadMsg = read_msg + 1
nextReadByte = current position in file
```
When message delivery is acknowledged, the read queue needs to be advanced, and possibly switched to read from the current write_queue:
```c
if nextReadByte == read_byte:
return error // nothing was delivered
else if nextReadByte = EOF:
// end of file is reached, possibly some other condition,
// but it should allow changing max_file_messages on server restart
currReadFile = read_file
read_file = write_file
read_msg = 0
read_byte = 0
append updated queue state to queue.log
delete currReadFile
else
read_msg += 1
read_byte = nextReadByte
// `seek` should not be necessary, as the handle is already at nextReadByte position here
// seek to read_byte
append updated queue state to queue.log
```
The above algorithm delegates the problem of compaction and fragmentation management to file system, that is very optimized for such scenarios.
Also, read and write files will grow to almost a constant size, so the space they used may be re-used.
An important consideration is that writes to queue.log and message.log files and queue state modifications have to be sequential, without concurrency - it can be managed with the usual locks.
##### Queue folders structure
Most Linux systems use EXT4 filesystem where the file lookup time scales linearly to the number of files. While alternatives with logarithmic lookup time exist (XFS), they may be very complex to configure on the existing systems.
So storing all queue folders in one folder won't scale.
To solve this problem we could use recipient queue ID in base64url format not as a folder name, but as a folder path, splitting it to path fragments of some length. The number of fragments can be configurable and migration to a different fragment size can be supported as the number of queues on a given server grows.
Currently, queue ID is 24 bytes random number, thus allowing 2^192 possible queue IDs. If we assume that a server must hold 1b queues, it means that we have ~2^162 possible addresses for each existing queue. 24 bytes in base64 is 32 characters that can be split into say 8 fragments with 4 characters each, so that queue folder path for queue with ID `abcdefghijklmnopqrstuvwxyz012345` would be:
`/var/opt/simplex/messages/abcd/efgh/ijkl/mnop/qrst/uvwx/yz01/2345`
The maximum theoretic number of the folders on the 1st level is 64^4, or 2^24 ~ 16m - this is probably still a large number of subfolders for EXT4. Given that addresses are random, all the possible combinations in the first folder can be used with a large number of queues.
So we could use an unequal split of path, two letters each and the last being long:
`/var/opt/simplex/messages/ab/cd/ef/ghijklmnopqrstuvwxyz012345`
The first three levels in this case can have 4096 subfolders each, and it gives 68b possible subfolders (64^2^3), so the last level will be sparse in case of 1b queues on the server. So we could make it 4 levels with 2 letters to never think about it, accounting for a large variance of the random numbers distribution:
`/var/opt/simplex/messages/ab/cd/ef/gh/ijklmnopqrstuvwxyz012345`
-81
View File
@@ -1,81 +0,0 @@
# Storage considerations for SMP queues
See [Short invitation links](./2024-06-21-short-links.md).
## Problem
1) queue records are created permanently, until the clients delete them.
2) clients only delete queue records based on some user action, pending connections do not expire.
While part 2 should be improved in the client, indefinite storage of queue records becomes a much bigger issue if each of them would result in a permanent storage of 4-16kb blob in server memory, without server-side expiration for short invitation links.
## Possible solutions
1) Add some queue timestamp, e.g. queue creation date, to expire unsecured queues after say 3 weeks.
The problem with this approach is that contact addresses are also unsecured queues, and they should not be expired.
We could set really large expiration time, and require that clients "update" the unsecured queues they need at least every 1-2 years, but it would not solve the problem of storing a large number of blobs in the server memory for unused/abandoned 1-time invitations.
2) Do not store blobs in memory / append-only log, and instead use something like RocksDB. While it may be a correct long term solution, it may be not expedient enough at the current POC stage for this feature. Also, the lack of expiration is wrong in any case and would indefinitely grow server storage.
3) Add flag allowing the server to differentiate permanent queues used as contact addresses, also using different blob sizes for them. In this case, messaging queues will be expired if not secured after 3 weeks, and contact address queues would be expired if not "updated" by the owner within 2 years.
Probably all three solutions need to be used, to avoid creating a non-expiring blob storage in memory, as in case too many of such blobs are created it would not be possible to differentiate between real users and resource exhaustion attacks, and unlike with messages, they won't be expiring too.
Servers already can differentiate messaging queues and contact address queues, if they want to:
- with the old 4-message handshake, the confirmation message on a normal queue was different, and also KEY command was eventually used.
- with the fast 2-message handshake, while the confirmation message has the same syntax, and the differences are inside encrypted envelope, the client still uses SKEY command.
- in both cases, the usual messaging queues are secured, and contact addresses are not, so this difference is visible in the storage as well (although it is not easy to differentiate between abandoned 1-time invitations and contact addresses).
Differentiating these queues can also allow different message retention times - e.g., the queues for contact addresses could have bigger size, but have lower message retention time.
## Proposed solution
1. Add queue updated_at date into queue records. While it adds some metadata, it seems necessary to manage retention and quality of service. It will not include exact time, only date, and the time of creation will be replaced by the time of any update - queue secured, a message is sent, or queue owner subscribes to the queue. To avoid the need to update store log on every message this information can be appended to store log on server termination. Or given that only one update per day is needed it may be ok to make these updates as they happen (temporarily making the sequence and time of these events available in storage).
2. Add flag to indicate the queue usage - messaging queue or queue for contact address connection requests. This would result in different queue size and different retention policy for queue and its messages. We already have "sender can secure flag" which is, effectively, this flag - contact address queues are never secured. So this does not increase stored metadata in any way.
## Possible changes to short links
This is a design considerations and a concept, not a design yet.
Instead of implementing a generic blob storage that can be used as an attack vector, and adds additional failure point (another server storing blob that is necessary to connect to the queue on the current server), but instead adds an extended queue information blobs, most of which could be dropped without the loss of connectivity, so that the attack can be mitigated by deleting these blobs without users losing the ability to connect, as long as the queue and minimal extended information is retained.
So, to make the connection there need to be these elements:
- queue server and queue ID - mandatory part, that can be included in short link
- SMP key - mandatory part for all queues. We are considering initializing ratchets earlier for contact addresses, and include ratchet keys and pre-keys into queue data as well, but it is out of scope here.
- Ratchet keys - mandatory part for 1-time invitation that won't fit in short link.
- PQ key - optional part that can be stored with addresses if ratchet keys are added and with 1-time invitations.
- App blobs - chat preferences for 1-time invitation links and profile information for contact addresses.
So rather that storing one blob with a large address inside it, not associated with the queue, increasing probability of failure and reducing our ability to mitigate resource exhaustion, we could store extended blobs associated with the queues.
Also, we need the address shared with the sender (party accepting the connection) to be short. We could use a similar approach that was proposed for data blobs, using a single random seed per queues to derive multiple keys and IDs from it. For example:
1. The queue owner:
- generates Ed25529 key pair `(sk, spk)` and X25519 key pair `(dhk, dhpk)` to use with the server, same as now sent in NEW command.
- generates queue recipient ID (this ID can still be server-generated).
- generates X25519 key pair `(k, pk)` to use with the accepting party.
- derives from `k`:
- sender ID.
- symmetric key for authenticated encryption of blobs.
- `k` will be used as short link.
2. All other data from the invitation can be included in queue creation request and be associated with the queue as 1-3 blobs with different priority:
- ratchet keys - it will have a small size, so only this blob cannot be removed, while other blobs can be removed in case of resource exhaustion.
- PQ keys - optional blob.
- conversation preferences and profile - can be removed depending on creation time, e.g. all new blobs can be removed.
The algorithm used to derive key and ID from `k` needs to be cryptographically secure, e.g. it could be some KDF or ChaCha DRG initialized with `k` as seed, TBC.
So, coupling blob storage with messaging queues has these pros/cons:
Cons:
- no additional layer of privacy - the server used for connection is visible in the link, even after the blobs are removed from the server.
Pros:
- no additional point of failure in the connection process - the same server will be used to retrieve necessary blobs as for connection.
- queue blobs of messaging blobs will be automatically removed once the queue is secured or expired, without additional request from the recipient - reducing the storage and the time these blobs are available.
- queue blobs for contact addresses will be structured and some of the large blobs can be removed in case of resource exhaustion attack (and recreated by the client if needed), with the only downside that PQ handshake will be postponed (which is the case now) and profile will not be available at a point of connection.
-80
View File
@@ -1,80 +0,0 @@
# Blob extensions for SMP queues
Evolution of the design for short links, see [here](./2024-06-21-short-links.md) and [here](./2024-09-05-queue-storage.md).
## Problems
Allow storing extended information with SMP queues to improve UX and security of making connections:
- short invitation links and contact addresses.
- PQ encryption from the first message.
- present user profile with chat preferences and welcome message when the public address link is scanned.
## Design
1. Queue creation/update date is already added to server persistence, allowing to expire queues and blobs, depending on their usage.
2. Add "queue type" metadata to NEW command to indicate whether messaging queue is used as public address or as messaging queue (see previous docs on why it doesn't change threat model). While at the moment it would match sndSecure flag there may be future scenarios when they diverge. Initially only "invitation" and "contact" types will be supported.
3. Prohibit sndSecure flag for "contact" queues, prohibit securing contact queues.
4. Add "queue blobs" to NEW command:
- blob0: ratchetKeys up to N0 bytes - priority 0, can't be removed by the server, only in "invitation"
- blob1: PQ key up to N1 bytes - priority 1, can be removed by the server, only used in "invitation"
- blob2: Application data up to N2 bytes - priority 2, can be removed by the server.
5. Add linkId to NEW command
6. linkId and blobs will be removed when queue is secured.
7. Add recipient command to remove/upsert blob2 for contact queues.
8. Add sender command to retrieve blobs.
## Protocol
### Creating a queue:
The queue owner:
- generates Ed25529 key pair `(sk, spk)` and X25519 key pair `(dhk, dhpk)` to use with the server, same as now. `sk` and `dhk` will be sent in NEW command.
- generates X25519 key pair `(k, pk)` to use with the accepting party to encrypt queue messages.
- derives from `k` using HKDF:
- symmetric key `bk` for authenticated encryption of blobs.
- `linkId`, will be sent in NEW command.
- `k` will be used as short link.
- sends NEW command.
NEW command syntax:
```abnf
create = %s"NEW " linkId queueType recipientAuthPublicKey recipientDhPublicKey
basicAuth subscribe sndSecure [ "0" blob0 ] [ "1" blob1 ] [ "2" blob2 ]
queueType = %s"I" / %s "C" ; new parameter
linkId = length *OCTET ; new parameter,
; can be empty in which case blobs won't be allowed
blob0 = word16 *OCTET ; new parameter, encrypted ratchet keys,
; including nonce and auth tag
blob1 = word16 *OCTET ; new parameter, encrypted PQ key
blob2 = word16 *OCTET ; new parameter, encrypted application data
```
SET - command to update queue blobs (recipientId is used as entity ID):
```abnf
set = %s"SET " linkId [ "2" blob2 ] ; passing empty blob removes it
linkId ; updated (or the same) linkId, can be empty to remove blobs
; allows to change the address without removing the queue / changing blobs
; (e.g., to avoid losing the messages).
```
### Sending messages to the queue
GET - command to get queue blobs (linkId is used as entity ID):
```abnf
get = %s"GET"
```
Response to GET:
```abnf
blobs = %s"BLOB" senderId [ "0" blob0 ] [ "1" blob1 ] [ "2" blob2 ]
```
As blobs are retrieved using a separate linkId, once blobs are removed it will be impossible to find senderId from short link - it is a threat model improvement. Once server storage is compacted, it will be impossible to find queue related to the link even with the access to server data (unless server preserves the data).
### Possible privacy improvement
We could only allow unauthorized GET and authorized SET commands for long-term "contact" queues, and return BLOB in response to SKEY (or require that GET is authorized) - so that only the person who secures the queue will get access to data blobs. This way it ensures that the parties transmitting the invitation links cannot retrieve their content without the sender noticing it.
-26
View File
@@ -1,26 +0,0 @@
# Private rendezvous protocol
## Problem
Our current handshake protocol is open to this attack: whoever observes the link exchange, knows on which server connection is being made, and if the traffic on this server is observed, then it can confirm communication between parties. Further, even with the [last proposal](./2024-09-09-smp-blobs.md#possible-privacy-improvement), having real-time access to the server data allows to establish the exact messaging queue that is used to send messages.
## Solution
We could make the initial link exchange more private by making it harder for any observer to discover which server will be used for messaging by hiding this information from the server that hosts the initial link.
Preliminary, the protocol could be the following:
1. Connection initiator stores 224-256 bytes of encrypted connection link on a rendezvous server (link contains server host and linkId on another messaging server, not a rendezvous one).
2. Rendezvous server adds these links to buckets, up to 64 links per bucket. Bucket ID is the timestamp when the bucket was created + a sequential bucket number, in case more than one bucket is created per second.
3. The server responds to the link creator with a bucket ID where this link was added. That bucket ID is its timestamp + a number prevents server "fingerprinting" clients and using say one bucket for each client. If timestamp is different or a bucket number within this timestamp is too large, the client can refuse to use it, depending on the client settings.
4. The initiating party will pass to the accepting party the rendezvous server host, the hash of this bucket ID (bucket link) and the passphrase to derive the key from. The initiating party has an option to pass a link and passphrase via two channels - in which case the link will only contain the bucket ID.
5. The accepting party would then request the bucket via its ID hash (the server would store hashes to be able to look up - hash is used to prevent showing time in the link) and attempt to decrypt all contained links using the provided key.
The accepting party then will continue the connection via the decrypted link.
This obviously does not protect accepting party from the initiating party, if it can choose rendezvous server it controls. It also does not protect from the malicious rendezvous server that would collaborate with link observers. I think reunion doesnt protect from it too.
But it does protect connection from whoever observes the link, particularly if this link only contains the bucket and the key is passed separately, via some other channel.
-163
View File
@@ -1,163 +0,0 @@
# Sharing protocol ports with HTTPS
Some networks block all ports other than web ports, including port 5223 used for SMP protocol by default. Running SMP servers on a common web port 443 would allow them to work on more networks. The servers would need to provide an HTTPS page for browsers (and probes).
## Problem
Browsers and tools rely on system CA bundles instead of certificate pinning.
The crypto parameters used by HTTPS are different from what the protocols use.
Public certificate providers like LetsEncrypt can only sign specific types of keys and Ed25519 isn't one of them.
This means a server should distinguish browser and protocol clients and adjust its behavior to match.
## Solution
`tls` package has a server hook that allows producing a different set of `TLS.Credentials` according to a client-provided "Server Name Indication" extension.
Since LE certificates are only handed out to domain names, TLS client will be sending the SNI.
However client transports are constructed over connected sockets and the SNI wouldn't be present unless explicitly requested.
When a client sends SNI, then it's a browser and a web credentials should be used.
Otherwise it's a protocol client to be offered the self-signed ca, cert and key.
When a transport colocated with a HTTPS, its ALPN list should be extended with `h2 http/1.1`.
The browsers will send it, and it should be checked before running transport client.
If HTTP ALPN is detected, then the client connection is served with HTTP `Application` instead (the same "server information" page).
If some client connects to server IP, doesn't send SNI and doesn't send ALPN, it will look like a pre-handshake client.
In that case a server will send its handshake first.
This can be mitigated by delaying its handshake and letting the probe to issue its HTTP request.
## Implementation plan
An unmodified client should be able to use protocols on port 443 right away.
The switchover happens inside `runTransportServerState` before `runClient`:
```haskell
runServer (tcpPort, ATransport t) = do
-- ...
runTransportServerState_ ss started tcpPort serverParams tCfg $ \socket h -> do -- expose raw socket for warp-tls internals to attach
negotiated <- getSessionALPN
if allowHTTP t && isHTTP negotiated -- only attempt the switch for the TLS transport
then runHTTP socket (tlsContext h)-- ... collect data and produce values needed to run WAI Application
else runClient serverSignKey t h `runReaderT` env -- performs serverHandshake etc as usual
```
The web app and server live outside, so `runHttp` has to be provided by the `runSMPServer` caller.
Additonally, Warp is using its `InternalInfo` object that's scoped to `withII` bracket.
```haskell
runServer ini = do
-- ...
runWebServer ini ServerInformation {config, information} $ if sharedHttps then Nothing else webHttpsParams -- suppress serving https
if sharedHttps
then withRunHTTP staticFilesPath \attachStatic -> runSMPServer cfg (Just attachStatic) -- provide wrapped application runner
else runSMPServer cfg Nothing
```
### Upstream
The implementation relies on a few modification to upstream code:
- `warp-tls`: The library provides `httpOverTls`, but it wants to do handshake itself.
Since we have to do the handshake to switch on ALPN, the setup function has to be split.
This is a resonable change that may be upstreamed and nothing blocks us from using the recent version.
- `warp`: Only the re-export of `serveConnection` is needed.
Unfortunately the most recent `warp` version can't be used right away due to dependency cascade around `http-5` and `auto-update-2`.
So a fork containing the backported re-export has to be used until the dependencies are refreshed.
### TLS.ServerParams
When a server has port sharing enabled, a new set of TLS params is loaded and combined with transport params:
```haskell
newEnv config = do
-- ...
tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig)
sharedServerParams <- forM ((,) <$> sharedHttpsCredentials config <*> alpn transportConfig) $ \((chain, key), alpn) ->
let ca = Nothing -- It is possible to provide CA certificate, but it is typical for web server to use combined certificate chains
loadHTTPSServerParams tlsServerParams ca chain key alpn
```
`loadHTTPSServerParams` extends params with:
1. `onALPNClientSuggest` hook gets `["h2", "http/1.1"]` added to the ALPN list which is now required.
2. `onServerNameIndication` hook added, which upon detecting client SNI prepends the web credentials.
3. `sharedCredentials = T.Credentials []` should be done to prevent transport credentials confusing browsers.
But that aborts key exchange somewhere in tls internals, so disabled for now.
As a workaround, another set of dummy credentials can be provided in the hope that any sane browser would reject them.
Like, RC4 ciphers, "impossible" digest combination, etc.
### supportedParameters
TLS certificate chains provided by LetsEncrypt use ECDSA/P256 and that requires extending `supportedParameters` with things disabled in transports:
```haskell
browserCiphers =
[ TE.cipher_TLS13_AES128CCM8_SHA256
, TE.cipher_ECDHE_ECDSA_AES128CCM8_SHA256
, TE.cipher_ECDHE_ECDSA_AES256CCM8_SHA256
]
browserGroups =
[ T.P256
]
browserSigs =
[ (T.HashSHA256, T.SignatureECDSA),
(T.HashSHA384, T.SignatureECDSA)
]
```
This may not be enough for other certificate providers.
## Configuration
> XXX: This is for the current implementation and should be updated.
Web certificate chain is picked up from the WEB section:
```ini
[TRANSPORT]
port: 443
[WEB]
https: 443
cert: /etc/opt/simplex/web.cert
key: /etc/opt/simplex/web.key
# Alternatively, with a proper access configuration, the paths can point to the LE creds directly:
# cert: /etc/letsencrypt/live/smp.hostname.tld/fullchain.pem
# key: /etc/letsencrypt/live/smp.hostname.tld/privkey.pem
```
When `TRANSPORT.port` matches `WEB.https` the transport server becomes shared.
Perhaps a more desirable option would be explicit configuration resulting in additional transported to run:
```ini
[TRANSPORT]
port: 5223 ; pure protocol transport
# control_port: 5224
shared_port: 443 ; variant 1: register in TRANSPORT
[WEB]
https: 443
cert: /etc/opt/simplex/web.cert
key: /etc/opt/simplex/web.key
# transport: on ; variant 2:
```
## Caveats
Serving static files and the protocols togother may pose a problem for those who currently use dedicated web servers as they should switch to embedded http handlers.
As before, using embedded HTTP server is increasing attack surface.
Users who want to run everything on a single host will have to add and extra IP address and bind servers to specific IPs instead of 0.0.0.0.
An amalgamated server binary can be provided that would contain both SMP and XFTP servers, where transport will dispatch connections by handshake ALPN.
## Alternative: Use transports routable with reverse-proxies
An "industrial" reverse proxy may do the ALPN routing, serving HTTP by itself and delegating `smp` and `xftp` to protocol servers.
Same with the `websockets`.
Since this in effect does TLS termination, the protocol servers will have to rely on credentials from protocol handshakes.
+1 -1
View File
@@ -53,7 +53,7 @@ The session invitation contains this data:
- CA TLS certificate fingerprint of the controller - this is part of long term identity of the controller established during the first session, and repeated in the subsequent session announcements.
- Session Ed25519 public key used to verify the announcement and commands - this mitigates the compromise of the long term signature key, as the controller will have to sign each command with this key first.
- Long-term Ed25519 public key used to verify the announcement and commands - this is part of the long term controller identity.
- Session X25519 DH key and sntrup761 KEM encapsulation key to agree session encryption (both for multicast announcement and for commands and responses in TLS), as described in https://datatracker.ietf.org/doc/draft-josefsson-ntruprime-hybrid/. The new keys are used for each session, and if client key is already available (from the previous session), the computed shared secret will be used to encrypt the announcement multicast packet. The out-of-band invitation is unencrypted. This DH public key is always sent unencrypted. NaCL Cryptobox is used for encryption.
- 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.
+21 -165
View File
@@ -1,174 +1,30 @@
#!/usr/bin/env sh
set -eu
# Common
# ------
path_conf_var="/var/opt"
path_conf_smp="$path_conf_var/simplex"
path_conf_xftp="$path_conf_var/simplex-xftp"
path_conf_storelog_smp="$path_conf_smp/smp-server-store.log"
path_conf_storelog_xftp="$path_conf_xftp/file-server-store.log"
date="$(date -u '+%Y-%m-%dT%H:%M:%S')"
GRN='\033[0;32m'
YLW='\033[1;33m'
BLU='\033[1;34m'
RED='\033[0;31m'
NC='\033[0m'
path_conf_var="/var/opt"
smp_variables() {
path_conf_smp="$path_conf_var/simplex"
path_conf_smp_archive="$path_conf_smp/backups"
path_conf_smp_archive_storelog="$path_conf_smp_archive/queues"
path_conf_smp_archive_stats="$path_conf_smp_archive/stats"
path_conf_smp_archive_messages="$path_conf_smp_archive/messages"
path_conf_storelog_smp="$path_conf_smp/smp-server-store.log"
path_conf_storelog_smp_out="$path_conf_smp_archive_storelog/smp-server-store.log.${date:-date-failed}"
path_conf_stats_smp="$path_conf_smp/smp-server-stats.log"
path_conf_stats_smp_out="$path_conf_smp_archive_stats/smp-server-stats.log.${date:-date-failed}"
path_conf_messages_smp="$path_conf_smp/smp-server-messages.log"
path_conf_messages_smp_out="$path_conf_smp_archive_messages/smp-server-messages.log.${date:-date-failed}"
backup_smp() {
if [ -e "$path_conf_storelog_smp" ]; then
cp "$path_conf_storelog_smp" "${path_conf_storelog_smp}.${date:-date-failed}"
fi
}
xftp_variables() {
path_conf_xftp="$path_conf_var/simplex-xftp"
path_conf_xftp_archive="$path_conf_xftp/backups"
path_conf_xftp_archive_storelog="$path_conf_xftp_archive/queues"
path_conf_xftp_archive_stats="$path_conf_xftp_archive/stats"
path_conf_storelog_xftp="$path_conf_xftp/file-server-store.log"
path_conf_storelog_xftp_out="$path_conf_xftp_archive_storelog/file-server-store.log.${date:-date-failed}"
path_conf_stats_xftp="$path_conf_xftp/file-server-stats.log"
path_conf_stats_xftp_out="$path_conf_xftp_archive_stats/file-server-stats.log.${date:-date-failed}"
backup_xftp() {
if [ -e "$path_conf_storelog_xftp" ]; then
cp "$path_conf_storelog_xftp" "${path_conf_storelog_xftp}.${date:-date-failed}"
fi
}
checks() {
result=${SERVICE_RESULT:-exit-code}
status=${EXIT_STATUS:-TERM}
case "$result" in
success)
case "$status" in
TERM)
printf "${RED}Refusing to backup files with failed service state${NC}\n"
exit 1
;;
*)
:
;;
esac
;;
*)
printf "${RED}Refusing to backup files with failed service state${NC}\n"
exit 1
;;
esac
}
smp_check() {
if [ ! -d "$path_conf_smp_archive_storelog" ]; then
mkdir -p "$path_conf_smp_archive_storelog"
fi
if [ ! -d "$path_conf_smp_archive_messages" ]; then
mkdir -p "$path_conf_smp_archive_messages"
fi
if [ ! -d "$path_conf_smp_archive_stats" ]; then
mkdir -p "$path_conf_smp_archive_stats"
fi
}
xftp_check() {
if [ ! -d "$path_conf_xftp_archive_storelog" ]; then
mkdir -p "$path_conf_xftp_archive_storelog"
fi
if [ ! -d "$path_conf_xftp_archive_stats" ]; then
mkdir -p "$path_conf_xftp_archive_stats"
fi
}
backup() {
file="$1"
out="$2"
file_type="$3"
if [ -e "$file" ]; then
if cp "$file" "$out"; then
printf "${YLW}${file_type}${NC} ${GRN}backup successful:${NC} ${BLU}%s${NC}\n" "${out}"
else
printf "${YLW}${file_type}${NC} ${RED}backup failed!${NC}\n"
fi
fi
unset file out file_type
}
cleanup() {
directory="$1"
file_type="$2"
files_date=$(find "$directory" -type f -exec stat --format="%y" {} + | awk '{print $1}' | sort -nr | uniq | awk 'NR==2')
if [ -n "$files_date" ]; then
files=$(find "$directory" -type f -not -newermt "$files_date" -printf "%T@ %Tc %p\n" | sort -n | awk '{print $NF}')
if [ -n "$files" ]; then
printf '%s' "$files" | xargs rm -f
printf "${YLW}Old ${file_type} files${NC}${GRN} has been deleted:${NC}\n"
files_colored=$(printf '%s' "$files" | awk '{print "\033[1;34m"$0"\033[0m"}')
printf "${files_colored}\n"
fi
fi
unset directory file_type files_date files
}
smp_backup() {
backup "$path_conf_storelog_smp" "$path_conf_storelog_smp_out" 'Storelog'
backup "$path_conf_messages_smp" "$path_conf_messages_smp_out" 'Messages'
backup "$path_conf_stats_smp" "$path_conf_stats_smp_out" 'Stats'
}
smp_cleanup() {
cleanup "$path_conf_smp_archive_storelog" 'storelog'
cleanup "$path_conf_smp_archive_stats" 'stats'
cleanup "$path_conf_smp_archive_messages" 'messages'
}
xftp_backup() {
backup "$path_conf_storelog_xftp" "$path_conf_storelog_xftp_out" 'Storelog'
backup "$path_conf_stats_xftp" "$path_conf_stats_xftp_out" 'Stats'
}
xftp_cleanup() {
cleanup "$path_conf_xftp_archive_storelog" 'storelog'
cleanup "$path_conf_xftp_archive_stats" 'stats'
}
main() {
type="${1:-}"
checks
case "$type" in
smp-server)
smp_variables
smp_check
smp_backup
smp_cleanup
;;
xftp-server)
xftp_variables
xftp_check
xftp_backup
xftp_cleanup
;;
*)
printf "${YLW}Unknown server type.${NC}\n"
exit 1
;;
esac
}
main "$@"
if [ "$1" = 'smp-server' ]; then
backup_smp
elif [ "$1" = 'xftp-server' ]; then
backup_xftp
else
backup_smp
backup_xftp
fi
+4 -8
View File
@@ -13,15 +13,11 @@ fi
printf "${RED}This action will permanently remove all configs, directories, binaries from Installation Script. Please backup any relevant configs if they are needed.${NC}\n\nPress ${GRN}ENTER${NC} to continue or ${RED}Ctrl+C${NC} to cancel installation"
read ans
systemctl disable --now smp-server 2>/dev/null || true
systemctl revert smp-server 2>/dev/null || true
systemctl disable --now xftp-server 2>/dev/null || true
systemctl revert xftp-server 2>/dev/null || true
systemctl daemon-reload 2>/dev/null || true
systemctl stop smp-server
systemctl stop xftp-server
rm -rf /var/opt/simplex /etc/opt/simplex /etc/opt/simplex-info /var/opt/simplex-xftp /etc/opt/simplex-xftp /srv/xftp /etc/systemd/system/smp-server.service /etc/systemd/system/xftp-server.service /usr/local/bin/smp-server /usr/local/bin/xftp-server /usr/local/bin/simplex-servers-update /usr/local/bin/simplex-servers-uninstall /usr/local/bin/simplex-servers-stopscript
rm -rf /var/opt/simplex /etc/opt/simplex /var/opt/simplex-xftp /etc/opt/simplex-xftp /srv/xftp /etc/systemd/system/smp-server.service /etc/systemd/system/xftp-server.service /usr/local/bin/smp-server /usr/local/bin/xftp-server /usr/local/bin/simplex-servers-update /usr/local/bin/simplex-servers-uninstall
userdel smp 2>/dev/null || true
userdel xftp 2>/dev/null || true
userdel smp && userdel xftp
printf "Uninstallation is complete! Thanks for trying out SimpleX!\n"
+54 -119
View File
@@ -2,6 +2,10 @@
set -eu
# Links to scripts/configs
bin="https://github.com/simplex-chat/simplexmq/releases/latest/download"
bin_smp="$bin/smp-server-ubuntu-20_04-x86-64"
bin_xftp="$bin/xftp-server-ubuntu-20_04-x86-64"
scripts="https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/scripts/main"
scripts_systemd_smp="$scripts/smp-server.service"
scripts_systemd_xftp="$scripts/xftp-server.service"
@@ -29,9 +33,6 @@ path_tmp_bin_stopscript="$path_tmp_bin/simplex-servers-stopscript"
path_tmp_systemd_smp="$path_tmp_bin/smp-server.service"
path_tmp_systemd_xftp="$path_tmp_bin/xftp-server.service"
path_conf_etc='/etc/opt'
path_conf_info='/etc/opt/simplex-info'
GRN='\033[0;32m'
BLU='\033[1;36m'
YLW='\033[1;33m'
@@ -39,55 +40,8 @@ RED='\033[0;31m'
NC='\033[0m'
# Currently, XFTP default to v0.1.0, so it doesn't make sense to check its version
os_test() {
. /etc/os-release
case "$VERSION_ID" in
20.04|22.04) : ;;
24.04) VERSION_ID='22.04' ;;
*) printf "${RED}Unsupported Ubuntu version!${NC}\nPlease file Github issue with request to support Ubuntu %s: https://github.com/simplex-chat/simplexmq/issues/new\n" "$VERSION_ID" && exit 1 ;;
esac
version="$(printf '%s' "$VERSION_ID" | tr '.' '_')"
arch="$(uname -p)"
case "$arch" in
x86_64) arch="$(printf '%s' "$arch" | tr '_' '-')" ;;
*) printf "${RED}Unsupported architecture!${NC}\nPlease file Github issue with request to support %s architecture: https://github.com/simplex-chat/simplexmq/issues/new" "$arch" && exit 1 ;;
esac
bin_smp="$bin/smp-server-ubuntu-${version}-${arch}"
bin_xftp="$bin/xftp-server-ubuntu-${version}-${arch}"
}
installed_test() {
set +u
for i in $path_conf_etc/*; do
if [ -d "$i" ]; then
case "$i" in
*simplex) apps="smp $apps" ;;
*simplex-xftp) apps="xftp $apps" ;;
esac
fi
done
set -u
}
set_version() {
ver="${VER:-latest}"
case "$ver" in
latest)
bin="https://github.com/simplex-chat/simplexmq/releases/latest/download"
remote_version="$(curl --proto '=https' --tlsv1.2 -sSf -L https://api.github.com/repos/simplex-chat/simplexmq/releases/latest | grep -i "tag_name" | awk -F \" '{print $4}')"
;;
*)
bin="https://github.com/simplex-chat/simplexmq/releases/download/${ver}"
remote_version="${ver}"
;;
esac
}
local_version="$($path_bin_smp -v | awk '{print $3}')"
remote_version="$(curl --proto '=https' --tlsv1.2 -sSf -L https://api.github.com/repos/simplex-chat/simplexmq/releases/latest | grep -i "tag_name" | awk -F \" '{print $4}')"
update_scripts() {
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_update" -o "$path_tmp_bin_update" && chmod +x "$path_tmp_bin_update"
@@ -102,7 +56,6 @@ update_scripts() {
mv "$path_tmp_bin_uninstall" "$path_bin_uninstall"
printf "${GRN}Done!${NC}\n"
fi
if diff -q "$path_bin_stopscript" "$path_tmp_bin_stopscript" > /dev/null 2>&1; then
printf -- "- ${YLW}Stopscript script is up-to-date${NC}.\n"
rm "$path_tmp_bin_stopscript"
@@ -111,7 +64,6 @@ update_scripts() {
mv "$path_tmp_bin_stopscript" "$path_bin_stopscript"
printf "${GRN}Done!${NC}\n"
fi
if diff -q "$path_bin_update" "$path_tmp_bin_update" > /dev/null 2>&1; then
printf -- "- ${YLW}Update script is up-to-date${NC}.\n"
rm "$path_tmp_bin_update"
@@ -119,82 +71,75 @@ update_scripts() {
printf -- "- Updating update script..."
mv "$path_tmp_bin_update" "$path_bin_update"
printf "${GRN}Done!${NC}\n"
printf -- "- Re-executing Update script with latest updates..."
printf "Re-executing Update script with latest updates..."
exec sh "$path_bin_update" "continue"
fi
}
update_systemd() {
service="${1}-server"
eval "scripts_systemd=\$scripts_systemd_${1}"
eval "path_systemd=\$path_systemd_${1}"
eval "path_tmp_systemd=\$path_tmp_systemd_${1}"
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd" -o "$path_tmp_systemd"
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd_smp" -o "$path_tmp_systemd_smp"
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd_xftp" -o "$path_tmp_systemd_xftp"
if diff -q "$path_systemd" "$path_tmp_systemd" > /dev/null 2>&1; then
printf -- "- ${YLW}%s service is up-to-date${NC}.\n" "$service"
rm "$path_tmp_systemd"
if diff -q "$path_systemd_smp" "$path_tmp_systemd_smp" > /dev/null 2>&1; then
printf -- "- ${YLW}smp-server service is up-to-date${NC}.\n"
rm "$path_tmp_systemd_smp"
else
printf -- "- Updating %s service..." "$service"
mv "$path_tmp_systemd" "$path_systemd"
printf -- "- Updating smp-server service..."
mv "$path_tmp_systemd_smp" "$path_systemd_smp"
systemctl daemon-reload
printf "${GRN}Done!${NC}\n"
fi
if diff -q "$path_systemd_xftp" "$path_tmp_systemd_xftp" > /dev/null 2>&1; then
printf -- "- ${YLW}xftp-server service is up-to-date${NC}.\n"
rm "$path_tmp_systemd_xftp"
else
printf -- "- Updating xftp-server service..."
mv "$path_tmp_systemd_xftp" "$path_systemd_xftp"
systemctl daemon-reload
printf "${GRN}Done!${NC}\n"
fi
unset service scripts_systemd path_systemd path_tmp_systemd
}
update_bins() {
service="${1}-server"
eval "bin=\$bin_${1}"
eval "path_bin=\$path_bin_${1}"
set_ver() {
local_version='unset'
sed -i -- "s/local_version_${1}=.*/local_version_${1}='${remote_version}'/" "$path_conf_info/release"
}
if [ -f "$path_conf_info/release" ]; then
. "$path_conf_info/release" 2>/dev/null
set +u
eval "local_version=\$local_version_${1}"
set -u
if [ -z "${local_version}" ]; then
set_ver "$1"
fi
else
printf 'local_version_xftp=\nlocal_version_smp=\n' > "$path_conf_info/release"
set_ver "$1"
fi
if [ "$local_version" != "$remote_version" ]; then
if systemctl is-active --quiet "$service"; then
printf -- "- Stopping %s service..." "$service"
systemctl stop "$service"
if systemctl is-active --quiet smp-server; then
printf -- "- Stopping smp-server service..."
systemctl stop smp-server
printf "${GRN}Done!${NC}\n"
printf -- "- Updating %s to %s..." "$service" "$remote_version"
curl --proto '=https' --tlsv1.2 -sSf -L "$bin" -o "$path_bin" && chmod +x "$path_bin"
printf -- "- Updating smp-server bin to %s..." "$remote_version"
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_smp" -o "$path_bin_smp" && chmod +x "$path_bin_smp"
printf "${GRN}Done!${NC}\n"
printf -- "- Starting %s service..." "$service"
systemctl start "$service"
printf -- "- Starting smp-server service..."
systemctl start smp-server
printf "${GRN}Done!${NC}\n"
else
printf -- "- Updating %s to %s..." "$service" "$remote_version"
curl --proto '=https' --tlsv1.2 -sSf -L "$bin" -o "$path_bin" && chmod +x "$path_bin"
printf -- "- Updating smp-server bin..."
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_smp" -o "$path_bin_smp" && chmod +x "$path_bin_smp"
printf "${GRN}Done!${NC}\n"
fi
if systemctl is-active --quiet xftp-server; then
printf -- "- Stopping xftp-server service..."
systemctl stop xftp-server
printf "${GRN}Done!${NC}\n"
printf -- "- Updating xftp-server bin to %s..." "$remote_version"
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_xftp" -o "$path_bin_xftp" && chmod +x "$path_bin_xftp"
printf "${GRN}Done!${NC}\n"
printf -- "- Starting xftp-server service..."
systemctl start xftp-server
printf "${GRN}Done!${NC}\n"
else
printf -- "- Updating xftp-server bin..."
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_smp" -o "$path_bin_xftp" && chmod +x "$path_bin_xftp"
printf "${GRN}Done!${NC}\n"
fi
else
printf -- "- ${YLW}%s is up-to-date${NC}.\n" "$service"
printf -- "- ${YLW}smp-server and xftp-server binaries is up-to-date${NC}.\n"
fi
set_ver "$1"
unset service bin path_bin local_version
}
checks() {
@@ -202,12 +147,6 @@ checks() {
printf "This script is intended to be run with root privileges. Please re-run script using sudo.\n"
exit 1
fi
set_version
os_test
installed_test
mkdir -p $path_conf_info
}
main() {
@@ -224,14 +163,10 @@ main() {
fi
printf "Updating systemd services...\n"
for i in $apps; do
update_systemd "$i"
done
update_systemd
printf "Updating simplex servers...\n"
for i in $apps; do
update_bins "$i"
done
printf "Updating simplex server binaries...\n"
update_bins
rm -rf "$path_tmp_bin"
}
-2
View File
@@ -9,9 +9,7 @@ ExecStart=/usr/local/bin/smp-server start +RTS -N -RTS
ExecStopPost=/usr/local/bin/simplex-servers-stopscript smp-server
LimitNOFILE=65535
KillSignal=SIGINT
TimeoutStartSec=infinity
TimeoutStopSec=infinity
AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
-1
View File
@@ -9,7 +9,6 @@ ExecStart=/usr/local/bin/xftp-server start +RTS -N -RTS
ExecStopPost=/usr/local/bin/simplex-servers-stopscript xftp-server
LimitNOFILE=65535
KillSignal=SIGINT
TimeoutStartSec=infinity
TimeoutStopSec=infinity
AmbientCapabilities=CAP_NET_BIND_SERVICE
+9 -32
View File
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplexmq
version: 6.1.1.0
version: 6.0.0.1
synopsis: SimpleXMQ message broker
description: This package includes <./docs/Simplex-Messaging-Server.html server>,
<./docs/Simplex-Messaging-Client.html client> and
@@ -135,7 +135,6 @@ library
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240624_snd_secure
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240702_servers_stats
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240930_ntf_tokens_to_delete
Simplex.Messaging.Agent.TRcvQueues
Simplex.Messaging.Client
Simplex.Messaging.Client.Agent
@@ -154,7 +153,6 @@ library
Simplex.Messaging.Notifications.Client
Simplex.Messaging.Notifications.Protocol
Simplex.Messaging.Notifications.Server
Simplex.Messaging.Notifications.Server.Control
Simplex.Messaging.Notifications.Server.Env
Simplex.Messaging.Notifications.Server.Main
Simplex.Messaging.Notifications.Server.Push.APNS
@@ -175,7 +173,6 @@ library
Simplex.Messaging.Server.Main
Simplex.Messaging.Server.MsgStore
Simplex.Messaging.Server.MsgStore.STM
Simplex.Messaging.Server.NtfStore
Simplex.Messaging.Server.QueueStore
Simplex.Messaging.Server.QueueStore.QueueInfo
Simplex.Messaging.Server.QueueStore.STM
@@ -239,7 +236,6 @@ library
, direct-sqlcipher ==2.3.*
, directory ==1.3.*
, filepath ==1.4.*
, hashable ==1.4.*
, hourglass ==0.2.*
, http-types ==0.12.*
, http2 >=4.2.2 && <4.3
@@ -262,7 +258,7 @@ library
, temporary ==1.3.*
, time ==1.12.*
, time-manager ==0.0.*
, tls >=1.9.0 && <1.10
, tls >=1.7.0 && <1.8
, transformers ==0.6.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
@@ -314,7 +310,6 @@ executable ntf-server
, direct-sqlcipher ==2.3.*
, directory ==1.3.*
, filepath ==1.4.*
, hashable ==1.4.*
, hourglass ==0.2.*
, http-types ==0.12.*
, http2 >=4.2.2 && <4.3
@@ -338,7 +333,7 @@ executable ntf-server
, temporary ==1.3.*
, time ==1.12.*
, time-manager ==0.0.*
, tls >=1.9.0 && <1.10
, tls >=1.7.0 && <1.8
, transformers ==0.6.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
@@ -394,7 +389,6 @@ executable smp-server
, directory ==1.3.*
, file-embed
, filepath ==1.4.*
, hashable ==1.4.*
, hourglass ==0.2.*
, http-types ==0.12.*
, http2 >=4.2.2 && <4.3
@@ -418,14 +412,13 @@ executable smp-server
, temporary ==1.3.*
, time ==1.12.*
, time-manager ==0.0.*
, tls >=1.9.0 && <1.10
, tls >=1.7.0 && <1.8
, transformers ==0.6.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, wai
, wai-app-static
, warp ==3.3.30
, warp-tls ==3.4.7
, warp
, warp-tls
, websockets ==0.12.*
, yaml ==0.11.*
, zstd ==0.1.3.*
@@ -474,7 +467,6 @@ executable xftp
, direct-sqlcipher ==2.3.*
, directory ==1.3.*
, filepath ==1.4.*
, hashable ==1.4.*
, hourglass ==0.2.*
, http-types ==0.12.*
, http2 >=4.2.2 && <4.3
@@ -498,7 +490,7 @@ executable xftp
, temporary ==1.3.*
, time ==1.12.*
, time-manager ==0.0.*
, tls >=1.9.0 && <1.10
, tls >=1.7.0 && <1.8
, transformers ==0.6.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
@@ -550,7 +542,6 @@ executable xftp-server
, direct-sqlcipher ==2.3.*
, directory ==1.3.*
, filepath ==1.4.*
, hashable ==1.4.*
, hourglass ==0.2.*
, http-types ==0.12.*
, http2 >=4.2.2 && <4.3
@@ -574,7 +565,7 @@ executable xftp-server
, temporary ==1.3.*
, time ==1.12.*
, time-manager ==0.0.*
, tls >=1.9.0 && <1.10
, tls >=1.7.0 && <1.8
, transformers ==0.6.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
@@ -614,8 +605,6 @@ test-suite simplexmq-test
CoreTests.CryptoTests
CoreTests.EncodingTests
CoreTests.RetryIntervalTests
CoreTests.SOCKSSettings
CoreTests.StoreLogTests
CoreTests.TRcvQueuesTests
CoreTests.UtilTests
CoreTests.VersionRangeTests
@@ -632,12 +621,9 @@ test-suite simplexmq-test
XFTPCLI
XFTPClient
XFTPServerTests
Static
Static.Embedded
Paths_simplexmq
hs-source-dirs:
tests
apps/smp-server/web
default-extensions:
StrictData
ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2 -threaded -rtsopts -with-rtsopts=-A64M -with-rtsopts=-N1
@@ -665,15 +651,11 @@ test-suite simplexmq-test
, deepseq ==1.4.*
, direct-sqlcipher ==2.3.*
, directory ==1.3.*
, file-embed
, filepath ==1.4.*
, generic-random ==1.5.*
, hashable ==1.4.*
, hourglass ==0.2.*
, hspec ==2.11.*
, hspec-core ==2.11.*
, http-client
, http-client-tls
, http-types ==0.12.*
, http2 >=4.2.2 && <4.3
, ini ==0.4.1
@@ -699,15 +681,10 @@ test-suite simplexmq-test
, time ==1.12.*
, time-manager ==0.0.*
, timeit ==2.0.*
, tls >=1.9.0 && <1.10
, tls >=1.7.0 && <1.8
, transformers ==0.6.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, unordered-containers
, wai
, wai-app-static
, warp
, warp-tls
, websockets ==0.12.*
, yaml ==0.11.*
, zstd ==0.1.3.*
+34 -48
View File
@@ -12,7 +12,6 @@
module Simplex.FileTransfer.Agent
( startXFTPWorkers,
startXFTPSndWorkers,
closeXFTPAgent,
toFSFilePath,
-- Receiving files
@@ -43,9 +42,9 @@ import Data.Either (partitionEithers, rights)
import Data.Int (Int64)
import Data.List (foldl', partition, sortOn)
import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map)
import Data.Map (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Maybe (mapMaybe)
import qualified Data.Set as S
import Data.Text (Text)
import Data.Time.Clock (getCurrentTime)
@@ -74,7 +73,7 @@ import qualified Simplex.Messaging.Crypto.File as CF
import qualified Simplex.Messaging.Crypto.Lazy as LC
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String (strDecode, strEncode)
import Simplex.Messaging.Protocol (ProtocolServer, ProtocolType (..), XFTPServer)
import Simplex.Messaging.Protocol (EntityId, ProtocolServer, ProtocolType (..), XFTPServer)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Util (catchAll_, liftError, tshow, unlessM, whenM)
import System.FilePath (takeFileName, (</>))
@@ -83,21 +82,13 @@ import UnliftIO.Directory
import qualified UnliftIO.Exception as E
startXFTPWorkers :: AgentClient -> Maybe FilePath -> AM ()
startXFTPWorkers = startXFTPWorkers_ True
{-# INLINE startXFTPWorkers #-}
startXFTPSndWorkers :: AgentClient -> Maybe FilePath -> AM ()
startXFTPSndWorkers = startXFTPWorkers_ False
{-# INLINE startXFTPSndWorkers #-}
startXFTPWorkers_ :: Bool -> AgentClient -> Maybe FilePath -> AM ()
startXFTPWorkers_ allWorkers c workDir = do
startXFTPWorkers c workDir = do
wd <- asks $ xftpWorkDir . xftpAgent
atomically $ writeTVar wd workDir
cfg <- asks config
when allWorkers $ startRcvFiles cfg
startRcvFiles cfg
startSndFiles cfg
when allWorkers $ startDelFiles cfg
startDelFiles cfg
where
startRcvFiles :: AgentConfig -> AM ()
startRcvFiles AgentConfig {rcvFilesTTL} = do
@@ -184,18 +175,16 @@ runXFTPRcvWorker c srv Worker {doWork} = do
cfg <- asks config
forever $ do
lift $ waitForWork doWork
liftIO $ assertAgentForeground c
atomically $ assertAgentForeground c
runXFTPOperation cfg
where
runXFTPOperation :: AgentConfig -> AM ()
runXFTPOperation AgentConfig {rcvFilesTTL, reconnectInterval = ri, xftpConsecutiveRetries} =
withWork c doWork (\db -> getNextRcvChunkToDownload db srv rcvFilesTTL) $ \case
(RcvFileChunk {rcvFileId, rcvFileEntityId, fileTmpPath, replicas = []}, _, redirectEntityId_) ->
rcvWorkerInternalError c rcvFileId rcvFileEntityId redirectEntityId_ (Just fileTmpPath) (INTERNAL "chunk has no replicas")
(fc@RcvFileChunk {userId, rcvFileId, rcvFileEntityId, digest, fileTmpPath, replicas = replica@RcvFileChunkReplica {rcvChunkReplicaId, server, delay} : _}, approvedRelays, redirectEntityId_) -> do
(RcvFileChunk {rcvFileId, rcvFileEntityId, fileTmpPath, replicas = []}, _) -> rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) (INTERNAL "chunk has no replicas")
(fc@RcvFileChunk {userId, rcvFileId, rcvFileEntityId, digest, fileTmpPath, replicas = replica@RcvFileChunkReplica {rcvChunkReplicaId, server, delay} : _}, approvedRelays) -> do
let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay
withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do
liftIO $ waitWhileSuspended c
liftIO $ waitForUserNetwork c
atomically $ incXFTPServerStat c userId srv downloadAttempts
downloadFileChunk fc replica approvedRelays
@@ -203,16 +192,16 @@ runXFTPRcvWorker c srv Worker {doWork} = do
where
retryLoop loop e replicaDelay = do
flip catchAgentError (\_ -> pure ()) $ do
when (serverHostError e) $ notify c (fromMaybe rcvFileEntityId redirectEntityId_) (RFWARN e)
when (serverHostError e) $ notify c rcvFileEntityId $ RFWARN e
liftIO $ closeXFTPServerClient c userId server digest
withStore' c $ \db -> updateRcvChunkReplicaDelay db rcvChunkReplicaId replicaDelay
liftIO $ assertAgentForeground c
atomically $ assertAgentForeground c
loop
retryDone e = do
atomically . incXFTPServerStat c userId srv $ case e of
XFTP _ XFTP.AUTH -> downloadAuthErrs
_ -> downloadErrs
rcvWorkerInternalError c rcvFileId rcvFileEntityId redirectEntityId_ (Just fileTmpPath) e
rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) e
downloadFileChunk :: RcvFileChunk -> RcvFileChunkReplica -> Bool -> AM ()
downloadFileChunk RcvFileChunk {userId, rcvFileId, rcvFileEntityId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath} replica approvedRelays = do
unlessM ((approvedRelays ||) <$> ipAddressProtected') $ throwE $ FILE NOT_APPROVED
@@ -222,7 +211,7 @@ runXFTPRcvWorker c srv Worker {doWork} = do
chunkSpec = XFTPRcvChunkSpec chunkPath chSize (unFileDigest digest)
relChunkPath = fileTmpPath </> takeFileName chunkPath
agentXFTPDownloadChunk c userId digest replica chunkSpec
liftIO $ waitUntilForeground c
atomically $ waitUntilForeground c
(entityId, complete, progress) <- withStore c $ \db -> runExceptT $ do
liftIO $ updateRcvFileChunkReceived db (rcvChunkReplicaId replica) rcvChunkId relChunkPath
RcvFile {size = FileSize currentSize, chunks, redirect} <- ExceptT $ getRcvFile db rcvFileId
@@ -241,7 +230,7 @@ runXFTPRcvWorker c srv Worker {doWork} = do
where
ipAddressProtected' :: AM Bool
ipAddressProtected' = do
cfg <- liftIO $ getFastNetworkConfig c
cfg <- liftIO $ getNetworkConfig' c
pure $ ipAddressProtected cfg srv
receivedSize :: [RcvFileChunk] -> Int64
receivedSize = foldl' (\sz ch -> sz + receivedChunkSize ch) 0
@@ -263,25 +252,25 @@ retryOnError name loop done e = do
then loop
else done
rcvWorkerInternalError :: AgentClient -> DBRcvFileId -> RcvFileId -> Maybe RcvFileId -> Maybe FilePath -> AgentErrorType -> AM ()
rcvWorkerInternalError c rcvFileId rcvFileEntityId redirectEntityId_ tmpPath err = do
rcvWorkerInternalError :: AgentClient -> DBRcvFileId -> RcvFileId -> Maybe FilePath -> AgentErrorType -> AM ()
rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath err = do
lift $ forM_ tmpPath (removePath <=< toFSFilePath)
withStore' c $ \db -> updateRcvFileError db rcvFileId (show err)
notify c (fromMaybe rcvFileEntityId redirectEntityId_) (RFERR err)
notify c rcvFileEntityId $ RFERR err
runXFTPRcvLocalWorker :: AgentClient -> Worker -> AM ()
runXFTPRcvLocalWorker c Worker {doWork} = do
cfg <- asks config
forever $ do
lift $ waitForWork doWork
liftIO $ assertAgentForeground c
atomically $ assertAgentForeground c
runXFTPOperation cfg
where
runXFTPOperation :: AgentConfig -> AM ()
runXFTPOperation AgentConfig {rcvFilesTTL} =
withWork c doWork (`getNextRcvFileToDecrypt` rcvFilesTTL) $
\f@RcvFile {rcvFileId, rcvFileEntityId, tmpPath, redirect} ->
decryptFile f `catchAgentError` rcvWorkerInternalError c rcvFileId rcvFileEntityId (redirectEntityId <$> redirect) tmpPath
\f@RcvFile {rcvFileId, rcvFileEntityId, tmpPath} ->
decryptFile f `catchAgentError` rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath
decryptFile :: RcvFile -> AM ()
decryptFile RcvFile {rcvFileId, rcvFileEntityId, size, digest, key, nonce, tmpPath, saveFile, status, chunks, redirect} = do
let CryptoFile savePath cfArgs = saveFile
@@ -300,12 +289,12 @@ runXFTPRcvLocalWorker c Worker {doWork} = do
Nothing -> do
notify c rcvFileEntityId $ RFDONE fsSavePath
lift $ forM_ tmpPath (removePath <=< toFSFilePath)
liftIO $ waitUntilForeground c
atomically $ waitUntilForeground c
withStore' c (`updateRcvFileComplete` rcvFileId)
Just RcvFileRedirect {redirectFileInfo, redirectDbId} -> do
let RedirectFileInfo {size = redirectSize, digest = redirectDigest} = redirectFileInfo
lift $ forM_ tmpPath (removePath <=< toFSFilePath)
liftIO $ waitUntilForeground c
atomically $ waitUntilForeground c
withStore' c (`updateRcvFileComplete` rcvFileId)
-- proceed with redirect
yaml <- liftError (FILE . FILE_IO . show) (CF.readFile $ CryptoFile fsSavePath cfArgs) `agentFinally` (lift $ toFSFilePath fsSavePath >>= removePath)
@@ -347,7 +336,7 @@ xftpDeleteRcvFiles' c rcvFileEntityIds = do
batchFiles :: (DB.Connection -> DBRcvFileId -> IO a) -> [RcvFile] -> AM' [Either AgentErrorType a]
batchFiles f rcvFiles = withStoreBatch' c $ \db -> map (\RcvFile {rcvFileId} -> f db rcvFileId) rcvFiles
notify :: forall m e. (MonadIO m, AEntityI e) => AgentClient -> AEntityId -> AEvent e -> m ()
notify :: forall m e. (MonadIO m, AEntityI e) => AgentClient -> EntityId -> AEvent e -> m ()
notify c entId cmd = atomically $ writeTBQueue (subQ c) ("", entId, AEvt (sAEntity @e) cmd)
xftpSendFile' :: AgentClient -> UserId -> CryptoFile -> Int -> AM SndFileId
@@ -393,7 +382,7 @@ runXFTPSndPrepareWorker c Worker {doWork} = do
cfg <- asks config
forever $ do
lift $ waitForWork doWork
liftIO $ assertAgentForeground c
atomically $ assertAgentForeground c
runXFTPOperation cfg
where
runXFTPOperation :: AgentConfig -> AM ()
@@ -455,7 +444,7 @@ runXFTPSndPrepareWorker c Worker {doWork} = do
SndFileChunkReplica {server} : _ -> Right server
createChunk :: Int -> SndFileChunk -> AM (ProtocolServer 'PXFTP)
createChunk numRecipients' ch = do
liftIO $ assertAgentForeground c
atomically $ assertAgentForeground c
(replica, ProtoServerWithAuth srv _) <- tryCreate
withStore' c $ \db -> createSndFileReplica db ch replica
pure srv
@@ -463,9 +452,8 @@ runXFTPSndPrepareWorker c Worker {doWork} = do
tryCreate = do
usedSrvs <- newTVarIO ([] :: [XFTPServer])
let AgentClient {xftpServers} = c
userSrvCount <- liftIO $ length <$> TM.lookupIO userId xftpServers
userSrvCount <- length <$> atomically (TM.lookup userId xftpServers)
withRetryIntervalCount (riFast ri) $ \n _ loop -> do
liftIO $ waitWhileSuspended c
liftIO $ waitForUserNetwork c
let triedAllSrvs = n > userSrvCount
createWithNextSrv usedSrvs
@@ -475,7 +463,7 @@ runXFTPSndPrepareWorker c Worker {doWork} = do
retryLoop loop triedAllSrvs e = do
flip catchAgentError (\_ -> pure ()) $ do
when (triedAllSrvs && serverHostError e) $ notify c sndFileEntityId $ SFWARN e
liftIO $ assertAgentForeground c
atomically $ assertAgentForeground c
loop
createWithNextSrv usedSrvs = do
deleted <- withStore' c $ \db -> getSndFileDeleted db sndFileId
@@ -495,7 +483,7 @@ runXFTPSndWorker c srv Worker {doWork} = do
cfg <- asks config
forever $ do
lift $ waitForWork doWork
liftIO $ assertAgentForeground c
atomically $ assertAgentForeground c
runXFTPOperation cfg
where
runXFTPOperation :: AgentConfig -> AM ()
@@ -505,7 +493,6 @@ runXFTPSndWorker c srv Worker {doWork} = do
fc@SndFileChunk {userId, sndFileId, sndFileEntityId, filePrefixPath, digest, replicas = replica@SndFileChunkReplica {sndChunkReplicaId, server, delay} : _} -> do
let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay
withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do
liftIO $ waitWhileSuspended c
liftIO $ waitForUserNetwork c
atomically $ incXFTPServerStat c userId srv uploadAttempts
uploadFileChunk cfg fc replica
@@ -516,7 +503,7 @@ runXFTPSndWorker c srv Worker {doWork} = do
when (serverHostError e) $ notify c sndFileEntityId $ SFWARN e
liftIO $ closeXFTPServerClient c userId server digest
withStore' c $ \db -> updateSndChunkReplicaDelay db sndChunkReplicaId replicaDelay
liftIO $ assertAgentForeground c
atomically $ assertAgentForeground c
loop
retryDone e = do
atomically $ incXFTPServerStat c userId srv uploadErrs
@@ -527,9 +514,9 @@ runXFTPSndWorker c srv Worker {doWork} = do
fsFilePath <- lift $ toFSFilePath filePath
unlessM (doesFileExist fsFilePath) $ throwE $ FILE NO_FILE
let chunkSpec' = chunkSpec {filePath = fsFilePath} :: XFTPChunkSpec
liftIO $ assertAgentForeground c
atomically $ assertAgentForeground c
agentXFTPUploadChunk c userId chunkDigest replica' chunkSpec'
liftIO $ waitUntilForeground c
atomically $ waitUntilForeground c
sf@SndFile {sndFileEntityId, prefixPath, chunks} <- withStore c $ \db -> do
updateSndChunkReplicaStatus db sndChunkReplicaId SFRSUploaded
getSndFile db sndFileId
@@ -667,7 +654,7 @@ runXFTPDelWorker c srv Worker {doWork} = do
cfg <- asks config
forever $ do
lift $ waitForWork doWork
liftIO $ assertAgentForeground c
atomically $ assertAgentForeground c
runXFTPOperation cfg
where
runXFTPOperation :: AgentConfig -> AM ()
@@ -678,7 +665,6 @@ runXFTPDelWorker c srv Worker {doWork} = do
processDeletedReplica replica@DeletedSndChunkReplica {deletedSndChunkReplicaId, userId, server, chunkDigest, delay} = do
let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay
withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do
liftIO $ waitWhileSuspended c
liftIO $ waitForUserNetwork c
atomically $ incXFTPServerStat c userId srv deleteAttempts
deleteChunkReplica
@@ -689,7 +675,7 @@ runXFTPDelWorker c srv Worker {doWork} = do
when (serverHostError e) $ notify c "" $ SFWARN e
liftIO $ closeXFTPServerClient c userId server chunkDigest
withStore' c $ \db -> updateDeletedSndChunkReplicaDelay db deletedSndChunkReplicaId replicaDelay
liftIO $ assertAgentForeground c
atomically $ assertAgentForeground c
loop
retryDone e = do
atomically $ incXFTPServerStat c userId srv deleteErrs
@@ -704,7 +690,7 @@ delWorkerInternalError c deletedSndChunkReplicaId e = do
withStore' c $ \db -> deleteDeletedSndChunkReplica db deletedSndChunkReplicaId
notify c "" $ SFERR e
assertAgentForeground :: AgentClient -> IO ()
assertAgentForeground :: AgentClient -> STM ()
assertAgentForeground c = do
throwWhenInactive c
waitUntilForeground c
+11 -13
View File
@@ -22,7 +22,6 @@ import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Time.Clock (UTCTime)
import Data.Word (Word32)
import qualified Data.X509 as X
import qualified Data.X509.Validation as XV
@@ -37,8 +36,8 @@ import Simplex.Messaging.Client
TransportSession,
chooseTransportHost,
defaultNetworkConfig,
proxyUsername,
transportClientConfig,
clientSocksCredentials,
unexpectedResponse,
)
import qualified Simplex.Messaging.Crypto as C
@@ -51,9 +50,8 @@ import Simplex.Messaging.Protocol
ProtocolServer (..),
RecipientId,
SenderId,
pattern NoEntity,
)
import Simplex.Messaging.Transport (ALPN, HandshakeError (..), THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), defaultSupportedParams)
import Simplex.Messaging.Transport (ALPN, HandshakeError (..), THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), supportedParameters)
import Simplex.Messaging.Transport.Client (TransportClientConfig, TransportHost, alpn)
import Simplex.Messaging.Transport.HTTP2
import Simplex.Messaging.Transport.HTTP2.Client
@@ -99,21 +97,21 @@ defaultXFTPClientConfig =
clientALPN = Just supportedXFTPhandshakes
}
getXFTPClient :: TransportSession FileResponse -> XFTPClientConfig -> UTCTime -> (XFTPClient -> IO ()) -> IO (Either XFTPClientError XFTPClient)
getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, xftpNetworkConfig, serverVRange} proxySessTs disconnected = runExceptT $ do
let socksCreds = clientSocksCredentials xftpNetworkConfig proxySessTs transportSession
getXFTPClient :: TransportSession FileResponse -> XFTPClientConfig -> (XFTPClient -> IO ()) -> IO (Either XFTPClientError XFTPClient)
getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, xftpNetworkConfig, serverVRange} disconnected = runExceptT $ do
let username = proxyUsername transportSession
ProtocolServer _ host port keyHash = srv
useHost <- liftEither $ chooseTransportHost xftpNetworkConfig host
let tcConfig = (transportClientConfig xftpNetworkConfig useHost False) {alpn = clientALPN}
let tcConfig = (transportClientConfig xftpNetworkConfig useHost) {alpn = clientALPN}
http2Config = xftpHTTP2Config tcConfig config
clientVar <- newTVarIO Nothing
let usePort = if null port then "443" else port
clientDisconnected = readTVarIO clientVar >>= mapM_ disconnected
http2Client <- liftError' xftpClientError $ getVerifiedHTTP2Client socksCreds useHost usePort (Just keyHash) Nothing http2Config clientDisconnected
http2Client <- liftError' xftpClientError $ getVerifiedHTTP2Client (Just username) useHost usePort (Just keyHash) Nothing http2Config clientDisconnected
let HTTP2Client {sessionId, sessionALPN} = http2Client
v = VersionXFTP 1
thServerVRange = versionToRange v
thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = v, thServerVRange, thAuth = Nothing, implySessId = False, encryptBlock = Nothing, batch = True}
thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = v, thServerVRange, thAuth = Nothing, implySessId = False, batch = True}
logDebug $ "Client negotiated handshake protocol: " <> tshow sessionALPN
thParams@THandleParams {thVersion} <- case sessionALPN of
Just "xftp/1" -> xftpClientHandshakeV1 serverVRange keyHash http2Client thParams0
@@ -173,7 +171,7 @@ xftpHTTP2Config :: TransportClientConfig -> XFTPClientConfig -> HTTP2ClientConfi
xftpHTTP2Config transportConfig XFTPClientConfig {xftpNetworkConfig = NetworkConfig {tcpConnectTimeout}} =
defaultHTTP2ClientConfig
{ bodyHeadSize = xftpBlockSize,
suportedTLSParams = defaultSupportedParams,
suportedTLSParams = supportedParameters,
connTimeout = tcpConnectTimeout,
transportConfig
}
@@ -224,7 +222,7 @@ createXFTPChunk ::
Maybe BasicAuth ->
ExceptT XFTPClientError IO (SenderId, NonEmpty RecipientId)
createXFTPChunk c spKey file rcps auth_ =
sendXFTPCommand c spKey NoEntity (FNEW file rcps auth_) Nothing >>= \case
sendXFTPCommand c spKey "" (FNEW file rcps auth_) Nothing >>= \case
(FRSndIds sId rIds, body) -> noFile body (sId, rIds)
(r, _) -> throwE $ unexpectedResponse r
@@ -280,7 +278,7 @@ pingXFTP :: XFTPClient -> ExceptT XFTPClientError IO ()
pingXFTP c@XFTPClient {thParams} = do
t <-
liftEither . first PCETransportError $
xftpEncodeTransmission thParams ("", NoEntity, FileCmd SFRecipient PING)
xftpEncodeTransmission thParams ("", "", FileCmd SFRecipient PING)
(r, _) <- sendXFTPTransmission c t Nothing
case r of
FRPong -> pure ()
+5 -8
View File
@@ -16,7 +16,6 @@ import Data.Bifunctor (first)
import qualified Data.ByteString.Char8 as B
import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8)
import Data.Time.Clock (UTCTime, getCurrentTime)
import Simplex.FileTransfer.Client
import Simplex.Messaging.Agent.RetryInterval
import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientError (..), temporaryClientError)
@@ -31,7 +30,6 @@ type XFTPClientVar = TMVar (Either XFTPClientAgentError XFTPClient)
data XFTPClientAgent = XFTPClientAgent
{ xftpClients :: TMap XFTPServer XFTPClientVar,
startedAt :: UTCTime,
config :: XFTPClientAgentConfig
}
@@ -55,23 +53,22 @@ defaultXFTPClientAgentConfig =
data XFTPClientAgentError = XFTPClientAgentError XFTPServer XFTPClientError
deriving (Show, Exception)
newXFTPAgent :: XFTPClientAgentConfig -> IO XFTPClientAgent
newXFTPAgent :: XFTPClientAgentConfig -> STM XFTPClientAgent
newXFTPAgent config = do
xftpClients <- TM.emptyIO
startedAt <- getCurrentTime
pure XFTPClientAgent {xftpClients, startedAt, config}
xftpClients <- TM.empty
pure XFTPClientAgent {xftpClients, config}
type ME a = ExceptT XFTPClientAgentError IO a
getXFTPServerClient :: XFTPClientAgent -> XFTPServer -> ME XFTPClient
getXFTPServerClient XFTPClientAgent {xftpClients, startedAt, config} srv = do
getXFTPServerClient XFTPClientAgent {xftpClients, config} srv = do
atomically getClientVar >>= either newXFTPClient waitForXFTPClient
where
connectClient :: ME XFTPClient
connectClient =
ExceptT $
first (XFTPClientAgentError srv)
<$> getXFTPClient (1, srv, Nothing) (xftpConfig config) startedAt clientDisconnected
<$> getXFTPClient (1, srv, Nothing) (xftpConfig config) clientDisconnected
clientDisconnected :: XFTPClient -> IO ()
clientDisconnected _ = do
+5 -5
View File
@@ -43,8 +43,8 @@ import Data.Int (Int64)
import Data.List (foldl', sortOn)
import Data.List.NonEmpty (NonEmpty (..), nonEmpty)
import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Map (Map)
import qualified Data.Map as M
import Data.Maybe (fromMaybe, listToMaybe)
import qualified Data.Text as T
import Data.Word (Word32)
@@ -313,7 +313,7 @@ cliSendFileOpts SendOptions {filePath, outputDir, numRecipients, xftpServers, re
pure (encPath, fdRcv, fdSnd, chunkSpecs, encSize)
uploadFile :: TVar ChaChaDRG -> [XFTPChunkSpec] -> TVar [Int64] -> Int64 -> ExceptT CLIError IO [SentFileChunk]
uploadFile g chunks uploadedChunks encSize = do
a <- liftIO $ newXFTPAgent defaultXFTPClientAgentConfig
a <- atomically $ newXFTPAgent defaultXFTPClientAgentConfig
gen <- newTVarIO =<< liftIO newStdGen
let xftpSrvs = fromMaybe defaultXFTPServers (nonEmpty xftpServers)
srvs <- liftIO $ replicateM (length chunks) $ getXFTPServer gen xftpSrvs
@@ -429,7 +429,7 @@ cliReceiveFile ReceiveOptions {fileDescription, filePath, retryCount, tempPath,
receive (ValidFileDescription FileDescription {size, digest, key, nonce, chunks}) = do
encPath <- getEncPath tempPath "xftp"
createDirectory encPath
a <- liftIO $ newXFTPAgent defaultXFTPClientAgentConfig
a <- atomically $ newXFTPAgent defaultXFTPClientAgentConfig
liftIO $ printNoNewLine "Downloading file..."
downloadedChunks <- newTVarIO []
let srv FileChunk {replicas} = case replicas of
@@ -494,7 +494,7 @@ cliDeleteFile DeleteOptions {fileDescription, retryCount, yes} = do
where
deleteFile :: ValidFileDescription 'FSender -> ExceptT CLIError IO ()
deleteFile (ValidFileDescription FileDescription {chunks}) = do
a <- liftIO $ newXFTPAgent defaultXFTPClientAgentConfig
a <- atomically $ newXFTPAgent defaultXFTPClientAgentConfig
forM_ chunks $ deleteFileChunk a
liftIO $ do
printNoNewLine "File deleted!"
+11 -6
View File
@@ -1,9 +1,7 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
@@ -54,8 +52,8 @@ import Data.Int (Int64)
import Data.List (foldl', sortOn)
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.Map (Map)
import qualified Data.Map as M
import Data.Maybe (fromMaybe)
import Data.String
import Data.Text (Text)
@@ -141,9 +139,12 @@ data FileChunkReplica = FileChunkReplica
}
deriving (Eq, Show)
newtype ChunkReplicaId = ChunkReplicaId {unChunkReplicaId :: XFTPFileId}
newtype ChunkReplicaId = ChunkReplicaId {unChunkReplicaId :: ByteString}
deriving (Eq, Show)
deriving newtype (StrEncoding)
instance StrEncoding ChunkReplicaId where
strEncode (ChunkReplicaId fid) = strEncode fid
strP = ChunkReplicaId <$> strP
instance FromJSON ChunkReplicaId where
parseJSON = strParseJSON "ChunkReplicaId"
@@ -152,6 +153,10 @@ instance ToJSON ChunkReplicaId where
toJSON = strToJSON
toEncoding = strToJEncoding
instance FromField ChunkReplicaId where fromField f = ChunkReplicaId <$> fromField f
instance ToField ChunkReplicaId where toField (ChunkReplicaId s) = toField s
data YAMLFileDescription = YAMLFileDescription
{ party :: FileParty,
size :: String,
+3 -4
View File
@@ -41,7 +41,6 @@ import Simplex.Messaging.Protocol
ProtocolType (..),
RcvPublicAuthKey,
RcvPublicDhKey,
EntityId (..),
RecipientId,
SenderId,
SentRawTransmission,
@@ -171,7 +170,7 @@ data FileInfo = FileInfo
}
deriving (Show)
type XFTPFileId = EntityId
type XFTPFileId = ByteString
instance FilePartyI p => ProtocolEncoding XFTPVersion XFTPErrorType (FileCommand p) where
type Tag (FileCommand p) = FileCommandTag p
@@ -192,7 +191,7 @@ instance FilePartyI p => ProtocolEncoding XFTPVersion XFTPErrorType (FileCommand
fromProtocolError = fromProtocolError @XFTPVersion @XFTPErrorType @FileResponse
{-# INLINE fromProtocolError #-}
checkCredentials (auth, _, EntityId fileId, _) cmd = case cmd of
checkCredentials (auth, _, fileId, _) cmd = case cmd of
-- FNEW must not have signature and chunk ID
FNEW {}
| isNothing auth -> Left $ CMD NO_AUTH
@@ -302,7 +301,7 @@ instance ProtocolEncoding XFTPVersion XFTPErrorType FileResponse where
PEBlock -> BLOCK
{-# INLINE fromProtocolError #-}
checkCredentials (_, _, EntityId entId, _) cmd = case cmd of
checkCredentials (_, _, entId, _) cmd = case cmd of
FRSndIds {} -> noEntity
-- ERR response does not always have entity ID
FRErr _ -> Right cmd
+54 -55
View File
@@ -9,7 +9,6 @@
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
@@ -33,11 +32,11 @@ import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe, isJust)
import qualified Data.Text as T
import Data.Time.Clock (UTCTime (..), diffTimeToPicoseconds, getCurrentTime)
import Data.Time.Clock.System (SystemTime (..), getSystemTime)
import Data.Time.Format.ISO8601 (iso8601Show)
import Data.Word (Word32)
import qualified Data.X509 as X
import GHC.IO.Handle (hSetNewlineMode)
import GHC.IORef (atomicSwapIORef)
import GHC.Stats (getRTSStats)
import qualified Network.HTTP.Types as N
import qualified Network.HTTP2.Server as H
@@ -53,20 +52,18 @@ import qualified Simplex.Messaging.Crypto as C
import qualified Simplex.Messaging.Crypto.Lazy as LC
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (CorrId (..), EntityId (..), RcvPublicAuthKey, RcvPublicDhKey, RecipientId, TransmissionAuth, pattern NoEntity)
import Simplex.Messaging.Protocol (CorrId (..), RcvPublicAuthKey, RcvPublicDhKey, RecipientId, TransmissionAuth)
import Simplex.Messaging.Server (dummyVerifyCmd, verifyCmdAuthorization)
import Simplex.Messaging.Server.Control (CPClientRole (..))
import Simplex.Messaging.Server.Expiration
import Simplex.Messaging.Server.QueueStore (RoundedSystemTime, getRoundedSystemTime)
import Simplex.Messaging.Server.Stats
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport (ALPN, SessionId, THandleAuth (..), THandleParams (..), TransportPeer (..), defaultSupportedParams)
import Simplex.Messaging.Transport (SessionId, THandleAuth (..), THandleParams (..), TransportPeer (..))
import Simplex.Messaging.Transport.Buffer (trimCR)
import Simplex.Messaging.Transport.HTTP2
import Simplex.Messaging.Transport.HTTP2.File (fileBlockSize)
import Simplex.Messaging.Transport.HTTP2.Server
import Simplex.Messaging.Transport.Server (runLocalTCPServer)
import Simplex.Messaging.Transport.Server (runTCPServer, tlsServerCredentials)
import Simplex.Messaging.Util
import Simplex.Messaging.Version
import System.Exit (exitFailure)
@@ -92,35 +89,36 @@ data XFTPTransportRequest = XFTPTransportRequest
runXFTPServer :: XFTPServerConfig -> IO ()
runXFTPServer cfg = do
started <- newEmptyTMVarIO
runXFTPServerBlocking started cfg $ Just supportedXFTPhandshakes
runXFTPServerBlocking started cfg
runXFTPServerBlocking :: TMVar Bool -> XFTPServerConfig -> Maybe [ALPN] -> IO ()
runXFTPServerBlocking started cfg alpn_ = newXFTPServerEnv cfg >>= runReaderT (xftpServer cfg started alpn_)
runXFTPServerBlocking :: TMVar Bool -> XFTPServerConfig -> IO ()
runXFTPServerBlocking started cfg = newXFTPServerEnv cfg >>= runReaderT (xftpServer cfg started)
data Handshake
= HandshakeSent C.PrivateKeyX25519
| HandshakeAccepted (THandleParams XFTPVersion 'TServer)
xftpServer :: XFTPServerConfig -> TMVar Bool -> Maybe [ALPN] -> M ()
xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration, fileExpiration, xftpServerVRange} started alpn_ = do
xftpServer :: XFTPServerConfig -> TMVar Bool -> M ()
xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration, fileExpiration, xftpServerVRange} started = do
mapM_ (expireServerFiles Nothing) fileExpiration
restoreServerStats
raceAny_ (runServer : expireFilesThread_ cfg <> serverStatsThread_ cfg <> controlPortThread_ cfg) `finally` stopServer
where
runServer :: M ()
runServer = do
srvCreds@(chain, pk) <- asks tlsServerCreds
serverParams <- asks tlsServerParams
let (chain, pk) = tlsServerCredentials serverParams
signKey <- liftIO $ case C.x509ToPrivate (pk, []) >>= C.privKey of
Right pk' -> pure pk'
Left e -> putStrLn ("servers has no valid key: " <> show e) >> exitFailure
env <- ask
sessions <- liftIO TM.emptyIO
sessions <- atomically TM.empty
let cleanup sessionId = atomically $ TM.delete sessionId sessions
liftIO . runHTTP2Server started xftpPort defaultHTTP2BufferSize defaultSupportedParams srvCreds alpn_ transportConfig inactiveClientExpiration cleanup $ \sessionId sessionALPN r sendResponse -> do
liftIO . runHTTP2Server started xftpPort defaultHTTP2BufferSize serverParams transportConfig inactiveClientExpiration cleanup $ \sessionId sessionALPN r sendResponse -> do
reqBody <- getHTTP2Body r xftpBlockSize
let v = VersionXFTP 1
thServerVRange = versionToRange v
thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = v, thServerVRange, thAuth = Nothing, implySessId = False, encryptBlock = Nothing, batch = True}
thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = v, thServerVRange, thAuth = Nothing, implySessId = False, batch = True}
req0 = XFTPTransportRequest {thParams = thParams0, request = r, reqBody, sendResponse}
flip runReaderT env $ case sessionALPN of
Nothing -> processRequest req0
@@ -209,17 +207,17 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
withFile statsFilePath AppendMode $ \h -> liftIO $ do
hSetBuffering h LineBuffering
ts <- getCurrentTime
fromTime' <- atomicSwapIORef fromTime ts
filesCreated' <- atomicSwapIORef filesCreated 0
fileRecipients' <- atomicSwapIORef fileRecipients 0
filesUploaded' <- atomicSwapIORef filesUploaded 0
filesExpired' <- atomicSwapIORef filesExpired 0
filesDeleted' <- atomicSwapIORef filesDeleted 0
files <- liftIO $ periodStatCounts filesDownloaded ts
fileDownloads' <- atomicSwapIORef fileDownloads 0
fileDownloadAcks' <- atomicSwapIORef fileDownloadAcks 0
filesCount' <- readIORef filesCount
filesSize' <- readIORef filesSize
fromTime' <- atomically $ swapTVar fromTime ts
filesCreated' <- atomically $ swapTVar filesCreated 0
fileRecipients' <- atomically $ swapTVar fileRecipients 0
filesUploaded' <- atomically $ swapTVar filesUploaded 0
filesExpired' <- atomically $ swapTVar filesExpired 0
filesDeleted' <- atomically $ swapTVar filesDeleted 0
files <- atomically $ periodStatCounts filesDownloaded ts
fileDownloads' <- atomically $ swapTVar fileDownloads 0
fileDownloadAcks' <- atomically $ swapTVar fileDownloadAcks 0
filesCount' <- readTVarIO filesCount
filesSize' <- readTVarIO filesSize
hPutStrLn h $
intercalate
","
@@ -249,7 +247,7 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
u <- askUnliftIO
liftIO $ do
labelMyThread "control port server"
runLocalTCPServer cpStarted port $ runCPClient u
runTCPServer cpStarted port $ runCPClient u
where
runCPClient :: UnliftIO (ReaderT XFTPEnv IO) -> Socket -> IO ()
runCPClient u sock = do
@@ -311,7 +309,7 @@ data ServerFile = ServerFile
processRequest :: XFTPTransportRequest -> M ()
processRequest XFTPTransportRequest {thParams, reqBody = body@HTTP2Body {bodyHead}, sendResponse}
| B.length bodyHead /= xftpBlockSize = sendXFTPResponse ("", NoEntity, FRErr BLOCK) Nothing
| B.length bodyHead /= xftpBlockSize = sendXFTPResponse ("", "", FRErr BLOCK) Nothing
| otherwise = do
case xftpDecodeTransmission thParams bodyHead of
Right (sig_, signed, (corrId, fId, cmdOrErr)) ->
@@ -324,7 +322,7 @@ processRequest XFTPTransportRequest {thParams, reqBody = body@HTTP2Body {bodyHea
Left e -> send (FRErr e) Nothing
where
send resp = sendXFTPResponse (corrId, fId, resp)
Left e -> sendXFTPResponse ("", NoEntity, FRErr e) Nothing
Left e -> sendXFTPResponse ("", "", FRErr e) Nothing
where
sendXFTPResponse (corrId, fId, resp) serverFile_ = do
let t_ = xftpEncodeTransmission thParams (corrId, fId, resp)
@@ -399,7 +397,7 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
r <- runExceptT $ do
sizes <- asks $ allowedChunkSizes . config
unless (size file `elem` sizes) $ throwE SIZE
ts <- liftIO getFileTime
ts <- liftIO getSystemTime
-- TODO validate body empty
sId <- ExceptT $ addFileRetry st file 3 ts
rcps <- mapM (ExceptT . addRecipientRetry st 3 sId) rks
@@ -407,12 +405,12 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
logAddFile sl sId file ts
logAddRecipients sl sId rcps
stats <- asks serverStats
lift $ incFileStat filesCreated
liftIO $ atomicModifyIORef'_ (fileRecipients stats) (+ length rks)
atomically $ modifyTVar' (filesCreated stats) (+ 1)
atomically $ modifyTVar' (fileRecipients stats) (+ length rks)
let rIds = L.map (\(FileRecipient rId _) -> rId) rcps
pure $ FRSndIds sId rIds
pure $ either FRErr id r
addFileRetry :: FileStore -> FileInfo -> Int -> RoundedSystemTime -> M (Either XFTPErrorType XFTPFileId)
addFileRetry :: FileStore -> FileInfo -> Int -> SystemTime -> M (Either XFTPErrorType XFTPFileId)
addFileRetry st file n ts =
retryAdd n $ \sId -> runExceptT $ do
ExceptT $ addFile st sId file ts
@@ -437,7 +435,7 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
rcps <- mapM (ExceptT . addRecipientRetry st 3 sId) rks
lift $ withFileLog $ \sl -> logAddRecipients sl sId rcps
stats <- asks serverStats
liftIO $ atomicModifyIORef'_ (fileRecipients stats) (+ length rks)
atomically $ modifyTVar' (fileRecipients stats) (+ length rks)
let rIds = L.map (\(FileRecipient rId _) -> rId) rcps
pure $ FRRcvIds rIds
pure $ either FRErr id r
@@ -465,19 +463,19 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
\used -> let used' = used + fromIntegral size in if used' <= quota then (True, used') else (False, used)
receive = do
path <- asks $ filesPath . config
let fPath = path </> B.unpack (B64.encode $ unEntityId senderId)
let fPath = path </> B.unpack (B64.encode senderId)
receiveChunk (XFTPRcvChunkSpec fPath size digest) >>= \case
Right () -> do
stats <- asks serverStats
withFileLog $ \sl -> logPutFile sl senderId fPath
atomically $ writeTVar filePath (Just fPath)
incFileStat filesUploaded
incFileStat filesCount
liftIO $ atomicModifyIORef'_ (filesSize stats) (+ fromIntegral size)
atomically $ modifyTVar' (filesUploaded stats) (+ 1)
atomically $ modifyTVar' (filesCount stats) (+ 1)
atomically $ modifyTVar' (filesSize stats) (+ fromIntegral size)
pure FROk
Left e -> do
us <- asks $ usedStorage . store
atomically $ modifyTVar' us $ subtract (fromIntegral size)
atomically . modifyTVar' us $ subtract (fromIntegral size)
liftIO $ whenM (doesFileExist fPath) (removeFile fPath) `catch` logFileError
pure $ FRErr e
receiveChunk spec = do
@@ -496,8 +494,8 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
case LC.cbInit dhSecret cbNonce of
Right sbState -> do
stats <- asks serverStats
incFileStat fileDownloads
liftIO $ updatePeriodStats (filesDownloaded stats) senderId
atomically $ modifyTVar' (fileDownloads stats) (+ 1)
atomically $ updatePeriodStats (filesDownloaded stats) senderId
pure (FRFile sDhKey cbNonce, Just ServerFile {filePath = path, fileSize = size, sbState})
_ -> pure (FRErr INTERNAL, Nothing)
_ -> pure (FRErr NO_FILE, Nothing)
@@ -513,7 +511,8 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
withFileLog (`logAckFile` rId)
st <- asks store
atomically $ deleteRecipient st rId fr
incFileStat fileDownloadAcks
stats <- asks serverStats
atomically $ modifyTVar' (fileDownloadAcks stats) (+ 1)
pure FROk
deleteServerFile_ :: FileRec -> M (Either XFTPErrorType ())
@@ -525,14 +524,11 @@ deleteServerFile_ FileRec {senderId, fileInfo, filePath} = do
ExceptT $ first (\(_ :: SomeException) -> FILE_IO) <$> try (forM_ path $ \p -> whenM (doesFileExist p) (removeFile p >> deletedStats stats))
st <- asks store
void $ atomically $ deleteFile st senderId
lift $ incFileStat filesDeleted
atomically $ modifyTVar' (filesDeleted stats) (+ 1)
where
deletedStats stats = do
liftIO $ atomicModifyIORef'_ (filesCount stats) (subtract 1)
liftIO $ atomicModifyIORef'_ (filesSize stats) (subtract $ fromIntegral $ size fileInfo)
getFileTime :: IO RoundedSystemTime
getFileTime = getRoundedSystemTime fileTimePrecision
atomically $ modifyTVar' (filesCount stats) (subtract 1)
atomically $ modifyTVar' (filesSize stats) (subtract $ fromIntegral $ size fileInfo)
expireServerFiles :: Maybe Int -> ExpirationConfig -> M ()
expireServerFiles itemDelay expCfg = do
@@ -558,26 +554,29 @@ expireServerFiles itemDelay expCfg = do
delete st sId = do
withFileLog (`logDeleteFile` sId)
void . atomically $ deleteFile st sId -- will not update usedStorage if sId isn't in store
incFileStat filesExpired
FileServerStats {filesExpired} <- asks serverStats
atomically $ modifyTVar' filesExpired (+ 1)
randomId :: Int -> M ByteString
randomId n = atomically . C.randomBytes n =<< asks random
getFileId :: M XFTPFileId
getFileId = fmap EntityId . randomId =<< asks (fileIdSize . config)
getFileId = do
size <- asks (fileIdSize . config)
atomically . C.randomBytes size =<< asks random
withFileLog :: (StoreLog 'WriteMode -> IO a) -> M ()
withFileLog action = liftIO . mapM_ action =<< asks storeLog
incFileStat :: (FileServerStats -> IORef Int) -> M ()
incFileStat :: (FileServerStats -> TVar Int) -> M ()
incFileStat statSel = do
stats <- asks serverStats
liftIO $ atomicModifyIORef'_ (statSel stats) (+ 1)
atomically $ modifyTVar (statSel stats) (+ 1)
saveServerStats :: M ()
saveServerStats =
asks (serverStatsBackupFile . config)
>>= mapM_ (\f -> asks serverStats >>= liftIO . getFileServerStatsData >>= liftIO . saveStats f)
>>= mapM_ (\f -> asks serverStats >>= atomically . getFileServerStatsData >>= liftIO . saveStats f)
where
saveStats f stats = do
logInfo $ "saving server stats to file " <> T.pack f
@@ -595,7 +594,7 @@ restoreServerStats = asks (serverStatsBackupFile . config) >>= mapM_ restoreStat
FileStore {files, usedStorage} <- asks store
_filesCount <- M.size <$> readTVarIO files
_filesSize <- readTVarIO usedStorage
liftIO $ setFileServerStats s d {_filesCount, _filesSize}
atomically $ setFileServerStats s d {_filesCount, _filesSize}
renameFile f $ f <> ".bak"
logInfo "server stats restored"
when (statsFilesCount /= _filesCount) $ logWarn $ "Files count differs: stats: " <> tshow statsFilesCount <> ", store: " <> tshow _filesCount
+4 -2
View File
@@ -4,14 +4,16 @@
module Simplex.FileTransfer.Server.Control where
import qualified Data.Attoparsec.ByteString.Char8 as A
import Simplex.FileTransfer.Protocol (XFTPFileId)
import Data.ByteString (ByteString)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (BasicAuth)
data CPClientRole = CPRNone | CPRUser | CPRAdmin
data ControlProtocol
= CPAuth BasicAuth
| CPStatsRTS
| CPDelete XFTPFileId
| CPDelete ByteString
| CPHelp
| CPQuit
| CPSkip
+16 -12
View File
@@ -11,6 +11,7 @@ module Simplex.FileTransfer.Server.Env where
import Control.Logger.Simple
import Control.Monad
import Control.Monad.IO.Unlift
import Crypto.Random
import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty)
@@ -29,7 +30,7 @@ import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Protocol (BasicAuth, RcvPublicAuthKey)
import Simplex.Messaging.Server.Expiration
import Simplex.Messaging.Transport (ALPN)
import Simplex.Messaging.Transport.Server (ServerCredentials (..), TransportServerConfig (..), loadFingerprint, loadServerCredential)
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), loadFingerprint, loadTLSServerParams)
import Simplex.Messaging.Util (tshow)
import System.IO (IOMode (..))
import UnliftIO.STM
@@ -57,7 +58,10 @@ data XFTPServerConfig = XFTPServerConfig
fileTimeout :: Int,
-- | time after which inactive clients can be disconnected and check interval, seconds
inactiveClientExpiration :: Maybe ExpirationConfig,
xftpCredentials :: ServerCredentials,
-- CA certificate private key is not needed for initialization
caCertificateFile :: FilePath,
privateKeyFile :: FilePath,
certificateFile :: FilePath,
-- | XFTP client-server protocol version range
xftpServerVRange :: VersionRangeXFTP,
-- stats config - see SMP server config
@@ -72,7 +76,7 @@ data XFTPServerConfig = XFTPServerConfig
defaultInactiveClientExpiration :: ExpirationConfig
defaultInactiveClientExpiration =
ExpirationConfig
{ ttl = 21600, -- seconds, 6 hours
{ ttl = 43200, -- seconds, 12 hours
checkInterval = 3600 -- seconds, 1 hours
}
@@ -82,7 +86,7 @@ data XFTPEnv = XFTPEnv
storeLog :: Maybe (StoreLog 'WriteMode),
random :: TVar ChaChaDRG,
serverIdentity :: C.KeyHash,
tlsServerCreds :: T.Credential,
tlsServerParams :: T.ServerParams,
serverStats :: FileServerStats
}
@@ -100,19 +104,19 @@ supportedXFTPhandshakes :: [ALPN]
supportedXFTPhandshakes = ["xftp/1"]
newXFTPServerEnv :: XFTPServerConfig -> IO XFTPEnv
newXFTPServerEnv config@XFTPServerConfig {storeLogFile, fileSizeQuota, xftpCredentials} = do
random <- C.newRandom
store <- newFileStore
storeLog <- mapM (`readWriteFileStore` store) storeLogFile
newXFTPServerEnv config@XFTPServerConfig {storeLogFile, fileSizeQuota, caCertificateFile, certificateFile, privateKeyFile, transportConfig} = do
random <- liftIO C.newRandom
store <- atomically newFileStore
storeLog <- liftIO $ mapM (`readWriteFileStore` store) storeLogFile
used <- countUsedStorage <$> readTVarIO (files store)
atomically $ writeTVar (usedStorage store) used
forM_ fileSizeQuota $ \quota -> do
logInfo $ "Total / available storage: " <> tshow quota <> " / " <> tshow (quota - used)
when (quota < used) $ logInfo "WARNING: storage quota is less than used storage, no files can be uploaded!"
tlsServerCreds <- loadServerCredential xftpCredentials
Fingerprint fp <- loadFingerprint xftpCredentials
serverStats <- newFileServerStats =<< getCurrentTime
pure XFTPEnv {config, store, storeLog, random, tlsServerCreds, serverIdentity = C.KeyHash fp, serverStats}
tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig)
Fingerprint fp <- liftIO $ loadFingerprint caCertificateFile
serverStats <- atomically . newFileServerStats =<< liftIO getCurrentTime
pure XFTPEnv {config, store, storeLog, random, tlsServerParams, serverIdentity = C.KeyHash fp, serverStats}
countUsedStorage :: M.Map k FileRec -> Int64
countUsedStorage = M.foldl' (\acc FileRec {fileInfo = FileInfo {size}} -> acc + fromIntegral size) 0
+7 -10
View File
@@ -19,7 +19,7 @@ import Options.Applicative
import Simplex.FileTransfer.Chunks
import Simplex.FileTransfer.Description (FileSize (..))
import Simplex.FileTransfer.Server (runXFTPServer)
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defFileExpirationHours, defaultFileExpiration, defaultInactiveClientExpiration)
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defFileExpirationHours, defaultFileExpiration, defaultInactiveClientExpiration, supportedXFTPhandshakes)
import Simplex.FileTransfer.Transport (supportedFileServerVRange)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
@@ -28,7 +28,7 @@ import Simplex.Messaging.Server.CLI
import Simplex.Messaging.Server.Expiration
import Simplex.Messaging.Transport (simplexMQVersion)
import Simplex.Messaging.Transport.Client (TransportHost (..))
import Simplex.Messaging.Transport.Server (ServerCredentials (..), TransportServerConfig (..), defaultTransportServerConfig)
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig)
import Simplex.Messaging.Util (safeDecodeUtf8, tshow)
import System.Directory (createDirectoryIfMissing, doesFileExist)
import System.FilePath (combine)
@@ -101,7 +101,6 @@ xftpServerCLI cfgPath logPath = do
\\n\
\# control_port_admin_password:\n\
\# control_port_user_password:\n\
\\n\
\[TRANSPORT]\n\
\# host is only used to print server address on start\n"
<> ("host: " <> T.pack host <> "\n")
@@ -174,12 +173,9 @@ xftpServerCLI cfgPath logPath = do
{ ttl = readStrictIni "INACTIVE_CLIENTS" "ttl" ini,
checkInterval = readStrictIni "INACTIVE_CLIENTS" "check_interval" ini
},
xftpCredentials =
ServerCredentials
{ caCertificateFile = Just $ c caCrtFile,
privateKeyFile = c serverKeyFile,
certificateFile = c serverCrtFile
},
caCertificateFile = c caCrtFile,
privateKeyFile = c serverKeyFile,
certificateFile = c serverCrtFile,
xftpServerVRange = supportedFileServerVRange,
logStatsInterval = logStats $> 86400, -- seconds
logStatsStartTime = 0, -- seconds from 00:00 UTC
@@ -187,7 +183,8 @@ xftpServerCLI cfgPath logPath = do
serverStatsBackupFile = logStats $> combine logPath "file-server-stats.log",
transportConfig =
defaultTransportServerConfig
{ logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini
{ logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini,
alpn = Just supportedXFTPhandshakes
},
responseDelay = 0
}
+47 -47
View File
@@ -7,24 +7,25 @@ module Simplex.FileTransfer.Server.Stats where
import Control.Applicative ((<|>))
import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Char8 as B
import Data.IORef
import Data.Int (Int64)
import Data.Time.Clock (UTCTime)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (SenderId)
import Simplex.Messaging.Server.Stats (PeriodStats, PeriodStatsData, getPeriodStatsData, newPeriodStats, setPeriodStats)
import UnliftIO.STM
data FileServerStats = FileServerStats
{ fromTime :: IORef UTCTime,
filesCreated :: IORef Int,
fileRecipients :: IORef Int,
filesUploaded :: IORef Int,
filesExpired :: IORef Int,
filesDeleted :: IORef Int,
filesDownloaded :: PeriodStats,
fileDownloads :: IORef Int,
fileDownloadAcks :: IORef Int,
filesCount :: IORef Int,
filesSize :: IORef Int64
{ fromTime :: TVar UTCTime,
filesCreated :: TVar Int,
fileRecipients :: TVar Int,
filesUploaded :: TVar Int,
filesExpired :: TVar Int,
filesDeleted :: TVar Int,
filesDownloaded :: PeriodStats SenderId,
fileDownloads :: TVar Int,
fileDownloadAcks :: TVar Int,
filesCount :: TVar Int,
filesSize :: TVar Int64
}
data FileServerStatsData = FileServerStatsData
@@ -34,7 +35,7 @@ data FileServerStatsData = FileServerStatsData
_filesUploaded :: Int,
_filesExpired :: Int,
_filesDeleted :: Int,
_filesDownloaded :: PeriodStatsData,
_filesDownloaded :: PeriodStatsData SenderId,
_fileDownloads :: Int,
_fileDownloadAcks :: Int,
_filesCount :: Int,
@@ -42,50 +43,49 @@ data FileServerStatsData = FileServerStatsData
}
deriving (Show)
newFileServerStats :: UTCTime -> IO FileServerStats
newFileServerStats :: UTCTime -> STM FileServerStats
newFileServerStats ts = do
fromTime <- newIORef ts
filesCreated <- newIORef 0
fileRecipients <- newIORef 0
filesUploaded <- newIORef 0
filesExpired <- newIORef 0
filesDeleted <- newIORef 0
fromTime <- newTVar ts
filesCreated <- newTVar 0
fileRecipients <- newTVar 0
filesUploaded <- newTVar 0
filesExpired <- newTVar 0
filesDeleted <- newTVar 0
filesDownloaded <- newPeriodStats
fileDownloads <- newIORef 0
fileDownloadAcks <- newIORef 0
filesCount <- newIORef 0
filesSize <- newIORef 0
fileDownloads <- newTVar 0
fileDownloadAcks <- newTVar 0
filesCount <- newTVar 0
filesSize <- newTVar 0
pure FileServerStats {fromTime, filesCreated, fileRecipients, filesUploaded, filesExpired, filesDeleted, filesDownloaded, fileDownloads, fileDownloadAcks, filesCount, filesSize}
getFileServerStatsData :: FileServerStats -> IO FileServerStatsData
getFileServerStatsData :: FileServerStats -> STM FileServerStatsData
getFileServerStatsData s = do
_fromTime <- readIORef $ fromTime (s :: FileServerStats)
_filesCreated <- readIORef $ filesCreated s
_fileRecipients <- readIORef $ fileRecipients s
_filesUploaded <- readIORef $ filesUploaded s
_filesExpired <- readIORef $ filesExpired s
_filesDeleted <- readIORef $ filesDeleted s
_fromTime <- readTVar $ fromTime (s :: FileServerStats)
_filesCreated <- readTVar $ filesCreated s
_fileRecipients <- readTVar $ fileRecipients s
_filesUploaded <- readTVar $ filesUploaded s
_filesExpired <- readTVar $ filesExpired s
_filesDeleted <- readTVar $ filesDeleted s
_filesDownloaded <- getPeriodStatsData $ filesDownloaded s
_fileDownloads <- readIORef $ fileDownloads s
_fileDownloadAcks <- readIORef $ fileDownloadAcks s
_filesCount <- readIORef $ filesCount s
_filesSize <- readIORef $ filesSize s
_fileDownloads <- readTVar $ fileDownloads s
_fileDownloadAcks <- readTVar $ fileDownloadAcks s
_filesCount <- readTVar $ filesCount s
_filesSize <- readTVar $ filesSize s
pure FileServerStatsData {_fromTime, _filesCreated, _fileRecipients, _filesUploaded, _filesExpired, _filesDeleted, _filesDownloaded, _fileDownloads, _fileDownloadAcks, _filesCount, _filesSize}
-- this function is not thread safe, it is used on server start only
setFileServerStats :: FileServerStats -> FileServerStatsData -> IO ()
setFileServerStats :: FileServerStats -> FileServerStatsData -> STM ()
setFileServerStats s d = do
writeIORef (fromTime (s :: FileServerStats)) $! _fromTime (d :: FileServerStatsData)
writeIORef (filesCreated s) $! _filesCreated d
writeIORef (fileRecipients s) $! _fileRecipients d
writeIORef (filesUploaded s) $! _filesUploaded d
writeIORef (filesExpired s) $! _filesExpired d
writeIORef (filesDeleted s) $! _filesDeleted d
writeTVar (fromTime (s :: FileServerStats)) $! _fromTime (d :: FileServerStatsData)
writeTVar (filesCreated s) $! _filesCreated d
writeTVar (fileRecipients s) $! _fileRecipients d
writeTVar (filesUploaded s) $! _filesUploaded d
writeTVar (filesExpired s) $! _filesExpired d
writeTVar (filesDeleted s) $! _filesDeleted d
setPeriodStats (filesDownloaded s) $! _filesDownloaded d
writeIORef (fileDownloads s) $! _fileDownloads d
writeIORef (fileDownloadAcks s) $! _fileDownloadAcks d
writeIORef (filesCount s) $! _filesCount d
writeIORef (filesSize s) $! _filesSize d
writeTVar (fileDownloads s) $! _fileDownloads d
writeTVar (fileDownloadAcks s) $! _fileDownloadAcks d
writeTVar (filesCount s) $! _filesCount d
writeTVar (filesSize s) $! _filesSize d
instance StrEncoding FileServerStatsData where
strEncode FileServerStatsData {_fromTime, _filesCreated, _fileRecipients, _filesUploaded, _filesExpired, _filesDeleted, _filesDownloaded, _fileDownloads, _fileDownloadAcks, _filesCount, _filesSize} =
+10 -15
View File
@@ -17,7 +17,6 @@ module Simplex.FileTransfer.Server.Store
expiredFilePath,
getFile,
ackFile,
fileTimePrecision,
)
where
@@ -26,12 +25,12 @@ import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Int (Int64)
import Data.Set (Set)
import qualified Data.Set as S
import Data.Time.Clock.System (SystemTime (..))
import Simplex.FileTransfer.Protocol (FileInfo (..), SFileParty (..), XFTPFileId)
import Simplex.FileTransfer.Transport (XFTPErrorType (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (RcvPublicAuthKey, RecipientId, SenderId)
import Simplex.Messaging.Server.QueueStore (RoundedSystemTime (..))
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Util (ifM, ($>>=))
@@ -47,34 +46,30 @@ data FileRec = FileRec
fileInfo :: FileInfo,
filePath :: TVar (Maybe FilePath),
recipientIds :: TVar (Set RecipientId),
createdAt :: RoundedSystemTime
createdAt :: SystemTime
}
fileTimePrecision :: Int64
fileTimePrecision = 3600 -- truncate creation time to 1 hour
data FileRecipient = FileRecipient RecipientId RcvPublicAuthKey
deriving (Show)
instance StrEncoding FileRecipient where
strEncode (FileRecipient rId rKey) = strEncode rId <> ":" <> strEncode rKey
strP = FileRecipient <$> strP <* A.char ':' <*> strP
newFileStore :: IO FileStore
newFileStore :: STM FileStore
newFileStore = do
files <- TM.emptyIO
recipients <- TM.emptyIO
usedStorage <- newTVarIO 0
files <- TM.empty
recipients <- TM.empty
usedStorage <- newTVar 0
pure FileStore {files, recipients, usedStorage}
addFile :: FileStore -> SenderId -> FileInfo -> RoundedSystemTime -> STM (Either XFTPErrorType ())
addFile :: FileStore -> SenderId -> FileInfo -> SystemTime -> STM (Either XFTPErrorType ())
addFile FileStore {files} sId fileInfo createdAt =
ifM (TM.member sId files) (pure $ Left DUPLICATE_) $ do
f <- newFileRec sId fileInfo createdAt
TM.insert sId f files
pure $ Right ()
newFileRec :: SenderId -> FileInfo -> RoundedSystemTime -> STM FileRec
newFileRec :: SenderId -> FileInfo -> SystemTime -> STM FileRec
newFileRec senderId fileInfo createdAt = do
recipientIds <- newTVar S.empty
filePath <- newTVar Nothing
@@ -125,8 +120,8 @@ getFile st party fId = case party of
expiredFilePath :: FileStore -> XFTPFileId -> Int64 -> STM (Maybe (Maybe FilePath))
expiredFilePath FileStore {files} sId old =
TM.lookup sId files
$>>= \FileRec {filePath, createdAt = RoundedSystemTime createdAt} ->
if createdAt + fileTimePrecision < old
$>>= \FileRec {filePath, createdAt} ->
if systemSeconds createdAt < old
then Just <$> readTVar filePath
else pure Nothing
+3 -4
View File
@@ -28,23 +28,22 @@ import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Time.Clock.System (SystemTime)
import Simplex.FileTransfer.Protocol (FileInfo (..))
import Simplex.FileTransfer.Server.Store
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (RcvPublicAuthKey, RecipientId, SenderId)
import Simplex.Messaging.Server.QueueStore (RoundedSystemTime)
import Simplex.Messaging.Server.StoreLog
import Simplex.Messaging.Util (bshow, whenM)
import System.Directory (doesFileExist, renameFile)
import System.IO
data FileStoreLogRecord
= AddFile SenderId FileInfo RoundedSystemTime
= AddFile SenderId FileInfo SystemTime
| PutFile SenderId FilePath
| AddRecipients SenderId (NonEmpty FileRecipient)
| DeleteFile SenderId
| AckFile RecipientId
deriving (Show)
instance StrEncoding FileStoreLogRecord where
strEncode = \case
@@ -65,7 +64,7 @@ instance StrEncoding FileStoreLogRecord where
logFileStoreRecord :: StoreLog 'WriteMode -> FileStoreLogRecord -> IO ()
logFileStoreRecord = writeStoreLogRecord
logAddFile :: StoreLog 'WriteMode -> SenderId -> FileInfo -> RoundedSystemTime -> IO ()
logAddFile :: StoreLog 'WriteMode -> SenderId -> FileInfo -> SystemTime -> IO ()
logAddFile s = logFileStoreRecord s .:. AddFile
logPutFile :: StoreLog 'WriteMode -> SenderId -> FilePath -> IO ()
+2 -2
View File
@@ -25,9 +25,9 @@ import Simplex.Messaging.Parsers
import Simplex.Messaging.Protocol (XFTPServer)
import System.FilePath ((</>))
type RcvFileId = ByteString -- Agent entity ID
type RcvFileId = ByteString
type SndFileId = ByteString -- Agent entity ID
type SndFileId = ByteString
authTagSize :: Int64
authTagSize = fromIntegral C.authTagSize
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+33 -34
View File
@@ -41,7 +41,6 @@ module Simplex.Messaging.Agent.Env.SQLite
)
where
import Control.Concurrent (ThreadId)
import Control.Monad.Except
import Control.Monad.IO.Unlift
import Control.Monad.Reader
@@ -52,7 +51,7 @@ import Data.ByteArray (ScrubbedBytes)
import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map)
import Data.Map (Map)
import Data.Maybe (fromMaybe)
import Data.Time.Clock (NominalDiffTime, nominalDay)
import Data.Time.Clock.System (SystemTime (..))
@@ -74,11 +73,11 @@ import Simplex.Messaging.Parsers (defaultJSON)
import Simplex.Messaging.Protocol (NtfServer, ProtoServerWithAuth, ProtocolServer, ProtocolType (..), ProtocolTypeI, VersionRangeSMPC, XFTPServer, supportedSMPClientVRange)
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport (SMPVersion)
import Simplex.Messaging.Transport (SMPVersion, TLS, Transport (..))
import Simplex.Messaging.Transport.Client (defaultSMPPort)
import Simplex.Messaging.Util (allFinally, catchAllErrors, catchAllErrors', tryAllErrors, tryAllErrors')
import System.Mem.Weak (Weak)
import System.Random (StdGen, newStdGen)
import UnliftIO (SomeException)
import UnliftIO (Async, SomeException)
import UnliftIO.STM
type AM' a = ReaderT Env IO a
@@ -149,9 +148,10 @@ data AgentConfig = AgentConfig
xftpMaxRecipientsPerRequest :: Int,
deleteErrorCount :: Int,
ntfCron :: Word16,
ntfBatchSize :: Int,
ntfSubFirstCheckInterval :: NominalDiffTime,
ntfWorkerDelay :: Int,
ntfSMPWorkerDelay :: Int,
ntfSubCheckInterval :: NominalDiffTime,
ntfMaxMessages :: Int,
caCertificateFile :: FilePath,
privateKeyFile :: FilePath,
certificateFile :: FilePath,
@@ -165,7 +165,7 @@ defaultReconnectInterval =
RetryInterval
{ initialInterval = 2_000000,
increaseAfter = 10_000000,
maxInterval = 180_000000
maxInterval = 60_000000
}
defaultMessageRetryInterval :: RetryInterval2
@@ -175,7 +175,7 @@ defaultMessageRetryInterval =
RetryInterval
{ initialInterval = 2_000000,
increaseAfter = 10_000000,
maxInterval = 120_000000
maxInterval = 60_000000
},
riSlow =
RetryInterval
@@ -194,9 +194,9 @@ defaultAgentConfig =
rcvAuthAlg = C.AuthAlg C.SEd25519, -- this will stay as Ed25519
sndAuthAlg = C.AuthAlg C.SEd25519, -- TODO replace with X25519 when switching to v7
connIdBytes = 12,
tbqSize = 128,
smpCfg = defaultSMPClientConfig,
ntfCfg = defaultNTFClientConfig,
tbqSize = 64,
smpCfg = defaultSMPClientConfig {defaultTransport = (show defaultSMPPort, transport @TLS)},
ntfCfg = defaultNTFClientConfig {defaultTransport = ("443", transport @TLS)},
xftpCfg = defaultXFTPClientConfig,
reconnectInterval = defaultReconnectInterval,
messageRetryInterval = defaultMessageRetryInterval,
@@ -220,9 +220,10 @@ defaultAgentConfig =
xftpMaxRecipientsPerRequest = 200,
deleteErrorCount = 10,
ntfCron = 20, -- minutes
ntfBatchSize = 150,
ntfSubFirstCheckInterval = nominalDay,
ntfSubCheckInterval = 3 * nominalDay,
ntfWorkerDelay = 100000, -- microseconds
ntfSMPWorkerDelay = 500000, -- microseconds
ntfSubCheckInterval = nominalDay,
ntfMaxMessages = 3,
-- CA certificate private key is not needed for initialization
-- ! we do not generate these
caCertificateFile = "/etc/opt/simplex-agent/ca.crt",
@@ -247,8 +248,8 @@ newSMPAgentEnv :: AgentConfig -> SQLiteStore -> IO Env
newSMPAgentEnv config store = do
random <- C.newRandom
randomServer <- newTVarIO =<< liftIO newStdGen
ntfSupervisor <- newNtfSubSupervisor $ tbqSize config
xftpAgent <- newXFTPAgent
ntfSupervisor <- atomically . newNtfSubSupervisor $ tbqSize config
xftpAgent <- atomically newXFTPAgent
multicastSubscribers <- newTMVarIO 0
pure Env {config, store, random, randomServer, ntfSupervisor, xftpAgent, multicastSubscribers}
@@ -257,23 +258,21 @@ createAgentStore dbFilePath dbKey keepKey = createSQLiteStore dbFilePath dbKey k
data NtfSupervisor = NtfSupervisor
{ ntfTkn :: TVar (Maybe NtfToken),
ntfSubQ :: TBQueue (NtfSupervisorCommand, NonEmpty ConnId),
ntfSubQ :: TBQueue (ConnId, NtfSupervisorCommand),
ntfWorkers :: TMap NtfServer Worker,
ntfSMPWorkers :: TMap SMPServer Worker,
ntfTknDelWorkers :: TMap NtfServer Worker
ntfSMPWorkers :: TMap SMPServer Worker
}
data NtfSupervisorCommand = NSCCreate | NSCSmpDelete | NSCDeleteSub
data NtfSupervisorCommand = NSCCreate | NSCDelete | NSCSmpDelete | NSCNtfWorker NtfServer | NSCNtfSMPWorker SMPServer
deriving (Show)
newNtfSubSupervisor :: Natural -> IO NtfSupervisor
newNtfSubSupervisor :: Natural -> STM NtfSupervisor
newNtfSubSupervisor qSize = do
ntfTkn <- newTVarIO Nothing
ntfSubQ <- newTBQueueIO qSize
ntfWorkers <- TM.emptyIO
ntfSMPWorkers <- TM.emptyIO
ntfTknDelWorkers <- TM.emptyIO
pure NtfSupervisor {ntfTkn, ntfSubQ, ntfWorkers, ntfSMPWorkers, ntfTknDelWorkers}
ntfTkn <- newTVar Nothing
ntfSubQ <- newTBQueue qSize
ntfWorkers <- TM.empty
ntfSMPWorkers <- TM.empty
pure NtfSupervisor {ntfTkn, ntfSubQ, ntfWorkers, ntfSMPWorkers}
data XFTPAgent = XFTPAgent
{ -- if set, XFTP file paths will be considered as relative to this directory
@@ -283,12 +282,12 @@ data XFTPAgent = XFTPAgent
xftpDelWorkers :: TMap XFTPServer Worker
}
newXFTPAgent :: IO XFTPAgent
newXFTPAgent :: STM XFTPAgent
newXFTPAgent = do
xftpWorkDir <- newTVarIO Nothing
xftpRcvWorkers <- TM.emptyIO
xftpSndWorkers <- TM.emptyIO
xftpDelWorkers <- TM.emptyIO
xftpWorkDir <- newTVar Nothing
xftpRcvWorkers <- TM.empty
xftpSndWorkers <- TM.empty
xftpDelWorkers <- TM.empty
pure XFTPAgent {xftpWorkDir, xftpRcvWorkers, xftpSndWorkers, xftpDelWorkers}
tryAgentError :: AM a -> AM (Either AgentErrorType a)
@@ -319,7 +318,7 @@ mkInternal = INTERNAL . show
data Worker = Worker
{ workerId :: Int,
doWork :: TMVar (),
action :: TMVar (Maybe (Weak ThreadId)),
action :: TMVar (Maybe (Async ())),
restarts :: TVar RestartCount
}
+223 -469
View File
@@ -1,9 +1,7 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
@@ -15,7 +13,6 @@ module Simplex.Messaging.Agent.NtfSubSupervisor
nsRemoveNtfToken,
sendNtfSubCommand,
instantNotifications,
deleteToken,
closeNtfSupervisor,
getNtfServer,
)
@@ -23,170 +20,117 @@ where
import Control.Logger.Simple (logError, logInfo)
import Control.Monad
import Control.Monad.Except
import Control.Monad.Reader
import Control.Monad.Trans.Except
import Crypto.Random (ChaChaDRG)
import Data.Bifunctor (first)
import Data.Either (fromRight, partitionEithers)
import Data.Functor (($>))
import Data.List (foldl')
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as L
import qualified Data.Map.Strict as M
import Data.Maybe (catMaybes)
import qualified Data.Set as S
import Data.Text (Text)
import Data.Time (UTCTime, addUTCTime, getCurrentTime)
import Data.Time.Clock (diffUTCTime)
import Simplex.Messaging.Agent.Client
import Simplex.Messaging.Agent.Env.SQLite
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.Protocol (AEvent (..), AEvt (..), AgentErrorType (..), BrokerErrorType (..), ConnId, NotificationsMode (..), SAEntity (..))
import Simplex.Messaging.Agent.RetryInterval
import Simplex.Messaging.Agent.Stats
import Simplex.Messaging.Agent.Store
import Simplex.Messaging.Agent.Store.SQLite
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Notifications.Protocol
import Simplex.Messaging.Notifications.Protocol (NtfSubStatus (..), NtfTknStatus (..), SMPQueueNtf (..))
import Simplex.Messaging.Notifications.Types
import Simplex.Messaging.Protocol (NtfServer, sameSrvAddr)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.Util (diffToMicroseconds, threadDelay', tshow)
import Simplex.Messaging.Protocol (NtfServer, SMPServer, sameSrvAddr)
import Simplex.Messaging.Util (diffToMicroseconds, threadDelay', tshow, unlessM)
import System.Random (randomR)
import UnliftIO
import UnliftIO.Concurrent (forkIO)
import UnliftIO.Concurrent (forkIO, threadDelay)
import qualified UnliftIO.Exception as E
runNtfSupervisor :: AgentClient -> AM' ()
runNtfSupervisor c = do
ns <- asks ntfSupervisor
runExceptT startTknDelete >>= \case
Left e -> notifyErr e
Right _ -> pure ()
forever $ do
cmd <- atomically . readTBQueue $ ntfSubQ ns
handleErr . agentOperationBracket c AONtfNetwork waitUntilActive $
runExceptT (processNtfCmd c cmd) >>= \case
Left e -> notifyErr e
cmd@(connId, _) <- atomically . readTBQueue $ ntfSubQ ns
handleErr connId . agentOperationBracket c AONtfNetwork waitUntilActive $
runExceptT (processNtfSub c cmd) >>= \case
Left e -> notifyErr connId e
Right _ -> return ()
where
startTknDelete :: AM ()
startTknDelete = do
pendingDelServers <- withStore' c getPendingDelTknServers
lift . forM_ pendingDelServers $ getNtfTknDelWorker True c
handleErr :: AM' () -> AM' ()
handleErr = E.handle $ \(e :: E.SomeException) -> do
handleErr :: ConnId -> AM' () -> AM' ()
handleErr connId = E.handle $ \(e :: E.SomeException) -> do
logError $ "runNtfSupervisor error " <> tshow e
notifyErr e
notifyErr e = notifyInternalError' c $ "runNtfSupervisor error " <> show e
notifyErr connId e
notifyErr connId e = notifyInternalError c connId $ "runNtfSupervisor error " <> show e
partitionErrs :: (a -> ConnId) -> [a] -> [Either AgentErrorType b] -> ([(ConnId, AgentErrorType)], [b])
partitionErrs f xs = partitionEithers . zipWith (\x -> first (f x,)) xs
{-# INLINE partitionErrs #-}
ntfSubConnId :: NtfSubscription -> ConnId
ntfSubConnId NtfSubscription {connId} = connId
processNtfCmd :: AgentClient -> (NtfSupervisorCommand, NonEmpty ConnId) -> AM ()
processNtfCmd c (cmd, connIds) = do
logInfo $ "processNtfCmd - cmd = " <> tshow cmd
let connIds' = L.toList connIds
processNtfSub :: AgentClient -> (ConnId, NtfSupervisorCommand) -> AM ()
processNtfSub c (connId, cmd) = do
logInfo $ "processNtfSub - connId = " <> tshow connId <> " - cmd = " <> tshow cmd
case cmd of
NSCCreate -> do
(cErrs, rqSubActions) <- lift $ partitionErrs id connIds' <$> withStoreBatch c (\db -> map (getQueueSub db) connIds')
notifyErrs c cErrs
logInfo $ "processNtfCmd, NSCCreate - length rqSubs = " <> tshow (length rqSubActions)
let (ns, rs, css, cns) = partitionQueueSubActions rqSubActions
createNewSubs ns
resetSubs rs
lift $ do
mapM_ (getNtfSMPWorker True c) (S.fromList css)
mapM_ (getNtfNTFWorker True c) (S.fromList cns)
where
getQueueSub ::
DB.Connection ->
ConnId ->
IO (Either AgentErrorType (RcvQueue, Maybe NtfSupervisorSub))
getQueueSub db connId = fmap (first storeError) $ runExceptT $ do
rq <- ExceptT $ getPrimaryRcvQueue db connId
sub <- liftIO $ getNtfSubscription db connId
pure (rq, sub)
createNewSubs :: [RcvQueue] -> AM ()
createNewSubs rqs = do
(a, RcvQueue {server = smpServer, clientNtfCreds}) <- withStore c $ \db -> runExceptT $ do
a <- liftIO $ getNtfSubscription db connId
q <- ExceptT $ getPrimaryRcvQueue db connId
pure (a, q)
logInfo $ "processNtfSub, NSCCreate - a = " <> tshow a
case a of
Nothing -> do
withTokenServer $ \ntfServer -> do
let newSubs = map (rqToNewSub ntfServer) rqs
(cErrs, _) <- lift $ partitionErrs ntfSubConnId newSubs <$> withStoreBatch c (\db -> map (storeNewSub db) newSubs)
notifyErrs c cErrs
kickSMPWorkers rqs
case clientNtfCreds of
Just ClientNtfCreds {notifierId} -> do
let newSub = newNtfSubscription connId smpServer (Just notifierId) ntfServer NASKey
withStore c $ \db -> createNtfSubscription db newSub $ NtfSubNTFAction NSACreate
lift . void $ getNtfNTFWorker True c ntfServer
Nothing -> do
let newSub = newNtfSubscription connId smpServer Nothing ntfServer NASNew
withStore c $ \db -> createNtfSubscription db newSub $ NtfSubSMPAction NSASmpKey
lift . void $ getNtfSMPWorker True c smpServer
(Just (sub@NtfSubscription {ntfSubStatus, ntfServer = subNtfServer, smpServer = smpServer', ntfQueueId}, action_)) -> do
case (clientNtfCreds, ntfQueueId) of
(Just ClientNtfCreds {notifierId}, Just ntfQueueId')
| sameSrvAddr smpServer smpServer' && notifierId == ntfQueueId' -> create
| otherwise -> rotate
(Nothing, Nothing) -> create
_ -> rotate
where
rqToNewSub :: NtfServer -> RcvQueue -> NtfSubscription
rqToNewSub ntfServer RcvQueue {userId, connId, server} = newNtfSubscription userId connId server Nothing ntfServer NASNew
storeNewSub :: DB.Connection -> NtfSubscription -> IO (Either AgentErrorType ())
storeNewSub db sub = first storeError <$> createNtfSubscription db sub (NSASMP NSASmpKey)
resetSubs :: [(RcvQueue, NtfSubscription)] -> AM ()
resetSubs rqSubs = do
withTokenServer $ \ntfServer -> do
let subsToReset = map (toResetSub ntfServer) rqSubs
(cErrs, _) <- lift $ partitionErrs ntfSubConnId subsToReset <$> withStoreBatch' c (\db -> map (storeResetSub db) subsToReset)
notifyErrs c cErrs
let rqs = map fst rqSubs
kickSMPWorkers rqs
where
toResetSub :: NtfServer -> (RcvQueue, NtfSubscription) -> NtfSubscription
toResetSub ntfServer (rq, sub) =
let RcvQueue {server = smpServer} = rq
in sub {smpServer, ntfQueueId = Nothing, ntfServer, ntfSubId = Nothing, ntfSubStatus = NASNew}
storeResetSub :: DB.Connection -> NtfSubscription -> IO ()
storeResetSub db sub = supervisorUpdateNtfSub db sub (NSASMP NSASmpKey)
partitionQueueSubActions ::
[(RcvQueue, Maybe NtfSupervisorSub)] ->
( [RcvQueue], -- new subs
[(RcvQueue, NtfSubscription)], -- reset subs
[SMPServer], -- continue work (SMP)
[NtfServer] -- continue work (Ntf)
)
partitionQueueSubActions = foldr decideSubWork ([], [], [], [])
where
-- sub = Nothing, needs to be created
decideSubWork (rq, Nothing) (ns, rs, css, cns) = (rq : ns, rs, css, cns)
decideSubWork (rq, Just (sub, subAction_)) (ns, rs, css, cns) =
case (clientNtfCreds rq, ntfQueueId sub) of
-- notifier ID created on SMP server (on ntf server subscription can be registered or not yet),
-- need to clarify action
(Just ClientNtfCreds {notifierId}, Just ntfQueueId')
| sameSrvAddr (qServer rq) subSMPServer && notifierId == ntfQueueId' -> contOrReset
| otherwise -> reset
(Nothing, Nothing) -> contOrReset
_ -> reset
where
NtfSubscription {ntfServer = subNtfServer, smpServer = subSMPServer} = sub
contOrReset = case subAction_ of
-- action was set to NULL after worker internal error
Nothing -> reset
Just (action, _)
-- subscription was marked for deletion / is being deleted
| isDeleteNtfSubAction action -> reset
-- continue work on subscription (e.g. supervisor was repeatedly tasked with creating a subscription)
| otherwise -> case action of
NSASMP _ -> (ns, rs, qServer rq : css, cns)
NSANtf _ -> (ns, rs, css, subNtfServer : cns)
reset = (ns, (rq, sub) : rs, css, cns)
create :: AM ()
create = case action_ of
-- action was set to NULL after worker internal error
Nothing -> resetSubscription
Just (action, _)
-- subscription was marked for deletion / is being deleted
| isDeleteNtfSubAction action -> do
if ntfSubStatus == NASNew || ntfSubStatus == NASOff || ntfSubStatus == NASDeleted
then resetSubscription
else withTokenServer $ \ntfServer -> do
withStore' c $ \db -> supervisorUpdateNtfSub db sub {ntfServer} (NtfSubNTFAction NSACreate)
lift . void $ getNtfNTFWorker True c ntfServer
| otherwise -> case action of
NtfSubNTFAction _ -> lift . void $ getNtfNTFWorker True c subNtfServer
NtfSubSMPAction _ -> lift . void $ getNtfSMPWorker True c smpServer
rotate :: AM ()
rotate = do
withStore' c $ \db -> supervisorUpdateNtfSub db sub (NtfSubNTFAction NSARotate)
lift . void $ getNtfNTFWorker True c subNtfServer
resetSubscription :: AM ()
resetSubscription =
withTokenServer $ \ntfServer -> do
let sub' = sub {ntfQueueId = Nothing, ntfServer, ntfSubId = Nothing, ntfSubStatus = NASNew}
withStore' c $ \db -> supervisorUpdateNtfSub db sub' (NtfSubSMPAction NSASmpKey)
lift . void $ getNtfSMPWorker True c smpServer
NSCDelete -> do
sub_ <- withStore' c $ \db -> do
supervisorUpdateNtfAction db connId (NtfSubNTFAction NSADelete)
getNtfSubscription db connId
logInfo $ "processNtfSub, NSCDelete - sub_ = " <> tshow sub_
case sub_ of
(Just (NtfSubscription {ntfServer}, _)) -> lift . void $ getNtfNTFWorker True c ntfServer
_ -> pure () -- err "NSCDelete - no subscription"
NSCSmpDelete -> do
(cErrs, rqs) <- lift $ partitionErrs id connIds' <$> withStoreBatch c (\db -> map (getQueue db) connIds')
logInfo $ "processNtfCmd, NSCSmpDelete - length rqs = " <> tshow (length rqs)
(cErrs', _) <- lift $ partitionErrs qConnId rqs <$> withStoreBatch' c (\db -> map (updateAction db) rqs)
notifyErrs c (cErrs <> cErrs')
kickSMPWorkers rqs
where
getQueue :: DB.Connection -> ConnId -> IO (Either AgentErrorType RcvQueue)
getQueue db connId = first storeError <$> getPrimaryRcvQueue db connId
updateAction :: DB.Connection -> RcvQueue -> IO ()
updateAction db rq = supervisorUpdateNtfAction db (qConnId rq) (NSASMP NSASmpDelete)
NSCDeleteSub -> void $ lift $ withStoreBatch' c $ \db -> map (deleteNtfSubscription' db) connIds'
where
kickSMPWorkers :: [RcvQueue] -> AM ()
kickSMPWorkers rqs = do
let smpServers = S.fromList $ map qServer rqs
lift $ mapM_ (getNtfSMPWorker True c) smpServers
withStore' c (`getPrimaryRcvQueue` connId) >>= \case
Right rq@RcvQueue {server = smpServer} -> do
logInfo $ "processNtfSub, NSCSmpDelete - rq = " <> tshow rq
withStore' c $ \db -> supervisorUpdateNtfAction db connId (NtfSubSMPAction NSASmpDelete)
lift . void $ getNtfSMPWorker True c smpServer
_ -> notifyInternalError c connId "NSCSmpDelete - no rcv queue"
NSCNtfWorker ntfServer -> lift . void $ getNtfNTFWorker True c ntfServer
NSCNtfSMPWorker smpServer -> lift . void $ getNtfSMPWorker True c smpServer
getNtfNTFWorker :: Bool -> AgentClient -> NtfServer -> AM' Worker
getNtfNTFWorker hasWork c server = do
@@ -198,303 +142,162 @@ getNtfSMPWorker hasWork c server = do
ws <- asks $ ntfSMPWorkers . ntfSupervisor
getAgentWorker "ntf_smp" hasWork c server ws $ runNtfSMPWorker c server
getNtfTknDelWorker :: Bool -> AgentClient -> NtfServer -> AM' Worker
getNtfTknDelWorker hasWork c server = do
ws <- asks $ ntfTknDelWorkers . ntfSupervisor
getAgentWorker "ntf_tkn_del" hasWork c server ws $ runNtfTknDelWorker c server
withTokenServer :: (NtfServer -> AM ()) -> AM ()
withTokenServer action = lift getNtfToken >>= mapM_ (\NtfToken {ntfServer} -> action ntfServer)
runNtfWorker :: AgentClient -> NtfServer -> Worker -> AM ()
runNtfWorker c srv Worker {doWork} =
runNtfWorker c srv Worker {doWork} = do
delay <- asks $ ntfWorkerDelay . config
forever $ do
waitForWork doWork
ExceptT $ agentOperationBracket c AONtfNetwork throwWhenInactive $ runExceptT runNtfOperation
threadDelay delay
where
runNtfOperation :: AM ()
runNtfOperation = do
ntfBatchSize <- asks $ ntfBatchSize . config
withWorkItems c doWork (\db -> getNextNtfSubNTFActions db srv ntfBatchSize) $ \nextSubs -> do
logInfo $ "runNtfWorker - length nextSubs = " <> tshow (length nextSubs)
currTs <- liftIO getCurrentTime
let (creates, checks, deletes, rotates) = splitActions currTs nextSubs
if null creates && null checks && null deletes && null rotates
then
let (_, _, firstActionTs) = L.head nextSubs
in lift $ rescheduleWork doWork currTs firstActionTs
else do
retrySubActions c creates createSubs
retrySubActions c checks checkSubs
retrySubActions c deletes deleteSubs
retrySubActions c rotates rotateSubs
splitActions :: UTCTime -> NonEmpty (NtfSubNTFAction, NtfSubscription, NtfActionTs) -> ([NtfSubscription], [NtfSubscription], [NtfSubscription], [NtfSubscription])
splitActions currTs = foldr addAction ([], [], [], [])
runNtfOperation =
withWork c doWork (`getNextNtfSubNTFAction` srv) $
\nextSub@(NtfSubscription {connId}, _, _) -> do
logInfo $ "runNtfWorker, nextSub " <> tshow nextSub
ri <- asks $ reconnectInterval . config
withRetryInterval ri $ \_ loop -> do
liftIO $ waitForUserNetwork c
processSub nextSub
`catchAgentError` retryOnError c "NtfWorker" loop (workerInternalError c connId . show)
processSub :: (NtfSubscription, NtfSubNTFAction, NtfActionTs) -> AM ()
processSub (sub@NtfSubscription {connId, smpServer, ntfSubId}, action, actionTs) = do
ts <- liftIO getCurrentTime
unlessM (lift $ rescheduleAction doWork ts actionTs) $
case action of
NSACreate ->
lift getNtfToken >>= \case
Just tkn@NtfToken {ntfTokenId = Just tknId, ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
RcvQueue {clientNtfCreds} <- withStore c (`getPrimaryRcvQueue` connId)
case clientNtfCreds of
Just ClientNtfCreds {ntfPrivateKey, notifierId} -> do
nSubId <- agentNtfCreateSubscription c tknId tkn (SMPQueueNtf smpServer notifierId) ntfPrivateKey
-- possible improvement: smaller retry until Active, less frequently (daily?) once Active
let actionTs' = addUTCTime 30 ts
withStore' c $ \db ->
updateNtfSubscription db sub {ntfSubId = Just nSubId, ntfSubStatus = NASCreated NSNew} (NtfSubNTFAction NSACheck) actionTs'
_ -> workerInternalError c connId "NSACreate - no notifier queue credentials"
_ -> workerInternalError c connId "NSACreate - no active token"
NSACheck ->
lift getNtfToken >>= \case
Just tkn ->
case ntfSubId of
Just nSubId ->
agentNtfCheckSubscription c nSubId tkn >>= \case
NSAuth -> do
lift (getNtfServer c) >>= \case
Just ntfServer -> do
withStore' c $ \db ->
updateNtfSubscription db sub {ntfServer, ntfQueueId = Nothing, ntfSubId = Nothing, ntfSubStatus = NASNew} (NtfSubSMPAction NSASmpKey) ts
ns <- asks ntfSupervisor
atomically $ writeTBQueue (ntfSubQ ns) (connId, NSCNtfSMPWorker smpServer)
_ -> workerInternalError c connId "NSACheck - failed to reset subscription, notification server not configured"
status -> updateSubNextCheck ts status
Nothing -> workerInternalError c connId "NSACheck - no subscription ID"
_ -> workerInternalError c connId "NSACheck - no active token"
NSADelete -> case ntfSubId of
Just nSubId ->
(lift getNtfToken >>= mapM_ (agentNtfDeleteSubscription c nSubId))
`agentFinally` continueDeletion
_ -> continueDeletion
where
continueDeletion = do
let sub' = sub {ntfSubId = Nothing, ntfSubStatus = NASOff}
withStore' c $ \db -> updateNtfSubscription db sub' (NtfSubSMPAction NSASmpDelete) ts
ns <- asks ntfSupervisor
atomically $ writeTBQueue (ntfSubQ ns) (connId, NSCNtfSMPWorker smpServer)
NSARotate -> case ntfSubId of
Just nSubId ->
(lift getNtfToken >>= mapM_ (agentNtfDeleteSubscription c nSubId))
`agentFinally` deleteCreate
_ -> deleteCreate
where
deleteCreate = do
withStore' c $ \db -> deleteNtfSubscription db connId
ns <- asks ntfSupervisor
atomically $ writeTBQueue (ntfSubQ ns) (connId, NSCCreate)
where
addAction (cmd, sub, ts) acc@(creates, checks, deletes, rotates) = case cmd of
NSACreate -> (sub : creates, checks, deletes, rotates)
NSACheck
| ts <= currTs -> (creates, sub : checks, deletes, rotates)
| otherwise -> acc
NSADelete -> (creates, checks, sub : deletes, rotates)
NSARotate -> (creates, checks, deletes, sub : rotates)
createSubs :: [NtfSubscription] -> AM' [NtfSubscription]
createSubs ntfSubs =
getNtfToken >>= \case
Just tkn@NtfToken {ntfServer, ntfTokenId = Just tknId, ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
subsRqs_ <- zip ntfSubs <$> withStoreBatch c (\db -> map (getQueue db) ntfSubs)
let (errs1, subs_, newSubs_) = splitSubs tknId subsRqs_
incStatByUserId ntfServer ntfCreateAttempts subs_
case (L.nonEmpty subs_, L.nonEmpty newSubs_) of
(Just subs, Just newSubs) -> do
rs <- L.zip subs <$> agentNtfCreateSubscriptions c tkn newSubs
let (ntfSubs', errs2, nSubIds) = splitResults $ L.toList rs
subs' = map fst nSubIds
errs2' = map (first ntfSubConnId) errs2
incStatByUserId ntfServer ntfCreated subs'
ts <- liftIO getCurrentTime
int <- asks $ ntfSubFirstCheckInterval . config
let checkTs = addUTCTime int ts
(errs3, _) <- partitionErrs ntfSubConnId subs' <$> withStoreBatch' c (\db -> map (updateSubNSACheck db checkTs) nSubIds)
workerErrors c $ errs1 <> errs2' <> errs3
pure ntfSubs'
_ -> workerErrors c errs1 $> []
_ -> do
let errs = map (\sub -> (ntfSubConnId sub, INTERNAL "NSACreate - no active token")) ntfSubs
workerErrors c errs
pure []
where
getQueue :: DB.Connection -> NtfSubscription -> IO (Either AgentErrorType RcvQueue)
getQueue db NtfSubscription {connId} = first storeError <$> getPrimaryRcvQueue db connId
splitSubs :: NtfTokenId -> [(NtfSubscription, Either AgentErrorType RcvQueue)] -> ([(ConnId, AgentErrorType)], [NtfSubscription], [NewNtfEntity 'Subscription])
splitSubs tknId = foldr splitSub ([], [], [])
where
splitSub (sub, rq) (errs, subs, newSubs) = case rq of
Right RcvQueue {clientNtfCreds = Just creds} -> (errs, sub : subs, toNewSub sub creds : newSubs)
Right _ -> ((ntfSubConnId sub, INTERNAL "NSACreate - no notifier queue credentials") : errs, subs, newSubs)
Left e -> ((ntfSubConnId sub, e) : errs, subs, newSubs)
toNewSub NtfSubscription {smpServer} ClientNtfCreds {ntfPrivateKey, notifierId} =
NewNtfSub tknId (SMPQueueNtf smpServer notifierId) ntfPrivateKey
updateSubNSACheck :: DB.Connection -> UTCTime -> (NtfSubscription, NtfSubscriptionId) -> IO ()
updateSubNSACheck db checkTs (sub, nSubId) = updateNtfSubscription db sub {ntfSubId = Just nSubId, ntfSubStatus = NASCreated NSNew} (NSANtf NSACheck) checkTs
checkSubs :: [NtfSubscription] -> AM' [NtfSubscription]
checkSubs ntfSubs =
getNtfToken >>= \case
Just tkn@NtfToken {ntfServer, ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
let (errs1, subs_, subIds_) = splitSubs ntfSubs
incStatByUserId ntfServer ntfCheckAttempts subs_
case (L.nonEmpty subs_, L.nonEmpty subIds_) of
(Just subs, Just subIds) -> do
rs <- L.zip subs <$> agentNtfCheckSubscriptions c tkn subIds
let (ntfSubs', errs2, nSubStatuses) = splitResults $ L.toList rs
subs' = map fst nSubStatuses
(errs2', authSubs) = partitionEithers $ map (\case (sub, NTF _ SMP.AUTH) -> Right sub; e -> Left $ first ntfSubConnId e) errs2
incStatByUserId ntfServer ntfChecked subs'
ts <- liftIO getCurrentTime
int <- asks $ ntfSubCheckInterval . config
let nextCheckTs = addUTCTime int ts
(errs3, srvs) <- partitionErrs ntfSubConnId subs' <$> withStoreBatch' c (\db -> map (updateSub db ntfServer ts nextCheckTs) nSubStatuses)
(errs4, srvs') <- partitionErrs ntfSubConnId authSubs <$> withStoreBatch' c (\db -> map (recreateNtfSub db ntfServer ts) authSubs)
mapM_ (getNtfSMPWorker True c) $ S.fromList (catMaybes srvs <> srvs')
workerErrors c $ errs1 <> errs2' <> errs3 <> errs4
pure ntfSubs'
_ -> workerErrors c errs1 $> []
_ -> do
let errs = map (\sub -> (ntfSubConnId sub, INTERNAL "NSACheck - no active token")) ntfSubs
workerErrors c errs
pure []
where
splitSubs :: [NtfSubscription] -> ([(ConnId, AgentErrorType)], [NtfSubscription], [NtfSubscriptionId])
splitSubs = foldr splitSub ([], [], [])
where
splitSub sub (errs, subs, subIds) = case sub of
NtfSubscription {ntfSubId = Just subId} -> (errs, sub : subs, subId : subIds)
_ -> ((ntfSubConnId sub, INTERNAL "NSACheck - no subscription ID") : errs, subs, subIds)
updateSub :: DB.Connection -> NtfServer -> UTCTime -> UTCTime -> (NtfSubscription, NtfSubStatus) -> IO (Maybe SMPServer)
updateSub db ntfServer ts nextCheckTs (sub, status)
| ntfShouldSubscribe status =
let sub' = sub {ntfSubStatus = NASCreated status}
in Nothing <$ updateNtfSubscription db sub' (NSANtf NSACheck) nextCheckTs
-- ntf server stopped subscribing to this queue
| otherwise = Just <$> recreateNtfSub db ntfServer ts sub
recreateNtfSub :: DB.Connection -> NtfServer -> UTCTime -> NtfSubscription -> IO SMPServer
recreateNtfSub db ntfServer ts sub@NtfSubscription {smpServer} =
let sub' = sub {ntfServer, ntfQueueId = Nothing, ntfSubId = Nothing, ntfSubStatus = NASNew}
in smpServer <$ updateNtfSubscription db sub' (NSASMP NSASmpKey) ts
incStatByUserId :: NtfServer -> (AgentNtfServerStats -> TVar Int) -> [NtfSubscription] -> AM' ()
incStatByUserId ntfServer sel ss =
forM_ (M.assocs userIdsCounts) $ \(userId, count) ->
atomically $ incNtfServerStat' c userId ntfServer sel count
where
userIdsCounts = foldl' (\acc NtfSubscription {userId} -> M.insertWith (+) userId 1 acc) M.empty ss
-- NSADelete and NSARotate are deprecated, but their processing is kept for legacy db records;
-- These actions are not batched
deleteSubs :: [NtfSubscription] -> AM' [NtfSubscription]
deleteSubs ntfSubs = do
retrySubs_ <- mapM (runCatching deleteSub) ntfSubs
pure $ catMaybes retrySubs_
where
deleteSub :: NtfSubscription -> AM (Maybe NtfSubscription)
deleteSub sub@NtfSubscription {smpServer} =
deleteNtfSub sub $ do
let sub' = sub {ntfSubId = Nothing, ntfSubStatus = NASOff}
ts <- liftIO getCurrentTime
withStore' c $ \db -> updateNtfSubscription db sub' (NSASMP NSASmpDelete) ts
lift . void $ getNtfSMPWorker True c smpServer
rotateSubs :: [NtfSubscription] -> AM' [NtfSubscription]
rotateSubs ntfSubs = do
retrySubs_ <- mapM (runCatching rotateSub) ntfSubs
pure $ catMaybes retrySubs_
where
rotateSub :: NtfSubscription -> AM (Maybe NtfSubscription)
rotateSub sub@NtfSubscription {connId} =
deleteNtfSub sub $ do
withStore' c $ \db -> deleteNtfSubscription db connId
ns <- asks ntfSupervisor
atomically $ writeTBQueue (ntfSubQ ns) (NSCCreate, [connId])
runCatching :: (NtfSubscription -> AM (Maybe NtfSubscription)) -> NtfSubscription -> AM' (Maybe NtfSubscription)
runCatching action sub@NtfSubscription {connId} =
fromRight Nothing
<$> runExceptT (action sub `catchAgentError` \e -> workerInternalError c connId (show e) $> Nothing)
-- deleteNtfSub is only used in NSADelete and NSARotate, so also deprecated
deleteNtfSub :: NtfSubscription -> AM () -> AM (Maybe NtfSubscription)
deleteNtfSub sub@NtfSubscription {userId, ntfSubId} continue = case ntfSubId of
Just nSubId ->
lift getNtfToken >>= \case
Just tkn@NtfToken {ntfServer} -> do
atomically $ incNtfServerStat c userId ntfServer ntfDelAttempts
tryAgentError (agentNtfDeleteSubscription c nSubId tkn) >>= \case
Right _ -> do
atomically $ incNtfServerStat c userId ntfServer ntfDeleted
continue'
Left e
| temporaryOrHostError e -> pure $ Just sub -- don't continue, retry
| otherwise -> continue'
Nothing -> continue'
_ -> continue'
where
continue' = continue $> Nothing -- continue without retry
updateSubNextCheck ts toStatus = do
checkInterval <- asks $ ntfSubCheckInterval . config
let nextCheckTs = addUTCTime checkInterval ts
updateSub (NASCreated toStatus) (NtfSubNTFAction NSACheck) nextCheckTs
updateSub toStatus toAction actionTs' =
withStore' c $ \db ->
updateNtfSubscription db sub {ntfSubStatus = toStatus} toAction actionTs'
runNtfSMPWorker :: AgentClient -> SMPServer -> Worker -> AM ()
runNtfSMPWorker c srv Worker {doWork} = forever $ do
waitForWork doWork
ExceptT $ agentOperationBracket c AONtfNetwork throwWhenInactive $ runExceptT runNtfSMPOperation
runNtfSMPWorker c srv Worker {doWork} = do
env <- ask
delay <- asks $ ntfSMPWorkerDelay . config
forever $ do
waitForWork doWork
ExceptT . liftIO . agentOperationBracket c AONtfNetwork throwWhenInactive $
runReaderT (runExceptT runNtfSMPOperation) env
threadDelay delay
where
runNtfSMPOperation :: AM ()
runNtfSMPOperation = do
ntfBatchSize <- asks $ ntfBatchSize . config
withWorkItems c doWork (\db -> getNextNtfSubSMPActions db srv ntfBatchSize) $ \nextSubs -> do
logInfo $ "runNtfSMPWorker - length nextSubs = " <> tshow (length nextSubs)
let (creates, deletes) = splitActions nextSubs
retrySubActions c creates createNotifierKeys
retrySubActions c deletes deleteNotifierKeys
splitActions :: NonEmpty (NtfSubSMPAction, NtfSubscription) -> ([NtfSubscription], [NtfSubscription])
splitActions = foldr addAction ([], [])
where
addAction (cmd, sub) (creates, deletes) = case cmd of
NSASmpKey -> (sub : creates, deletes)
NSASmpDelete -> (creates, sub : deletes)
createNotifierKeys :: [NtfSubscription] -> AM' [NtfSubscription]
createNotifierKeys ntfSubs =
getNtfToken >>= \case
Just NtfToken {ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
(errs1, subRqKeys) <- prepareQueueSmpKey ntfSubs
rs <- enableQueuesNtfs c subRqKeys
let (subRqKeys', errs2, successes) = splitResults rs
ntfSubs' = map eqnrNtfSub subRqKeys'
errs2' = map (first (qConnId . eqnrRq)) errs2
ts <- liftIO getCurrentTime
(errs3, srvs) <- partitionErrs (qConnId . eqnrRq . fst) successes <$> withStoreBatch' c (\db -> map (storeNtfSubCreds db ts) successes)
mapM_ (getNtfNTFWorker True c) $ S.fromList srvs
workerErrors c $ errs1 <> errs2' <> errs3
pure ntfSubs'
_ -> do
let errs = map (\sub -> (ntfSubConnId sub, INTERNAL "NSASmpKey - no active token")) ntfSubs
workerErrors c errs
pure []
where
prepareQueueSmpKey :: [NtfSubscription] -> AM' ([(ConnId, AgentErrorType)], [EnableQueueNtfReq])
prepareQueueSmpKey subs = do
alg <- asks (rcvAuthAlg . config)
g <- asks random
partitionErrs ntfSubConnId subs <$> withStoreBatch c (\db -> map (getQueue db alg g) subs)
where
getQueue :: DB.Connection -> C.AuthAlg -> TVar ChaChaDRG -> NtfSubscription -> IO (Either AgentErrorType EnableQueueNtfReq)
getQueue db (C.AuthAlg a) g sub = fmap (first storeError) $ runExceptT $ do
rq <- ExceptT $ getPrimaryRcvQueue db (ntfSubConnId sub)
authKeyPair <- atomically $ C.generateAuthKeyPair a g
rcvNtfKeyPair <- atomically $ C.generateKeyPair g
pure (EnableQueueNtfReq sub rq authKeyPair rcvNtfKeyPair)
storeNtfSubCreds :: DB.Connection -> UTCTime -> (EnableQueueNtfReq, (SMP.NotifierId, SMP.RcvNtfPublicDhKey)) -> IO NtfServer
storeNtfSubCreds db ts (EnableQueueNtfReq {eqnrNtfSub, eqnrAuthKeyPair = (ntfPublicKey, ntfPrivateKey), eqnrRcvKeyPair = (_, pk)}, (notifierId, srvPubDhKey)) = do
let NtfSubscription {ntfServer} = eqnrNtfSub
rcvNtfDhSecret = C.dh' srvPubDhKey pk
setRcvQueueNtfCreds db (ntfSubConnId eqnrNtfSub) $ Just ClientNtfCreds {ntfPublicKey, ntfPrivateKey, notifierId, rcvNtfDhSecret}
updateNtfSubscription db eqnrNtfSub {ntfQueueId = Just notifierId, ntfSubStatus = NASKey} (NSANtf NSACreate) ts
pure ntfServer
deleteNotifierKeys :: [NtfSubscription] -> AM' [NtfSubscription]
deleteNotifierKeys ntfSubs = do
(errs1, subRqs) <- partitionErrs ntfSubConnId ntfSubs <$> withStoreBatch c (\db -> map (resetCredsGetQueue db) ntfSubs)
rs <- disableQueuesNtfs c subRqs
let (subRqs', errs2, successes) = splitResults rs
ntfSubs' = map fst subRqs'
errs2' = map (first (qConnId . snd)) errs2
disabledRqs = map (snd . fst) successes
(errs3, _) <- partitionErrs qConnId disabledRqs <$> withStoreBatch' c (\db -> map (deleteSub db) disabledRqs)
workerErrors c $ errs1 <> errs2' <> errs3
pure ntfSubs'
where
resetCredsGetQueue :: DB.Connection -> NtfSubscription -> IO (Either AgentErrorType DisableQueueNtfReq)
resetCredsGetQueue db sub@NtfSubscription {connId} = fmap (first storeError) $ runExceptT $ do
liftIO $ setRcvQueueNtfCreds db connId Nothing
rq <- ExceptT $ getPrimaryRcvQueue db connId
pure (sub, rq)
deleteSub :: DB.Connection -> RcvQueue -> IO ()
deleteSub db rq = deleteNtfSubscription db (qConnId rq)
runNtfSMPOperation =
withWork c doWork (`getNextNtfSubSMPAction` srv) $
\nextSub@(NtfSubscription {connId}, _, _) -> do
logInfo $ "runNtfSMPWorker, nextSub " <> tshow nextSub
ri <- asks $ reconnectInterval . config
withRetryInterval ri $ \_ loop -> do
liftIO $ waitForUserNetwork c
processSub nextSub
`catchAgentError` retryOnError c "NtfSMPWorker" loop (workerInternalError c connId . show)
processSub :: (NtfSubscription, NtfSubSMPAction, NtfActionTs) -> AM ()
processSub (sub@NtfSubscription {connId, ntfServer}, smpAction, actionTs) = do
ts <- liftIO getCurrentTime
unlessM (lift $ rescheduleAction doWork ts actionTs) $
case smpAction of
NSASmpKey ->
lift getNtfToken >>= \case
Just NtfToken {ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
rq <- withStore c (`getPrimaryRcvQueue` connId)
C.AuthAlg a <- asks (rcvAuthAlg . config)
g <- asks random
(ntfPublicKey, ntfPrivateKey) <- atomically $ C.generateAuthKeyPair a g
(rcvNtfPubDhKey, rcvNtfPrivDhKey) <- atomically $ C.generateKeyPair g
(notifierId, rcvNtfSrvPubDhKey) <- enableQueueNotifications c rq ntfPublicKey rcvNtfPubDhKey
let rcvNtfDhSecret = C.dh' rcvNtfSrvPubDhKey rcvNtfPrivDhKey
withStore' c $ \db -> do
setRcvQueueNtfCreds db connId $ Just ClientNtfCreds {ntfPublicKey, ntfPrivateKey, notifierId, rcvNtfDhSecret}
updateNtfSubscription db sub {ntfQueueId = Just notifierId, ntfSubStatus = NASKey} (NtfSubNTFAction NSACreate) ts
ns <- asks ntfSupervisor
atomically $ sendNtfSubCommand ns (connId, NSCNtfWorker ntfServer)
_ -> workerInternalError c connId "NSASmpKey - no active token"
NSASmpDelete -> do
rq_ <- withStore' c $ \db -> do
setRcvQueueNtfCreds db connId Nothing
getPrimaryRcvQueue db connId
mapM_ (disableQueueNotifications c) rq_
withStore' c $ \db -> deleteNtfSubscription db connId
retrySubActions :: AgentClient -> [NtfSubscription] -> ([NtfSubscription] -> AM' [NtfSubscription]) -> AM ()
retrySubActions _ [] _ = pure ()
retrySubActions c subs action = do
v <- newTVarIO subs
ri <- asks $ reconnectInterval . config
withRetryInterval ri $ \_ loop -> do
liftIO $ waitWhileSuspended c
liftIO $ waitForUserNetwork c
subs' <- readTVarIO v
retrySubs <- lift $ action subs'
unless (null retrySubs) $ do
atomically $ writeTVar v retrySubs
retryNetworkLoop c loop
rescheduleAction :: TMVar () -> UTCTime -> UTCTime -> AM' Bool
rescheduleAction doWork ts actionTs
| actionTs <= ts = pure False
| otherwise = do
void . atomically $ tryTakeTMVar doWork
void . forkIO $ do
liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime actionTs ts
atomically $ hasWorkToDo' doWork
pure True
-- (temporary errs, other errs, successes)
splitResults :: [(a, Either AgentErrorType r)] -> ([a], [(a, AgentErrorType)], [(a, r)])
splitResults = foldr addRes ([], [], [])
retryOnError :: AgentClient -> Text -> AM () -> (AgentErrorType -> AM ()) -> AgentErrorType -> AM ()
retryOnError c name loop done e = do
logError $ name <> " error: " <> tshow e
case e of
BROKER _ NETWORK -> retryLoop
BROKER _ TIMEOUT -> retryLoop
_ -> done e
where
addRes (a, r_) (as, errs, rs) = case r_ of
Right r -> (as, errs, (a, r) : rs)
Left e
| temporaryOrHostError e -> (a : as, errs, rs)
| otherwise -> (as, (a, e) : errs, rs)
rescheduleWork :: TMVar () -> UTCTime -> UTCTime -> AM' ()
rescheduleWork doWork ts actionTs = do
void . atomically $ tryTakeTMVar doWork
void . forkIO $ do
liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime actionTs ts
atomically $ hasWorkToDo' doWork
retryNetworkLoop :: AgentClient -> AM () -> AM ()
retryNetworkLoop c loop = do
atomically $ endAgentOperation c AONtfNetwork
liftIO $ throwWhenInactive c
atomically $ beginAgentOperation c AONtfNetwork
loop
workerErrors :: AgentClient -> [(ConnId, AgentErrorType)] -> AM' ()
workerErrors c connErrs =
unless (null connErrs) $ do
void $ withStoreBatch' c (\db -> map (setNullNtfSubscriptionAction db . fst) connErrs)
notifyErrs c connErrs
retryLoop = do
atomically $ endAgentOperation c AONtfNetwork
atomically $ throwWhenInactive c
atomically $ beginAgentOperation c AONtfNetwork
loop
workerInternalError :: AgentClient -> ConnId -> String -> AM ()
workerInternalError c connId internalErrStr = do
@@ -506,14 +309,6 @@ notifyInternalError :: MonadIO m => AgentClient -> ConnId -> String -> m ()
notifyInternalError AgentClient {subQ} connId internalErrStr = atomically $ writeTBQueue subQ ("", connId, AEvt SAEConn $ ERR $ INTERNAL internalErrStr)
{-# INLINE notifyInternalError #-}
notifyInternalError' :: MonadIO m => AgentClient -> String -> m ()
notifyInternalError' AgentClient {subQ} internalErrStr = atomically $ writeTBQueue subQ ("", "", AEvt SAEConn $ ERR $ INTERNAL internalErrStr)
{-# INLINE notifyInternalError' #-}
notifyErrs :: MonadIO m => AgentClient -> [(ConnId, AgentErrorType)] -> m ()
notifyErrs AgentClient {subQ} connErrs = unless (null connErrs) $ atomically $ writeTBQueue subQ ("", "", AEvt SAENone $ ERRS connErrs)
{-# INLINE notifyErrs #-}
getNtfToken :: AM' (Maybe NtfToken)
getNtfToken = do
tkn <- asks $ ntfTkn . ntfSupervisor
@@ -525,7 +320,7 @@ nsUpdateToken ns tkn = writeTVar (ntfTkn ns) $ Just tkn
nsRemoveNtfToken :: NtfSupervisor -> STM ()
nsRemoveNtfToken ns = writeTVar (ntfTkn ns) Nothing
sendNtfSubCommand :: NtfSupervisor -> (NtfSupervisorCommand, NonEmpty ConnId) -> STM ()
sendNtfSubCommand :: NtfSupervisor -> (ConnId, NtfSupervisorCommand) -> STM ()
sendNtfSubCommand ns cmd = do
tkn <- readTVar (ntfTkn ns)
when (instantNotifications tkn) $ writeTBQueue (ntfSubQ ns) cmd
@@ -535,51 +330,10 @@ instantNotifications = \case
Just NtfToken {ntfTknStatus = NTActive, ntfMode = NMInstant} -> True
_ -> False
deleteToken :: AgentClient -> NtfToken -> AM ()
deleteToken c tkn@NtfToken {ntfServer, ntfTokenId, ntfPrivKey} = do
setToDelete <- withStore' c $ \db -> do
removeNtfToken db tkn
case ntfTokenId of
Just tknId -> addNtfTokenToDelete db ntfServer ntfPrivKey tknId $> True
Nothing -> pure False
ns <- asks ntfSupervisor
atomically $ nsRemoveNtfToken ns
when setToDelete $ void $ lift $ getNtfTknDelWorker True c ntfServer
runNtfTknDelWorker :: AgentClient -> NtfServer -> Worker -> AM ()
runNtfTknDelWorker c srv Worker {doWork} =
forever $ do
waitForWork doWork
ExceptT $ agentOperationBracket c AONtfNetwork throwWhenInactive $ runExceptT runNtfOperation
where
runNtfOperation :: AM ()
runNtfOperation =
withWork c doWork (`getNextNtfTokenToDelete` srv) $
\nextTknToDelete -> do
logInfo $ "runNtfTknDelWorker, nextTknToDelete " <> tshow nextTknToDelete
ri <- asks $ reconnectInterval . config
withRetryInterval ri $ \_ loop -> do
liftIO $ waitWhileSuspended c
liftIO $ waitForUserNetwork c
processTknToDelete nextTknToDelete `catchAgentError` retryTmpError loop nextTknToDelete
retryTmpError :: AM () -> NtfTokenToDelete -> AgentErrorType -> AM ()
retryTmpError loop (tknDbId, _, _) e = do
logError $ "ntf tkn del error: " <> tshow e
if temporaryOrHostError e
then retryNetworkLoop c loop
else do
withStore' c $ \db -> deleteNtfTokenToDelete db tknDbId
notifyInternalError' c (show e)
processTknToDelete :: NtfTokenToDelete -> AM ()
processTknToDelete (tknDbId, ntfPrivKey, tknId) = do
agentNtfDeleteToken c srv ntfPrivKey tknId
withStore' c $ \db -> deleteNtfTokenToDelete db tknDbId
closeNtfSupervisor :: NtfSupervisor -> IO ()
closeNtfSupervisor ns = do
stopWorkers $ ntfWorkers ns
stopWorkers $ ntfSMPWorkers ns
stopWorkers $ ntfTknDelWorkers ns
where
stopWorkers workers = atomically (swapTVar workers M.empty) >>= mapM_ (liftIO . cancelWorker)
+11 -30
View File
@@ -42,7 +42,6 @@ module Simplex.Messaging.Agent.Protocol
deliveryRcptsSMPAgentVersion,
pqdrSMPAgentVersion,
sndAuthKeySMPAgentVersion,
ratchetOnConfSMPAgentVersion,
currentSMPAgentVersion,
supportedSMPAgentVRange,
e2eEncConnInfoLength,
@@ -50,8 +49,6 @@ module Simplex.Messaging.Agent.Protocol
-- * SMP agent protocol types
ConnInfo,
SndQueueSecured,
AEntityId,
ACommand (..),
AEvent (..),
AEvt (..),
@@ -156,8 +153,8 @@ import Data.Int (Int64)
import Data.Kind (Type)
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.Map (Map)
import qualified Data.Map as M
import Data.Maybe (fromMaybe, isJust)
import Data.Text (Text)
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
@@ -191,6 +188,7 @@ import Simplex.Messaging.Parsers
import Simplex.Messaging.Protocol
( AProtocolType,
BrokerErrorType (..),
EntityId,
ErrorType,
MsgBody,
MsgFlags,
@@ -198,6 +196,7 @@ import Simplex.Messaging.Protocol
NMsgMeta,
ProtocolServer (..),
SMPClientVersion,
SMPMsgMeta,
SMPServer,
SMPServerWithAuth,
SndPublicAuthKey,
@@ -258,14 +257,11 @@ pqdrSMPAgentVersion = VersionSMPA 5
sndAuthKeySMPAgentVersion :: VersionSMPA
sndAuthKeySMPAgentVersion = VersionSMPA 6
ratchetOnConfSMPAgentVersion :: VersionSMPA
ratchetOnConfSMPAgentVersion = VersionSMPA 7
minSupportedSMPAgentVersion :: VersionSMPA
minSupportedSMPAgentVersion = duplexHandshakeSMPAgentVersion
currentSMPAgentVersion :: VersionSMPA
currentSMPAgentVersion = VersionSMPA 7
currentSMPAgentVersion = VersionSMPA 6
supportedSMPAgentVRange :: VersionRangeSMPA
supportedSMPAgentVRange = mkVersionRange minSupportedSMPAgentVersion currentSMPAgentVersion
@@ -276,22 +272,20 @@ supportedSMPAgentVRange = mkVersionRange minSupportedSMPAgentVersion currentSMPA
e2eEncConnInfoLength :: VersionSMPA -> PQSupport -> Int
e2eEncConnInfoLength v = \case
-- reduced by 3726 (roughly the increase of message ratchet header size + key and ciphertext in reply link)
PQSupportOn | v >= pqdrSMPAgentVersion -> 11106
_ -> 14832
PQSupportOn | v >= pqdrSMPAgentVersion -> 11122
_ -> 14848
e2eEncAgentMsgLength :: VersionSMPA -> PQSupport -> Int
e2eEncAgentMsgLength v = \case
-- reduced by 2222 (the increase of message ratchet header size)
PQSupportOn | v >= pqdrSMPAgentVersion -> 13618
_ -> 15840
PQSupportOn | v >= pqdrSMPAgentVersion -> 13634
_ -> 15856
-- | SMP agent event
type ATransmission = (ACorrId, AEntityId, AEvt)
type ATransmission = (ACorrId, EntityId, AEvt)
type UserId = Int64
type AEntityId = ByteString
type ACorrId = ByteString
data AEntity = AEConn | AERcvFile | AESndFile | AENone
@@ -333,8 +327,6 @@ deriving instance Show AEvt
type ConnInfo = ByteString
type SndQueueSecured = Bool
-- | Parameterized type for SMP agent events
data AEvent (e :: AEntity) where
INV :: AConnectionRequestUri -> AEvent AEConn
@@ -343,7 +335,6 @@ data AEvent (e :: AEntity) where
INFO :: PQSupport -> ConnInfo -> AEvent AEConn
CON :: PQEncryption -> AEvent AEConn -- notification that connection is established
END :: AEvent AEConn
DELD :: AEvent AEConn
CONNECT :: AProtocolType -> TransportHost -> AEvent AENone
DISCONNECT :: AProtocolType -> TransportHost -> AEvent AENone
DOWN :: SMPServer -> [ConnId] -> AEvent AENone
@@ -355,7 +346,7 @@ data AEvent (e :: AEntity) where
MERR :: AgentMsgId -> AgentErrorType -> AEvent AEConn
MERRS :: NonEmpty AgentMsgId -> AgentErrorType -> AEvent AEConn
MSG :: MsgMeta -> MsgFlags -> MsgBody -> AEvent AEConn
MSGNTF :: MsgId -> Maybe UTCTime -> AEvent AEConn
MSGNTF :: SMPMsgMeta -> AEvent AEConn
RCVD :: MsgMeta -> NonEmpty MsgReceipt -> AEvent AEConn
QCONT :: AEvent AEConn
DEL_RCVQ :: SMPServer -> SMP.RecipientId -> Maybe AgentErrorType -> AEvent AEConn
@@ -363,9 +354,7 @@ data AEvent (e :: AEntity) where
DEL_USER :: Int64 -> AEvent AENone
STAT :: ConnectionStats -> AEvent AEConn
OK :: AEvent AEConn
JOINED :: SndQueueSecured -> AEvent AEConn
ERR :: AgentErrorType -> AEvent AEConn
ERRS :: [(ConnId, AgentErrorType)] -> AEvent AENone
SUSPENDED :: AEvent AENone
RFPROG :: Int64 -> Int64 -> AEvent AERcvFile
RFDONE :: FilePath -> AEvent AERcvFile
@@ -414,7 +403,6 @@ data AEventTag (e :: AEntity) where
INFO_ :: AEventTag AEConn
CON_ :: AEventTag AEConn
END_ :: AEventTag AEConn
DELD_ :: AEventTag AEConn
CONNECT_ :: AEventTag AENone
DISCONNECT_ :: AEventTag AENone
DOWN_ :: AEventTag AENone
@@ -434,9 +422,7 @@ data AEventTag (e :: AEntity) where
DEL_USER_ :: AEventTag AENone
STAT_ :: AEventTag AEConn
OK_ :: AEventTag AEConn
JOINED_ :: AEventTag AEConn
ERR_ :: AEventTag AEConn
ERRS_ :: AEventTag AENone
SUSPENDED_ :: AEventTag AENone
-- XFTP commands and responses
RFDONE_ :: AEventTag AERcvFile
@@ -469,7 +455,6 @@ aEventTag = \case
INFO {} -> INFO_
CON _ -> CON_
END -> END_
DELD -> DELD_
CONNECT {} -> CONNECT_
DISCONNECT {} -> DISCONNECT_
DOWN {} -> DOWN_
@@ -489,9 +474,7 @@ aEventTag = \case
DEL_USER _ -> DEL_USER_
STAT _ -> STAT_
OK -> OK_
JOINED _ -> JOINED_
ERR _ -> ERR_
ERRS _ -> ERRS_
SUSPENDED -> SUSPENDED_
RFPROG {} -> RFPROG_
RFDONE {} -> RFDONE_
@@ -1343,8 +1326,6 @@ data AgentErrorType
CMD {cmdErr :: CommandErrorType, errContext :: String}
| -- | connection errors
CONN {connErr :: ConnectionErrorType}
| -- | user not found in database
NO_USER
| -- | SMP protocol errors forwarded to agent clients
SMP {serverAddress :: String, smpErr :: ErrorType}
| -- | NTF protocol errors forwarded to agent clients
+1 -24
View File
@@ -9,7 +9,6 @@ module Simplex.Messaging.Agent.RetryInterval
RI2State (..),
withRetryInterval,
withRetryIntervalCount,
withRetryForeground,
withRetryLock2,
updateRetryInterval2,
nextRetryDelay,
@@ -17,11 +16,10 @@ module Simplex.Messaging.Agent.RetryInterval
where
import Control.Concurrent (forkIO)
import Control.Concurrent.STM (retry)
import Control.Monad (void)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Int (Int64)
import Simplex.Messaging.Util (threadDelay', unlessM, whenM)
import Simplex.Messaging.Util (threadDelay', whenM)
import UnliftIO.STM
data RetryInterval = RetryInterval
@@ -65,27 +63,6 @@ withRetryIntervalCount ri action = callAction 0 0 $ initialInterval ri
let elapsed' = elapsed + delay
callAction (n + 1) elapsed' $ nextRetryDelay elapsed' delay ri
withRetryForeground :: forall m a. MonadIO m => RetryInterval -> STM Bool -> STM Bool -> (Int64 -> m a -> m a) -> m a
withRetryForeground ri isForeground isOnline action = callAction 0 $ initialInterval ri
where
callAction :: Int64 -> Int64 -> m a
callAction elapsed delay = action delay loop
where
loop = do
-- limit delay to max Int value (~36 minutes on for 32 bit architectures)
d <- registerDelay $ fromIntegral $ min delay (fromIntegral (maxBound :: Int))
(wasForeground, wasOnline) <- atomically $ (,) <$> isForeground <*> isOnline
reset <- atomically $ do
foreground <- isForeground
online <- isOnline
let reset = (not wasForeground && foreground) || (not wasOnline && online)
unlessM ((reset ||) <$> readTVar d) retry
pure reset
let (elapsed', delay')
| reset = (0, initialInterval ri)
| otherwise = (elapsed + delay, nextRetryDelay elapsed' delay ri)
callAction elapsed' delay'
-- This function allows action to toggle between slow and fast retry intervals.
withRetryLock2 :: forall m. MonadIO m => RetryInterval2 -> TMVar () -> (RI2State -> (RetryIntervalMode -> m ()) -> m ()) -> m ()
withRetryLock2 RetryInterval2 {riSlow, riFast} lock action =
+10 -166
View File
@@ -1,20 +1,17 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE TemplateHaskell #-}
module Simplex.Messaging.Agent.Stats where
import Data.Aeson (FromJSON (..), FromJSONKey, ToJSON (..))
import qualified Data.Aeson.TH as J
import Data.Int (Int64)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Map (Map)
import Database.SQLite.Simple.FromField (FromField (..))
import Database.SQLite.Simple.ToField (ToField (..))
import Simplex.Messaging.Agent.Protocol (UserId)
import Simplex.Messaging.Parsers (defaultJSON, fromTextField_)
import Simplex.Messaging.Protocol (SMPServer, XFTPServer, NtfServer)
import Simplex.Messaging.Protocol (SMPServer, XFTPServer)
import Simplex.Messaging.Util (decodeJSON, encodeJSON)
import UnliftIO.STM
@@ -47,12 +44,7 @@ data AgentSMPServerStats = AgentSMPServerStats
connSubscribed :: TVar Int, -- total successful subscription
connSubAttempts :: TVar Int, -- subscription attempts
connSubIgnored :: TVar Int, -- subscription results ignored (client switched to different session or it was not pending)
connSubErrs :: TVar Int, -- permanent subscription errors (temporary accounted for in attempts)
-- notifications stats
ntfKey :: TVar Int,
ntfKeyAttempts :: TVar Int,
ntfKeyDeleted :: TVar Int,
ntfKeyDeleteAttempts :: TVar Int
connSubErrs :: TVar Int -- permanent subscription errors (temporary accounted for in attempts)
}
data AgentSMPServerStatsData = AgentSMPServerStatsData
@@ -83,17 +75,10 @@ data AgentSMPServerStatsData = AgentSMPServerStatsData
_connSubscribed :: Int,
_connSubAttempts :: Int,
_connSubIgnored :: Int,
_connSubErrs :: Int,
_ntfKey :: OptionalInt,
_ntfKeyAttempts :: OptionalInt,
_ntfKeyDeleted :: OptionalInt,
_ntfKeyDeleteAttempts :: OptionalInt
_connSubErrs :: Int
}
deriving (Show)
newtype OptionalInt = OInt {toInt :: Int}
deriving (Num, Show, ToJSON)
newAgentSMPServerStats :: STM AgentSMPServerStats
newAgentSMPServerStats = do
sentDirect <- newTVar 0
@@ -124,10 +109,6 @@ newAgentSMPServerStats = do
connSubAttempts <- newTVar 0
connSubIgnored <- newTVar 0
connSubErrs <- newTVar 0
ntfKey <- newTVar 0
ntfKeyAttempts <- newTVar 0
ntfKeyDeleted <- newTVar 0
ntfKeyDeleteAttempts <- newTVar 0
pure
AgentSMPServerStats
{ sentDirect,
@@ -157,11 +138,7 @@ newAgentSMPServerStats = do
connSubscribed,
connSubAttempts,
connSubIgnored,
connSubErrs,
ntfKey,
ntfKeyAttempts,
ntfKeyDeleted,
ntfKeyDeleteAttempts
connSubErrs
}
newAgentSMPServerStatsData :: AgentSMPServerStatsData
@@ -194,11 +171,7 @@ newAgentSMPServerStatsData =
_connSubscribed = 0,
_connSubAttempts = 0,
_connSubIgnored = 0,
_connSubErrs = 0,
_ntfKey = 0,
_ntfKeyAttempts = 0,
_ntfKeyDeleted = 0,
_ntfKeyDeleteAttempts = 0
_connSubErrs = 0
}
newAgentSMPServerStats' :: AgentSMPServerStatsData -> STM AgentSMPServerStats
@@ -231,10 +204,6 @@ newAgentSMPServerStats' s = do
connSubAttempts <- newTVar $ _connSubAttempts s
connSubIgnored <- newTVar $ _connSubIgnored s
connSubErrs <- newTVar $ _connSubErrs s
ntfKey <- newTVar $ toInt $ _ntfKey s
ntfKeyAttempts <- newTVar $ toInt $ _ntfKeyAttempts s
ntfKeyDeleted <- newTVar $ toInt $ _ntfKeyDeleted s
ntfKeyDeleteAttempts <- newTVar $ toInt $ _ntfKeyDeleteAttempts s
pure
AgentSMPServerStats
{ sentDirect,
@@ -264,11 +233,7 @@ newAgentSMPServerStats' s = do
connSubscribed,
connSubAttempts,
connSubIgnored,
connSubErrs,
ntfKey,
ntfKeyAttempts,
ntfKeyDeleted,
ntfKeyDeleteAttempts
connSubErrs
}
-- as this is used to periodically update stats in db,
@@ -303,10 +268,6 @@ getAgentSMPServerStats s = do
_connSubAttempts <- readTVarIO $ connSubAttempts s
_connSubIgnored <- readTVarIO $ connSubIgnored s
_connSubErrs <- readTVarIO $ connSubErrs s
_ntfKey <- OInt <$> readTVarIO (ntfKey s)
_ntfKeyAttempts <- OInt <$> readTVarIO (ntfKeyAttempts s)
_ntfKeyDeleted <- OInt <$> readTVarIO (ntfKeyDeleted s)
_ntfKeyDeleteAttempts <- OInt <$> readTVarIO (ntfKeyDeleteAttempts s)
pure
AgentSMPServerStatsData
{ _sentDirect,
@@ -336,11 +297,7 @@ getAgentSMPServerStats s = do
_connSubscribed,
_connSubAttempts,
_connSubIgnored,
_connSubErrs,
_ntfKey,
_ntfKeyAttempts,
_ntfKeyDeleted,
_ntfKeyDeleteAttempts
_connSubErrs
}
addSMPStatsData :: AgentSMPServerStatsData -> AgentSMPServerStatsData -> AgentSMPServerStatsData
@@ -373,11 +330,7 @@ addSMPStatsData sd1 sd2 =
_connSubscribed = _connSubscribed sd1 + _connSubscribed sd2,
_connSubAttempts = _connSubAttempts sd1 + _connSubAttempts sd2,
_connSubIgnored = _connSubIgnored sd1 + _connSubIgnored sd2,
_connSubErrs = _connSubErrs sd1 + _connSubErrs sd2,
_ntfKey = _ntfKey sd1 + _ntfKey sd2,
_ntfKeyAttempts = _ntfKeyAttempts sd1 + _ntfKeyAttempts sd2,
_ntfKeyDeleted = _ntfKeyDeleted sd1 + _ntfKeyDeleted sd2,
_ntfKeyDeleteAttempts = _ntfKeyDeleteAttempts sd1 + _ntfKeyDeleteAttempts sd2
_connSubErrs = _connSubErrs sd1 + _connSubErrs sd2
}
data AgentXFTPServerStats = AgentXFTPServerStats
@@ -537,127 +490,18 @@ addXFTPStatsData sd1 sd2 =
_deleteErrs = _deleteErrs sd1 + _deleteErrs sd2
}
data AgentNtfServerStats = AgentNtfServerStats
{ ntfCreated :: TVar Int,
ntfCreateAttempts :: TVar Int,
ntfChecked :: TVar Int,
ntfCheckAttempts :: TVar Int,
ntfDeleted :: TVar Int,
ntfDelAttempts :: TVar Int
}
data AgentNtfServerStatsData = AgentNtfServerStatsData
{ _ntfCreated :: Int,
_ntfCreateAttempts :: Int,
_ntfChecked :: Int,
_ntfCheckAttempts :: Int,
_ntfDeleted :: Int,
_ntfDelAttempts :: Int
}
deriving (Show)
newAgentNtfServerStats :: STM AgentNtfServerStats
newAgentNtfServerStats = do
ntfCreated <- newTVar 0
ntfCreateAttempts <- newTVar 0
ntfChecked <- newTVar 0
ntfCheckAttempts <- newTVar 0
ntfDeleted <- newTVar 0
ntfDelAttempts <- newTVar 0
pure
AgentNtfServerStats
{ ntfCreated,
ntfCreateAttempts,
ntfChecked,
ntfCheckAttempts,
ntfDeleted,
ntfDelAttempts
}
newAgentNtfServerStatsData :: AgentNtfServerStatsData
newAgentNtfServerStatsData =
AgentNtfServerStatsData
{ _ntfCreated = 0,
_ntfCreateAttempts = 0,
_ntfChecked = 0,
_ntfCheckAttempts = 0,
_ntfDeleted = 0,
_ntfDelAttempts = 0
}
newAgentNtfServerStats' :: AgentNtfServerStatsData -> STM AgentNtfServerStats
newAgentNtfServerStats' s = do
ntfCreated <- newTVar $ _ntfCreated s
ntfCreateAttempts <- newTVar $ _ntfCreateAttempts s
ntfChecked <- newTVar $ _ntfChecked s
ntfCheckAttempts <- newTVar $ _ntfCheckAttempts s
ntfDeleted <- newTVar $ _ntfDeleted s
ntfDelAttempts <- newTVar $ _ntfDelAttempts s
pure
AgentNtfServerStats
{ ntfCreated,
ntfCreateAttempts,
ntfChecked,
ntfCheckAttempts,
ntfDeleted,
ntfDelAttempts
}
getAgentNtfServerStats :: AgentNtfServerStats -> IO AgentNtfServerStatsData
getAgentNtfServerStats s = do
_ntfCreated <- readTVarIO $ ntfCreated s
_ntfCreateAttempts <- readTVarIO $ ntfCreateAttempts s
_ntfChecked <- readTVarIO $ ntfChecked s
_ntfCheckAttempts <- readTVarIO $ ntfCheckAttempts s
_ntfDeleted <- readTVarIO $ ntfDeleted s
_ntfDelAttempts <- readTVarIO $ ntfDelAttempts s
pure
AgentNtfServerStatsData
{ _ntfCreated,
_ntfCreateAttempts,
_ntfChecked,
_ntfCheckAttempts,
_ntfDeleted,
_ntfDelAttempts
}
addNtfStatsData :: AgentNtfServerStatsData -> AgentNtfServerStatsData -> AgentNtfServerStatsData
addNtfStatsData sd1 sd2 =
AgentNtfServerStatsData
{ _ntfCreated = _ntfCreated sd1 + _ntfCreated sd2,
_ntfCreateAttempts = _ntfCreateAttempts sd1 + _ntfCreateAttempts sd2,
_ntfChecked = _ntfChecked sd1 + _ntfChecked sd2,
_ntfCheckAttempts = _ntfCheckAttempts sd1 + _ntfCheckAttempts sd2,
_ntfDeleted = _ntfDeleted sd1 + _ntfDeleted sd2,
_ntfDelAttempts = _ntfDelAttempts sd1 + _ntfDelAttempts sd2
}
-- Type for gathering both smp and xftp stats across all users and servers,
-- to then be persisted to db as a single json.
data AgentPersistedServerStats = AgentPersistedServerStats
{ smpServersStats :: Map (UserId, SMPServer) AgentSMPServerStatsData,
xftpServersStats :: Map (UserId, XFTPServer) AgentXFTPServerStatsData,
ntfServersStats :: OptionalMap (UserId, NtfServer) AgentNtfServerStatsData
xftpServersStats :: Map (UserId, XFTPServer) AgentXFTPServerStatsData
}
deriving (Show)
instance FromJSON OptionalInt where
parseJSON v = OInt <$> parseJSON v
omittedField = Just (OInt 0)
newtype OptionalMap k v = OptionalMap (Map k v)
deriving (Show, ToJSON)
instance (FromJSONKey k, Ord k, FromJSON v) => FromJSON (OptionalMap k v) where
parseJSON v = OptionalMap <$> parseJSON v
omittedField = Just (OptionalMap M.empty)
$(J.deriveJSON defaultJSON ''AgentSMPServerStatsData)
$(J.deriveJSON defaultJSON ''AgentXFTPServerStatsData)
$(J.deriveJSON defaultJSON ''AgentNtfServerStatsData)
$(J.deriveJSON defaultJSON ''AgentPersistedServerStats)
instance ToField AgentPersistedServerStats where
+79 -219
View File
@@ -1,12 +1,10 @@
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
@@ -17,7 +15,6 @@
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeOperators #-}
@@ -60,7 +57,6 @@ module Simplex.Messaging.Agent.Store.SQLite
getDeletedConns,
getConnData,
setConnDeleted,
setConnUserId,
setConnAgentVersion,
setConnPQSupport,
getDeletedConnIds,
@@ -114,7 +110,6 @@ module Simplex.Messaging.Agent.Store.SQLite
getSndMsgViaRcpt,
updateSndMsgRcpt,
getPendingQueueMsg,
getConnectionsForDelivery,
updatePendingMsgRIState,
deletePendingMsgs,
getExpiredSndMessages,
@@ -122,7 +117,6 @@ module Simplex.Messaging.Agent.Store.SQLite
getRcvMsg,
getLastMsg,
checkRcvMsgHashExists,
getRcvMsgBrokerTs,
deleteMsg,
deleteDeliveredSndMsg,
deleteSndMsgDelivery,
@@ -141,7 +135,6 @@ module Simplex.Messaging.Agent.Store.SQLite
createCommand,
getPendingCommandServers,
getPendingServerCommand,
updateCommandServer,
deleteCommand,
-- Notification device token persistence
createNtfToken,
@@ -151,15 +144,7 @@ module Simplex.Messaging.Agent.Store.SQLite
updateNtfMode,
updateNtfToken,
removeNtfToken,
addNtfTokenToDelete,
deleteExpiredNtfTokensToDelete,
NtfTokenToDelete,
getNextNtfTokenToDelete,
markNtfTokenToDeleteFailed_, -- exported for tests
getPendingDelTknServers,
deleteNtfTokenToDelete,
-- Notification subscription persistence
NtfSupervisorSub,
getNtfSubscription,
createNtfSubscription,
supervisorUpdateNtfSub,
@@ -167,10 +152,9 @@ module Simplex.Messaging.Agent.Store.SQLite
updateNtfSubscription,
setNullNtfSubscriptionAction,
deleteNtfSubscription,
deleteNtfSubscription',
getNextNtfSubNTFActions,
getNextNtfSubNTFAction,
markNtfSubActionNtfFailed_, -- exported for tests
getNextNtfSubSMPActions,
getNextNtfSubSMPAction,
markNtfSubActionSMPFailed_, -- exported for tests
getActiveNtfToken,
getNtfRcvQueue,
@@ -236,7 +220,7 @@ module Simplex.Messaging.Agent.Store.SQLite
-- * utilities
withConnection,
withTransaction,
withTransactionPriority,
withTransactionCtx,
firstRow,
firstRow',
maybeFirstRow,
@@ -408,10 +392,10 @@ connectSQLiteStore dbFilePath key keepKey = do
dbNew <- not <$> doesFileExist dbFilePath
dbConn <- dbBusyLoop (connectDB dbFilePath key)
dbConnection <- newMVar dbConn
dbKey <- newTVarIO $! storeKey key keepKey
dbClosed <- newTVarIO False
dbSem <- newTVarIO 0
pure SQLiteStore {dbFilePath, dbKey, dbSem, dbConnection, dbNew, dbClosed}
atomically $ do
dbKey <- newTVar $! storeKey key keepKey
dbClosed <- newTVar False
pure SQLiteStore {dbFilePath, dbKey, dbConnection, dbNew, dbClosed}
connectDB :: FilePath -> ScrubbedBytes -> IO DB.Connection
connectDB path key = do
@@ -981,12 +965,12 @@ createRcvMsg db connId rq rcvMsgData@RcvMsgData {msgMeta = MsgMeta {sndMsgId}, i
insertRcvMsgDetails_ db connId rq rcvMsgData
updateRcvMsgHash db connId sndMsgId internalRcvId internalHash
updateSndIds :: DB.Connection -> ConnId -> IO (Either StoreError (InternalId, InternalSndId, PrevSndMsgHash))
updateSndIds db connId = runExceptT $ do
(lastInternalId, lastInternalSndId, prevSndHash) <- ExceptT $ retrieveLastIdsAndHashSnd_ db connId
updateSndIds :: DB.Connection -> ConnId -> IO (InternalId, InternalSndId, PrevSndMsgHash)
updateSndIds db connId = do
(lastInternalId, lastInternalSndId, prevSndHash) <- retrieveLastIdsAndHashSnd_ db connId
let internalId = InternalId $ unId lastInternalId + 1
internalSndId = InternalSndId $ unSndId lastInternalSndId + 1
liftIO $ updateLastIdsSnd_ db connId internalId internalSndId
updateLastIdsSnd_ db connId internalId internalSndId
pure (internalId, internalSndId, prevSndHash)
createSndMsg :: DB.Connection -> ConnId -> SndMsgData -> IO ()
@@ -1024,10 +1008,6 @@ updateSndMsgRcpt db connId sndMsgId MsgReceipt {agentMsgId, msgRcptStatus} =
"UPDATE snd_messages SET rcpt_internal_id = ?, rcpt_status = ? WHERE conn_id = ? AND internal_snd_id = ?"
(agentMsgId, msgRcptStatus, connId, sndMsgId)
getConnectionsForDelivery :: DB.Connection -> IO [ConnId]
getConnectionsForDelivery db =
map fromOnly <$> DB.query_ db "SELECT DISTINCT conn_id FROM snd_message_deliveries WHERE failed = 0"
getPendingQueueMsg :: DB.Connection -> ConnId -> SndQueue -> IO (Either StoreError (Maybe (Maybe RcvQueue, PendingMsgData)))
getPendingQueueMsg db connId SndQueue {dbQueueId} =
getWorkItem "message" getMsgId getMsgData markMsgFailed
@@ -1071,26 +1051,17 @@ getPendingQueueMsg db connId SndQueue {dbQueueId} =
getWorkItem :: Show i => ByteString -> IO (Maybe i) -> (i -> IO (Either StoreError a)) -> (i -> IO ()) -> IO (Either StoreError (Maybe a))
getWorkItem itemName getId getItem markFailed =
runExceptT $ handleWrkErr itemName "getId" getId >>= mapM (tryGetItem itemName getItem markFailed)
getWorkItems :: Show i => ByteString -> IO [i] -> (i -> IO (Either StoreError a)) -> (i -> IO ()) -> IO (Either StoreError [Either StoreError a])
getWorkItems itemName getIds getItem markFailed =
runExceptT $ handleWrkErr itemName "getIds" getIds >>= mapM (tryE . tryGetItem itemName getItem markFailed)
tryGetItem :: Show i => ByteString -> (i -> IO (Either StoreError a)) -> (i -> IO ()) -> i -> ExceptT StoreError IO a
tryGetItem itemName getItem markFailed itemId = ExceptT (getItem itemId) `catchStoreError` \e -> mark >> throwE e
runExceptT $ handleErr "getId" getId >>= mapM tryGetItem
where
mark = handleWrkErr itemName ("markFailed ID " <> bshow itemId) $ markFailed itemId
catchStoreError :: ExceptT StoreError IO a -> (StoreError -> ExceptT StoreError IO a) -> ExceptT StoreError IO a
catchStoreError = catchAllErrors (SEInternal . bshow)
-- Errors caught by this function will suspend worker as if there is no more work,
handleWrkErr :: ByteString -> ByteString -> IO a -> ExceptT StoreError IO a
handleWrkErr itemName opName action = ExceptT $ first mkError <$> E.try action
where
mkError :: E.SomeException -> StoreError
mkError e = SEWorkItemError $ itemName <> " " <> opName <> " error: " <> bshow e
tryGetItem itemId = ExceptT (getItem itemId) `catchStoreErrors` \e -> mark itemId >> throwE e
mark itemId = handleErr ("markFailed ID " <> bshow itemId) $ markFailed itemId
catchStoreErrors = catchAllErrors (SEInternal . bshow)
-- Errors caught by this function will suspend worker as if there is no more work,
handleErr :: ByteString -> IO a -> ExceptT StoreError IO a
handleErr opName action = ExceptT $ first mkError <$> E.try action
where
mkError :: E.SomeException -> StoreError
mkError e = SEWorkItemError $ itemName <> " " <> opName <> " error: " <> bshow e
updatePendingMsgRIState :: DB.Connection -> ConnId -> InternalId -> RI2State -> IO ()
updatePendingMsgRIState db connId msgId RI2State {slowInterval, fastInterval} =
@@ -1186,11 +1157,6 @@ checkRcvMsgHashExists db connId hash = do
(connId, hash)
)
getRcvMsgBrokerTs :: DB.Connection -> ConnId -> SMP.MsgId -> IO (Either StoreError BrokerTs)
getRcvMsgBrokerTs db connId msgId =
firstRow fromOnly SEMsgNotFound $
DB.query db "SELECT broker_ts FROM rcv_messages WHERE conn_id = ? AND broker_id = ?" (connId, msgId)
deleteMsg :: DB.Connection -> ConnId -> InternalId -> IO ()
deleteMsg db connId msgId =
DB.execute db "DELETE FROM messages WHERE conn_id = ? AND internal_id = ?;" (connId, msgId)
@@ -1351,39 +1317,38 @@ getPendingCommandServers db connId = do
where
smpServer (host, port, keyHash) = SMPServer <$> host <*> port <*> keyHash
getPendingServerCommand :: DB.Connection -> ConnId -> Maybe SMPServer -> IO (Either StoreError (Maybe PendingCommand))
getPendingServerCommand db connId srv_ = getWorkItem "command" getCmdId getCommand markCommandFailed
getPendingServerCommand :: DB.Connection -> Maybe SMPServer -> IO (Either StoreError (Maybe PendingCommand))
getPendingServerCommand db srv_ = getWorkItem "command" getCmdId getCommand markCommandFailed
where
getCmdId :: IO (Maybe Int64)
getCmdId =
maybeFirstRow fromOnly $ case srv_ of
Nothing ->
DB.query
DB.query_
db
[sql|
SELECT command_id FROM commands
WHERE conn_id = ? AND host IS NULL AND port IS NULL AND failed = 0
WHERE host IS NULL AND port IS NULL AND failed = 0
ORDER BY created_at ASC, command_id ASC
LIMIT 1
|]
(Only connId)
Just (SMPServer host port _) ->
DB.query
db
[sql|
SELECT command_id FROM commands
WHERE conn_id = ? AND host = ? AND port = ? AND failed = 0
WHERE host = ? AND port = ? AND failed = 0
ORDER BY created_at ASC, command_id ASC
LIMIT 1
|]
(connId, host, port)
(host, port)
getCommand :: Int64 -> IO (Either StoreError PendingCommand)
getCommand cmdId =
firstRow pendingCommand err $
DB.query
db
[sql|
SELECT c.corr_id, cs.user_id, c.command
SELECT c.corr_id, cs.user_id, c.conn_id, c.command
FROM commands c
JOIN connections cs USING (conn_id)
WHERE c.command_id = ?
@@ -1391,22 +1356,9 @@ getPendingServerCommand db connId srv_ = getWorkItem "command" getCmdId getComma
(Only cmdId)
where
err = SEInternal $ "command " <> bshow cmdId <> " returned []"
pendingCommand (corrId, userId, command) = PendingCommand {cmdId, corrId, userId, connId, command}
pendingCommand (corrId, userId, connId, command) = PendingCommand {cmdId, corrId, userId, connId, command}
markCommandFailed cmdId = DB.execute db "UPDATE commands SET failed = 1 WHERE command_id = ?" (Only cmdId)
updateCommandServer :: DB.Connection -> AsyncCmdId -> SMPServer -> IO (Either StoreError ())
updateCommandServer db cmdId srv@(SMPServer host port _) = runExceptT $ do
serverKeyHash_ <- ExceptT $ getServerKeyHash_ db srv
liftIO $
DB.execute
db
[sql|
UPDATE commands
SET host = ?, port = ?, server_key_hash = ?
WHERE command_id = ?
|]
(host, port, serverKeyHash_, cmdId)
deleteCommand :: DB.Connection -> AsyncCmdId -> IO ()
deleteCommand db cmdId =
DB.execute db "DELETE FROM commands WHERE command_id = ?" (Only cmdId)
@@ -1499,96 +1451,29 @@ removeNtfToken db NtfToken {deviceToken = DeviceToken provider token, ntfServer
|]
(provider, token, host, port)
addNtfTokenToDelete :: DB.Connection -> NtfServer -> C.APrivateAuthKey -> NtfTokenId -> IO ()
addNtfTokenToDelete db ProtocolServer {host, port, keyHash} ntfPrivKey tknId =
DB.execute db "INSERT INTO ntf_tokens_to_delete (ntf_host, ntf_port, ntf_key_hash, tkn_id, tkn_priv_key) VALUES (?,?,?,?,?)" (host, port, keyHash, tknId, ntfPrivKey)
deleteExpiredNtfTokensToDelete :: DB.Connection -> NominalDiffTime -> IO ()
deleteExpiredNtfTokensToDelete db ttl = do
cutoffTs <- addUTCTime (-ttl) <$> getCurrentTime
DB.execute db "DELETE FROM ntf_tokens_to_delete WHERE created_at < ?" (Only cutoffTs)
type NtfTokenToDelete = (Int64, C.APrivateAuthKey, NtfTokenId)
getNextNtfTokenToDelete :: DB.Connection -> NtfServer -> IO (Either StoreError (Maybe NtfTokenToDelete))
getNextNtfTokenToDelete db (NtfServer ntfHost ntfPort _) =
getWorkItem "ntf tkn del" getNtfTknDbId getNtfTknToDelete (markNtfTokenToDeleteFailed_ db)
where
getNtfTknDbId :: IO (Maybe Int64)
getNtfTknDbId =
maybeFirstRow fromOnly $
DB.query
db
[sql|
SELECT ntf_token_to_delete_id
FROM ntf_tokens_to_delete
WHERE ntf_host = ? AND ntf_port = ?
AND del_failed = 0
ORDER BY created_at ASC
LIMIT 1
|]
(ntfHost, ntfPort)
getNtfTknToDelete :: Int64 -> IO (Either StoreError NtfTokenToDelete)
getNtfTknToDelete tknDbId =
firstRow ntfTokenToDelete err $
DB.query
db
[sql|
SELECT tkn_priv_key, tkn_id
FROM ntf_tokens_to_delete
WHERE ntf_token_to_delete_id = ?
|]
(Only tknDbId)
where
err = SEInternal $ "ntf token to delete " <> bshow tknDbId <> " returned []"
ntfTokenToDelete (tknPrivKey, tknId) = (tknDbId, tknPrivKey, tknId)
markNtfTokenToDeleteFailed_ :: DB.Connection -> Int64 -> IO ()
markNtfTokenToDeleteFailed_ db tknDbId =
DB.execute db "UPDATE ntf_tokens_to_delete SET del_failed = 1 where ntf_token_to_delete_id = ?" (Only tknDbId)
getPendingDelTknServers :: DB.Connection -> IO [NtfServer]
getPendingDelTknServers db =
map toNtfServer
<$> DB.query_
db
[sql|
SELECT DISTINCT ntf_host, ntf_port, ntf_key_hash
FROM ntf_tokens_to_delete
|]
where
toNtfServer (host, port, keyHash) = NtfServer host port keyHash
deleteNtfTokenToDelete :: DB.Connection -> Int64 -> IO ()
deleteNtfTokenToDelete db tknDbId =
DB.execute db "DELETE FROM ntf_tokens_to_delete WHERE ntf_token_to_delete_id = ?" (Only tknDbId)
type NtfSupervisorSub = (NtfSubscription, Maybe (NtfSubAction, NtfActionTs))
getNtfSubscription :: DB.Connection -> ConnId -> IO (Maybe NtfSupervisorSub)
getNtfSubscription :: DB.Connection -> ConnId -> IO (Maybe (NtfSubscription, Maybe (NtfSubAction, NtfActionTs)))
getNtfSubscription db connId =
maybeFirstRow ntfSubscription $
DB.query
db
[sql|
SELECT c.user_id, s.host, s.port, COALESCE(nsb.smp_server_key_hash, s.key_hash), ns.ntf_host, ns.ntf_port, ns.ntf_key_hash,
SELECT s.host, s.port, COALESCE(nsb.smp_server_key_hash, s.key_hash), ns.ntf_host, ns.ntf_port, ns.ntf_key_hash,
nsb.smp_ntf_id, nsb.ntf_sub_id, nsb.ntf_sub_status, nsb.ntf_sub_action, nsb.ntf_sub_smp_action, nsb.ntf_sub_action_ts
FROM ntf_subscriptions nsb
JOIN connections c USING (conn_id)
JOIN servers s ON s.host = nsb.smp_host AND s.port = nsb.smp_port
JOIN ntf_servers ns USING (ntf_host, ntf_port)
WHERE nsb.conn_id = ?
|]
(Only connId)
where
ntfSubscription ((userId, smpHost, smpPort, smpKeyHash, ntfHost, ntfPort, ntfKeyHash) :. (ntfQueueId, ntfSubId, ntfSubStatus, ntfAction_, smpAction_, actionTs_)) =
ntfSubscription (smpHost, smpPort, smpKeyHash, ntfHost, ntfPort, ntfKeyHash, ntfQueueId, ntfSubId, ntfSubStatus, ntfAction_, smpAction_, actionTs_) =
let smpServer = SMPServer smpHost smpPort smpKeyHash
ntfServer = NtfServer ntfHost ntfPort ntfKeyHash
action = case (ntfAction_, smpAction_, actionTs_) of
(Just ntfAction, Nothing, Just actionTs) -> Just (NSANtf ntfAction, actionTs)
(Nothing, Just smpAction, Just actionTs) -> Just (NSASMP smpAction, actionTs)
(Just ntfAction, Nothing, Just actionTs) -> Just (NtfSubNTFAction ntfAction, actionTs)
(Nothing, Just smpAction, Just actionTs) -> Just (NtfSubSMPAction smpAction, actionTs)
_ -> Nothing
in (NtfSubscription {userId, connId, smpServer, ntfQueueId, ntfServer, ntfSubId, ntfSubStatus}, action)
in (NtfSubscription {connId, smpServer, ntfQueueId, ntfServer, ntfSubId, ntfSubStatus}, action)
createNtfSubscription :: DB.Connection -> NtfSubscription -> NtfSubAction -> IO (Either StoreError ())
createNtfSubscription db ntfSubscription action = runExceptT $ do
@@ -1611,19 +1496,16 @@ createNtfSubscription db ntfSubscription action = runExceptT $ do
(ntfSubAction, ntfSubSMPAction) = ntfSubAndSMPAction action
supervisorUpdateNtfSub :: DB.Connection -> NtfSubscription -> NtfSubAction -> IO ()
supervisorUpdateNtfSub db NtfSubscription {connId, smpServer = (SMPServer smpHost smpPort _), ntfQueueId, ntfServer = (NtfServer ntfHost ntfPort _), ntfSubId, ntfSubStatus} action = do
supervisorUpdateNtfSub db NtfSubscription {connId, ntfQueueId, ntfServer = (NtfServer ntfHost ntfPort _), ntfSubId, ntfSubStatus} action = do
ts <- getCurrentTime
DB.execute
db
[sql|
UPDATE ntf_subscriptions
SET smp_host = ?, smp_port = ?, smp_ntf_id = ?, ntf_host = ?, ntf_port = ?, ntf_sub_id = ?,
ntf_sub_status = ?, ntf_sub_action = ?, ntf_sub_smp_action = ?, ntf_sub_action_ts = ?, updated_by_supervisor = ?, updated_at = ?
SET smp_ntf_id = ?, ntf_host = ?, ntf_port = ?, ntf_sub_id = ?, ntf_sub_status = ?, ntf_sub_action = ?, ntf_sub_smp_action = ?, ntf_sub_action_ts = ?, updated_by_supervisor = ?, updated_at = ?
WHERE conn_id = ?
|]
( (smpHost, smpPort, ntfQueueId, ntfHost, ntfPort, ntfSubId)
:. (ntfSubStatus, ntfSubAction, ntfSubSMPAction, ts, True, ts, connId)
)
((ntfQueueId, ntfHost, ntfPort, ntfSubId) :. (ntfSubStatus, ntfSubAction, ntfSubSMPAction, ts, True, ts, connId))
where
(ntfSubAction, ntfSubSMPAction) = ntfSubAndSMPAction action
@@ -1698,20 +1580,16 @@ deleteNtfSubscription db connId = do
WHERE conn_id = ?
|]
(Nothing :: Maybe SMP.NotifierId, Nothing :: Maybe NtfSubscriptionId, NASDeleted, False, updatedAt, connId)
else deleteNtfSubscription' db connId
else DB.execute db "DELETE FROM ntf_subscriptions WHERE conn_id = ?" (Only connId)
deleteNtfSubscription' :: DB.Connection -> ConnId -> IO ()
deleteNtfSubscription' db connId = do
DB.execute db "DELETE FROM ntf_subscriptions WHERE conn_id = ?" (Only connId)
getNextNtfSubNTFActions :: DB.Connection -> NtfServer -> Int -> IO (Either StoreError [Either StoreError (NtfSubNTFAction, NtfSubscription, NtfActionTs)])
getNextNtfSubNTFActions db ntfServer@(NtfServer ntfHost ntfPort _) ntfBatchSize =
getWorkItems "ntf NTF" getNtfConnIds getNtfSubAction (markNtfSubActionNtfFailed_ db)
getNextNtfSubNTFAction :: DB.Connection -> NtfServer -> IO (Either StoreError (Maybe (NtfSubscription, NtfSubNTFAction, NtfActionTs)))
getNextNtfSubNTFAction db ntfServer@(NtfServer ntfHost ntfPort _) =
getWorkItem "ntf NTF" getNtfConnId getNtfSubAction (markNtfSubActionNtfFailed_ db)
where
getNtfConnIds :: IO [ConnId]
getNtfConnIds =
map fromOnly
<$> DB.query
getNtfConnId :: IO (Maybe ConnId)
getNtfConnId =
maybeFirstRow fromOnly $
DB.query
db
[sql|
SELECT conn_id
@@ -1719,43 +1597,42 @@ getNextNtfSubNTFActions db ntfServer@(NtfServer ntfHost ntfPort _) ntfBatchSize
WHERE ntf_host = ? AND ntf_port = ? AND ntf_sub_action IS NOT NULL
AND (ntf_failed = 0 OR updated_by_supervisor = 1)
ORDER BY ntf_sub_action_ts ASC
LIMIT ?
LIMIT 1
|]
(ntfHost, ntfPort, ntfBatchSize)
getNtfSubAction :: ConnId -> IO (Either StoreError (NtfSubNTFAction, NtfSubscription, NtfActionTs))
(ntfHost, ntfPort)
getNtfSubAction :: ConnId -> IO (Either StoreError (NtfSubscription, NtfSubNTFAction, NtfActionTs))
getNtfSubAction connId = do
markUpdatedByWorker db connId
firstRow ntfSubAction err $
DB.query
db
[sql|
SELECT c.user_id, s.host, s.port, COALESCE(ns.smp_server_key_hash, s.key_hash),
SELECT s.host, s.port, COALESCE(ns.smp_server_key_hash, s.key_hash),
ns.smp_ntf_id, ns.ntf_sub_id, ns.ntf_sub_status, ns.ntf_sub_action_ts, ns.ntf_sub_action
FROM ntf_subscriptions ns
JOIN connections c USING (conn_id)
JOIN servers s ON s.host = ns.smp_host AND s.port = ns.smp_port
WHERE ns.conn_id = ?
|]
(Only connId)
where
err = SEInternal $ "ntf subscription " <> bshow connId <> " returned []"
ntfSubAction (userId, smpHost, smpPort, smpKeyHash, ntfQueueId, ntfSubId, ntfSubStatus, actionTs, action) =
ntfSubAction (smpHost, smpPort, smpKeyHash, ntfQueueId, ntfSubId, ntfSubStatus, actionTs, action) =
let smpServer = SMPServer smpHost smpPort smpKeyHash
ntfSubscription = NtfSubscription {userId, connId, smpServer, ntfQueueId, ntfServer, ntfSubId, ntfSubStatus}
in (action, ntfSubscription, actionTs)
ntfSubscription = NtfSubscription {connId, smpServer, ntfQueueId, ntfServer, ntfSubId, ntfSubStatus}
in (ntfSubscription, action, actionTs)
markNtfSubActionNtfFailed_ :: DB.Connection -> ConnId -> IO ()
markNtfSubActionNtfFailed_ db connId =
DB.execute db "UPDATE ntf_subscriptions SET ntf_failed = 1 where conn_id = ?" (Only connId)
getNextNtfSubSMPActions :: DB.Connection -> SMPServer -> Int -> IO (Either StoreError [Either StoreError (NtfSubSMPAction, NtfSubscription)])
getNextNtfSubSMPActions db smpServer@(SMPServer smpHost smpPort _) ntfBatchSize =
getWorkItems "ntf SMP" getNtfConnIds getNtfSubAction (markNtfSubActionSMPFailed_ db)
getNextNtfSubSMPAction :: DB.Connection -> SMPServer -> IO (Either StoreError (Maybe (NtfSubscription, NtfSubSMPAction, NtfActionTs)))
getNextNtfSubSMPAction db smpServer@(SMPServer smpHost smpPort _) =
getWorkItem "ntf SMP" getNtfConnId getNtfSubAction (markNtfSubActionSMPFailed_ db)
where
getNtfConnIds :: IO [ConnId]
getNtfConnIds =
map fromOnly
<$> DB.query
getNtfConnId :: IO (Maybe ConnId)
getNtfConnId =
maybeFirstRow fromOnly $
DB.query
db
[sql|
SELECT conn_id
@@ -1763,30 +1640,29 @@ getNextNtfSubSMPActions db smpServer@(SMPServer smpHost smpPort _) ntfBatchSize
WHERE smp_host = ? AND smp_port = ? AND ntf_sub_smp_action IS NOT NULL AND ntf_sub_action_ts IS NOT NULL
AND (smp_failed = 0 OR updated_by_supervisor = 1)
ORDER BY ntf_sub_action_ts ASC
LIMIT ?
LIMIT 1
|]
(smpHost, smpPort, ntfBatchSize)
getNtfSubAction :: ConnId -> IO (Either StoreError (NtfSubSMPAction, NtfSubscription))
(smpHost, smpPort)
getNtfSubAction :: ConnId -> IO (Either StoreError (NtfSubscription, NtfSubSMPAction, NtfActionTs))
getNtfSubAction connId = do
markUpdatedByWorker db connId
firstRow ntfSubAction err $
DB.query
db
[sql|
SELECT c.user_id, s.ntf_host, s.ntf_port, s.ntf_key_hash,
ns.smp_ntf_id, ns.ntf_sub_id, ns.ntf_sub_status, ns.ntf_sub_smp_action
SELECT s.ntf_host, s.ntf_port, s.ntf_key_hash,
ns.smp_ntf_id, ns.ntf_sub_id, ns.ntf_sub_status, ns.ntf_sub_action_ts, ns.ntf_sub_smp_action
FROM ntf_subscriptions ns
JOIN connections c USING (conn_id)
JOIN ntf_servers s USING (ntf_host, ntf_port)
WHERE ns.conn_id = ?
|]
(Only connId)
where
err = SEInternal $ "ntf subscription " <> bshow connId <> " returned []"
ntfSubAction (userId, ntfHost, ntfPort, ntfKeyHash, ntfQueueId, ntfSubId, ntfSubStatus, action) =
ntfSubAction (ntfHost, ntfPort, ntfKeyHash, ntfQueueId, ntfSubId, ntfSubStatus, actionTs, action) =
let ntfServer = NtfServer ntfHost ntfPort ntfKeyHash
ntfSubscription = NtfSubscription {userId, connId, smpServer, ntfQueueId, ntfServer, ntfSubId, ntfSubStatus}
in (action, ntfSubscription)
ntfSubscription = NtfSubscription {connId, smpServer, ntfQueueId, ntfServer, ntfSubId, ntfSubStatus}
in (ntfSubscription, action, actionTs)
markNtfSubActionSMPFailed_ :: DB.Connection -> ConnId -> IO ()
markNtfSubActionSMPFailed_ db connId =
@@ -1914,14 +1790,6 @@ instance ToField (Version v) where toField (Version v) = toField v
instance FromField (Version v) where fromField f = Version <$> fromField f
deriving newtype instance ToField EntityId
deriving newtype instance FromField EntityId
deriving newtype instance ToField ChunkReplicaId
deriving newtype instance FromField ChunkReplicaId
listToEither :: e -> [a] -> Either e a
listToEither _ (x : _) = Right x
listToEither e _ = Left e
@@ -2038,11 +1906,9 @@ newQueueId_ (Only maxId : _) = DBQueueId (maxId + 1)
getConn :: DB.Connection -> ConnId -> IO (Either StoreError SomeConn)
getConn = getAnyConn False
{-# INLINE getConn #-}
getDeletedConn :: DB.Connection -> ConnId -> IO (Either StoreError SomeConn)
getDeletedConn = getAnyConn True
{-# INLINE getDeletedConn #-}
getAnyConn :: Bool -> DB.Connection -> ConnId -> IO (Either StoreError SomeConn)
getAnyConn deleted' dbConn connId =
@@ -2063,11 +1929,9 @@ getAnyConn deleted' dbConn connId =
getConns :: DB.Connection -> [ConnId] -> IO [Either StoreError SomeConn]
getConns = getAnyConns_ False
{-# INLINE getConns #-}
getDeletedConns :: DB.Connection -> [ConnId] -> IO [Either StoreError SomeConn]
getDeletedConns = getAnyConns_ True
{-# INLINE getDeletedConns #-}
getAnyConns_ :: Bool -> DB.Connection -> [ConnId] -> IO [Either StoreError SomeConn]
getAnyConns_ deleted' db connIds = forM connIds $ E.handle handleDBError . getAnyConn deleted' db
@@ -2100,10 +1964,6 @@ setConnDeleted db waitDelivery connId
| otherwise =
DB.execute db "UPDATE connections SET deleted = ? WHERE conn_id = ?" (True, connId)
setConnUserId :: DB.Connection -> UserId -> ConnId -> UserId -> IO ()
setConnUserId db oldUserId connId newUserId =
DB.execute db "UPDATE connections SET user_id = ? WHERE conn_id = ? and user_id = ?" (newUserId, connId, oldUserId)
setConnAgentVersion :: DB.Connection -> ConnId -> VersionSMPA -> IO ()
setConnAgentVersion db connId aVersion =
DB.execute db "UPDATE connections SET smp_agent_version = ? WHERE conn_id = ?" (aVersion, connId)
@@ -2316,9 +2176,9 @@ updateRcvMsgHash db connId sndMsgId internalRcvId internalHash =
-- * updateSndIds helpers
retrieveLastIdsAndHashSnd_ :: DB.Connection -> ConnId -> IO (Either StoreError (InternalId, InternalSndId, PrevSndMsgHash))
retrieveLastIdsAndHashSnd_ :: DB.Connection -> ConnId -> IO (InternalId, InternalSndId, PrevSndMsgHash)
retrieveLastIdsAndHashSnd_ dbConn connId = do
firstRow id SEConnNotFound $
[(lastInternalId, lastInternalSndId, lastSndHash)] <-
DB.queryNamed
dbConn
[sql|
@@ -2327,6 +2187,7 @@ retrieveLastIdsAndHashSnd_ dbConn connId = do
WHERE conn_id = :conn_id;
|]
[":conn_id" := connId]
return (lastInternalId, lastInternalSndId, lastSndHash)
updateLastIdsSnd_ :: DB.Connection -> ConnId -> InternalId -> InternalSndId -> IO ()
updateLastIdsSnd_ dbConn connId newInternalId newInternalSndId =
@@ -2411,8 +2272,8 @@ randomId :: TVar ChaChaDRG -> Int -> IO ByteString
randomId gVar n = atomically $ U.encode <$> C.randomBytes n gVar
ntfSubAndSMPAction :: NtfSubAction -> (Maybe NtfSubNTFAction, Maybe NtfSubSMPAction)
ntfSubAndSMPAction (NSANtf action) = (Just action, Nothing)
ntfSubAndSMPAction (NSASMP action) = (Nothing, Just action)
ntfSubAndSMPAction (NtfSubNTFAction action) = (Just action, Nothing)
ntfSubAndSMPAction (NtfSubSMPAction action) = (Nothing, Just action)
createXFTPServer_ :: DB.Connection -> XFTPServer -> IO Int64
createXFTPServer_ db newSrv@ProtocolServer {host, port, keyHash} =
@@ -2622,7 +2483,7 @@ deleteRcvFile' :: DB.Connection -> DBRcvFileId -> IO ()
deleteRcvFile' db rcvFileId =
DB.execute db "DELETE FROM rcv_files WHERE rcv_file_id = ?" (Only rcvFileId)
getNextRcvChunkToDownload :: DB.Connection -> XFTPServer -> NominalDiffTime -> IO (Either StoreError (Maybe (RcvFileChunk, Bool, Maybe RcvFileId)))
getNextRcvChunkToDownload :: DB.Connection -> XFTPServer -> NominalDiffTime -> IO (Either StoreError (Maybe (RcvFileChunk, Bool)))
getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = do
getWorkItem "rcv_file_download" getReplicaId getChunkData (markRcvFileFailed db . snd)
where
@@ -2646,7 +2507,7 @@ getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = d
LIMIT 1
|]
(host, port, keyHash, RFSReceiving, cutoffTs)
getChunkData :: (Int64, DBRcvFileId) -> IO (Either StoreError (RcvFileChunk, Bool, Maybe RcvFileId))
getChunkData :: (Int64, DBRcvFileId) -> IO (Either StoreError (RcvFileChunk, Bool))
getChunkData (rcvFileChunkReplicaId, _fileId) =
firstRow toChunk SEFileNotFound $
DB.query
@@ -2655,7 +2516,7 @@ getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = d
SELECT
f.rcv_file_id, f.rcv_file_entity_id, f.user_id, c.rcv_file_chunk_id, c.chunk_no, c.chunk_size, c.digest, f.tmp_path, c.tmp_path,
r.rcv_file_chunk_replica_id, r.replica_id, r.replica_key, r.received, r.delay, r.retries,
f.approved_relays, f.redirect_entity_id
f.approved_relays
FROM rcv_file_chunk_replicas r
JOIN xftp_servers s ON s.xftp_server_id = r.xftp_server_id
JOIN rcv_file_chunks c ON c.rcv_file_chunk_id = r.rcv_file_chunk_id
@@ -2664,8 +2525,8 @@ getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = d
|]
(Only rcvFileChunkReplicaId)
where
toChunk :: ((DBRcvFileId, RcvFileId, UserId, Int64, Int, FileSize Word32, FileDigest, FilePath, Maybe FilePath) :. (Int64, ChunkReplicaId, C.APrivateAuthKey, Bool, Maybe Int64, Int) :. (Bool, Maybe RcvFileId)) -> (RcvFileChunk, Bool, Maybe RcvFileId)
toChunk ((rcvFileId, rcvFileEntityId, userId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath, chunkTmpPath) :. (rcvChunkReplicaId, replicaId, replicaKey, received, delay, retries) :. (approvedRelays, redirectEntityId_)) =
toChunk :: ((DBRcvFileId, RcvFileId, UserId, Int64, Int, FileSize Word32, FileDigest, FilePath, Maybe FilePath) :. (Int64, ChunkReplicaId, C.APrivateAuthKey, Bool, Maybe Int64, Int) :. Only Bool) -> (RcvFileChunk, Bool)
toChunk ((rcvFileId, rcvFileEntityId, userId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath, chunkTmpPath) :. (rcvChunkReplicaId, replicaId, replicaKey, received, delay, retries) :. (Only approvedRelays)) =
( RcvFileChunk
{ rcvFileId,
rcvFileEntityId,
@@ -2678,8 +2539,7 @@ getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = d
chunkTmpPath,
replicas = [RcvFileChunkReplica {rcvChunkReplicaId, server, replicaId, replicaKey, received, delay, retries}]
},
approvedRelays,
redirectEntityId_
approvedRelays
)
getNextRcvFileToDecrypt :: DB.Connection -> NominalDiffTime -> IO (Either StoreError (Maybe RcvFile))
@@ -1,5 +1,4 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ScopedTypeVariables #-}
@@ -9,20 +8,20 @@ module Simplex.Messaging.Agent.Store.SQLite.Common
withConnection',
withTransaction,
withTransaction',
withTransactionPriority,
withTransactionCtx,
dbBusyLoop,
storeKey,
)
where
import Control.Concurrent (threadDelay)
import Control.Concurrent.STM (retry)
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
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import Simplex.Messaging.Util (ifM, unlessM)
import Simplex.Messaging.Util (diffToMilliseconds)
import qualified UnliftIO.Exception as E
import UnliftIO.MVar
import UnliftIO.STM
@@ -33,40 +32,35 @@ storeKey key keepKey = if keepKey || BA.null key then Just key else Nothing
data SQLiteStore = SQLiteStore
{ dbFilePath :: FilePath,
dbKey :: TVar (Maybe ScrubbedBytes),
dbSem :: TVar Int,
dbConnection :: MVar DB.Connection,
dbClosed :: TVar Bool,
dbNew :: Bool
}
withConnectionPriority :: SQLiteStore -> Bool -> (DB.Connection -> IO a) -> IO a
withConnectionPriority SQLiteStore {dbSem, dbConnection} priority action
| priority = E.bracket_ signal release $ withMVar dbConnection action
| otherwise = lowPriority
where
lowPriority = wait >> withMVar dbConnection (\db -> ifM free (Just <$> action db) (pure Nothing)) >>= maybe lowPriority pure
signal = atomically $ modifyTVar' dbSem (+ 1)
release = atomically $ modifyTVar' dbSem $ \sem -> if sem > 0 then sem - 1 else 0
wait = unlessM free $ atomically $ unlessM ((0 ==) <$> readTVar dbSem) retry
free = (0 ==) <$> readTVarIO dbSem
withConnection :: SQLiteStore -> (DB.Connection -> IO a) -> IO a
withConnection st = withConnectionPriority st False
withConnection SQLiteStore {dbConnection} = withMVar dbConnection
withConnection' :: SQLiteStore -> (SQL.Connection -> IO a) -> IO a
withConnection' st action = withConnection st $ action . DB.conn
withTransaction :: SQLiteStore -> (DB.Connection -> IO a) -> IO a
withTransaction = withTransactionCtx Nothing
withTransaction' :: SQLiteStore -> (SQL.Connection -> IO a) -> IO a
withTransaction' st action = withTransaction st $ action . DB.conn
withTransaction :: SQLiteStore -> (DB.Connection -> IO a) -> IO a
withTransaction st = withTransactionPriority st False
{-# INLINE withTransaction #-}
withTransactionPriority :: SQLiteStore -> Bool -> (DB.Connection -> IO a) -> IO a
withTransactionPriority st priority action = withConnectionPriority st priority $ dbBusyLoop . transaction
withTransactionCtx :: Maybe String -> SQLiteStore -> (DB.Connection -> IO a) -> IO a
withTransactionCtx ctx_ st action = withConnection st $ dbBusyLoop . transactionWithCtx
where
transaction db@DB.Connection {conn} = SQL.withImmediateTransaction conn $ action db
transactionWithCtx db@DB.Connection {conn} = case ctx_ of
Nothing -> SQL.withImmediateTransaction conn $ action db
Just ctx -> do
t1 <- getCurrentTime
r <- SQL.withImmediateTransaction conn $ action db
t2 <- getCurrentTime
putStrLn $ "withTransactionCtx start :: " <> show t1 <> " :: " <> ctx
putStrLn $ "withTransactionCtx end :: " <> show t2 <> " :: " <> ctx <> " :: duration=" <> show (diffToMilliseconds $ diffUTCTime t2 t1)
pure r
dbBusyLoop :: forall a. IO a -> IO a
dbBusyLoop action = loop 500 3000000
+8 -21
View File
@@ -1,5 +1,4 @@
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TemplateHaskell #-}
@@ -21,19 +20,15 @@ where
import Control.Concurrent.STM
import Control.Monad (when)
import Control.Exception
import qualified Data.Aeson.TH as J
import Data.Int (Int64)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Text (Text)
import Data.Time (diffUTCTime, getCurrentTime)
import Database.SQLite.Simple (FromRow, NamedParam, Query, ToRow)
import qualified Database.SQLite.Simple as SQL
import Simplex.Messaging.Parsers (defaultJSON)
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Util (diffToMilliseconds, tshow)
import Simplex.Messaging.Util (diffToMilliseconds)
data Connection = Connection
{ conn :: SQL.Connection,
@@ -43,41 +38,33 @@ data Connection = Connection
data SlowQueryStats = SlowQueryStats
{ count :: Int64,
timeMax :: Int64,
timeAvg :: Int64,
errs :: Map Text Int
timeAvg :: Int64
}
deriving (Show)
timeIt :: TMap Query SlowQueryStats -> Query -> IO a -> IO a
timeIt slow sql a = do
t <- getCurrentTime
r <- a `catch` \e -> do
atomically $ TM.alter (Just . updateQueryErrors e) sql slow
throwIO e
r <- a
t' <- getCurrentTime
let diff = diffToMilliseconds $ diffUTCTime t' t
when (diff > 1) $ atomically $ TM.alter (updateQueryStats diff) sql slow
atomically $ when (diff > 5) $ TM.alter (updateQueryStats diff) sql slow
pure r
where
updateQueryErrors :: SomeException -> Maybe SlowQueryStats -> SlowQueryStats
updateQueryErrors e Nothing = SlowQueryStats 0 0 0 $ M.singleton (tshow e) 1
updateQueryErrors e (Just stats@SlowQueryStats {errs}) =
stats {errs = M.alter (Just . maybe 1 (+ 1)) (tshow e) errs}
updateQueryStats :: Int64 -> Maybe SlowQueryStats -> Maybe SlowQueryStats
updateQueryStats diff Nothing = Just $ SlowQueryStats 1 diff diff M.empty
updateQueryStats diff (Just SlowQueryStats {count, timeMax, timeAvg, errs}) =
updateQueryStats diff Nothing = Just $ SlowQueryStats 1 diff diff
updateQueryStats diff (Just SlowQueryStats {count, timeMax, timeAvg}) =
Just $
SlowQueryStats
{ count = count + 1,
timeMax = max timeMax diff,
timeAvg = (timeAvg * count + diff) `div` (count + 1),
errs
timeAvg = (timeAvg * count + diff) `div` (count + 1)
}
open :: String -> IO Connection
open f = do
conn <- SQL.open f
slow <- TM.emptyIO
slow <- atomically $ TM.empty
pure Connection {conn, slow}
close :: Connection -> IO ()
@@ -29,7 +29,7 @@ import Control.Monad (forM_, when)
import qualified Data.Aeson.TH as J
import Data.List (intercalate, sortOn)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.Map.Strict as M
import qualified Data.Map as M
import Data.Maybe (isNothing, mapMaybe)
import Data.Text (Text)
import Data.Text.Encoding (decodeLatin1)
@@ -74,7 +74,6 @@ import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240225_ratchet_kem
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240624_snd_secure
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240702_servers_stats
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240930_ntf_tokens_to_delete
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
import Simplex.Messaging.Transport.Client (TransportHost)
@@ -117,8 +116,7 @@ schemaMigrations =
("m20240225_ratchet_kem", m20240225_ratchet_kem, Just down_m20240225_ratchet_kem),
("m20240417_rcv_files_approved_relays", m20240417_rcv_files_approved_relays, Just down_m20240417_rcv_files_approved_relays),
("m20240624_snd_secure", m20240624_snd_secure, Just down_m20240624_snd_secure),
("m20240702_servers_stats", m20240702_servers_stats, Just down_m20240702_servers_stats),
("m20240930_ntf_tokens_to_delete", m20240930_ntf_tokens_to_delete, Just down_m20240930_ntf_tokens_to_delete)
("m20240702_servers_stats", m20240702_servers_stats, Just down_m20240702_servers_stats)
]
-- | The list of migrations in ascending order by date
@@ -1,27 +0,0 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240930_ntf_tokens_to_delete where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20240930_ntf_tokens_to_delete :: Query
m20240930_ntf_tokens_to_delete =
[sql|
CREATE TABLE ntf_tokens_to_delete (
ntf_token_to_delete_id INTEGER PRIMARY KEY,
ntf_host TEXT NOT NULL,
ntf_port TEXT NOT NULL,
ntf_key_hash BLOB NOT NULL,
tkn_id BLOB NOT NULL, -- token ID assigned by notifications server
tkn_priv_key BLOB NOT NULL, -- client's private key to sign token commands,
del_failed INTEGER DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
|]
down_m20240930_ntf_tokens_to_delete :: Query
down_m20240930_ntf_tokens_to_delete =
[sql|
DROP TABLE ntf_tokens_to_delete;
|]
@@ -1 +0,0 @@
CREATE INDEX 'ratchets_conn_id' ON 'ratchets'('conn_id'); --> connections(conn_id)
@@ -403,16 +403,6 @@ CREATE TABLE servers_stats(
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
CREATE TABLE ntf_tokens_to_delete(
ntf_token_to_delete_id INTEGER PRIMARY KEY,
ntf_host TEXT NOT NULL,
ntf_port TEXT NOT NULL,
ntf_key_hash BLOB NOT NULL,
tkn_id BLOB NOT NULL, -- token ID assigned by notifications server
tkn_priv_key BLOB NOT NULL, -- client's private key to sign token commands,
del_failed INTEGER DEFAULT 0,
created_at TEXT NOT NULL DEFAULT(datetime('now'))
);
CREATE UNIQUE INDEX idx_rcv_queues_ntf ON rcv_queues(host, port, ntf_id);
CREATE UNIQUE INDEX idx_rcv_queue_id ON rcv_queues(conn_id, rcv_queue_id);
CREATE UNIQUE INDEX idx_snd_queue_id ON snd_queues(conn_id, snd_queue_id);
+22 -38
View File
@@ -1,9 +1,7 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
module Simplex.Messaging.Agent.TRcvQueues
( TRcvQueues (getRcvQueues, getConnections),
Queue (..),
empty,
clear,
deleteConn,
@@ -11,9 +9,9 @@ module Simplex.Messaging.Agent.TRcvQueues
addQueue,
batchAddQueues,
deleteQueue,
hasSessQueues,
getSessQueues,
getDelSessQueues,
qKey,
)
where
@@ -27,51 +25,46 @@ import Simplex.Messaging.Agent.Store (RcvQueue, StoredRcvQueue (..))
import Simplex.Messaging.Protocol (RecipientId, SMPServer)
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport
class Queue q where
connId' :: q -> ConnId
qKey :: q -> (UserId, SMPServer, RecipientId)
-- the fields in this record have the same data with swapped keys for lookup efficiency,
-- and all methods must maintain this invariant.
data TRcvQueues q = TRcvQueues
{ getRcvQueues :: TMap (UserId, SMPServer, RecipientId) q,
data TRcvQueues = TRcvQueues
{ getRcvQueues :: TMap (UserId, SMPServer, RecipientId) RcvQueue,
getConnections :: TMap ConnId (NonEmpty (UserId, SMPServer, RecipientId))
}
empty :: IO (TRcvQueues q)
empty = TRcvQueues <$> TM.emptyIO <*> TM.emptyIO
empty :: STM TRcvQueues
empty = TRcvQueues <$> TM.empty <*> TM.empty
clear :: TRcvQueues q -> STM ()
clear :: TRcvQueues -> STM ()
clear (TRcvQueues qs cs) = TM.clear qs >> TM.clear cs
deleteConn :: ConnId -> TRcvQueues q -> STM ()
deleteConn :: ConnId -> TRcvQueues -> STM ()
deleteConn cId (TRcvQueues qs cs) =
TM.lookupDelete cId cs >>= \case
Just ks -> modifyTVar' qs $ \qs' -> foldl' (flip M.delete) qs' ks
Nothing -> pure ()
hasConn :: ConnId -> TRcvQueues q -> STM Bool
hasConn :: ConnId -> TRcvQueues -> STM Bool
hasConn cId (TRcvQueues _ cs) = TM.member cId cs
addQueue :: Queue q => q -> TRcvQueues q -> STM ()
addQueue :: RcvQueue -> TRcvQueues -> STM ()
addQueue rq (TRcvQueues qs cs) = do
TM.insert k rq qs
TM.alter addQ (connId' rq) cs
TM.alter addQ (connId rq) cs
where
addQ = Just . maybe (k :| []) (k <|)
k = qKey rq
-- Save time by aggregating modifyTVar'
batchAddQueues :: (Foldable t, Queue q) => TRcvQueues q -> t q -> STM ()
-- Save time by aggregating modifyTVar
batchAddQueues :: Foldable t => TRcvQueues -> t RcvQueue -> STM ()
batchAddQueues (TRcvQueues qs cs) rqs = do
modifyTVar' qs $ \now -> foldl' (\rqs' rq -> M.insert (qKey rq) rq rqs') now rqs
modifyTVar' cs $ \now -> foldl' (\cs' rq -> M.alter (addQ $ qKey rq) (connId' rq) cs') now rqs
modifyTVar' cs $ \now -> foldl' (\cs' rq -> M.alter (addQ $ qKey rq) (connId rq) cs') now rqs
where
addQ k = Just . maybe (k :| []) (k <|)
deleteQueue :: RcvQueue -> TRcvQueues RcvQueue -> STM ()
deleteQueue :: RcvQueue -> TRcvQueues -> STM ()
deleteQueue rq (TRcvQueues qs cs) = do
TM.delete k qs
TM.update delQ (connId rq) cs
@@ -79,25 +72,21 @@ deleteQueue rq (TRcvQueues qs cs) = do
delQ = L.nonEmpty . L.filter (/= k)
k = qKey rq
hasSessQueues :: (UserId, SMPServer, Maybe ConnId) -> TRcvQueues RcvQueue -> STM Bool
hasSessQueues tSess (TRcvQueues qs _) = any (`isSession` tSess) <$> readTVar qs
getSessQueues :: (UserId, SMPServer, Maybe ConnId) -> TRcvQueues RcvQueue -> IO [RcvQueue]
getSessQueues tSess (TRcvQueues qs _) = M.foldl' addQ [] <$> readTVarIO qs
getSessQueues :: (UserId, SMPServer, Maybe ConnId) -> TRcvQueues -> STM [RcvQueue]
getSessQueues tSess (TRcvQueues qs _) = M.foldl' addQ [] <$> readTVar qs
where
addQ qs' rq = if rq `isSession` tSess then rq : qs' else qs'
getDelSessQueues :: (UserId, SMPServer, Maybe ConnId) -> SessionId -> TRcvQueues (SessionId, RcvQueue) -> STM ([RcvQueue], [ConnId])
getDelSessQueues tSess sessId' (TRcvQueues qs cs) = do
getDelSessQueues :: (UserId, SMPServer, Maybe ConnId) -> TRcvQueues -> STM ([RcvQueue], [ConnId])
getDelSessQueues tSess (TRcvQueues qs cs) = do
(removedQs, qs'') <- (\qs' -> M.foldl' delQ ([], qs') qs') <$> readTVar qs
writeTVar qs $! qs''
removedConns <- stateTVar cs $ \cs' -> foldl' delConn ([], cs') removedQs
pure (removedQs, removedConns)
where
delQ acc@(removed, qs') (sessId, rq)
| rq `isSession` tSess && sessId == sessId' = (rq : removed, M.delete (qKey rq) qs')
delQ acc@(removed, qs') rq
| rq `isSession` tSess = (rq : removed, M.delete (qKey rq) qs')
| otherwise = acc
delConn :: ([ConnId], M.Map ConnId (NonEmpty (UserId, SMPServer, RecipientId))) -> RcvQueue -> ([ConnId], M.Map ConnId (NonEmpty (UserId, SMPServer, RecipientId)))
delConn (removed, cs') rq = M.alterF f cId cs'
where
cId = connId rq
@@ -111,10 +100,5 @@ isSession :: RcvQueue -> (UserId, SMPServer, Maybe ConnId) -> Bool
isSession rq (uId, srv, connId_) =
userId rq == uId && server rq == srv && maybe True (connId rq ==) connId_
instance Queue RcvQueue where
connId' = connId
qKey rq = (userId rq, server rq, rcvId rq)
instance Queue (SessionId, RcvQueue) where
connId' = connId . snd
qKey = qKey . snd
qKey :: RcvQueue -> (UserId, SMPServer, ConnId)
qKey rq = (userId rq, server rq, connId rq)
+68 -124
View File
@@ -63,13 +63,11 @@ module Simplex.Messaging.Client
forwardSMPTransmission,
getSMPQueueInfo,
sendProtocolCommand,
sendProtocolCommands,
-- * Supporting types and client configuration
ProtocolClientError (..),
SMPClientError,
ProxyClientError (..),
Response (..),
unexpectedResponse,
ProtocolClientConfig (..),
NetworkConfig (..),
@@ -82,11 +80,10 @@ module Simplex.Messaging.Client
defaultSMPClientConfig,
defaultNetworkConfig,
transportClientConfig,
clientSocksCredentials,
chooseTransportHost,
proxyUsername,
temporaryClientError,
smpProxyError,
textToHostMode,
ServerTransmissionBatch,
ServerTransmission (..),
ClientCommand,
@@ -119,20 +116,16 @@ import qualified Data.Aeson.TH as J
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Base64 as B64
import Data.Functor (($>))
import Data.Int (Int64)
import Data.List (find)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as L
import Data.Maybe (catMaybes, fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time.Clock (UTCTime (..), diffUTCTime, getCurrentTime)
import qualified Data.X509 as X
import qualified Data.X509.Validation as XV
import Network.Socket (ServiceName)
import Network.Socks5 (SocksCredentials (..))
import Numeric.Natural
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
@@ -143,7 +136,7 @@ import Simplex.Messaging.Server.QueueStore.QueueInfo
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport
import Simplex.Messaging.Transport.Client (SocksAuth (..), SocksProxyWithAuth (..), TransportClientConfig (..), TransportHost (..), defaultSMPPort, defaultTcpConnectTimeout, runTransportClient)
import Simplex.Messaging.Transport.Client (SocksProxy, TransportClientConfig (..), TransportHost (..), defaultTcpConnectTimeout, runTransportClient)
import Simplex.Messaging.Transport.KeepAlive
import Simplex.Messaging.Transport.WebSockets (WS)
import Simplex.Messaging.Util (bshow, diffToMicroseconds, ifM, liftEitherWith, raceAny_, threadDelay', tshow, whenM)
@@ -177,17 +170,17 @@ data PClient v err msg = PClient
msgQ :: Maybe (TBQueue (ServerTransmissionBatch v err msg))
}
smpClientStub :: TVar ChaChaDRG -> ByteString -> VersionSMP -> Maybe (THandleAuth 'TClient) -> IO SMPClient
smpClientStub :: TVar ChaChaDRG -> ByteString -> VersionSMP -> Maybe (THandleAuth 'TClient) -> STM SMPClient
smpClientStub g sessionId thVersion thAuth = do
let ts = UTCTime (read "2024-03-31") 0
connected <- newTVarIO False
clientCorrId <- atomically $ C.newRandomDRG g
sentCommands <- TM.emptyIO
sendPings <- newTVarIO False
lastReceived <- newTVarIO ts
timeoutErrorCount <- newTVarIO 0
sndQ <- newTBQueueIO 100
rcvQ <- newTBQueueIO 100
connected <- newTVar False
clientCorrId <- C.newRandomDRG g
sentCommands <- TM.empty
sendPings <- newTVar False
lastReceived <- newTVar ts
timeoutErrorCount <- newTVar 0
sndQ <- newTBQueue 100
rcvQ <- newTBQueue 100
return
ProtocolClient
{ action = Nothing,
@@ -199,7 +192,6 @@ smpClientStub g sessionId thVersion thAuth = do
thAuth,
blockSize = smpBlockSize,
implySessId = thVersion >= authCmdsSMPVersion,
encryptBlock = Nothing,
batch = True
},
sessionTs = ts,
@@ -244,12 +236,6 @@ data HostMode
HMPublic
deriving (Eq, Show)
textToHostMode :: Text -> Either String HostMode
textToHostMode = \case
"public" -> Right HMPublic
"onion" -> Right HMOnionViaSocks
s -> Left $ T.unpack $ "Invalid host_mode: " <> s
data SocksMode
= -- | always use SOCKS proxy when enabled
SMAlways
@@ -258,20 +244,10 @@ data SocksMode
SMOnion
deriving (Eq, Show)
instance StrEncoding SocksMode where
strEncode = \case
SMAlways -> "always"
SMOnion -> "onion"
strP =
A.takeTill (== ' ') >>= \case
"always" -> pure SMAlways
"onion" -> pure SMOnion
_ -> fail "Invalid Socks mode"
-- | network configuration for the client
data NetworkConfig = NetworkConfig
{ -- | use SOCKS5 proxy
socksProxy :: Maybe SocksProxyWithAuth,
socksProxy :: Maybe SocksProxy,
-- | when to use SOCKS proxy
socksMode :: SocksMode,
-- | determines critera which host is chosen from the list
@@ -284,8 +260,6 @@ data NetworkConfig = NetworkConfig
smpProxyMode :: SMPProxyMode,
-- | Fallback to direct connection when destination SMP relay does not support SMP proxy protocol extensions
smpProxyFallback :: SMPProxyFallback,
-- | use web port 443 for SMP protocol
smpWebPort :: Bool,
-- | timeout for the initial client TCP/TLS connection (microseconds)
tcpConnectTimeout :: Int,
-- | timeout of protocol commands (microseconds)
@@ -304,7 +278,7 @@ data NetworkConfig = NetworkConfig
}
deriving (Eq, Show)
data TransportSessionMode = TSMUser | TSMSession | TSMServer | TSMEntity
data TransportSessionMode = TSMUser | TSMEntity
deriving (Eq, Show)
-- SMP proxy mode for sending messages
@@ -354,10 +328,9 @@ defaultNetworkConfig =
socksMode = SMAlways,
hostMode = HMOnionViaSocks,
requiredHostMode = False,
sessionMode = TSMSession,
sessionMode = TSMUser,
smpProxyMode = SPMNever,
smpProxyFallback = SPFAllow,
smpWebPort = False,
tcpConnectTimeout = defaultTcpConnectTimeout,
tcpTimeout = 15_000_000,
tcpTimeoutPerKb = 5_000,
@@ -368,31 +341,15 @@ defaultNetworkConfig =
logTLSErrors = False
}
transportClientConfig :: NetworkConfig -> TransportHost -> Bool -> TransportClientConfig
transportClientConfig NetworkConfig {socksProxy, socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors} host useSNI =
TransportClientConfig {socksProxy = useSocksProxy socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing, alpn = Nothing, useSNI}
transportClientConfig :: NetworkConfig -> TransportHost -> TransportClientConfig
transportClientConfig NetworkConfig {socksProxy, socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors} host =
TransportClientConfig {socksProxy = useSocksProxy socksMode, tcpConnectTimeout, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing, alpn = Nothing}
where
socksProxy' = (\(SocksProxyWithAuth _ proxy) -> proxy) <$> socksProxy
useSocksProxy SMAlways = socksProxy'
useSocksProxy SMAlways = socksProxy
useSocksProxy SMOnion = case host of
THOnionHost _ -> socksProxy'
THOnionHost _ -> socksProxy
_ -> Nothing
clientSocksCredentials :: ProtocolTypeI (ProtoType msg) => NetworkConfig -> UTCTime -> TransportSession msg -> Maybe SocksCredentials
clientSocksCredentials NetworkConfig {socksProxy, sessionMode} proxySessTs (userId, srv, entityId_) = case socksProxy of
Just (SocksProxyWithAuth auth _) -> case auth of
SocksAuthUsername {username, password} -> Just $ SocksCredentials username password
SocksAuthNull -> Nothing
SocksIsolateByAuth -> Just $ SocksCredentials sessionUsername ""
Nothing -> Nothing
where
sessionUsername =
B64.encode $ C.sha256Hash $
bshow userId <> case sessionMode of
TSMUser -> ""
TSMSession -> ":" <> bshow proxySessTs
TSMServer -> ":" <> bshow proxySessTs <> "@" <> strEncode srv
TSMEntity -> ":" <> bshow proxySessTs <> "@" <> strEncode srv <> maybe "" ("/" <>) entityId_
{-# INLINE transportClientConfig #-}
-- | protocol client configuration.
data ProtocolClientConfig v = ProtocolClientConfig
@@ -406,31 +363,24 @@ data ProtocolClientConfig v = ProtocolClientConfig
-- | client-server protocol version range
serverVRange :: VersionRange v,
-- | agree shared session secret (used in SMP proxy for additional encryption layer)
agreeSecret :: Bool,
-- | send SNI to server, False for SMP
useSNI :: Bool
agreeSecret :: Bool
}
-- | Default protocol client configuration.
defaultClientConfig :: Maybe [ALPN] -> Bool -> VersionRange v -> ProtocolClientConfig v
defaultClientConfig clientALPN useSNI serverVRange =
defaultClientConfig :: Maybe [ALPN] -> VersionRange v -> ProtocolClientConfig v
defaultClientConfig clientALPN serverVRange =
ProtocolClientConfig
{ qSize = 64,
defaultTransport = ("443", transport @TLS),
networkConfig = defaultNetworkConfig,
clientALPN,
serverVRange,
agreeSecret = False,
useSNI
agreeSecret = False
}
{-# INLINE defaultClientConfig #-}
defaultSMPClientConfig :: ProtocolClientConfig SMPVersion
defaultSMPClientConfig =
(defaultClientConfig (Just supportedSMPHandshakes) False supportedClientSMPRelayVRange)
{ defaultTransport = (show defaultSMPPort, transport @TLS),
agreeSecret = True
}
defaultSMPClientConfig = defaultClientConfig (Just supportedSMPHandshakes) supportedClientSMPRelayVRange
{-# INLINE defaultSMPClientConfig #-}
data Request err msg = Request
@@ -481,33 +431,32 @@ transportSession' = transportSession . client_
type UserId = Int64
-- | Transport session key - includes entity ID if `sessionMode = TSMEntity`.
-- Please note that for SMP connection ID is used as entity ID, not queue ID.
type TransportSession msg = (UserId, ProtoServer msg, Maybe ByteString)
type TransportSession msg = (UserId, ProtoServer msg, Maybe EntityId)
-- | Connects to 'ProtocolServer' using passed client configuration
-- and queue for messages and notifications.
--
-- A single queue can be used for multiple 'SMPClient' instances,
-- as 'SMPServerTransmission' includes server information.
getProtocolClient :: forall v err msg. Protocol v err msg => TVar ChaChaDRG -> TransportSession msg -> ProtocolClientConfig v -> Maybe (TBQueue (ServerTransmissionBatch v err msg)) -> UTCTime -> (ProtocolClient v err msg -> IO ()) -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg))
getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, clientALPN, serverVRange, agreeSecret, useSNI} msgQ proxySessTs disconnected = do
getProtocolClient :: forall v err msg. Protocol v err msg => TVar ChaChaDRG -> TransportSession msg -> ProtocolClientConfig v -> Maybe (TBQueue (ServerTransmissionBatch v err msg)) -> (ProtocolClient v err msg -> IO ()) -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg))
getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, clientALPN, serverVRange, agreeSecret} msgQ disconnected = do
case chooseTransportHost networkConfig (host srv) of
Right useHost ->
(getCurrentTime >>= mkProtocolClient useHost >>= runClient useTransport useHost)
(getCurrentTime >>= atomically . mkProtocolClient useHost >>= runClient useTransport useHost)
`catch` \(e :: IOException) -> pure . Left $ PCEIOError e
Left e -> pure $ Left e
where
NetworkConfig {smpWebPort, tcpConnectTimeout, tcpTimeout, smpPingInterval} = networkConfig
mkProtocolClient :: TransportHost -> UTCTime -> IO (PClient v err msg)
NetworkConfig {tcpConnectTimeout, tcpTimeout, smpPingInterval} = networkConfig
mkProtocolClient :: TransportHost -> UTCTime -> STM (PClient v err msg)
mkProtocolClient transportHost ts = do
connected <- newTVarIO False
sendPings <- newTVarIO False
lastReceived <- newTVarIO ts
timeoutErrorCount <- newTVarIO 0
clientCorrId <- atomically $ C.newRandomDRG g
sentCommands <- TM.emptyIO
sndQ <- newTBQueueIO qSize
rcvQ <- newTBQueueIO qSize
connected <- newTVar False
sendPings <- newTVar False
lastReceived <- newTVar ts
timeoutErrorCount <- newTVar 0
clientCorrId <- C.newRandomDRG g
sentCommands <- TM.empty
sndQ <- newTBQueue qSize
rcvQ <- newTBQueue qSize
return
PClient
{ connected,
@@ -528,10 +477,10 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
runClient :: (ServiceName, ATransport) -> TransportHost -> PClient v err msg -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg))
runClient (port', ATransport t) useHost c = do
cVar <- newEmptyTMVarIO
let tcConfig = (transportClientConfig networkConfig useHost useSNI) {alpn = clientALPN}
socksCreds = clientSocksCredentials networkConfig proxySessTs transportSession
let tcConfig = (transportClientConfig networkConfig useHost) {alpn = clientALPN}
username = proxyUsername transportSession
tId <-
runTransportClient tcConfig socksCreds useHost port' (Just $ keyHash srv) (client t c cVar)
runTransportClient tcConfig (Just username) useHost port' (Just $ keyHash srv) (client t c cVar)
`forkFinally` \_ -> void (atomically . tryPutTMVar cVar $ Left PCENetworkError)
c_ <- tcpConnectTimeout `timeout` atomically (takeTMVar cVar)
case c_ of
@@ -541,9 +490,7 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
useTransport :: (ServiceName, ATransport)
useTransport = case port srv of
"" -> case protocolTypeI @(ProtoType msg) of
SPSMP | smpWebPort -> ("443", transport @TLS)
_ -> defaultTransport cfg
"" -> defaultTransport cfg
"80" -> ("80", transport @WS)
p -> (p, transport @TLS)
@@ -559,8 +506,9 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
atomically $ do
writeTVar (connected c) True
putTMVar cVar $ Right c'
raceAny_ ([send c' th, process c', receive c' th] <> [monitor c' | smpPingInterval > 0])
`finally` disconnected c'
raceAny_ ([send c' th, process c', receive c' th] <> [monitor c' | smpPingInterval > 0]) `finally` do
atomically $ writeTVar (connected c) False
disconnected c'
send :: Transport c => ProtocolClient v err msg -> THandle v c 'TClient -> IO ()
send ProtocolClient {client_ = PClient {sndQ}} h = forever $ atomically (readTBQueue sndQ) >>= sendPending
@@ -587,7 +535,7 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
if remaining > 1_000_000 -- delay pings only for significant time
then loop remaining
else do
whenM (readTVarIO sendPings) $ void . runExceptT $ sendProtocolCommand c Nothing NoEntity (protocolPing @v @err @msg)
whenM (readTVarIO sendPings) $ void . runExceptT $ sendProtocolCommand c Nothing "" (protocolPing @v @err @msg)
-- sendProtocolCommand/getResponse updates counter for each command
cnt <- readTVarIO timeoutErrorCount
-- drop client when maxCnt of commands have timed out in sequence, but only after some time has passed after last received response
@@ -608,7 +556,7 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
processMsg ProtocolClient {client_ = PClient {sentCommands}} (_, _, (corrId, entId, respOrErr))
| B.null $ bs corrId = sendMsg $ STEvent clientResp
| otherwise =
TM.lookupIO corrId sentCommands >>= \case
atomically (TM.lookup corrId sentCommands) >>= \case
Nothing -> sendMsg $ STUnexpectedError unexpected
Just Request {entityId, command, pending, responseVar} -> do
wasPending <-
@@ -639,6 +587,10 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
unexpectedResponse :: Show r => r -> ProtocolClientError err
unexpectedResponse = PCEUnexpectedResponse . B.pack . take 32 . show
proxyUsername :: TransportSession msg -> ByteString
proxyUsername (userId, _, entityId_) = C.sha256Hash $ bshow userId <> maybe "" (":" <>) entityId_
{-# INLINE proxyUsername #-}
-- | Disconnects client from the server and terminates client threads.
closeProtocolClient :: ProtocolClient v err msg -> IO ()
closeProtocolClient = mapM_ (deRefWeak >=> mapM_ killThread) . action
@@ -709,7 +661,7 @@ createSMPQueue ::
Bool ->
ExceptT SMPClientError IO QueueIdsKeys
createSMPQueue c (rKey, rpKey) dhKey auth subMode sndSecure =
sendSMPCommand c (Just rpKey) NoEntity (NEW rKey dhKey auth subMode sndSecure) >>= \case
sendSMPCommand c (Just rpKey) "" (NEW rKey dhKey auth subMode sndSecure) >>= \case
IDS qik -> pure qik
r -> throwE $ unexpectedResponse r
@@ -717,8 +669,8 @@ createSMPQueue c (rKey, rpKey) dhKey auth subMode sndSecure =
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#subscribe-to-queue
subscribeSMPQueue :: SMPClient -> RcvPrivateAuthKey -> RecipientId -> ExceptT SMPClientError IO ()
subscribeSMPQueue c rpKey rId = do
liftIO $ enablePings c
subscribeSMPQueue c@ProtocolClient {client_ = PClient {sendPings}} rpKey rId = do
liftIO . atomically $ writeTVar sendPings True
sendSMPCommand c (Just rpKey) rId SUB >>= \case
OK -> pure ()
cmd@MSG {} -> liftIO $ writeSMPMessage c rId cmd
@@ -726,8 +678,8 @@ subscribeSMPQueue c rpKey rId = do
-- | Subscribe to multiple SMP queues batching commands if supported.
subscribeSMPQueues :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId) -> IO (NonEmpty (Either SMPClientError ()))
subscribeSMPQueues c qs = do
liftIO $ enablePings c
subscribeSMPQueues c@ProtocolClient {client_ = PClient {sendPings}} qs = do
atomically $ writeTVar sendPings True
sendProtocolCommands c cs >>= mapM (processSUBResponse c)
where
cs = L.map (\(rpKey, rId) -> (Just rpKey, rId, Cmd SRecipient SUB)) qs
@@ -766,22 +718,14 @@ getSMPMessage c rpKey rId =
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#subscribe-to-queue-notifications
subscribeSMPQueueNotifications :: SMPClient -> NtfPrivateAuthKey -> NotifierId -> ExceptT SMPClientError IO ()
subscribeSMPQueueNotifications c npKey nId = do
liftIO $ enablePings c
okSMPCommand NSUB c npKey nId
subscribeSMPQueueNotifications = okSMPCommand NSUB
{-# INLINE subscribeSMPQueueNotifications #-}
-- | Subscribe to multiple SMP queues notifications batching commands if supported.
subscribeSMPQueuesNtfs :: SMPClient -> NonEmpty (NtfPrivateAuthKey, NotifierId) -> IO (NonEmpty (Either SMPClientError ()))
subscribeSMPQueuesNtfs c qs = do
liftIO $ enablePings c
okSMPCommands NSUB c qs
subscribeSMPQueuesNtfs = okSMPCommands NSUB
{-# INLINE subscribeSMPQueuesNtfs #-}
enablePings :: SMPClient -> IO ()
enablePings ProtocolClient {client_ = PClient {sendPings}} = atomically $ writeTVar sendPings True
{-# INLINE enablePings #-}
-- | Secure the SMP queue by adding a sender public key.
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#secure-queue-command
@@ -876,7 +820,7 @@ deleteSMPQueues = okSMPCommands DEL
connectSMPProxiedRelay :: SMPClient -> SMPServer -> Maybe BasicAuth -> ExceptT SMPClientError IO ProxiedRelay
connectSMPProxiedRelay c@ProtocolClient {client_ = PClient {tcpConnectTimeout, tcpTimeout}} relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth
| thVersion (thParams c) >= sendingProxySMPVersion =
sendProtocolCommand_ c Nothing tOut Nothing NoEntity (Cmd SProxiedClient (PRXY relayServ proxyAuth)) >>= \case
sendProtocolCommand_ c Nothing tOut Nothing "" (Cmd SProxiedClient (PRXY relayServ proxyAuth)) >>= \case
PKEY sId vr (chain, key) ->
case supportedClientSMPRelayVRange `compatibleVersion` vr of
Nothing -> throwE $ transportErr TEVersion
@@ -978,7 +922,7 @@ proxySMPCommand c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c
et <- liftEitherWith PCECryptoError $ EncTransmission <$> C.cbEncrypt cmdSecret nonce b paddedProxiedTLength
-- proxy interaction errors are wrapped
let tOut = Just $ 2 * tcpTimeout
tryE (sendProtocolCommand_ c (Just nonce) tOut Nothing (EntityId sessionId) (Cmd SProxiedClient (PFWD v cmdPubKey et))) >>= \case
tryE (sendProtocolCommand_ c (Just nonce) tOut Nothing sessionId (Cmd SProxiedClient (PFWD v cmdPubKey et))) >>= \case
Right r -> case r of
PRES (EncResponse er) -> do
-- server interaction errors are thrown directly
@@ -1014,7 +958,7 @@ forwardSMPTransmission c@ProtocolClient {thParams, client_ = PClient {clientCorr
let fwdT = FwdTransmission {fwdCorrId, fwdVersion, fwdKey, fwdTransmission}
eft = EncFwdTransmission $ C.cbEncryptNoPad sessSecret nonce (smpEncode fwdT)
-- send
sendProtocolCommand_ c (Just nonce) Nothing Nothing NoEntity (Cmd SSender (RFWD eft)) >>= \case
sendProtocolCommand_ c (Just nonce) Nothing Nothing "" (Cmd SSender (RFWD eft)) >>= \case
RRES (EncFwdResponse efr) -> do
-- unwrap
r' <- liftEitherWith PCECryptoError $ C.cbDecryptNoPad sessSecret (C.reverseNonce nonce) efr
@@ -1062,7 +1006,7 @@ sendProtocolCommands c@ProtocolClient {thParams = THandleParams {batch, blockSiz
| diff == 0 = pure $ L.fromList rs
| diff > 0 = do
putStrLn "send error: fewer responses than expected"
pure $ L.fromList $ rs <> replicate diff (Response NoEntity $ Left $ PCETransportError TEBadBlock)
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
@@ -1136,13 +1080,13 @@ mkTransmission_ ProtocolClient {thParams, client_ = PClient {clientCorrId, sentC
nonce@(C.CbNonce corrId) <- maybe (atomically $ C.randomCbNonce clientCorrId) pure nonce_
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, entityId, command)
auth = authTransmission (thAuth thParams) pKey_ nonce tForAuth
r <- mkRequest (CorrId corrId)
r <- atomically $ mkRequest (CorrId corrId)
pure ((,tToSend) <$> auth, r)
where
mkRequest :: CorrId -> IO (Request err msg)
mkRequest :: CorrId -> STM (Request err msg)
mkRequest corrId = do
pending <- newTVarIO True
responseVar <- newEmptyTMVarIO
pending <- newTVar True
responseVar <- newEmptyTMVar
let r =
Request
{ corrId,
@@ -1151,7 +1095,7 @@ mkTransmission_ ProtocolClient {thParams, client_ = PClient {clientCorrId, sentC
pending,
responseVar
}
atomically $ TM.insert corrId r sentCommands
TM.insert corrId r sentCommands
pure r
authTransmission :: Maybe (THandleAuth 'TClient) -> Maybe C.APrivateAuthKey -> C.CbNonce -> ByteString -> Either TransportError (Maybe TransmissionAuth)
+44 -54
View File
@@ -76,7 +76,7 @@ data SMPClientAgentConfig = SMPClientAgentConfig
defaultSMPClientAgentConfig :: SMPClientAgentConfig
defaultSMPClientAgentConfig =
SMPClientAgentConfig
{ smpCfg = defaultSMPClientConfig,
{ smpCfg = defaultSMPClientConfig {defaultTransport = ("5223", transport @TLS)},
reconnectInterval =
RetryInterval
{ initialInterval = second,
@@ -84,8 +84,8 @@ defaultSMPClientAgentConfig =
maxInterval = 10 * second
},
persistErrorInterval = 30, -- seconds
msgQSize = 2048,
agentQSize = 2048,
msgQSize = 1024,
agentQSize = 1024,
agentSubsBatchSize = 1360,
ownServerDomains = []
}
@@ -95,13 +95,12 @@ defaultSMPClientAgentConfig =
data SMPClientAgent = SMPClientAgent
{ agentCfg :: SMPClientAgentConfig,
active :: TVar Bool,
startedAt :: UTCTime,
msgQ :: TBQueue (ServerTransmissionBatch SMPVersion ErrorType BrokerMsg),
agentQ :: TBQueue SMPClientAgentEvent,
randomDrg :: TVar ChaChaDRG,
smpClients :: TMap SMPServer SMPClientVar,
smpSessions :: TMap SessionId (OwnServer, SMPClient),
srvSubs :: TMap SMPServer (TMap SMPSub (SessionId, C.APrivateAuthKey)),
srvSubs :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey),
pendingSrvSubs :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey),
smpSubWorkers :: TMap SMPServer (SessionVar (Async ())),
workerSeq :: TVar Int
@@ -109,23 +108,21 @@ data SMPClientAgent = SMPClientAgent
type OwnServer = Bool
newSMPClientAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> IO SMPClientAgent
newSMPClientAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> STM SMPClientAgent
newSMPClientAgent agentCfg@SMPClientAgentConfig {msgQSize, agentQSize} randomDrg = do
active <- newTVarIO True
startedAt <- getCurrentTime
msgQ <- newTBQueueIO msgQSize
agentQ <- newTBQueueIO agentQSize
smpClients <- TM.emptyIO
smpSessions <- TM.emptyIO
srvSubs <- TM.emptyIO
pendingSrvSubs <- TM.emptyIO
smpSubWorkers <- TM.emptyIO
workerSeq <- newTVarIO 0
active <- newTVar True
msgQ <- newTBQueue msgQSize
agentQ <- newTBQueue agentQSize
smpClients <- TM.empty
smpSessions <- TM.empty
srvSubs <- TM.empty
pendingSrvSubs <- TM.empty
smpSubWorkers <- TM.empty
workerSeq <- newTVar 0
pure
SMPClientAgent
{ agentCfg,
active,
startedAt,
msgQ,
agentQ,
randomDrg,
@@ -197,8 +194,8 @@ isOwnServer SMPClientAgent {agentCfg} ProtocolServer {host} =
-- | Run an SMP client for SMPClientVar
connectClient :: SMPClientAgent -> SMPServer -> SMPClientVar -> IO (Either SMPClientError SMPClient)
connectClient ca@SMPClientAgent {agentCfg, smpClients, smpSessions, msgQ, randomDrg, startedAt} srv v =
getProtocolClient randomDrg (1, srv, Nothing) (smpCfg agentCfg) (Just msgQ) startedAt clientDisconnected
connectClient ca@SMPClientAgent {agentCfg, smpClients, smpSessions, msgQ, randomDrg} srv v =
getProtocolClient randomDrg (1, srv, Nothing) (smpCfg agentCfg) (Just msgQ) clientDisconnected
where
clientDisconnected :: SMPClient -> IO ()
clientDisconnected smp = do
@@ -207,17 +204,14 @@ connectClient ca@SMPClientAgent {agentCfg, smpClients, smpSessions, msgQ, random
removeClientAndSubs :: SMPClient -> IO (Maybe (Map SMPSub C.APrivateAuthKey))
removeClientAndSubs smp = atomically $ do
TM.delete sessId smpSessions
removeSessVar v srv smpClients
TM.lookup srv (srvSubs ca) >>= mapM updateSubs
TM.delete (sessionId $ thParams smp) smpSessions
TM.lookupDelete srv (srvSubs ca) >>= mapM updateSubs
where
sessId = sessionId $ thParams smp
updateSubs sVar = do
-- removing subscriptions that have matching sessionId to disconnected client
-- and keep the other ones (they can be made by the new client)
pending <- M.map snd <$> stateTVar sVar (M.partition ((sessId ==) . fst))
addSubs_ (pendingSrvSubs ca) srv pending
pure pending
ss <- readTVar sVar
addSubs_ (pendingSrvSubs ca) srv ss
pure ss
serverDown :: Map SMPSub C.APrivateAuthKey -> IO ()
serverDown ss = unless (M.null ss) $ do
@@ -232,7 +226,7 @@ reconnectClient ca@SMPClientAgent {active, agentCfg, smpSubWorkers, workerSeq} s
where
getWorkerVar ts =
ifM
(noPending)
(null <$> getPending)
(pure Nothing) -- prevent race with cleanup and adding pending queues in another call
(Just <$> getSessVar workerSeq srv smpSubWorkers ts)
newSubWorker :: SessionVar (Async ()) -> IO ()
@@ -241,13 +235,12 @@ reconnectClient ca@SMPClientAgent {active, agentCfg, smpSubWorkers, workerSeq} s
atomically $ putTMVar (sessionVar v) a
runSubWorker =
withRetryInterval (reconnectInterval agentCfg) $ \_ loop -> do
pending <- liftIO getPending
pending <- atomically getPending
unless (null pending) $ whenM (readTVarIO active) $ do
void $ tcpConnectTimeout `timeout` runExceptT (reconnectSMPClient ca srv pending)
loop
ProtocolClientConfig {networkConfig = NetworkConfig {tcpConnectTimeout}} = smpCfg agentCfg
noPending = maybe (pure True) (fmap M.null . readTVar) =<< TM.lookup srv (pendingSrvSubs ca)
getPending = maybe (pure M.empty) readTVarIO =<< TM.lookupIO srv (pendingSrvSubs ca)
getPending = maybe (pure M.empty) readTVar =<< TM.lookup srv (pendingSrvSubs ca)
cleanup :: SessionVar (Async ()) -> STM ()
cleanup v = do
-- Here we wait until TMVar is not empty to prevent worker cleanup happening before worker is added to TMVar.
@@ -258,14 +251,14 @@ reconnectClient ca@SMPClientAgent {active, agentCfg, smpSubWorkers, workerSeq} s
reconnectSMPClient :: SMPClientAgent -> SMPServer -> Map SMPSub C.APrivateAuthKey -> ExceptT SMPClientError IO ()
reconnectSMPClient ca@SMPClientAgent {agentCfg} srv cs =
withSMP ca srv $ \smp -> liftIO $ do
currSubs <- maybe (pure M.empty) readTVarIO =<< TM.lookupIO srv (srvSubs ca)
currSubs <- atomically $ maybe (pure M.empty) readTVar =<< TM.lookup srv (srvSubs ca)
let (nSubs, rSubs) = foldr (groupSub currSubs) ([], []) $ M.assocs cs
subscribe_ smp SPNotifier nSubs
subscribe_ smp SPRecipient rSubs
where
groupSub :: Map SMPSub (SessionId, C.APrivateAuthKey) -> (SMPSub, C.APrivateAuthKey) -> ([(QueueId, C.APrivateAuthKey)], [(QueueId, C.APrivateAuthKey)]) -> ([(QueueId, C.APrivateAuthKey)], [(QueueId, C.APrivateAuthKey)])
groupSub currSubs (s@(party, qId), k) acc@(nSubs, rSubs)
| M.member s currSubs = acc
groupSub :: Map SMPSub C.APrivateAuthKey -> (SMPSub, C.APrivateAuthKey) -> ([(QueueId, C.APrivateAuthKey)], [(QueueId, C.APrivateAuthKey)]) -> ([(QueueId, C.APrivateAuthKey)], [(QueueId, C.APrivateAuthKey)])
groupSub currSubs (s@(party, qId), k) (nSubs, rSubs)
| M.member s currSubs = (nSubs, rSubs)
| otherwise = case party of
SPNotifier -> (s' : nSubs, rSubs)
SPRecipient -> (nSubs, s' : rSubs)
@@ -293,8 +286,8 @@ getConnectedSMPServerClient SMPClientAgent {smpClients} srv =
(Nothing <$ atomically (removeSessVar v srv smpClients)) -- proxy will create a new connection
(pure $ Just $ Left e) -- not expired, returning error
lookupSMPServerClient :: SMPClientAgent -> SessionId -> IO (Maybe (OwnServer, SMPClient))
lookupSMPServerClient SMPClientAgent {smpSessions} sessId = TM.lookupIO sessId smpSessions
lookupSMPServerClient :: SMPClientAgent -> SessionId -> STM (Maybe (OwnServer, SMPClient))
lookupSMPServerClient SMPClientAgent {smpSessions} sessId = TM.lookup sessId smpSessions
closeSMPClientAgent :: SMPClientAgent -> IO ()
closeSMPClientAgent c = do
@@ -353,18 +346,17 @@ smpSubscribeQueues party ca smp srv subs = do
when tempErrs $ reconnectClient ca srv
Nothing -> reconnectClient ca srv
where
processSubscriptions :: NonEmpty (Either SMPClientError ()) -> STM (Bool, [(QueueId, SMPClientError)], [(QueueId, (SessionId, C.APrivateAuthKey))], [QueueId])
processSubscriptions :: NonEmpty (Either SMPClientError ()) -> STM (Bool, [(QueueId, SMPClientError)], [(QueueId, C.APrivateAuthKey)], [QueueId])
processSubscriptions rs = do
pending <- maybe (pure M.empty) readTVar =<< TM.lookup srv (pendingSrvSubs ca)
let acc@(_, _, oks, notPending) = foldr (groupSub pending) (False, [], [], []) (L.zip subs rs)
unless (null oks) $ addSubscriptions ca srv party oks
unless (null notPending) $ removePendingSubs ca srv party notPending
pure acc
sessId = sessionId $ thParams smp
groupSub :: Map SMPSub C.APrivateAuthKey -> ((QueueId, C.APrivateAuthKey), Either SMPClientError ()) -> (Bool, [(QueueId, SMPClientError)], [(QueueId, (SessionId, C.APrivateAuthKey))], [QueueId]) -> (Bool, [(QueueId, SMPClientError)], [(QueueId, (SessionId, C.APrivateAuthKey))], [QueueId])
groupSub pending ((qId, pk), r) acc@(!tempErrs, finalErrs, oks, notPending) = case r of
groupSub :: Map SMPSub C.APrivateAuthKey -> ((QueueId, C.APrivateAuthKey), Either SMPClientError ()) -> (Bool, [(QueueId, SMPClientError)], [(QueueId, C.APrivateAuthKey)], [QueueId]) -> (Bool, [(QueueId, SMPClientError)], [(QueueId, C.APrivateAuthKey)], [QueueId])
groupSub pending (s@(qId, _), r) acc@(!tempErrs, finalErrs, oks, notPending) = case r of
Right ()
| M.member (party, qId) pending -> (tempErrs, finalErrs, (qId, (sessId, pk)) : oks, qId : notPending)
| M.member (party, qId) pending -> (tempErrs, finalErrs, s : oks, qId : notPending)
| otherwise -> acc
Left e
| temporaryClientError e -> (True, finalErrs, oks, notPending)
@@ -375,21 +367,19 @@ smpSubscribeQueues party ca smp srv subs = do
notify_ :: (SMPServer -> SMPSubParty -> NonEmpty a -> SMPClientAgentEvent) -> [a] -> IO ()
notify_ evt qs = mapM_ (notify ca . evt srv party) $ L.nonEmpty qs
activeClientSession' :: SMPClientAgent -> SessionId -> SMPServer -> STM Bool
activeClientSession' ca sessId srv = sameSess <$> tryReadSessVar srv (smpClients ca)
where
sameSess = \case
Just (Right (_, smp')) -> sessId == sessionId (thParams smp')
_ -> False
activeClientSession :: SMPClientAgent -> SMPClient -> SMPServer -> STM Bool
activeClientSession ca = activeClientSession' ca . sessionId . thParams
activeClientSession ca smp srv = sameSess <$> tryReadSessVar srv (smpClients ca)
where
sessId = sessionId . thParams
sameSess = \case
Just (Right (_, smp')) -> sessId smp == sessId smp'
_ -> False
showServer :: SMPServer -> ByteString
showServer ProtocolServer {host, port} =
strEncode host <> B.pack (if null port then "" else ':' : port)
addSubscriptions :: SMPClientAgent -> SMPServer -> SMPSubParty -> [(QueueId, (SessionId, C.APrivateAuthKey))] -> STM ()
addSubscriptions :: SMPClientAgent -> SMPServer -> SMPSubParty -> [(QueueId, C.APrivateAuthKey)] -> STM ()
addSubscriptions = addSubsList_ . srvSubs
{-# INLINE addSubscriptions #-}
@@ -397,12 +387,12 @@ addPendingSubs :: SMPClientAgent -> SMPServer -> SMPSubParty -> [(QueueId, C.APr
addPendingSubs = addSubsList_ . pendingSrvSubs
{-# INLINE addPendingSubs #-}
addSubsList_ :: TMap SMPServer (TMap SMPSub s) -> SMPServer -> SMPSubParty -> [(QueueId, s)] -> STM ()
addSubsList_ :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey) -> SMPServer -> SMPSubParty -> [(QueueId, C.APrivateAuthKey)] -> STM ()
addSubsList_ subs srv party ss = addSubs_ subs srv ss'
where
ss' = M.fromList $ map (first (party,)) ss
addSubs_ :: TMap SMPServer (TMap SMPSub s) -> SMPServer -> Map SMPSub s -> STM ()
addSubs_ :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey) -> SMPServer -> Map SMPSub C.APrivateAuthKey -> STM ()
addSubs_ subs srv ss =
TM.lookup srv subs >>= \case
Just m -> TM.union ss m
@@ -412,7 +402,7 @@ removeSubscription :: SMPClientAgent -> SMPServer -> SMPSub -> STM ()
removeSubscription = removeSub_ . srvSubs
{-# INLINE removeSubscription #-}
removeSub_ :: TMap SMPServer (TMap SMPSub s) -> SMPServer -> SMPSub -> STM ()
removeSub_ :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey) -> SMPServer -> SMPSub -> STM ()
removeSub_ subs srv s = TM.lookup srv subs >>= mapM_ (TM.delete s)
removePendingSubs :: SMPClientAgent -> SMPServer -> SMPSubParty -> [QueueId] -> STM ()
-27
View File
@@ -152,12 +152,6 @@ module Simplex.Messaging.Crypto
unsafeSbKey,
randomSbKey,
-- * secret_box chains
SbChainKey,
SbKeyNonce,
sbcInit,
sbcHkdf,
-- * pseudo-random bytes
randomBytes,
@@ -204,7 +198,6 @@ import qualified Crypto.Cipher.Types as AES
import qualified Crypto.Cipher.XSalsa as XSalsa
import qualified Crypto.Error as CE
import Crypto.Hash (Digest, SHA256 (..), SHA512 (..), hash, hashDigestSize)
import qualified Crypto.KDF.HKDF as H
import qualified Crypto.MAC.Poly1305 as Poly1305
import qualified Crypto.PubKey.Curve25519 as X25519
import qualified Crypto.PubKey.Curve448 as X448
@@ -1341,26 +1334,6 @@ unsafeSbKey s = either error id $ sbKey s
randomSbKey :: TVar ChaChaDRG -> STM SbKey
randomSbKey gVar = SecretBoxKey <$> randomBytes 32 gVar
newtype SbChainKey = SecretBoxChainKey {unSbChainKey :: ByteString}
deriving (Eq, Show)
sbcInit :: ByteArrayAccess secret => ByteString -> secret -> (SbChainKey, SbChainKey)
sbcInit salt secret = (SecretBoxChainKey ck1, SecretBoxChainKey ck2)
where
prk = H.extract salt secret :: H.PRK SHA512
out = H.expand prk ("SimpleXSbChainInit" :: ByteString) 64
(ck1, ck2) = B.splitAt 32 out
type SbKeyNonce = (SbKey, CbNonce)
sbcHkdf :: SbChainKey -> (SbKeyNonce, SbChainKey)
sbcHkdf (SecretBoxChainKey ck) = ((SecretBoxKey sk, CryptoBoxNonce nonce), SecretBoxChainKey ck')
where
prk = H.extract B.empty ck :: H.PRK SHA512
out = H.expand prk ("SimpleXSbChain" :: ByteString) 88 -- = 32 (new chain key) + 32 (secret_box key) + 24 (nonce)
(ck', rest) = B.splitAt 32 out
(sk, nonce) = B.splitAt 32 rest
xSalsa20 :: ByteArrayAccess key => key -> ByteString -> ByteString -> (ByteString, ByteString)
xSalsa20 secret nonce msg = (rs, msg')
where
+1 -16
View File
@@ -17,8 +17,6 @@ module Simplex.Messaging.Crypto.Lazy
kcbEncryptTailTag,
sbDecryptTailTag,
kcbDecryptTailTag,
sbEncryptTailTagNoPad,
sbDecryptTailTagNoPad,
fastReplicate,
secretBox,
secretBoxTailTag,
@@ -51,7 +49,7 @@ import Data.Composition ((.:.))
import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty (..))
import Foreign (sizeOf)
import Simplex.Messaging.Crypto (CbNonce, CryptoError (..), DhSecret (..), DhSecretX25519, SbKey, SbKeyNonce, pattern CbNonce, pattern SbKey)
import Simplex.Messaging.Crypto (CbNonce, CryptoError (..), DhSecret (..), DhSecretX25519, SbKey, pattern CbNonce, pattern SbKey)
import Simplex.Messaging.Crypto.SNTRUP761 (KEMHybridSecret (..))
import Simplex.Messaging.Encoding
@@ -144,10 +142,6 @@ sbEncryptTailTag_ :: ByteArrayAccess key => key -> CbNonce -> LazyByteString ->
sbEncryptTailTag_ key (CbNonce nonce) msg len paddedLen =
LB.fromChunks <$> (secretBoxTailTag sbEncryptChunk key nonce =<< pad msg len paddedLen)
sbEncryptTailTagNoPad :: SbKeyNonce -> LazyByteString -> Either CryptoError LazyByteString
sbEncryptTailTagNoPad (SbKey key, CbNonce nonce) msg =
LB.fromChunks <$> secretBoxTailTag sbEncryptChunk key nonce msg
-- | NaCl @secret_box@ decrypt with a symmetric 256-bit key and 192-bit nonce with appended auth tag (more efficient with large files).
-- paddedLen should NOT include the tag length, it should be the same number that is passed to sbEncrypt / sbEncryptTailTag.
sbDecryptTailTag :: SbKey -> CbNonce -> Int64 -> LazyByteString -> Either CryptoError (Bool, LazyByteString)
@@ -171,15 +165,6 @@ sbDecryptTailTag_ key (CbNonce nonce) paddedLen packet =
where
(c, tag') = LB.splitAt paddedLen packet
sbDecryptTailTagNoPad :: SbKeyNonce -> Int64 -> LazyByteString -> Either CryptoError (Bool, LazyByteString)
sbDecryptTailTagNoPad (SbKey key, CbNonce nonce) paddedLen packet =
result <$> secretBox sbDecryptChunk key nonce c
where
result (tag :| cs) =
let valid = LB.length tag' == 16 && BA.constEq (LB.toStrict tag') tag
in (valid, LB.fromChunks cs)
(c, tag') = LB.splitAt paddedLen packet
secretBoxTailTag :: ByteArrayAccess key => (SbState -> ByteString -> (ByteString, SbState)) -> key -> ByteString -> LazyByteString -> Either CryptoError [ByteString]
secretBoxTailTag sbProcess secret nonce msg = run <$> sbInit_ secret nonce
where
+2 -2
View File
@@ -4,7 +4,7 @@
module Simplex.Messaging.Crypto.SNTRUP761 where
import Crypto.Hash (Digest, SHA3_256, hash)
import Crypto.Hash (Digest, SHA256, hash)
import Data.ByteArray (ScrubbedBytes)
import qualified Data.ByteArray as BA
import Data.ByteString (ByteString)
@@ -28,4 +28,4 @@ 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 SHA3_256)
in KEMHybridSecret $ BA.convert (hash $ BA.convert dh <> kem :: Digest SHA256)
-2
View File
@@ -42,12 +42,10 @@ class Encoding a where
-- | decoding of type (default implementation uses parser)
smpDecode :: ByteString -> Either String a
smpDecode = parseAll smpP
{-# INLINE smpDecode #-}
-- | protocol parser of type (default implementation parses protocol ByteString encoding)
smpP :: Parser a
smpP = smpDecode <$?> smpP
{-# INLINE smpP #-}
instance Encoding Char where
smpEncode = B.singleton
+5 -17
View File
@@ -28,8 +28,6 @@ import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Char (isAlphaNum)
import Data.Int (Int64)
import Data.IntSet (IntSet)
import qualified Data.IntSet as IS
import qualified Data.List.NonEmpty as L
import Data.Set (Set)
import qualified Data.Set as S
@@ -41,7 +39,7 @@ import Data.Time.Format.ISO8601
import Data.Word (Word16, Word32)
import Simplex.Messaging.Encoding
import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Util (bshow, (<$?>))
import Simplex.Messaging.Util ((<$?>))
class TextEncoding a where
textEncode :: a -> Text
@@ -55,20 +53,14 @@ class StrEncoding a where
-- Please note - if you only specify strDecode, it will use base64urlP as default parser before decoding the string
strDecode :: ByteString -> Either String a
strDecode = parseAll strP
{-# INLINE strDecode #-}
strP :: Parser a
strP = strDecode <$?> base64urlP
{-# INLINE strP #-}
-- base64url encoding/decoding of ByteStrings - the parser only allows non-empty strings
instance StrEncoding ByteString where
strEncode = U.encode
{-# INLINE strEncode #-}
strDecode = U.decode
{-# INLINE strDecode #-}
strP = base64urlP
{-# INLINE strP #-}
base64urlP :: Parser ByteString
base64urlP = do
@@ -127,15 +119,15 @@ instance StrEncoding Bool where
{-# INLINE strP #-}
instance StrEncoding Int where
strEncode = bshow
strEncode = B.pack . show
{-# INLINE strEncode #-}
strP = A.signed A.decimal
strP = A.decimal
{-# INLINE strP #-}
instance StrEncoding Int64 where
strEncode = bshow
strEncode = B.pack . show
{-# INLINE strEncode #-}
strP = A.signed A.decimal
strP = A.decimal
{-# INLINE strP #-}
instance StrEncoding SystemTime where
@@ -162,10 +154,6 @@ instance (StrEncoding a, Ord a) => StrEncoding (Set a) where
strEncode = strEncodeList . S.toList
strP = S.fromList <$> listItem `A.sepBy'` A.char ','
instance StrEncoding IntSet where
strEncode = strEncodeList . IS.toList
strP = IS.fromList <$> listItem `A.sepBy'` A.char ','
listItem :: StrEncoding a => Parser a
listItem = parseAll strP <$?> A.takeTill (\c -> c == ',' || c == ' ' || c == '\n')
+4 -30
View File
@@ -1,36 +1,28 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE TypeApplications #-}
module Simplex.Messaging.Notifications.Client where
import Control.Monad.Except
import Control.Monad.Trans.Except
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as L
import Data.Word (Word16)
import Simplex.Messaging.Client
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Notifications.Protocol
import Simplex.Messaging.Notifications.Transport (NTFVersion, supportedClientNTFVRange, supportedNTFHandshakes)
import Simplex.Messaging.Protocol (ErrorType, pattern NoEntity)
import Simplex.Messaging.Transport (TLS, Transport (..))
import Simplex.Messaging.Protocol (ErrorType)
type NtfClient = ProtocolClient NTFVersion ErrorType NtfResponse
type NtfClientError = ProtocolClientError ErrorType
defaultNTFClientConfig :: ProtocolClientConfig NTFVersion
defaultNTFClientConfig =
(defaultClientConfig (Just supportedNTFHandshakes) False supportedClientNTFVRange)
{defaultTransport = ("443", transport @TLS)}
{-# INLINE defaultNTFClientConfig #-}
defaultNTFClientConfig = defaultClientConfig (Just supportedNTFHandshakes) supportedClientNTFVRange
ntfRegisterToken :: NtfClient -> C.APrivateAuthKey -> NewNtfEntity 'Token -> ExceptT NtfClientError IO (NtfTokenId, C.PublicKeyX25519)
ntfRegisterToken c pKey newTkn =
sendNtfCommand c (Just pKey) NoEntity (TNEW newTkn) >>= \case
sendNtfCommand c (Just pKey) "" (TNEW newTkn) >>= \case
NRTknId tknId dhKey -> pure (tknId, dhKey)
r -> throwE $ unexpectedResponse r
@@ -54,34 +46,16 @@ ntfEnableCron c pKey tknId int = okNtfCommand (TCRN int) c pKey tknId
ntfCreateSubscription :: NtfClient -> C.APrivateAuthKey -> NewNtfEntity 'Subscription -> ExceptT NtfClientError IO NtfSubscriptionId
ntfCreateSubscription c pKey newSub =
sendNtfCommand c (Just pKey) NoEntity (SNEW newSub) >>= \case
sendNtfCommand c (Just pKey) "" (SNEW newSub) >>= \case
NRSubId subId -> pure subId
r -> throwE $ unexpectedResponse r
ntfCreateSubscriptions :: NtfClient -> C.APrivateAuthKey -> NonEmpty (NewNtfEntity 'Subscription) -> IO (NonEmpty (Either NtfClientError NtfSubscriptionId))
ntfCreateSubscriptions c pKey newSubs = L.map process <$> sendProtocolCommands c cs
where
cs = L.map (\newSub -> (Just pKey, NoEntity, NtfCmd SSubscription $ SNEW newSub)) newSubs
process (Response _ r) = case r of
Right (NRSubId subId) -> Right subId
Right r' -> Left $ unexpectedResponse r'
Left e -> Left e
ntfCheckSubscription :: NtfClient -> C.APrivateAuthKey -> NtfSubscriptionId -> ExceptT NtfClientError IO NtfSubStatus
ntfCheckSubscription c pKey subId =
sendNtfCommand c (Just pKey) subId SCHK >>= \case
NRSub stat -> pure stat
r -> throwE $ unexpectedResponse r
ntfCheckSubscriptions :: NtfClient -> C.APrivateAuthKey -> NonEmpty NtfSubscriptionId -> IO (NonEmpty (Either NtfClientError NtfSubStatus))
ntfCheckSubscriptions c pKey subIds = L.map process <$> sendProtocolCommands c cs
where
cs = L.map (\subId -> (Just pKey, subId, NtfCmd SSubscription SCHK)) subIds
process (Response _ r) = case r of
Right (NRSub stat) -> Right stat
Right r' -> Left $ unexpectedResponse r'
Left e -> Left e
ntfDeleteSubscription :: NtfClient -> C.APrivateAuthKey -> NtfSubscriptionId -> ExceptT NtfClientError IO ()
ntfDeleteSubscription = okNtfCommand SDEL
@@ -208,7 +208,7 @@ instance NtfEntityI e => ProtocolEncoding NTFVersion ErrorType (NtfCommand e) wh
fromProtocolError = fromProtocolError @NTFVersion @ErrorType @NtfResponse
{-# INLINE fromProtocolError #-}
checkCredentials (auth, _, EntityId entityId, _) cmd = case cmd of
checkCredentials (auth, _, entityId, _) cmd = case cmd of
-- TNEW and SNEW must have signature but NOT token/subscription IDs
TNEW {} -> sigNoEntity
SNEW {} -> sigNoEntity
@@ -322,7 +322,7 @@ instance ProtocolEncoding NTFVersion ErrorType NtfResponse where
PEBlock -> BLOCK
{-# INLINE fromProtocolError #-}
checkCredentials (_, _, EntityId entId, _) cmd = case cmd of
checkCredentials (_, _, entId, _) cmd = case cmd of
-- IDTKN response must not have queue ID
NRTknId {} -> noEntity
-- IDSUB response must not have queue ID
@@ -426,7 +426,7 @@ instance FromJSON DeviceToken where
t <- encodeUtf8 <$> o .: "token"
pure $ DeviceToken pp t
type NtfEntityId = EntityId
type NtfEntityId = ByteString
type NtfSubscriptionId = NtfEntityId
@@ -443,8 +443,6 @@ data NtfSubStatus
NSInactive
| -- | END received
NSEnd
| -- | DELD received (connection was deleted)
NSDeleted
| -- | SMP AUTH error
NSAuth
| -- | SMP error other than AUTH
@@ -458,7 +456,6 @@ ntfShouldSubscribe = \case
NSActive -> True
NSInactive -> True
NSEnd -> False
NSDeleted -> False
NSAuth -> False
NSErr _ -> False
@@ -469,7 +466,6 @@ instance Encoding NtfSubStatus where
NSActive -> "ACTIVE"
NSInactive -> "INACTIVE"
NSEnd -> "END"
NSDeleted -> "DELETED"
NSAuth -> "AUTH"
NSErr err -> "ERR " <> err
smpP =
@@ -479,7 +475,6 @@ instance Encoding NtfSubStatus where
"ACTIVE" -> pure NSActive
"INACTIVE" -> pure NSInactive
"END" -> pure NSEnd
"DELETED" -> pure NSDeleted
"AUTH" -> pure NSAuth
"ERR" -> NSErr <$> (A.space *> A.takeByteString)
_ -> fail "bad NtfSubStatus"
+60 -203
View File
@@ -1,5 +1,3 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
@@ -9,7 +7,6 @@
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
@@ -21,12 +18,9 @@ import Control.Monad.Except
import Control.Monad.Reader
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Either (partitionEithers)
import Data.Functor (($>))
import Data.IORef
import Data.Int (Int64)
import qualified Data.IntSet as IS
import Data.List (intercalate, partition, sort)
import Data.List (intercalate, sort)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as L
import qualified Data.Map.Strict as M
@@ -36,43 +30,34 @@ import Data.Text.Encoding (decodeLatin1)
import Data.Time.Clock (UTCTime (..), diffTimeToPicoseconds, getCurrentTime)
import Data.Time.Clock.System (getSystemTime)
import Data.Time.Format.ISO8601 (iso8601Show)
import GHC.IORef (atomicSwapIORef)
import GHC.Stats (getRTSStats)
import Network.Socket (ServiceName, Socket, socketToHandle)
import Network.Socket (ServiceName)
import Simplex.Messaging.Client (ProtocolClientError (..), SMPClientError, ServerTransmission (..))
import Simplex.Messaging.Client.Agent
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Notifications.Protocol
import Simplex.Messaging.Notifications.Server.Control
import Simplex.Messaging.Notifications.Server.Env
import Simplex.Messaging.Notifications.Server.Push.APNS (PNMessageData (..), PushNotification (..), PushProviderError (..))
import Simplex.Messaging.Notifications.Server.Stats
import Simplex.Messaging.Notifications.Server.Store
import Simplex.Messaging.Notifications.Server.StoreLog
import Simplex.Messaging.Notifications.Transport
import Simplex.Messaging.Protocol (EntityId (..), ErrorType (..), ProtocolServer (host), SMPServer, SignedTransmission, Transmission, pattern NoEntity, pattern SMPServer, encodeTransmission, tGet, tPut)
import Simplex.Messaging.Protocol (ErrorType (..), ProtocolServer (host), SMPServer, SignedTransmission, Transmission, encodeTransmission, tGet, tPut)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.Server
import Simplex.Messaging.Server.Control (CPClientRole (..))
import Simplex.Messaging.Server.Stats (PeriodStats (..), PeriodStatCounts (..), periodStatCounts, updatePeriodStats)
import Simplex.Messaging.TMap (TMap)
import Simplex.Messaging.Server.Stats
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport (ATransport (..), THandle (..), THandleAuth (..), THandleParams (..), TProxy, Transport (..), TransportPeer (..), defaultSupportedParams)
import Simplex.Messaging.Transport.Buffer (trimCR)
import Simplex.Messaging.Transport.Server (AddHTTP, runTransportServer, runLocalTCPServer)
import Simplex.Messaging.Transport (ATransport (..), THandle (..), THandleAuth (..), THandleParams (..), TProxy, Transport (..), TransportPeer (..))
import Simplex.Messaging.Transport.Server (runTransportServer, tlsServerCredentials)
import Simplex.Messaging.Util
import System.Exit (exitFailure)
import System.IO (BufferMode (..), hClose, hPrint, hPutStrLn, hSetBuffering, hSetNewlineMode, universalNewlineMode)
import System.IO (BufferMode (..), hPutStrLn, hSetBuffering)
import System.Mem.Weak (deRefWeak)
import UnliftIO (IOMode (..), UnliftIO, askUnliftIO, async, uninterruptibleCancel, unliftIO, withFile)
import UnliftIO (IOMode (..), async, uninterruptibleCancel, withFile)
import UnliftIO.Concurrent (forkIO, killThread, mkWeakThreadId)
import UnliftIO.Directory (doesFileExist, renameFile)
import UnliftIO.Exception
import UnliftIO.STM
#if MIN_VERSION_base(4,18,0)
import GHC.Conc (listThreads)
#endif
runNtfServer :: NtfServerConfig -> IO ()
runNtfServer cfg = do
@@ -90,14 +75,14 @@ ntfServer cfg@NtfServerConfig {transports, transportConfig = tCfg} started = do
s <- asks subscriber
ps <- asks pushServer
resubscribe s
raceAny_ (ntfSubscriber s : ntfPush ps : map runServer transports <> serverStatsThread_ cfg <> controlPortThread_ cfg) `finally` stopServer
raceAny_ (ntfSubscriber s : ntfPush ps : map runServer transports <> serverStatsThread_ cfg) `finally` stopServer
where
runServer :: (ServiceName, ATransport, AddHTTP) -> M ()
runServer (tcpPort, ATransport t, _addHTTP) = do
srvCreds <- asks tlsServerCreds
serverSignKey <- either fail pure $ fromTLSCredentials srvCreds
runServer :: (ServiceName, ATransport) -> M ()
runServer (tcpPort, ATransport t) = do
serverParams <- asks tlsServerParams
serverSignKey <- either fail pure . fromTLSCredentials $ tlsServerCredentials serverParams
env <- ask
liftIO $ runTransportServer started tcpPort defaultSupportedParams srvCreds (Just supportedNTFHandshakes) tCfg $ \h -> runClient serverSignKey t h `runReaderT` env
liftIO $ runTransportServer started tcpPort serverParams tCfg $ \h -> runClient serverSignKey t h `runReaderT` env
fromTLSCredentials (_, pk) = C.x509ToPrivate (pk, []) >>= C.privKey
runClient :: Transport c => C.APrivateSignKey -> TProxy c -> c -> M ()
@@ -133,16 +118,16 @@ ntfServer cfg@NtfServerConfig {transports, transportConfig = tCfg} started = do
withFile statsFilePath AppendMode $ \h -> liftIO $ do
hSetBuffering h LineBuffering
ts <- getCurrentTime
fromTime' <- atomicSwapIORef fromTime ts
tknCreated' <- atomicSwapIORef tknCreated 0
tknVerified' <- atomicSwapIORef tknVerified 0
tknDeleted' <- atomicSwapIORef tknDeleted 0
subCreated' <- atomicSwapIORef subCreated 0
subDeleted' <- atomicSwapIORef subDeleted 0
ntfReceived' <- atomicSwapIORef ntfReceived 0
ntfDelivered' <- atomicSwapIORef ntfDelivered 0
tkn <- liftIO $ periodStatCounts activeTokens ts
sub <- liftIO $ periodStatCounts activeSubs ts
fromTime' <- atomically $ swapTVar fromTime ts
tknCreated' <- atomically $ swapTVar tknCreated 0
tknVerified' <- atomically $ swapTVar tknVerified 0
tknDeleted' <- atomically $ swapTVar tknDeleted 0
subCreated' <- atomically $ swapTVar subCreated 0
subDeleted' <- atomically $ swapTVar subDeleted 0
ntfReceived' <- atomically $ swapTVar ntfReceived 0
ntfDelivered' <- atomically $ swapTVar ntfDelivered 0
tkn <- atomically $ periodStatCounts activeTokens ts
sub <- atomically $ periodStatCounts activeSubs ts
hPutStrLn h $
intercalate
","
@@ -163,131 +148,6 @@ ntfServer cfg@NtfServerConfig {transports, transportConfig = tCfg} started = do
]
liftIO $ threadDelay' interval
controlPortThread_ :: NtfServerConfig -> [M ()]
controlPortThread_ NtfServerConfig {controlPort = Just port} = [runCPServer port]
controlPortThread_ _ = []
runCPServer :: ServiceName -> M ()
runCPServer port = do
cpStarted <- newEmptyTMVarIO
u <- askUnliftIO
liftIO $ do
labelMyThread "control port server"
runLocalTCPServer cpStarted port $ runCPClient u
where
runCPClient :: UnliftIO (ReaderT NtfEnv IO) -> Socket -> IO ()
runCPClient u sock = do
labelMyThread "control port client"
h <- socketToHandle sock ReadWriteMode
hSetBuffering h LineBuffering
hSetNewlineMode h universalNewlineMode
hPutStrLn h "Ntf server control port\n'help' for supported commands"
role <- newTVarIO CPRNone
cpLoop h role
where
cpLoop h role = do
s <- trimCR <$> B.hGetLine h
case strDecode s of
Right CPQuit -> hClose h
Right cmd -> logCmd s cmd >> processCP h role cmd >> cpLoop h role
Left err -> hPutStrLn h ("error: " <> err) >> cpLoop h role
logCmd s cmd = when shouldLog $ logWarn $ "ControlPort: " <> tshow s
where
shouldLog = case cmd of
CPAuth _ -> False
CPHelp -> False
CPQuit -> False
CPSkip -> False
_ -> True
processCP h role = \case
CPAuth auth -> atomically $ writeTVar role $! newRole cfg
where
newRole NtfServerConfig {controlPortUserAuth = user, controlPortAdminAuth = admin}
| Just auth == admin = CPRAdmin
| Just auth == user = CPRUser
| otherwise = CPRNone
CPStats -> withUserRole $ do
ss <- unliftIO u $ asks serverStats
let getStat :: (NtfServerStats -> IORef a) -> IO a
getStat var = readIORef (var ss)
putStat :: Show a => String -> (NtfServerStats -> IORef a) -> IO ()
putStat label var = getStat var >>= \v -> hPutStrLn h $ label <> ": " <> show v
putStat "fromTime" fromTime
putStat "tknCreated" tknCreated
putStat "tknVerified" tknVerified
putStat "tknDeleted" tknDeleted
putStat "subCreated" subCreated
putStat "subDeleted" subDeleted
putStat "ntfReceived" ntfReceived
getStat (day . activeTokens) >>= \v -> hPutStrLn h $ "daily active tokens: " <> show (IS.size v)
getStat (day . activeSubs) >>= \v -> hPutStrLn h $ "daily active subscriptions: " <> show (IS.size v)
CPStatsRTS -> tryAny getRTSStats >>= either (hPrint h) (hPrint h)
CPServerInfo -> readTVarIO role >>= \case
CPRNone -> do
logError "Unauthorized control port command"
hPutStrLn h "AUTH"
r -> do
#if MIN_VERSION_base(4,18,0)
threads <- liftIO listThreads
hPutStrLn h $ "Threads: " <> show (length threads)
#else
hPutStrLn h "Threads: not available on GHC 8.10"
#endif
NtfSubscriber {smpSubscribers, smpAgent = a} <- unliftIO u $ asks subscriber
putSMPWorkers a "SMP subcscribers" smpSubscribers
let SMPClientAgent {smpClients, smpSessions, srvSubs, pendingSrvSubs, smpSubWorkers} = a
putSMPWorkers a "SMP clients" smpClients
putSMPWorkers a "SMP subscription workers" smpSubWorkers
sessions <- readTVarIO smpSessions
hPutStrLn h $ "SMP sessions count: " <> show (M.size sessions)
putSMPSubs a "SMP subscriptions" srvSubs
putSMPSubs a "Pending SMP subscriptions" pendingSrvSubs
where
putSMPSubs :: SMPClientAgent -> String -> TMap SMPServer (TMap SMPSub a) -> IO ()
putSMPSubs a name v = do
subs <- readTVarIO v
(totalCnt, ownCount, otherCnt, servers, ownByServer) <- foldM countSubs (0, 0, 0, [], M.empty) $ M.assocs subs
showServers a name servers
hPutStrLn h $ name <> " total: " <> show totalCnt
hPutStrLn h $ name <> " on own servers: " <> show ownCount
when (r == CPRAdmin && not (null ownByServer)) $
forM_ (M.assocs ownByServer) $ \(SMPServer (host :| _) _ _, cnt) ->
hPutStrLn h $ name <> " on " <> B.unpack (strEncode host) <> ": " <> show cnt
hPutStrLn h $ name <> " on other servers: " <> show otherCnt
where
countSubs :: (Int, Int, Int, [SMPServer], M.Map SMPServer Int) -> (SMPServer, TMap SMPSub a) -> IO (Int, Int, Int, [SMPServer], M.Map SMPServer Int)
countSubs (!totalCnt, !ownCount, !otherCnt, !servers, !ownByServer) (srv, srvSubs) = do
cnt <- M.size <$> readTVarIO srvSubs
let totalCnt' = totalCnt + cnt
ownServer = isOwnServer a srv
(ownCount', otherCnt')
| ownServer = (ownCount + cnt, otherCnt)
| otherwise = (ownCount, otherCnt + cnt)
servers' = if cnt > 0 then srv : servers else servers
ownByServer'
| r == CPRAdmin && ownServer && cnt > 0 = M.alter (Just . maybe cnt (+ cnt)) srv ownByServer
| otherwise = ownByServer
pure (totalCnt', ownCount', otherCnt', servers', ownByServer')
putSMPWorkers :: SMPClientAgent -> String -> TMap SMPServer a -> IO ()
putSMPWorkers a name v = readTVarIO v >>= showServers a name . M.keys
showServers :: SMPClientAgent -> String -> [SMPServer] -> IO ()
showServers a name srvs = do
let (ownSrvs, otherSrvs) = partition (isOwnServer a) srvs
hPutStrLn h $ name <> " own servers count: " <> show (length ownSrvs)
when (r == CPRAdmin) $ hPutStrLn h $ name <> " own servers: " <> intercalate "," (sort $ map (\(SMPServer (host :| _) _ _) -> B.unpack $ strEncode host) ownSrvs)
hPutStrLn h $ name <> " other servers count: " <> show (length otherSrvs)
CPHelp -> hPutStrLn h "commands: stats, stats-rts, server-info, help, quit"
CPQuit -> pure ()
CPSkip -> pure ()
where
withUserRole action =
readTVarIO role >>= \case
CPRAdmin -> action
CPRUser -> action
_ -> do
logError "Unauthorized control port command"
hPutStrLn h "AUTH"
resubscribe :: NtfSubscriber -> M ()
resubscribe NtfSubscriber {newSubQ} = do
logInfo "Preparing SMP resubscriptions..."
@@ -316,10 +176,10 @@ ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAge
getSMPSubscriber :: SMPServer -> M SMPSubscriber
getSMPSubscriber smpServer =
liftIO (TM.lookupIO smpServer smpSubscribers) >>= maybe createSMPSubscriber pure
atomically (TM.lookup smpServer smpSubscribers) >>= maybe createSMPSubscriber pure
where
createSMPSubscriber = do
sub@SMPSubscriber {subThreadId} <- liftIO newSMPSubscriber
sub@SMPSubscriber {subThreadId} <- atomically newSMPSubscriber
atomically $ TM.insert smpServer sub smpSubscribers
tId <- mkWeakThreadId =<< forkIO (runSMPSubscriber sub)
atomically . writeTVar subThreadId $ Just tId
@@ -343,7 +203,7 @@ ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAge
receiveSMP :: M ()
receiveSMP = forever $ do
((_, srv, _), _thVersion, sessionId, ts) <- atomically $ readTBQueue msgQ
((_, srv, _), _, _, ts) <- atomically $ readTBQueue msgQ
forM ts $ \(ntfId, t) -> case t of
STUnexpectedError e -> logError $ "SMP client unexpected error: " <> tshow e -- uncorrelated response, should not happen
STResponse {} -> pure () -- it was already reported as timeout error
@@ -355,14 +215,12 @@ ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAge
st <- asks store
NtfPushServer {pushQ} <- asks pushServer
stats <- asks serverStats
liftIO $ updatePeriodStats (activeSubs stats) ntfId
atomically (findNtfSubscriptionToken st smpQueue)
>>= mapM_ (\tkn -> atomically (writeTBQueue pushQ (tkn, PNMessage (PNMessageData {smpQueue, ntfTs, nmsgNonce, encNMsgMeta} :| []))))
atomically $ updatePeriodStats (activeSubs stats) ntfId
atomically $
findNtfSubscriptionToken st smpQueue
>>= mapM_ (\tkn -> writeTBQueue pushQ (tkn, PNMessage PNMessageData {smpQueue, ntfTs, nmsgNonce, encNMsgMeta}))
incNtfStat ntfReceived
Right SMP.END ->
whenM (atomically $ activeClientSession' ca sessionId srv) $
updateSubStatus smpQueue NSEnd
Right SMP.DELD -> updateSubStatus smpQueue NSDeleted
Right SMP.END -> updateSubStatus smpQueue NSEnd
Right (SMP.ERR e) -> logError $ "SMP server error: " <> tshow e
Right _ -> logError "SMP server unexpected response"
Left e -> logError $ "SMP client error: " <> tshow e
@@ -439,7 +297,7 @@ ntfPush s@NtfPushServer {pushQ} = forever $ do
void $ deliverNotification pp tkn ntf
PNMessage {} -> checkActiveTkn status $ do
stats <- asks serverStats
liftIO $ updatePeriodStats (activeTokens stats) ntfTknId
atomically $ updatePeriodStats (activeTokens stats) ntfTknId
void $ deliverNotification pp tkn ntf
incNtfStat ntfDelivered
where
@@ -475,7 +333,7 @@ runNtfClientTransport :: Transport c => THandleNTF c 'TServer -> M ()
runNtfClientTransport th@THandle {params} = do
qSize <- asks $ clientQSize . config
ts <- liftIO getSystemTime
c <- liftIO $ newNtfServerClient qSize params ts
c <- atomically $ newNtfServerClient qSize params ts
s <- asks subscriber
ps <- asks pushServer
expCfg <- asks $ inactiveClientExpiration . config
@@ -490,29 +348,28 @@ clientDisconnected NtfServerClient {connected} = atomically $ writeTVar connecte
receive :: Transport c => THandleNTF c 'TServer -> NtfServerClient -> M ()
receive th@THandle {params = THandleParams {thAuth}} NtfServerClient {rcvQ, sndQ, rcvActiveAt} = forever $ do
ts <- L.toList <$> liftIO (tGet th)
atomically . (writeTVar rcvActiveAt $!) =<< liftIO getSystemTime
(errs, cmds) <- partitionEithers <$> mapM cmdAction ts
write sndQ errs
write rcvQ cmds
ts <- liftIO $ tGet th
forM_ ts $ \t@(_, _, (corrId, entId, cmdOrError)) -> do
atomically . writeTVar rcvActiveAt =<< liftIO getSystemTime
logDebug "received transmission"
case cmdOrError of
Left e -> write sndQ (corrId, entId, NRErr e)
Right cmd ->
verifyNtfTransmission ((,C.cbNonce (SMP.bs corrId)) <$> thAuth) t cmd >>= \case
VRVerified req -> write rcvQ req
VRFailed -> write sndQ (corrId, entId, NRErr AUTH)
where
cmdAction t@(_, _, (corrId, entId, cmdOrError)) =
case cmdOrError of
Left e -> pure $ Left (corrId, entId, NRErr e)
Right cmd ->
verified <$> verifyNtfTransmission ((,C.cbNonce (SMP.bs corrId)) <$> thAuth) t cmd
where
verified = \case
VRVerified req -> Right req
VRFailed -> Left (corrId, entId, NRErr AUTH)
write q = mapM_ (atomically . writeTBQueue q) . L.nonEmpty
write q t = atomically $ writeTBQueue q t
send :: Transport c => THandleNTF c 'TServer -> NtfServerClient -> IO ()
send h@THandle {params} NtfServerClient {sndQ, sndActiveAt} = forever $ do
ts <- atomically $ readTBQueue sndQ
void . liftIO $ tPut h $ L.map (\t -> Right (Nothing, encodeTransmission params t)) ts
t <- atomically $ readTBQueue sndQ
void . liftIO $ tPut h [Right (Nothing, encodeTransmission params t)]
atomically . (writeTVar sndActiveAt $!) =<< liftIO getSystemTime
-- instance Show a => Show (TVar a) where
-- show x = unsafePerformIO $ show <$> readTVarIO x
data VerificationResult = VRVerified NtfRequest | VRFailed
verifyNtfTransmission :: Maybe (THandleAuth 'TServer, C.CbNonce) -> SignedTransmission ErrorType NtfCmd -> NtfCmd -> M VerificationResult
@@ -570,7 +427,7 @@ client :: NtfServerClient -> NtfSubscriber -> NtfPushServer -> M ()
client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPushServer {pushQ, intervalNotifiers} =
forever $
atomically (readTBQueue rcvQ)
>>= mapM processCommand
>>= processCommand
>>= atomically . writeTBQueue sndQ
where
processCommand :: NtfRequest -> M (Transmission NtfResponse)
@@ -587,7 +444,7 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
atomically $ writeTBQueue pushQ (tkn, PNVerification regCode)
withNtfLog (`logCreateToken` tkn)
incNtfStatT token tknCreated
pure (corrId, NoEntity, NRTknId tknId srvDhPubKey)
pure (corrId, "", NRTknId tknId srvDhPubKey)
NtfReqCmd SToken (NtfTkn tkn@NtfTknData {token, ntfTknId, tknStatus, tknRegCode, tknDhSecret, tknDhKeys = (srvDhPubKey, srvDhPrivKey), tknCronInterval}) (corrId, tknId, cmd) -> do
status <- readTVarIO tknStatus
(corrId,tknId,) <$> case cmd of
@@ -650,7 +507,7 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
| otherwise -> do
logDebug "TCRN"
atomically $ writeTVar tknCronInterval int
liftIO (TM.lookupIO tknId intervalNotifiers) >>= \case
atomically (TM.lookup tknId intervalNotifiers) >>= \case
Nothing -> runIntervalNotifier int
Just IntervalNotifier {interval, action} ->
unless (interval == int) $ do
@@ -678,7 +535,7 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
_ -> pure $ NRErr AUTH
withNtfLog (`logCreateSubscription` sub)
incNtfStat subCreated
pure (corrId, NoEntity, resp)
pure (corrId, "", resp)
NtfReqCmd SSubscription (NtfSub NtfSubData {smpQueue = SMPQueueNtf {smpServer, notifierId}, notifierKey = registeredNKey, subStatus}) (corrId, subId, cmd) -> do
status <- readTVarIO subStatus
(corrId,subId,) <$> case cmd of
@@ -703,7 +560,7 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
PING -> pure NRPong
NtfReqPing corrId entId -> pure (corrId, entId, NRPong)
getId :: M NtfEntityId
getId = fmap EntityId . randomBytes =<< asks (subIdBytes . config)
getId = randomBytes =<< asks (subIdBytes . config)
getRegCode :: M NtfRegCode
getRegCode = NtfRegCode <$> (randomBytes =<< asks (regCodeBytes . config))
randomBytes :: Int -> M ByteString
@@ -716,19 +573,19 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
withNtfLog :: (StoreLog 'WriteMode -> IO a) -> M ()
withNtfLog action = liftIO . mapM_ action =<< asks storeLog
incNtfStatT :: DeviceToken -> (NtfServerStats -> IORef Int) -> M ()
incNtfStatT :: DeviceToken -> (NtfServerStats -> TVar Int) -> M ()
incNtfStatT (DeviceToken PPApnsNull _) _ = pure ()
incNtfStatT _ statSel = incNtfStat statSel
incNtfStat :: (NtfServerStats -> IORef Int) -> M ()
incNtfStat :: (NtfServerStats -> TVar Int) -> M ()
incNtfStat statSel = do
stats <- asks serverStats
liftIO $ atomicModifyIORef'_ (statSel stats) (+ 1)
atomically $ modifyTVar' (statSel stats) (+ 1)
saveServerStats :: M ()
saveServerStats =
asks (serverStatsBackupFile . config)
>>= mapM_ (\f -> asks serverStats >>= liftIO . getNtfServerStatsData >>= liftIO . saveStats f)
>>= mapM_ (\f -> asks serverStats >>= atomically . getNtfServerStatsData >>= liftIO . saveStats f)
where
saveStats f stats = do
logInfo $ "saving server stats to file " <> T.pack f
@@ -743,7 +600,7 @@ restoreServerStats = asks (serverStatsBackupFile . config) >>= mapM_ restoreStat
liftIO (strDecode <$> B.readFile f) >>= \case
Right d -> do
s <- asks serverStats
liftIO $ setNtfServerStats s d
atomically $ setNtfServerStats s d
renameFile f $ f <> ".bak"
logInfo "server stats restored"
Left e -> do
@@ -1,37 +0,0 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Simplex.Messaging.Notifications.Server.Control where
import qualified Data.Attoparsec.ByteString.Char8 as A
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (BasicAuth)
data ControlProtocol
= CPAuth BasicAuth
| CPStats
| CPStatsRTS
| CPServerInfo
| CPHelp
| CPQuit
| CPSkip
instance StrEncoding ControlProtocol where
strEncode = \case
CPAuth tok -> "auth " <> strEncode tok
CPStats -> "stats"
CPStatsRTS -> "stats-rts"
CPServerInfo -> "server-info"
CPHelp -> "help"
CPQuit -> "quit"
CPSkip -> ""
strP =
A.takeTill (== ' ') >>= \case
"auth" -> CPAuth <$> _strP
"stats" -> pure CPStats
"stats-rts" -> pure CPStatsRTS
"server-info" -> pure CPServerInfo
"help" -> pure CPHelp
"quit" -> pure CPQuit
"" -> pure CPSkip
_ -> fail "bad ControlProtocol command"
@@ -10,6 +10,7 @@ module Simplex.Messaging.Notifications.Server.Env where
import Control.Concurrent (ThreadId)
import Control.Concurrent.Async (Async)
import Control.Logger.Simple
import Control.Monad.IO.Unlift
import Crypto.Random
import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty)
@@ -28,21 +29,18 @@ import Simplex.Messaging.Notifications.Server.Stats
import Simplex.Messaging.Notifications.Server.Store
import Simplex.Messaging.Notifications.Server.StoreLog
import Simplex.Messaging.Notifications.Transport (NTFVersion, VersionRangeNTF)
import Simplex.Messaging.Protocol (BasicAuth, CorrId, SMPServer, Transmission)
import Simplex.Messaging.Protocol (CorrId, SMPServer, Transmission)
import Simplex.Messaging.Server.Expiration
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport (ATransport, THandleParams, TransportPeer (..))
import Simplex.Messaging.Transport.Server (AddHTTP, ServerCredentials, TransportServerConfig, loadFingerprint, loadServerCredential)
import Simplex.Messaging.Transport.Server (TransportServerConfig, alpn, loadFingerprint, loadTLSServerParams)
import System.IO (IOMode (..))
import System.Mem.Weak (Weak)
import UnliftIO.STM
data NtfServerConfig = NtfServerConfig
{ transports :: [(ServiceName, ATransport, AddHTTP)],
controlPort :: Maybe ServiceName,
controlPortUserAuth :: Maybe BasicAuth,
controlPortAdminAuth :: Maybe BasicAuth,
{ transports :: [(ServiceName, ATransport)],
subIdBytes :: Int,
regCodeBytes :: Int,
clientQSize :: Natural,
@@ -53,7 +51,10 @@ data NtfServerConfig = NtfServerConfig
subsBatchSize :: Int,
inactiveClientExpiration :: Maybe ExpirationConfig,
storeLogFile :: Maybe FilePath,
ntfCredentials :: ServerCredentials,
-- CA certificate private key is not needed for initialization
caCertificateFile :: FilePath,
privateKeyFile :: FilePath,
certificateFile :: FilePath,
-- stats config - see SMP server config
logStatsInterval :: Maybe Int64,
logStatsStartTime :: Int64,
@@ -77,24 +78,24 @@ data NtfEnv = NtfEnv
store :: NtfStore,
storeLog :: Maybe (StoreLog 'WriteMode),
random :: TVar ChaChaDRG,
tlsServerCreds :: T.Credential,
tlsServerParams :: T.ServerParams,
serverIdentity :: C.KeyHash,
serverStats :: NtfServerStats
}
newNtfServerEnv :: NtfServerConfig -> IO NtfEnv
newNtfServerEnv config@NtfServerConfig {subQSize, pushQSize, smpAgentCfg, apnsConfig, storeLogFile, ntfCredentials} = do
random <- C.newRandom
store <- newNtfStore
newNtfServerEnv config@NtfServerConfig {subQSize, pushQSize, smpAgentCfg, apnsConfig, storeLogFile, caCertificateFile, certificateFile, privateKeyFile, transportConfig} = do
random <- liftIO C.newRandom
store <- atomically newNtfStore
logInfo "restoring subscriptions..."
storeLog <- mapM (`readWriteNtfStore` store) storeLogFile
storeLog <- liftIO $ mapM (`readWriteNtfStore` store) storeLogFile
logInfo "restored subscriptions"
subscriber <- newNtfSubscriber subQSize smpAgentCfg random
pushServer <- newNtfPushServer pushQSize apnsConfig
tlsServerCreds <- loadServerCredential ntfCredentials
Fingerprint fp <- loadFingerprint ntfCredentials
serverStats <- newNtfServerStats =<< getCurrentTime
pure NtfEnv {config, subscriber, pushServer, store, storeLog, random, tlsServerCreds, serverIdentity = C.KeyHash fp, serverStats}
subscriber <- atomically $ newNtfSubscriber subQSize smpAgentCfg random
pushServer <- atomically $ newNtfPushServer pushQSize apnsConfig
tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig)
Fingerprint fp <- liftIO $ loadFingerprint caCertificateFile
serverStats <- atomically . newNtfServerStats =<< liftIO getCurrentTime
pure NtfEnv {config, subscriber, pushServer, store, storeLog, random, tlsServerParams, serverIdentity = C.KeyHash fp, serverStats}
data NtfSubscriber = NtfSubscriber
{ smpSubscribers :: TMap SMPServer SMPSubscriber,
@@ -102,10 +103,10 @@ data NtfSubscriber = NtfSubscriber
smpAgent :: SMPClientAgent
}
newNtfSubscriber :: Natural -> SMPClientAgentConfig -> TVar ChaChaDRG -> IO NtfSubscriber
newNtfSubscriber :: Natural -> SMPClientAgentConfig -> TVar ChaChaDRG -> STM NtfSubscriber
newNtfSubscriber qSize smpAgentCfg random = do
smpSubscribers <- TM.emptyIO
newSubQ <- newTBQueueIO qSize
smpSubscribers <- TM.empty
newSubQ <- newTBQueue qSize
smpAgent <- newSMPClientAgent smpAgentCfg random
pure NtfSubscriber {smpSubscribers, newSubQ, smpAgent}
@@ -114,10 +115,10 @@ data SMPSubscriber = SMPSubscriber
subThreadId :: TVar (Maybe (Weak ThreadId))
}
newSMPSubscriber :: IO SMPSubscriber
newSMPSubscriber :: STM SMPSubscriber
newSMPSubscriber = do
newSubQ <- newTQueueIO
subThreadId <- newTVarIO Nothing
newSubQ <- newTQueue
subThreadId <- newTVar Nothing
pure SMPSubscriber {newSubQ, subThreadId}
data NtfPushServer = NtfPushServer
@@ -133,11 +134,11 @@ data IntervalNotifier = IntervalNotifier
interval :: Word16
}
newNtfPushServer :: Natural -> APNSPushClientConfig -> IO NtfPushServer
newNtfPushServer :: Natural -> APNSPushClientConfig -> STM NtfPushServer
newNtfPushServer qSize apnsConfig = do
pushQ <- newTBQueueIO qSize
pushClients <- TM.emptyIO
intervalNotifiers <- TM.emptyIO
pushQ <- newTBQueue qSize
pushClients <- TM.empty
intervalNotifiers <- TM.empty
pure NtfPushServer {pushQ, pushClients, intervalNotifiers, apnsConfig}
newPushClient :: NtfPushServer -> PushProvider -> IO PushProviderClient
@@ -150,7 +151,7 @@ newPushClient NtfPushServer {apnsConfig, pushClients} pp = do
getPushClient :: NtfPushServer -> PushProvider -> IO PushProviderClient
getPushClient s@NtfPushServer {pushClients} pp =
TM.lookupIO pp pushClients >>= maybe (newPushClient s pp) pure
atomically (TM.lookup pp pushClients) >>= maybe (newPushClient s pp) pure
data NtfRequest
= NtfReqNew CorrId ANewNtfEntity
@@ -158,19 +159,19 @@ data NtfRequest
| NtfReqPing CorrId NtfEntityId
data NtfServerClient = NtfServerClient
{ rcvQ :: TBQueue (NonEmpty NtfRequest),
sndQ :: TBQueue (NonEmpty (Transmission NtfResponse)),
{ rcvQ :: TBQueue NtfRequest,
sndQ :: TBQueue (Transmission NtfResponse),
ntfThParams :: THandleParams NTFVersion 'TServer,
connected :: TVar Bool,
rcvActiveAt :: TVar SystemTime,
sndActiveAt :: TVar SystemTime
}
newNtfServerClient :: Natural -> THandleParams NTFVersion 'TServer -> SystemTime -> IO NtfServerClient
newNtfServerClient :: Natural -> THandleParams NTFVersion 'TServer -> SystemTime -> STM NtfServerClient
newNtfServerClient qSize ntfThParams ts = do
rcvQ <- newTBQueueIO qSize
sndQ <- newTBQueueIO qSize
connected <- newTVarIO True
rcvActiveAt <- newTVarIO ts
sndActiveAt <- newTVarIO ts
rcvQ <- newTBQueue qSize
sndQ <- newTBQueue qSize
connected <- newTVar True
rcvActiveAt <- newTVar ts
sndActiveAt <- newTVar ts
return NtfServerClient {rcvQ, sndQ, ntfThParams, connected, rcvActiveAt, sndActiveAt}
@@ -1,11 +1,9 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
module Simplex.Messaging.Notifications.Server.Main where
@@ -14,23 +12,22 @@ import Data.Functor (($>))
import Data.Ini (lookupValue, readIniFile)
import Data.Maybe (fromMaybe)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import qualified Data.Text.IO as T
import Network.Socket (HostName)
import Options.Applicative
import Simplex.Messaging.Client (HostMode (..), NetworkConfig (..), ProtocolClientConfig (..), SocksMode (..), defaultNetworkConfig, textToHostMode)
import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientConfig (..), SocksMode (..), defaultNetworkConfig)
import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Notifications.Server (runNtfServer)
import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..), defaultInactiveClientExpiration)
import Simplex.Messaging.Notifications.Server.Push.APNS (defaultAPNSPushClientConfig)
import Simplex.Messaging.Notifications.Transport (supportedServerNTFVRange)
import Simplex.Messaging.Notifications.Transport (supportedNTFHandshakes, supportedServerNTFVRange)
import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), pattern NtfServer)
import Simplex.Messaging.Server.CLI
import Simplex.Messaging.Server.Expiration
import Simplex.Messaging.Transport (simplexMQVersion)
import Simplex.Messaging.Transport.Client (TransportHost (..))
import Simplex.Messaging.Transport.Server (ServerCredentials (..), TransportServerConfig (..), defaultTransportServerConfig)
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig)
import Simplex.Messaging.Util (tshow)
import System.Directory (createDirectoryIfMissing, doesFileExist)
import System.FilePath (combine)
@@ -86,34 +83,20 @@ ntfServerCLI cfgPath logPath =
\# Log is compacted on start (deleted objects are removed).\n"
<> ("enable: " <> onOff enableStoreLog <> "\n\n")
<> "log_stats: off\n\n\
\[AUTH]\n\
\# control_port_admin_password:\n\
\# control_port_user_password:\n\
\\n\
\[TRANSPORT]\n\
\# Host is only used to print server address on start.\n\
\# You can specify multiple server ports.\n"
\# host is only used to print server address on start\n"
<> ("host: " <> T.pack host <> "\n")
<> ("port: " <> T.pack defaultServerPort <> "\n")
<> "log_tls_errors: off\n\n\
\# Use `websockets: 443` to run websockets server in addition to plain TLS.\n\
\websockets: off\n\n\
\# control_port: 5227\n\
\\n\
<> "log_tls_errors: off\n"
<> "websockets: off\n\n\
\[SUBSCRIBER]\n\
\# Network configuration for notification server client.\n\
\# `host_mode` can be 'public' (default) or 'onion'.\n\
\# It defines prefferred hostname for destination servers with multiple hostnames.\n\
\# host_mode: public\n\
\# required_host_mode: off\n\n\
\# SOCKS proxy port for subscribing to SMP servers.\n\
\# You may need a separate instance of SOCKS proxy for incoming single-hop requests.\n\
\# socks_proxy: localhost:9050\n\n\
\# `socks_mode` can be 'onion' for SOCKS proxy to be used for .onion destination hosts only (default)\n\
\# or 'always' to be used for all destination hosts (can be used if it is an .onion server).\n\
\# socks_mode: onion\n\n\
\# The domain suffixes of the relays you operate (space-separated) to count as separate proxy statistics.\n\
\# own_server_domains: \n\n\
\[INACTIVE_CLIENTS]\n\
\# TTL and interval to check inactive clients\n\
\disconnect: off\n"
@@ -137,9 +120,6 @@ ntfServerCLI cfgPath logPath =
serverConfig =
NtfServerConfig
{ transports = iniTransports ini,
controlPort = either (const Nothing) (Just . T.unpack) $ lookupValue "TRANSPORT" "control_port" ini,
controlPortAdminAuth = either error id <$!> strDecodeIni "AUTH" "control_port_admin_password" ini,
controlPortUserAuth = either error id <$!> strDecodeIni "AUTH" "control_port_user_password" ini,
subIdBytes = 24,
regCodeBytes = 32,
clientQSize = 64,
@@ -152,13 +132,9 @@ ntfServerCLI cfgPath logPath =
{ networkConfig =
defaultNetworkConfig
{ socksProxy = either error id <$!> strDecodeIni "SUBSCRIBER" "socks_proxy" ini,
socksMode = maybe SMOnion (either error id) $! strDecodeIni "SUBSCRIBER" "socks_mode" ini,
hostMode = either (const HMPublic) (either error id . textToHostMode) $ lookupValue "SUBSCRIBER" "host_mode" ini,
requiredHostMode = fromMaybe False $ iniOnOff "SUBSCRIBER" "required_host_mode" ini,
smpPingInterval = 60_000_000 -- 1 minute
socksMode = either (const SMOnion) textToSocksMode $ lookupValue "SUBSCRIBER" "socks_mode" ini
}
},
ownServerDomains = either (const []) (map encodeUtf8 . T.words) $ lookupValue "SUBSCRIBER" "own_server_domains" ini,
persistErrorInterval = 0 -- seconds
},
apnsConfig = defaultAPNSPushClientConfig,
@@ -170,12 +146,9 @@ ntfServerCLI cfgPath logPath =
checkInterval = readStrictIni "INACTIVE_CLIENTS" "check_interval" ini
},
storeLogFile = enableStoreLog $> storeLogFilePath,
ntfCredentials =
ServerCredentials
{ caCertificateFile = Just $ c caCrtFile,
privateKeyFile = c serverKeyFile,
certificateFile = c serverCrtFile
},
caCertificateFile = c caCrtFile,
privateKeyFile = c serverKeyFile,
certificateFile = c serverCrtFile,
logStatsInterval = logStats $> 86400, -- seconds
logStatsStartTime = 0, -- seconds from 00:00 UTC
serverStatsLogFile = combine logPath "ntf-server-stats.daily.log",
@@ -183,7 +156,8 @@ ntfServerCLI cfgPath logPath =
ntfServerVRange = supportedServerNTFVRange,
transportConfig =
defaultTransportServerConfig
{ logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini
{ logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini,
alpn = Just supportedNTFHandshakes
}
}
@@ -28,16 +28,12 @@ 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 qualified Data.Attoparsec.ByteString.Char8 as A
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.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map)
import Data.Maybe (isNothing)
import Data.Text (Text)
@@ -107,20 +103,11 @@ readECPrivateKey f = do
data PushNotification
= PNVerification NtfRegCode
| PNMessage (NonEmpty PNMessageData)
| PNMessage PNMessageData
| -- | PNAlert Text
PNCheckMessages
deriving (Show)
-- List of PNMessageData uses semicolon-separated encoding instead of strEncode,
-- because strEncode of NonEmpty list uses comma for separator,
-- and encoding of PNMessageData's smpQueue has comma in list of hosts
encodePNMessages :: NonEmpty PNMessageData -> ByteString
encodePNMessages = B.intercalate ";" . map strEncode . L.toList
pnMessagesP :: A.Parser (NonEmpty PNMessageData)
pnMessagesP = L.fromList <$> strP `A.sepBy1` A.char ';'
data PNMessageData = PNMessageData
{ smpQueue :: SMPQueueNtf,
ntfTs :: SystemTime,
@@ -298,7 +285,7 @@ apnsNotification NtfTknData {tknDhSecret} nonce paddedLen = \case
encrypt code $ \code' ->
apn APNSBackground {contentAvailable = 1} . Just $ J.object ["nonce" .= nonce, "verification" .= code']
PNMessage pnMessageData ->
encrypt (encodePNMessages pnMessageData) $ \ntfData ->
encrypt (strEncode pnMessageData) $ \ntfData ->
apn apnMutableContent . Just $ J.object ["nonce" .= nonce, "message" .= ntfData]
-- PNAlert text -> Right $ apn (apnAlert $ APNSAlertText text) Nothing
PNCheckMessages -> Right $ apn APNSBackground {contentAvailable = 1} . Just $ J.object ["checkMessages" .= True]
@@ -7,22 +7,24 @@ module Simplex.Messaging.Notifications.Server.Stats where
import Control.Applicative (optional)
import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Char8 as B
import Data.IORef
import Data.Time.Clock (UTCTime)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Notifications.Protocol (NtfTokenId)
import Simplex.Messaging.Protocol (NotifierId)
import Simplex.Messaging.Server.Stats
import UnliftIO.STM
data NtfServerStats = NtfServerStats
{ fromTime :: IORef UTCTime,
tknCreated :: IORef Int,
tknVerified :: IORef Int,
tknDeleted :: IORef Int,
subCreated :: IORef Int,
subDeleted :: IORef Int,
ntfReceived :: IORef Int,
ntfDelivered :: IORef Int,
activeTokens :: PeriodStats,
activeSubs :: PeriodStats
{ fromTime :: TVar UTCTime,
tknCreated :: TVar Int,
tknVerified :: TVar Int,
tknDeleted :: TVar Int,
subCreated :: TVar Int,
subDeleted :: TVar Int,
ntfReceived :: TVar Int,
ntfDelivered :: TVar Int,
activeTokens :: PeriodStats NtfTokenId,
activeSubs :: PeriodStats NotifierId
}
data NtfServerStatsData = NtfServerStatsData
@@ -34,49 +36,48 @@ data NtfServerStatsData = NtfServerStatsData
_subDeleted :: Int,
_ntfReceived :: Int,
_ntfDelivered :: Int,
_activeTokens :: PeriodStatsData,
_activeSubs :: PeriodStatsData
_activeTokens :: PeriodStatsData NtfTokenId,
_activeSubs :: PeriodStatsData NotifierId
}
newNtfServerStats :: UTCTime -> IO NtfServerStats
newNtfServerStats :: UTCTime -> STM NtfServerStats
newNtfServerStats ts = do
fromTime <- newIORef ts
tknCreated <- newIORef 0
tknVerified <- newIORef 0
tknDeleted <- newIORef 0
subCreated <- newIORef 0
subDeleted <- newIORef 0
ntfReceived <- newIORef 0
ntfDelivered <- newIORef 0
fromTime <- newTVar ts
tknCreated <- newTVar 0
tknVerified <- newTVar 0
tknDeleted <- newTVar 0
subCreated <- newTVar 0
subDeleted <- newTVar 0
ntfReceived <- newTVar 0
ntfDelivered <- newTVar 0
activeTokens <- newPeriodStats
activeSubs <- newPeriodStats
pure NtfServerStats {fromTime, tknCreated, tknVerified, tknDeleted, subCreated, subDeleted, ntfReceived, ntfDelivered, activeTokens, activeSubs}
getNtfServerStatsData :: NtfServerStats -> IO NtfServerStatsData
getNtfServerStatsData :: NtfServerStats -> STM NtfServerStatsData
getNtfServerStatsData s@NtfServerStats {fromTime} = do
_fromTime <- readIORef fromTime
_tknCreated <- readIORef $ tknCreated s
_tknVerified <- readIORef $ tknVerified s
_tknDeleted <- readIORef $ tknDeleted s
_subCreated <- readIORef $ subCreated s
_subDeleted <- readIORef $ subDeleted s
_ntfReceived <- readIORef $ ntfReceived s
_ntfDelivered <- readIORef $ ntfDelivered s
_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}
-- this function is not thread safe, it is used on server start only
setNtfServerStats :: NtfServerStats -> NtfServerStatsData -> IO ()
setNtfServerStats :: NtfServerStats -> NtfServerStatsData -> STM ()
setNtfServerStats s@NtfServerStats {fromTime} d@NtfServerStatsData {_fromTime} = do
writeIORef fromTime $! _fromTime
writeIORef (tknCreated s) $! _tknCreated d
writeIORef (tknVerified s) $! _tknVerified d
writeIORef (tknDeleted s) $! _tknDeleted d
writeIORef (subCreated s) $! _subCreated d
writeIORef (subDeleted s) $! _subDeleted d
writeIORef (ntfReceived s) $! _ntfReceived d
writeIORef (ntfDelivered s) $! _ntfDelivered d
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)
@@ -33,13 +33,13 @@ data NtfStore = NtfStore
subscriptionLookup :: TMap SMPQueueNtf NtfSubscriptionId
}
newNtfStore :: IO NtfStore
newNtfStore :: STM NtfStore
newNtfStore = do
tokens <- TM.emptyIO
tokenRegistrations <- TM.emptyIO
subscriptions <- TM.emptyIO
tokenSubscriptions <- TM.emptyIO
subscriptionLookup <- TM.emptyIO
tokens <- TM.empty
tokenRegistrations <- TM.empty
subscriptions <- TM.empty
tokenSubscriptions <- TM.empty
subscriptionLookup <- TM.empty
pure NtfStore {tokens, tokenRegistrations, subscriptions, tokenSubscriptions, subscriptionLookup}
data NtfTknData = NtfTknData
@@ -77,9 +77,6 @@ data NtfEntityRec (e :: NtfEntity) where
getNtfToken :: NtfStore -> NtfTokenId -> STM (Maybe NtfTknData)
getNtfToken st tknId = TM.lookup tknId (tokens st)
getNtfTokenIO :: NtfStore -> NtfTokenId -> IO (Maybe NtfTknData)
getNtfTokenIO st tknId = TM.lookupIO tknId (tokens st)
addNtfToken :: NtfStore -> NtfTokenId -> NtfTknData -> STM ()
addNtfToken st tknId tkn@NtfTknData {token, tknVerifyKey} = do
TM.insert tknId tkn $ tokens st
@@ -50,7 +50,6 @@ data NtfStoreLogRecord
| CreateSubscription NtfSubRec
| SubscriptionStatus NtfSubscriptionId NtfSubStatus
| DeleteSubscription NtfSubscriptionId
deriving (Show)
data NtfTknRec = NtfTknRec
{ ntfTknId :: NtfTokenId,
@@ -62,7 +61,6 @@ data NtfTknRec = NtfTknRec
tknRegCode :: NtfRegCode,
tknCronInterval :: Word16
}
deriving (Show)
mkTknData :: NtfTknRec -> STM NtfTknData
mkTknData NtfTknRec {ntfTknId, token, tknStatus = status, tknVerifyKey, tknDhKeys, tknDhSecret, tknRegCode, tknCronInterval = cronInt} = do
@@ -83,7 +81,6 @@ data NtfSubRec = NtfSubRec
tokenId :: NtfTokenId,
subStatus :: NtfSubStatus
}
deriving (Show)
mkSubData :: NtfSubRec -> STM NtfSubData
mkSubData NtfSubRec {ntfSubId, smpQueue, notifierKey, tokenId, subStatus = status} = do
@@ -169,6 +169,5 @@ ntfTHandle c = THandle {connection = c, params}
thServerVRange = versionToRange v,
thAuth = Nothing,
implySessId = False,
encryptBlock = Nothing,
batch = False
}
+10 -13
View File
@@ -11,7 +11,7 @@ import Data.Text.Encoding (decodeLatin1, encodeUtf8)
import Data.Time (UTCTime)
import Database.SQLite.Simple.FromField (FromField (..))
import Database.SQLite.Simple.ToField (ToField (..))
import Simplex.Messaging.Agent.Protocol (ConnId, NotificationsMode (..), UserId)
import Simplex.Messaging.Agent.Protocol (ConnId, NotificationsMode (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Notifications.Protocol
@@ -48,7 +48,6 @@ data NtfToken = NtfToken
ntfServer :: NtfServer,
ntfTokenId :: Maybe NtfTokenId,
-- TODO combine keys to key pair as the types should match
-- | key used by the ntf server to verify transmissions
ntfPubKey :: C.APublicAuthKey,
-- | key used by the ntf client to sign transmissions
@@ -80,17 +79,17 @@ newNtfToken deviceToken ntfServer (ntfPubKey, ntfPrivKey) ntfDhKeys ntfMode =
ntfMode
}
data NtfSubAction = NSANtf NtfSubNTFAction | NSASMP NtfSubSMPAction
data NtfSubAction = NtfSubNTFAction NtfSubNTFAction | NtfSubSMPAction NtfSubSMPAction
deriving (Show)
isDeleteNtfSubAction :: NtfSubAction -> Bool
isDeleteNtfSubAction = \case
NSANtf a -> case a of
NtfSubNTFAction a -> case a of
NSACreate -> False
NSACheck -> False
NSADelete -> True
NSARotate -> True
NSASMP a -> case a of
NtfSubSMPAction a -> case a of
NSASmpKey -> False
NSASmpDelete -> True
@@ -99,8 +98,8 @@ type NtfActionTs = UTCTime
data NtfSubNTFAction
= NSACreate
| NSACheck
| NSADelete -- deprecated
| NSARotate -- deprecated
| NSADelete
| NSARotate
deriving (Show)
instance Encoding NtfSubNTFAction where
@@ -178,8 +177,7 @@ instance FromField NtfAgentSubStatus where fromField = fromTextField_ $ either (
instance ToField NtfAgentSubStatus where toField = toField . decodeLatin1 . smpEncode
data NtfSubscription = NtfSubscription
{ userId :: UserId,
connId :: ConnId,
{ connId :: ConnId,
smpServer :: SMPServer,
ntfQueueId :: Maybe NotifierId,
ntfServer :: NtfServer,
@@ -188,11 +186,10 @@ data NtfSubscription = NtfSubscription
}
deriving (Show)
newNtfSubscription :: UserId -> ConnId -> SMPServer -> Maybe NotifierId -> NtfServer -> NtfAgentSubStatus -> NtfSubscription
newNtfSubscription userId connId smpServer ntfQueueId ntfServer ntfSubStatus =
newNtfSubscription :: ConnId -> SMPServer -> Maybe NotifierId -> NtfServer -> NtfAgentSubStatus -> NtfSubscription
newNtfSubscription connId smpServer ntfQueueId ntfServer ntfSubStatus =
NtfSubscription
{ userId,
connId,
{ connId,
smpServer,
ntfQueueId,
ntfServer,
+20 -45
View File
@@ -2,13 +2,11 @@
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedLists #-}
@@ -101,8 +99,7 @@ module Simplex.Messaging.Protocol
BasicAuth (..),
SrvLoc (..),
CorrId (..),
EntityId (..),
pattern NoEntity,
EntityId,
QueueId,
RecipientId,
SenderId,
@@ -258,14 +255,13 @@ supportedSMPClientVRange = mkVersionRange initialSMPClientVersion currentSMPClie
-- TODO v6.0 remove dependency on version
maxMessageLength :: VersionSMP -> Int
maxMessageLength v
| v >= encryptedBlockSMPVersion = 16048 -- max 16051
| v >= sendingProxySMPVersion = 16064 -- max 16067
| otherwise = 16088 -- 16048 - always use this size to determine allowed ranges
| otherwise = 16088 -- 16064 - always use this size to determine allowed ranges
paddedProxiedTLength :: Int
paddedProxiedTLength = 16226 -- 16225 .. 16227
paddedProxiedTLength = 16242 -- 16241 .. 16243
-- TODO v7.0 change to 16048
-- TODO v6.0 change to 16064
type MaxMessageLen = 16088
-- 16 extra bytes: 8 for timestamp and 8 for flags (7 flags and the space, only 1 flag is currently used)
@@ -273,10 +269,10 @@ type MaxRcvMessageLen = MaxMessageLen + 16 -- 16104, the padded size is 16106
-- it is shorter to allow per-queue e2e encryption DH key in the "public" header
e2eEncConfirmationLength :: Int
e2eEncConfirmationLength = 15904 -- 15865 .. 15960
e2eEncConfirmationLength = 15920 -- 15881 .. 15976
e2eEncMessageLength :: Int
e2eEncMessageLength = 16000 -- 15988 .. 16005
e2eEncMessageLength = 16016 -- 16004 .. 16021
-- | SMP protocol clients
data Party = Recipient | Sender | Notifier | ProxiedClient
@@ -333,8 +329,8 @@ data RawTransmission = RawTransmission
{ authenticator :: ByteString, -- signature or encrypted transmission hash
authorized :: ByteString, -- authorized transmission
sessId :: SessionId,
corrId :: CorrId,
entityId :: EntityId,
corrId :: ByteString,
entityId :: ByteString,
command :: ByteString
}
deriving (Show)
@@ -361,7 +357,7 @@ instance IsString (Maybe TransmissionAuth) where
fromString = parseString $ B64.decode >=> C.decodeSignature >=> pure . fmap TASignature
-- | unparsed sent SMP transmission with signature, without session ID.
type SignedRawTransmission = (Maybe TransmissionAuth, CorrId, EntityId, ByteString)
type SignedRawTransmission = (Maybe TransmissionAuth, SessionId, ByteString, ByteString)
-- | unparsed sent SMP transmission with signature.
type SentRawTransmission = (Maybe TransmissionAuth, ByteString)
@@ -378,13 +374,7 @@ type NotifierId = QueueId
-- | SMP queue ID on the server.
type QueueId = EntityId
-- this type is used for server entities only
newtype EntityId = EntityId {unEntityId :: ByteString}
deriving (Eq, Ord, Show)
deriving newtype (Encoding, StrEncoding)
pattern NoEntity :: EntityId
pattern NoEntity = EntityId ""
type EntityId = ByteString
-- | Parameterized type for SMP protocol commands from all clients.
data Command (p :: Party) where
@@ -485,7 +475,6 @@ data BrokerMsg where
RRES :: EncFwdResponse -> BrokerMsg -- relay to proxy
PRES :: EncResponse -> BrokerMsg -- proxy to client
END :: BrokerMsg
DELD :: BrokerMsg
INFO :: QueueInfo -> BrokerMsg
OK :: BrokerMsg
ERR :: ErrorType -> BrokerMsg
@@ -707,7 +696,6 @@ data BrokerMsgTag
| RRES_
| PRES_
| END_
| DELD_
| INFO_
| OK_
| ERR_
@@ -781,7 +769,6 @@ instance Encoding BrokerMsgTag where
RRES_ -> "RRES"
PRES_ -> "PRES"
END_ -> "END"
DELD_ -> "DELD"
INFO_ -> "INFO"
OK_ -> "OK"
ERR_ -> "ERR"
@@ -798,7 +785,6 @@ instance ProtocolMsgTag BrokerMsgTag where
"RRES" -> Just RRES_
"PRES" -> Just PRES_
"END" -> Just END_
"DELD" -> Just DELD_
"INFO" -> Just INFO_
"OK" -> Just OK_
"ERR" -> Just ERR_
@@ -1111,13 +1097,10 @@ serverStrP = do
portP = show <$> (A.char ':' *> (A.decimal :: Parser Int))
-- | Transmission correlation ID.
newtype CorrId = CorrId {bs :: ByteString}
deriving (Eq, Ord, Show)
deriving newtype (Encoding)
newtype CorrId = CorrId {bs :: ByteString} deriving (Eq, Ord, Show)
instance IsString CorrId where
fromString = CorrId . fromString
{-# INLINE fromString #-}
instance StrEncoding CorrId where
strEncode (CorrId cId) = strEncode cId
@@ -1198,8 +1181,6 @@ data ErrorType
CRYPTO
| -- | SMP queue capacity is exceeded on the server
QUOTA
| -- | SMP server storage error
STORE {storeErr :: String}
| -- | ACK command is sent without message to be acknowledged
NO_MSG
| -- | sent message is too large (> maxMessageLength = 16088 bytes)
@@ -1281,7 +1262,7 @@ transmissionP THandleParams {sessionId, implySessId} = do
command <- A.takeByteString
pure RawTransmission {authenticator, authorized = authorized', sessId, corrId, entityId, command}
class (ProtocolTypeI (ProtoType msg), ProtocolEncoding v err msg, ProtocolEncoding v err (ProtoCommand msg), Show err, Show msg) => Protocol v err msg | msg -> v, msg -> err where
class (ProtocolEncoding v err msg, ProtocolEncoding v err (ProtoCommand msg), Show err, Show msg) => Protocol v err msg | msg -> v, msg -> err where
type ProtoCommand msg = cmd | cmd -> msg
type ProtoType msg = (sch :: ProtocolType) | sch -> msg
protocolClientHandshake :: forall c. Transport c => c -> Maybe C.KeyPairX25519 -> C.KeyHash -> VersionRange v -> ExceptT TransportError IO (THandle v c 'TClient)
@@ -1342,7 +1323,7 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where
fromProtocolError = fromProtocolError @SMPVersion @ErrorType @BrokerMsg
{-# INLINE fromProtocolError #-}
checkCredentials (auth, _, EntityId entId, _) cmd = case cmd of
checkCredentials (auth, _, entId, _) cmd = case cmd of
-- NEW must have signature but NOT queue ID
NEW {}
| isNothing auth -> Left $ CMD NO_AUTH
@@ -1430,9 +1411,6 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
RRES (EncFwdResponse encBlock) -> e (RRES_, ' ', Tail encBlock)
PRES (EncResponse encBlock) -> e (PRES_, ' ', Tail encBlock)
END -> e END_
DELD
| v >= deletedEventSMPVersion -> e DELD_
| otherwise -> e END_
INFO info -> e (INFO_, ' ', info)
OK -> e OK_
ERR err -> e (ERR_, ' ', err)
@@ -1458,7 +1436,6 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
RRES_ -> RRES <$> (EncFwdResponse . unTail <$> _smpP)
PRES_ -> PRES <$> (EncResponse . unTail <$> _smpP)
END_ -> pure END
DELD_ -> pure DELD
INFO_ -> INFO <$> _smpP
OK_ -> pure OK
ERR_ -> ERR <$> _smpP
@@ -1471,7 +1448,7 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
PEBlock -> BLOCK
{-# INLINE fromProtocolError #-}
checkCredentials (_, _, EntityId entId, _) cmd = case cmd of
checkCredentials (_, _, entId, _) cmd = case cmd of
-- IDS response should not have queue ID
IDS _ -> Right cmd
-- ERR response does not always have queue ID
@@ -1515,7 +1492,6 @@ instance Encoding ErrorType where
AUTH -> "AUTH"
CRYPTO -> "CRYPTO"
QUOTA -> "QUOTA"
STORE err -> "STORE " <> smpEncode err
EXPIRED -> "EXPIRED"
NO_MSG -> "NO_MSG"
LARGE_MSG -> "LARGE_MSG"
@@ -1531,7 +1507,6 @@ instance Encoding ErrorType where
"AUTH" -> pure AUTH
"CRYPTO" -> pure CRYPTO
"QUOTA" -> pure QUOTA
"STORE" -> STORE <$> _smpP
"EXPIRED" -> pure EXPIRED
"NO_MSG" -> pure NO_MSG
"LARGE_MSG" -> pure LARGE_MSG
@@ -1668,8 +1643,8 @@ batchTransmissions' batch bSize ts
batchTransmissions_ :: Int -> NonEmpty (Either TransportError ByteString, r) -> [TransportBatch r]
batchTransmissions_ bSize = addBatch . foldr addTransmission ([], 0, 0, [], [])
where
-- 19 = 2 bytes reserved for pad size + 1 for transmission count + 16 auth tag from block encryption
bSize' = bSize - 19
-- 3 = 2 bytes reserved for pad size + 1 for transmission count
bSize' = bSize - 3
addTransmission :: (Either TransportError ByteString, r) -> ([TransportBatch r], Int, Int, [ByteString], [r]) -> ([TransportBatch r], Int, Int, [ByteString], [r])
addTransmission (t_, r) acc@(bs, !len, !n, ss, rs) = case t_ of
Left e -> (TBError e r : addBatch acc, 0, 0, [], [])
@@ -1746,16 +1721,16 @@ tDecodeParseValidate THandleParams {sessionId, thVersion = v, implySessId} = \ca
| implySessId || sessId == sessionId ->
let decodedTransmission = (,corrId,entityId,command) <$> decodeTAuthBytes authenticator
in either (const $ tError corrId) (tParseValidate authorized) decodedTransmission
| otherwise -> (Nothing, "", (corrId, NoEntity, Left $ fromProtocolError @v @err @cmd PESession))
| otherwise -> (Nothing, "", (CorrId corrId, "", Left $ fromProtocolError @v @err @cmd PESession))
Left _ -> tError ""
where
tError :: CorrId -> SignedTransmission err cmd
tError corrId = (Nothing, "", (corrId, NoEntity, Left $ fromProtocolError @v @err @cmd PEBlock))
tError :: ByteString -> SignedTransmission err cmd
tError corrId = (Nothing, "", (CorrId corrId, "", Left $ fromProtocolError @v @err @cmd PEBlock))
tParseValidate :: ByteString -> SignedRawTransmission -> SignedTransmission err cmd
tParseValidate signed t@(sig, corrId, entityId, command) =
let cmd = parseProtocol @v @err @cmd v command >>= checkCredentials t
in (sig, signed, (corrId, entityId, cmd))
in (sig, signed, (CorrId corrId, entityId, cmd))
$(J.deriveJSON defaultJSON ''MsgFlags)
File diff suppressed because it is too large Load Diff
+17 -24
View File
@@ -6,8 +6,6 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
module Simplex.Messaging.Server.CLI where
@@ -18,7 +16,6 @@ import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Either (fromRight)
import Data.Ini (Ini, lookupValue)
import Data.List ((\\))
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
@@ -27,10 +24,11 @@ import qualified Data.X509.File as XF
import Data.X509.Validation (Fingerprint (..))
import Network.Socket (HostName, ServiceName)
import Options.Applicative
import Simplex.Messaging.Client (SocksMode (..))
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI)
import Simplex.Messaging.Transport (ATransport (..), TLS, Transport (..))
import Simplex.Messaging.Transport.Server (AddHTTP, loadFileFingerprint)
import Simplex.Messaging.Transport.Server (loadFingerprint)
import Simplex.Messaging.Transport.WebSockets (WS)
import Simplex.Messaging.Util (eitherToMaybe, whenM)
import System.Directory (doesDirectoryExist, listDirectory, removeDirectoryRecursive, removePathForcibly)
@@ -140,7 +138,7 @@ createServerX509_ createCA cfgPath x509cfg = do
)
saveFingerprint = do
Fingerprint fp <- loadFileFingerprint $ c caCrtFile
Fingerprint fp <- loadFingerprint $ c caCrtFile
withFile (c fingerprintFile) WriteMode (`B.hPutStrLn` strEncode fp)
pure fp
@@ -269,37 +267,26 @@ settingIsOn section name ini
checkSavedFingerprint :: FilePath -> X509Config -> IO ByteString
checkSavedFingerprint cfgPath x509cfg = do
savedFingerprint <- withFile (c fingerprintFile) ReadMode hGetLine
Fingerprint fp <- loadFileFingerprint (c caCrtFile)
Fingerprint fp <- loadFingerprint (c caCrtFile)
when (B.pack savedFingerprint /= strEncode fp) $
exitError "Stored fingerprint is invalid."
pure fp
where
c = combine cfgPath . ($ x509cfg)
iniTransports :: Ini -> [(ServiceName, ATransport, AddHTTP)]
iniTransports :: Ini -> [(String, ATransport)]
iniTransports ini =
let smpPorts = ports $ strictIni "TRANSPORT" "port" ini
ws = strictIni "TRANSPORT" "websockets" ini
wsPorts
| ws == "off" = []
| ws == "on" = ["80"]
| otherwise = ports ws \\ smpPorts
in ts (transport @TLS) smpPorts <> ts (transport @WS) wsPorts
where
ts :: ATransport -> [ServiceName] -> [(ServiceName, ATransport, AddHTTP)]
ts t = map (\port -> (port, t, webPort == Just port))
webPort = T.unpack <$> eitherToMaybe (lookupValue "WEB" "https" ini)
ports = map T.unpack . T.splitOn ","
let port = T.unpack $ strictIni "TRANSPORT" "port" ini
enableWebsockets = (== "on") $ strictIni "TRANSPORT" "websockets" ini
in (port, transport @TLS) : [("80", transport @WS) | enableWebsockets]
printServerConfig :: [(ServiceName, ATransport, AddHTTP)] -> Maybe FilePath -> IO ()
printServerConfig :: [(ServiceName, ATransport)] -> Maybe FilePath -> IO ()
printServerConfig transports logFile = do
putStrLn $ case logFile of
Just f -> "Store log: " <> f
_ -> "Store log disabled."
forM_ transports $ \(p, ATransport t, addHTTP) -> do
let descr = p <> " (" <> transportName t <> ")..."
putStrLn $ "Serving SMP protocol on port " <> descr
when addHTTP $ putStrLn $ "Serving static site on port " <> descr
forM_ transports $ \(p, ATransport t) ->
putStrLn $ "Listening on port " <> p <> " (" <> transportName t <> ")..."
deleteDirIfExists :: FilePath -> IO ()
deleteDirIfExists path = whenM (doesDirectoryExist path) $ removeDirectoryRecursive path
@@ -315,3 +302,9 @@ clearDirIfExists path = whenM (doesDirectoryExist path) $ listDirectory path >>=
getEnvPath :: String -> FilePath -> IO FilePath
getEnvPath name def = maybe def (\case "" -> def; f -> f) <$> lookupEnv name
textToSocksMode :: Text -> SocksMode
textToSocksMode = \case
"always" -> SMAlways
"onion" -> SMOnion
s -> error . T.unpack $ "Invalid socks_mode: " <> s
+3 -2
View File
@@ -4,8 +4,9 @@
module Simplex.Messaging.Server.Control where
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.ByteString (ByteString)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (BasicAuth, SenderId)
import Simplex.Messaging.Protocol (BasicAuth)
data CPClientRole = CPRNone | CPRUser | CPRAdmin
deriving (Eq)
@@ -21,7 +22,7 @@ data ControlProtocol
| CPSockets
| CPSocketThreads
| CPServerInfo
| CPDelete SenderId
| CPDelete ByteString
| CPSave
| CPHelp
| CPQuit
+56 -126
View File
@@ -1,31 +1,24 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StrictData #-}
module Simplex.Messaging.Server.Env.STM where
import Control.Concurrent (ThreadId)
import Control.Logger.Simple
import Control.Monad
import qualified Crypto.PubKey.RSA as RSA
import Control.Monad.IO.Unlift
import Crypto.Random
import Data.ByteString.Char8 (ByteString)
import Data.Int (Int64)
import Data.IntMap.Strict (IntMap)
import qualified Data.IntMap.Strict as IM
import Data.List (intercalate)
import Data.List.NonEmpty (NonEmpty)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (isJust, isNothing)
import qualified Data.Text as T
import Data.Time.Clock (getCurrentTime)
import Data.Time.Clock.System (SystemTime)
import qualified Data.X509 as X
import Data.X509.Validation (Fingerprint (..))
import Network.Socket (ServiceName)
import qualified Network.TLS as T
@@ -38,7 +31,6 @@ import Simplex.Messaging.Protocol
import Simplex.Messaging.Server.Expiration
import Simplex.Messaging.Server.Information
import Simplex.Messaging.Server.MsgStore.STM
import Simplex.Messaging.Server.NtfStore
import Simplex.Messaging.Server.QueueStore (NtfCreds (..), QueueRec (..))
import Simplex.Messaging.Server.QueueStore.STM
import Simplex.Messaging.Server.Stats
@@ -46,15 +38,13 @@ import Simplex.Messaging.Server.StoreLog
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport (ATransport, VersionRangeSMP, VersionSMP)
import Simplex.Messaging.Transport.Server
import System.Directory (doesFileExist)
import System.Exit (exitFailure)
import Simplex.Messaging.Transport.Server (SocketState, TransportServerConfig, alpn, loadFingerprint, loadTLSServerParams, newSocketState)
import System.IO (IOMode (..))
import System.Mem.Weak (Weak)
import UnliftIO.STM
data ServerConfig = ServerConfig
{ transports :: [(ServiceName, ATransport, AddHTTP)],
{ transports :: [(ServiceName, ATransport)],
smpHandshakeTimeout :: Int,
tbqSize :: Natural,
msgQueueQuota :: Int,
@@ -62,7 +52,6 @@ data ServerConfig = ServerConfig
msgIdBytes :: Int,
storeLogFile :: Maybe FilePath,
storeMsgsFile :: Maybe FilePath,
storeNtfsFile :: Maybe FilePath,
-- | set to False to prohibit creating new queues
allowNewQueues :: Bool,
-- | simple password that the clients need to pass in handshake to be able to create new queues
@@ -72,8 +61,6 @@ data ServerConfig = ServerConfig
controlPortAdminAuth :: Maybe BasicAuth,
-- | time after which the messages can be removed from the queues and check interval, seconds
messageExpiration :: Maybe ExpirationConfig,
-- | notification expiration interval (seconds)
notificationExpiration :: ExpirationConfig,
-- | time after which the socket with inactive client can be disconnected (without any messages or commands, incl. PING),
-- and check interval, seconds
inactiveClientExpiration :: Maybe ExpirationConfig,
@@ -86,12 +73,10 @@ data ServerConfig = ServerConfig
serverStatsLogFile :: FilePath,
-- | file to save and restore stats
serverStatsBackupFile :: Maybe FilePath,
-- | notification delivery interval
ntfDeliveryInterval :: Int,
-- | interval between sending pending END events to unsubscribed clients, seconds
pendingENDInterval :: Int,
smpCredentials :: ServerCredentials,
httpCredentials :: Maybe ServerCredentials,
-- | CA certificate private key is not needed for initialization
caCertificateFile :: FilePath,
privateKeyFile :: FilePath,
certificateFile :: FilePath,
-- | SMP client-server protocol version range
smpServerVRange :: VersionRangeSMP,
-- | TCP transport config
@@ -116,20 +101,10 @@ defaultMessageExpiration =
checkInterval = 43200 -- seconds, 12 hours
}
defNtfExpirationHours :: Int64
defNtfExpirationHours = 24
defaultNtfExpiration :: ExpirationConfig
defaultNtfExpiration =
ExpirationConfig
{ ttl = defNtfExpirationHours * 3600, -- seconds
checkInterval = 3600 -- seconds, 1 hour
}
defaultInactiveClientExpiration :: ExpirationConfig
defaultInactiveClientExpiration =
ExpirationConfig
{ ttl = 21600, -- seconds, 6 hours
{ ttl = 43200, -- seconds, 12 hours
checkInterval = 3600 -- seconds, 1 hours
}
@@ -138,35 +113,26 @@ defaultProxyClientConcurrency = 32
data Env = Env
{ config :: ServerConfig,
serverActive :: TVar Bool,
serverInfo :: ServerInformation,
server :: Server,
serverIdentity :: KeyHash,
queueStore :: QueueStore,
msgStore :: STMMsgStore,
ntfStore :: NtfStore,
random :: TVar ChaChaDRG,
storeLog :: Maybe (StoreLog 'WriteMode),
tlsServerCreds :: T.Credential,
httpServerCreds :: Maybe T.Credential,
tlsServerParams :: T.ServerParams,
serverStats :: ServerStats,
sockets :: TVar [(ServiceName, SocketState)],
sockets :: SocketState,
clientSeq :: TVar ClientId,
clients :: TVar (IntMap (Maybe Client)),
clients :: TVar (IntMap Client),
proxyAgent :: ProxyAgent -- senders served on this proxy
}
type Subscribed = Bool
data Server = Server
{ subscribedQ :: TQueue (RecipientId, ClientId, Subscribed),
subscribers :: TMap RecipientId (TVar Client),
ntfSubscribedQ :: TQueue (NotifierId, ClientId, Subscribed),
notifiers :: TMap NotifierId (TVar Client),
subClients :: TVar (IntMap Client), -- clients with SMP subscriptions
ntfSubClients :: TVar (IntMap Client), -- clients with Ntf subscriptions
pendingSubEvents :: TVar (IntMap (NonEmpty (RecipientId, Subscribed))),
pendingNtfSubEvents :: TVar (IntMap (NonEmpty (NotifierId, Subscribed))),
{ subscribedQ :: TQueue (RecipientId, Client),
subscribers :: TMap RecipientId Client,
ntfSubscribedQ :: TQueue (NotifierId, Client),
notifiers :: TMap NotifierId Client,
savingLock :: Lock
}
@@ -194,104 +160,68 @@ data Client = Client
sndActiveAt :: TVar SystemTime
}
data ServerSub = ServerSub (TVar SubscriptionThread) | ProhibitSub
data SubscriptionThread = NoSub | SubPending | SubThread (Weak ThreadId)
data SubscriptionThread = NoSub | SubPending | SubThread (Weak ThreadId) | ProhibitSub
data Sub = Sub
{ subThread :: ServerSub, -- Nothing value indicates that sub
{ subThread :: TVar SubscriptionThread,
delivered :: TMVar MsgId
}
newServer :: IO Server
newServer :: STM Server
newServer = do
subscribedQ <- newTQueueIO
subscribers <- TM.emptyIO
ntfSubscribedQ <- newTQueueIO
notifiers <- TM.emptyIO
subClients <- newTVarIO IM.empty
ntfSubClients <- newTVarIO IM.empty
pendingSubEvents <- newTVarIO IM.empty
pendingNtfSubEvents <- newTVarIO IM.empty
savingLock <- atomically createLock
return Server {subscribedQ, subscribers, ntfSubscribedQ, notifiers, subClients, ntfSubClients, pendingSubEvents, pendingNtfSubEvents, savingLock}
subscribedQ <- newTQueue
subscribers <- TM.empty
ntfSubscribedQ <- newTQueue
notifiers <- TM.empty
savingLock <- createLock
return Server {subscribedQ, subscribers, ntfSubscribedQ, notifiers, savingLock}
newClient :: ClientId -> Natural -> VersionSMP -> ByteString -> SystemTime -> IO Client
newClient clientId qSize thVersion sessionId createdAt = do
subscriptions <- TM.emptyIO
ntfSubscriptions <- TM.emptyIO
rcvQ <- newTBQueueIO qSize
sndQ <- newTBQueueIO qSize
msgQ <- newTBQueueIO qSize
procThreads <- newTVarIO 0
endThreads <- newTVarIO IM.empty
endThreadSeq <- newTVarIO 0
connected <- newTVarIO True
rcvActiveAt <- newTVarIO createdAt
sndActiveAt <- newTVarIO createdAt
newClient :: TVar ClientId -> Natural -> VersionSMP -> ByteString -> SystemTime -> STM Client
newClient nextClientId qSize thVersion sessionId createdAt = do
clientId <- stateTVar nextClientId $ \next -> (next, next + 1)
subscriptions <- TM.empty
ntfSubscriptions <- TM.empty
rcvQ <- newTBQueue qSize
sndQ <- newTBQueue qSize
msgQ <- newTBQueue qSize
procThreads <- newTVar 0
endThreads <- newTVar IM.empty
endThreadSeq <- newTVar 0
connected <- newTVar True
rcvActiveAt <- newTVar createdAt
sndActiveAt <- newTVar createdAt
return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, msgQ, procThreads, endThreads, endThreadSeq, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt}
newSubscription :: SubscriptionThread -> STM Sub
newSubscription st = do
delivered <- newEmptyTMVar
subThread <- ServerSub <$> newTVar st
subThread <- newTVar st
return Sub {subThread, delivered}
newProhibitedSub :: STM Sub
newProhibitedSub = do
delivered <- newEmptyTMVar
return Sub {subThread = ProhibitSub, delivered}
newEnv :: ServerConfig -> IO Env
newEnv config@ServerConfig {smpCredentials, httpCredentials, storeLogFile, smpAgentCfg, information, messageExpiration} = do
serverActive <- newTVarIO True
server <- newServer
queueStore <- newQueueStore
msgStore <- newMsgStore
ntfStore <- NtfStore <$> TM.emptyIO
random <- C.newRandom
storeLog <-
forM storeLogFile $ \f -> do
logInfo $ "restoring queues from file " <> T.pack f
restoreQueues queueStore f
tlsServerCreds <- getCredentials "SMP" smpCredentials
httpServerCreds <- mapM (getCredentials "HTTPS") httpCredentials
mapM_ checkHTTPSCredentials httpServerCreds
Fingerprint fp <- loadFingerprint smpCredentials
newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, storeLogFile, smpAgentCfg, transportConfig, information, messageExpiration} = do
server <- atomically newServer
queueStore <- atomically newQueueStore
msgStore <- atomically newMsgStore
random <- liftIO C.newRandom
storeLog <- restoreQueues queueStore `mapM` storeLogFile
tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig)
Fingerprint fp <- loadFingerprint caCertificateFile
let serverIdentity = KeyHash fp
serverStats <- newServerStats =<< getCurrentTime
sockets <- newTVarIO []
serverStats <- atomically . newServerStats =<< getCurrentTime
sockets <- atomically newSocketState
clientSeq <- newTVarIO 0
clients <- newTVarIO mempty
proxyAgent <- newSMPProxyAgent smpAgentCfg random
pure Env {serverActive, config, serverInfo, server, serverIdentity, queueStore, msgStore, ntfStore, random, storeLog, tlsServerCreds, httpServerCreds, serverStats, sockets, clientSeq, clients, proxyAgent}
proxyAgent <- atomically $ newSMPProxyAgent smpAgentCfg random
pure Env {config, serverInfo, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerParams, serverStats, sockets, clientSeq, clients, proxyAgent}
where
getCredentials protocol creds = do
files <- missingCreds
unless (null files) $ do
putStrLn $ "Error: no " <> protocol <> " credentials: " <> intercalate ", " files
when (protocol == "HTTPS") $ putStrLn letsEncrypt
exitFailure
loadServerCredential creds
where
missingfile f = (\y -> [f | not y]) <$> doesFileExist f
missingCreds = do
let files = maybe id (:) (caCertificateFile creds) [certificateFile creds, privateKeyFile creds]
in concat <$> mapM missingfile files
checkHTTPSCredentials (X.CertificateChain cc, _k) =
-- LetsEncrypt provides ECDSA with insecure curve p256 (https://safecurves.cr.yp.to)
case map (X.signedObject . X.getSigned) cc of
X.Certificate {X.certPubKey = X.PubKeyRSA rsa} : _ca | RSA.public_size rsa >= 512 -> pure ()
_ -> do
putStrLn $ "Error: unsupported HTTPS credentials, required 4096-bit RSA\n" <> letsEncrypt
exitFailure
letsEncrypt = "Use Let's Encrypt to generate: certbot certonly --standalone -d yourdomainname --key-type rsa --rsa-key-size 4096"
restoreQueues :: QueueStore -> FilePath -> IO (StoreLog 'WriteMode)
restoreQueues QueueStore {queues, senders, notifiers} f = do
(qs, s) <- readWriteStoreLog f
atomically . writeTVar queues =<< mapM newTVarIO qs
atomically $ writeTVar senders $! M.foldr' addSender M.empty qs
atomically $ writeTVar notifiers $! M.foldr' addNotifier M.empty qs
atomically $ do
writeTVar queues =<< mapM newTVar qs
writeTVar senders $! M.foldr' addSender M.empty qs
writeTVar notifiers $! M.foldr' addNotifier M.empty qs
pure s
addSender :: QueueRec -> Map SenderId RecipientId -> Map SenderId RecipientId
addSender q = M.insert (senderId q) (recipientId q)
@@ -317,7 +247,7 @@ newEnv config@ServerConfig {smpCredentials, httpCredentials, storeLogFile, smpAg
| isJust (storeMsgsFile config) = SPMMessages
| otherwise = SPMQueues
newSMPProxyAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> IO ProxyAgent
newSMPProxyAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> STM ProxyAgent
newSMPProxyAgent smpAgentCfg random = do
smpAgent <- newSMPClientAgent smpAgentCfg random
pure ProxyAgent {smpAgent}
@@ -1,21 +1,14 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TemplateHaskell #-}
module Simplex.Messaging.Server.Information where
import Data.Aeson (FromJSON (..), ToJSON (..))
import qualified Data.Aeson.TH as J
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Int (Int64)
import Data.Maybe (isJust)
import Data.Text (Text)
import Simplex.Messaging.Agent.Protocol (ConnectionMode (..), ConnectionRequestUri)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON)
data ServerInformation = ServerInformation
@@ -43,59 +36,16 @@ data ServerPublicInfo = ServerPublicInfo
adminContacts :: Maybe ServerContactAddress,
complaintsContacts :: Maybe ServerContactAddress,
hosting :: Maybe Entity,
hostingType :: Maybe HostingType,
serverCountry :: Maybe Text
}
deriving (Show)
emptyServerInfo :: Text -> ServerPublicInfo
emptyServerInfo sourceCode =
ServerPublicInfo
{ sourceCode,
usageConditions = Nothing,
operator = Nothing,
website = Nothing,
adminContacts = Nothing,
complaintsContacts = Nothing,
hosting = Nothing,
hostingType = Nothing,
serverCountry = Nothing
}
hasServerInfo :: ServerPublicInfo -> Bool
hasServerInfo ServerPublicInfo {usageConditions, operator, website, adminContacts, complaintsContacts, hosting, hostingType, serverCountry} =
isJust usageConditions || isJust operator || isJust website || isJust adminContacts || isJust complaintsContacts || isJust hosting || isJust hostingType || isJust serverCountry
data ServerPersistenceMode = SPMMemoryOnly | SPMQueues | SPMMessages
deriving (Show)
data ServerConditions = ServerConditions {conditions :: Text, amendments :: Maybe Text}
deriving (Show)
data HostingType = HTVirtual | HTDedicated | HTColocation | HTOwned
deriving (Show)
instance StrEncoding HostingType where
strEncode = \case
HTVirtual -> "virtual"
HTDedicated -> "dedicated"
HTColocation -> "colocation"
HTOwned -> "owned"
strP =
A.takeTill (== ' ') >>= \case
"virtual" -> pure HTVirtual
"dedicated" -> pure HTDedicated
"colocation" -> pure HTColocation
"owned" -> pure HTOwned
_ -> fail "bad HostingType"
instance FromJSON HostingType where
parseJSON = strParseJSON "HostingType"
instance ToJSON HostingType where
toJSON = strToJSON
toEncoding = strToJEncoding
data Entity = Entity {name :: Text, country :: Maybe Text}
deriving (Show)
+106 -252
View File
@@ -1,5 +1,4 @@
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
@@ -9,14 +8,12 @@
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
module Simplex.Messaging.Server.Main where
import Control.Concurrent.STM
import Control.Exception (finally)
import Control.Logger.Simple
import Control.Monad
import Control.Monad (void, when, (<$!>))
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Char (isAlpha, isAscii, toUpper)
@@ -27,44 +24,37 @@ import qualified Data.List.NonEmpty as L
import Data.Maybe (fromMaybe, isJust, isNothing)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8, decodeLatin1)
import Data.Text.Encoding (encodeUtf8)
import qualified Data.Text.IO as T
import Network.Socket (HostName)
import Options.Applicative
import Simplex.Messaging.Agent.Protocol (connReqUriP')
import Simplex.Messaging.Client (HostMode (..), NetworkConfig (..), ProtocolClientConfig (..), SocksMode (..), defaultNetworkConfig, textToHostMode)
import Simplex.Messaging.Client (HostMode (..), NetworkConfig (..), ProtocolClientConfig (..), SocksMode (..), defaultNetworkConfig)
import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (ProtoServerWithAuth), pattern SMPServer)
import Simplex.Messaging.Server (AttachHTTP, runSMPServer)
import Simplex.Messaging.Server (runSMPServer)
import Simplex.Messaging.Server.CLI
import Simplex.Messaging.Server.Env.STM
import Simplex.Messaging.Server.Env.STM (ServerConfig (..), defMsgExpirationDays, defaultInactiveClientExpiration, defaultMessageExpiration, defaultProxyClientConcurrency)
import Simplex.Messaging.Server.Expiration
import Simplex.Messaging.Server.Information
import Simplex.Messaging.Transport (batchCmdsSMPVersion, sendingProxySMPVersion, simplexMQVersion, supportedServerSMPRelayVRange)
import Simplex.Messaging.Transport.Client (SocksProxy, TransportHost (..), defaultSocksProxy)
import Simplex.Messaging.Transport.Server (ServerCredentials (..), TransportServerConfig (..), defaultTransportServerConfig)
import Simplex.Messaging.Transport (batchCmdsSMPVersion, sendingProxySMPVersion, simplexMQVersion, supportedSMPHandshakes, supportedServerSMPRelayVRange)
import Simplex.Messaging.Transport.Client (TransportHost (..))
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig)
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, tshow)
import Simplex.Messaging.Version (mkVersionRange)
import System.Directory (createDirectoryIfMissing, doesFileExist)
import System.Exit (exitFailure)
import System.FilePath (combine)
import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
import Text.Read (readMaybe)
smpServerCLI :: FilePath -> FilePath -> IO ()
smpServerCLI = smpServerCLI_ (\_ _ _ -> pure ()) (\_ -> pure ()) (\_ -> error "attachStaticFiles not available")
smpServerCLI = smpServerCLI_ (\_ _ _ -> pure ()) (\_ -> pure ())
smpServerCLI_ ::
(ServerInformation -> Maybe TransportHost -> FilePath -> IO ()) ->
(EmbeddedWebParams -> IO ()) ->
(FilePath -> (AttachHTTP -> IO ()) -> IO ()) ->
FilePath ->
FilePath ->
IO ()
smpServerCLI_ generateSite serveStaticFiles attachStaticFiles cfgPath logPath =
smpServerCLI_ :: (ServerInformation -> Maybe TransportHost -> FilePath -> IO ()) -> (EmbeddedWebParams -> IO ()) -> FilePath -> FilePath -> IO ()
smpServerCLI_ generateSite serveStaticFiles cfgPath logPath =
getCliCommand' (cliCommandP cfgPath logPath iniFile) serverVersion >>= \case
Init opts ->
doesFileExist iniFile >>= \case
@@ -86,17 +76,16 @@ smpServerCLI_ generateSite serveStaticFiles attachStaticFiles cfgPath logPath =
where
iniFile = combine cfgPath "smp-server.ini"
serverVersion = "SMP server v" <> simplexMQVersion
defaultServerPorts = "5223,443"
defaultServerPort = "5223"
executableName = "smp-server"
storeLogFilePath = combine logPath "smp-server-store.log"
httpsCertFile = combine cfgPath "web.crt"
httpsCertFile = combine cfgPath "web.cert"
httpsKeyFile = combine cfgPath "web.key"
defaultStaticPath = combine logPath "www"
initializeServer opts@InitOptions {ip, fqdn, sourceCode = src', webStaticPath = sp', disableWeb = noWeb', scripted}
| scripted = initialize opts
| otherwise = do
putStrLn "Use `smp-server init -h` for available options."
checkInitOptions opts
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
@@ -106,15 +95,16 @@ smpServerCLI_ generateSite serveStaticFiles attachStaticFiles cfgPath logPath =
host' <- withPrompt ("Enter server FQDN or IP address for certificate (" <> host <> "): ") getLine
sourceCode' <- withPrompt ("Enter server source code URI (" <> maybe simplexmqSource T.unpack src' <> "): ") getServerSourceCode
staticPath' <- withPrompt ("Enter path to store generated static site with server information (" <> fromMaybe defaultStaticPath sp' <> "): ") getLine
enableWeb <- onOffPrompt "Enable built-in web server for static site" (not noWeb')
initialize
opts
{ enableStoreLog,
logStats,
fqdn = if null host' then fqdn else Just host',
password,
sourceCode = (T.pack <$> sourceCode') <|> src' <|> Just (T.pack simplexmqSource),
sourceCode = (T.pack <$> sourceCode') <|> src',
webStaticPath = if null staticPath' then sp' else Just staticPath',
disableWeb = noWeb'
disableWeb = not enableWeb
}
where
serverPassword =
@@ -126,18 +116,7 @@ smpServerCLI_ generateSite serveStaticFiles attachStaticFiles cfgPath logPath =
case strDecode $ encodeUtf8 $ T.pack s of
Right auth -> pure . Just $ ServerPassword auth
_ -> putStrLn "Invalid password. Only latin letters, digits and symbols other than '@' and ':' are allowed" >> serverPassword
checkInitOptions InitOptions {sourceCode, serverInfo, operatorCountry, hostingCountry} = do
let err_
| isNothing sourceCode && hasServerInfo serverInfo =
Just "Error: passing any server information requires passing --source-code"
| isNothing (operator serverInfo) && isJust operatorCountry =
Just "Error: passing --operator-country requires passing --operator"
| isNothing (hosting serverInfo) && isJust hostingCountry =
Just "Error: passing --hosting-country requires passing --hosting"
| otherwise = Nothing
forM_ err_ $ \err -> putStrLn err >> exitFailure
initialize opts'@InitOptions {enableStoreLog, logStats, signAlgorithm, password, controlPort, socksProxy, ownDomains, sourceCode, webStaticPath, disableWeb} = do
checkInitOptions opts'
initialize InitOptions {enableStoreLog, logStats, signAlgorithm, password, sourceCode, webStaticPath, disableWeb} = do
clearDirIfExists cfgPath
clearDirIfExists logPath
createDirectoryIfMissing True cfgPath
@@ -145,10 +124,9 @@ smpServerCLI_ generateSite serveStaticFiles attachStaticFiles cfgPath logPath =
let x509cfg = defaultX509Config {commonName = fromMaybe ip fqdn, signAlgorithm}
fp <- createServerX509 cfgPath x509cfg
basicAuth <- mapM createServerPassword password
controlPortPwds <- forM controlPort $ \_ -> let pwd = decodeLatin1 <$> randomBase64 18 in (,) <$> pwd <*> pwd
let host = fromMaybe (if ip == "127.0.0.1" then "<hostnames>" else ip) fqdn
srv = ProtoServerWithAuth (SMPServer [THDomainName host] "" (C.KeyHash fp)) basicAuth
T.writeFile iniFile $ iniFileContent host basicAuth controlPortPwds
T.writeFile iniFile $ iniFileContent host basicAuth $ Just "https://github.com/simplex-chat/simplexmq"
putStrLn $ "Server initialized, please provide additional server information in " <> iniFile <> "."
putStrLn $ "Run `" <> executableName <> " start` to start server."
warnCAPrivateKeyFile cfgPath x509cfg
@@ -157,10 +135,9 @@ smpServerCLI_ generateSite serveStaticFiles attachStaticFiles cfgPath logPath =
where
createServerPassword = \case
ServerPassword s -> pure s
SPRandom -> BasicAuth <$> randomBase64 32
randomBase64 n = strEncode <$> (atomically . C.randomBytes n =<< C.newRandom)
iniFileContent host basicAuth controlPortPwds =
informationIniContent opts'
SPRandom -> BasicAuth . strEncode <$> (atomically . C.randomBytes 32 =<< C.newRandom)
iniFileContent host basicAuth sourceCode' =
informationIniContent sourceCode'
<> "[STORE_LOG]\n\
\# The server uses STM memory for persistence,\n\
\# that will be lost on restart (e.g., as with redis).\n\
@@ -171,8 +148,7 @@ smpServerCLI_ generateSite serveStaticFiles attachStaticFiles cfgPath logPath =
<> "# Undelivered messages are optionally saved and restored when the server restarts,\n\
\# they are preserved in the .bak file until the next restart.\n"
<> ("restore_messages: " <> onOff enableStoreLog <> "\n")
<> ("expire_messages_days: " <> tshow defMsgExpirationDays <> "\n")
<> ("expire_ntfs_hours: " <> tshow defNtfExpirationHours <> "\n\n")
<> ("expire_messages_days: " <> tshow defMsgExpirationDays <> "\n\n")
<> "# Log daily server statistics to CSV file\n"
<> ("log_stats: " <> onOff logStats <> "\n\n")
<> "[AUTH]\n\
@@ -184,71 +160,61 @@ smpServerCLI_ generateSite serveStaticFiles attachStaticFiles cfgPath logPath =
\# smp://fingerprint:password@host1,host2\n\
\# 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"
<> ( let noPassword = "password to create new queues and forward messages (any printable ASCII characters without whitespace, '@', ':' and '/')"
in optDisabled basicAuth <> "create_password: " <> maybe noPassword (safeDecodeUtf8 . strEncode) basicAuth
<> ( case basicAuth of
Just auth -> "create_password: " <> safeDecodeUtf8 (strEncode auth)
_ -> "# create_password: password to create new queues (any printable ASCII characters without whitespace, '@', ':' and '/')"
)
<> "\n\n"
<> (optDisabled controlPortPwds <> "control_port_admin_password: " <> maybe "" fst controlPortPwds <> "\n")
<> (optDisabled controlPortPwds <> "control_port_user_password: " <> maybe "" snd controlPortPwds <> "\n")
<> "\n\
\[TRANSPORT]\n\
\# Host is only used to print server address on start.\n\
\# You can specify multiple server ports.\n"
<> ("host: " <> T.pack host <> "\n")
<> ("port: " <> T.pack defaultServerPorts <> "\n")
<> "log_tls_errors: off\n\n\
\# Use `websockets: 443` to run websockets server in addition to plain TLS.\n\
\# This option is deprecated and should be used for testing only.\n\
\# , port 443 should be specified in port above\n\
\websockets: off\n"
<> (optDisabled controlPort <> "control_port: " <> tshow (fromMaybe defaultControlPort controlPort))
<> "\n\n\
\# control_port_admin_password:\n\
\# control_port_user_password:\n\n\
\[TRANSPORT]\n\
\# host is only used to print server address on start\n"
<> ("host: " <> T.pack host <> "\n")
<> ("port: " <> T.pack defaultServerPort <> "\n")
<> "log_tls_errors: off\n\
\websockets: off\n\
\# control_port: 5224\n\n\
\[PROXY]\n\
\# Network configuration for SMP proxy client.\n\
\# `host_mode` can be 'public' (default) or 'onion'.\n\
\# It defines prefferred hostname for destination servers with multiple hostnames.\n\
\# host_mode: public\n\
\# required_host_mode: off\n\n\
\# The domain suffixes of the relays you operate (space-separated) to count as separate proxy statistics.\n"
<> (optDisabled ownDomains <> "own_server_domains: " <> maybe "" (safeDecodeUtf8 . strEncode) ownDomains)
<> "\n\n\
\# The domain suffixes of the relays you operate (space-separated) to count as separate proxy statistics.\n\
\# own_server_domains: \n\n\
\# SOCKS proxy port for forwarding messages to destination servers.\n\
\# You may need a separate instance of SOCKS proxy for incoming single-hop requests.\n"
<> (optDisabled socksProxy <> "socks_proxy: " <> maybe "localhost:9050" (safeDecodeUtf8 . strEncode) socksProxy)
<> "\n\n\
\# You may need a separate instance of SOCKS proxy for incoming single-hop requests.\n\
\# socks_proxy: localhost:9050\n\n\
\# `socks_mode` can be 'onion' for SOCKS proxy to be used for .onion destination hosts only (default)\n\
\# or 'always' to be used for all destination hosts (can be used if it is an .onion server).\n\
\# socks_mode: onion\n\n\
\# Limit number of threads a client can spawn to process proxy commands in parrallel.\n"
<> ("# client_concurrency: " <> tshow defaultProxyClientConcurrency)
<> "\n\n\
\[INACTIVE_CLIENTS]\n\
<> ("# client_concurrency: " <> tshow defaultProxyClientConcurrency <> "\n\n")
<> "[INACTIVE_CLIENTS]\n\
\# TTL and interval to check inactive clients\n\
\disconnect: on\n"
<> ("ttl: " <> tshow (ttl defaultInactiveClientExpiration) <> "\n")
<> ("check_interval: " <> tshow (checkInterval defaultInactiveClientExpiration))
\disconnect: off\n"
<> ("# ttl: " <> tshow (ttl defaultInactiveClientExpiration) <> "\n")
<> ("# check_interval: " <> tshow (checkInterval defaultInactiveClientExpiration) <> "\n")
<> "\n\n\
\[WEB]\n\
\# Set path to generate static mini-site for server information and qr codes/links\n"
<> ("static_path: " <> T.pack (fromMaybe defaultStaticPath webStaticPath) <> "\n\n")
<> "# Run an embedded server on this port\n\
\# Onion sites can use any port and register it in the hidden service config.\n\
\# Running on a port 80 may require setting process capabilities.\n\
\# http: 8000\n\n\
\# You can run an embedded TLS web server too if you provide port and cert and key files.\n\
\# Not required for running relay on onion address.\n"
<> (webDisabled <> "https: 443\n")
<> (webDisabled <> "cert: " <> T.pack httpsCertFile <> "\n")
<> (webDisabled <> "key: " <> T.pack httpsKeyFile <> "\n")
where
webDisabled = if disableWeb then "# " else ""
\# Running on a port 80 may require setting process capabilities.\n"
<> ((if disableWeb then "# " else "") <> "http: 8000\n\n")
<> "# You can run an embedded TLS web server too if you provide port and cert and key files.\n\
\# Not required for running relay on onion address.\n\
\# https: 443\n"
<> ("# cert: " <> T.pack httpsCertFile <> "\n")
<> ("# key: " <> T.pack httpsKeyFile <> "\n")
runServer ini = do
hSetBuffering stdout LineBuffering
hSetBuffering stderr LineBuffering
fp <- checkSavedFingerprint cfgPath defaultX509Config
let host = either (const "<hostnames>") T.unpack $ lookupValue "TRANSPORT" "host" ini
port = T.unpack $ strictIni "TRANSPORT" "port" ini
cfg@ServerConfig {information, storeLogFile, newQueueBasicAuth, messageExpiration, inactiveClientExpiration} = serverConfig
cfg@ServerConfig {information, transports, storeLogFile, newQueueBasicAuth, messageExpiration, inactiveClientExpiration} = serverConfig
sourceCode' = (\ServerPublicInfo {sourceCode} -> sourceCode) <$> information
srv = ProtoServerWithAuth (SMPServer [THDomainName host] (if port == "5223" then "" else port) (C.KeyHash fp)) newQueueBasicAuth
printServiceInfo serverVersion srv
@@ -278,48 +244,31 @@ smpServerCLI_ generateSite serveStaticFiles attachStaticFiles cfgPath logPath =
newQueuesAllowed = allowNewQueues cfg,
basicAuthEnabled = isJust newQueueBasicAuth
}
case webStaticPath' of
Just path | sharedHTTP -> do
runWebServer path Nothing ServerInformation {config, information}
attachStaticFiles path $ \attachHTTP -> do
logDebug "Allocated web server resources"
runSMPServer cfg (Just attachHTTP) `finally` logDebug "Releasing web server resources..."
Just path -> do
runWebServer path webHttpsParams' ServerInformation {config, information}
runSMPServer cfg Nothing
Nothing -> do
logWarn "No server static path set"
runSMPServer cfg Nothing
logDebug "Bye"
runWebServer ini ServerInformation {config, information}
runSMPServer cfg
where
enableStoreLog = settingIsOn "STORE_LOG" "enable" ini
logStats = settingIsOn "STORE_LOG" "log_stats" ini
c = combine cfgPath . ($ defaultX509Config)
restoreMessagesFile path = case iniOnOff "STORE_LOG" "restore_messages" ini of
Just True -> Just path
Just False -> Nothing
-- if the setting is not set, it is enabled when store log is enabled
_ -> enableStoreLog $> path
transports = iniTransports ini
sharedHTTP = any (\(_, _, addHTTP) -> addHTTP) transports
serverConfig =
ServerConfig
{ transports,
{ transports = iniTransports ini,
smpHandshakeTimeout = 120000000,
tbqSize = 128,
tbqSize = 64,
msgQueueQuota = 128,
queueIdBytes = 24,
msgIdBytes = 24, -- must be at least 24 bytes, it is used as 192-bit nonce for XSalsa20
smpCredentials =
ServerCredentials
{ caCertificateFile = Just $ c caCrtFile,
privateKeyFile = c serverKeyFile,
certificateFile = c serverCrtFile
},
httpCredentials = (\WebHttpsParams {key, cert} -> ServerCredentials {caCertificateFile = Nothing, privateKeyFile = key, certificateFile = cert}) <$> webHttpsParams',
caCertificateFile = c caCrtFile,
privateKeyFile = c serverKeyFile,
certificateFile = c serverCrtFile,
storeLogFile = enableStoreLog $> storeLogFilePath,
storeMsgsFile = restoreMessagesFile $ combine logPath "smp-server-messages.log",
storeNtfsFile = restoreMessagesFile $ combine logPath "smp-server-ntfs.log",
storeMsgsFile =
let messagesPath = combine logPath "smp-server-messages.log"
in case iniOnOff "STORE_LOG" "restore_messages" ini of
Just True -> Just messagesPath
Just False -> Nothing
-- if the setting is not set, it is enabled when store log is enabled
_ -> enableStoreLog $> messagesPath,
-- allow creating new queues by default
allowNewQueues = fromMaybe True $ iniOnOff "AUTH" "new_queues" ini,
newQueueBasicAuth = either error id <$!> strDecodeIni "AUTH" "create_password" ini,
@@ -330,10 +279,6 @@ smpServerCLI_ generateSite serveStaticFiles attachStaticFiles cfgPath logPath =
defaultMessageExpiration
{ ttl = 86400 * readIniDefault defMsgExpirationDays "STORE_LOG" "expire_messages_days" ini
},
notificationExpiration =
defaultNtfExpiration
{ ttl = 3600 * readIniDefault defNtfExpirationHours "STORE_LOG" "expire_ntfs_hours" ini
},
inactiveClientExpiration =
settingIsOn "INACTIVE_CLIENTS" "disconnect" ini
$> ExpirationConfig
@@ -344,12 +289,11 @@ smpServerCLI_ generateSite serveStaticFiles attachStaticFiles cfgPath logPath =
logStatsStartTime = 0, -- seconds from 00:00 UTC
serverStatsLogFile = combine logPath "smp-server-stats.daily.log",
serverStatsBackupFile = logStats $> combine logPath "smp-server-stats.log",
pendingENDInterval = 15000000, -- 15 seconds
ntfDeliveryInterval = 3000000, -- 3 seconds
smpServerVRange = supportedServerSMPRelayVRange,
transportConfig =
defaultTransportServerConfig
{ logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini
{ logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini,
alpn = Just supportedSMPHandshakes
},
controlPort = eitherToMaybe $ T.unpack <$> lookupValue "TRANSPORT" "control_port" ini,
smpAgentCfg =
@@ -361,8 +305,8 @@ smpServerCLI_ generateSite serveStaticFiles attachStaticFiles cfgPath logPath =
networkConfig =
defaultNetworkConfig
{ socksProxy = either error id <$!> strDecodeIni "PROXY" "socks_proxy" ini,
socksMode = maybe SMOnion (either error id) $! strDecodeIni "PROXY" "socks_mode" ini,
hostMode = either (const HMPublic) (either error id . textToHostMode) $ lookupValue "PROXY" "host_mode" ini,
socksMode = either (const SMOnion) textToSocksMode $ lookupValue "PROXY" "socks_mode" ini,
hostMode = either (const HMPublic) textToHostMode $ lookupValue "PROXY" "host_mode" ini,
requiredHostMode = fromMaybe False $ iniOnOff "PROXY" "required_host_mode" ini
}
},
@@ -373,25 +317,33 @@ smpServerCLI_ generateSite serveStaticFiles attachStaticFiles cfgPath logPath =
serverClientConcurrency = readIniDefault defaultProxyClientConcurrency "PROXY" "client_concurrency" ini,
information = serverPublicInfo ini
}
textToHostMode :: Text -> HostMode
textToHostMode = \case
"public" -> HMPublic
"onion" -> HMOnionViaSocks
s -> error . T.unpack $ "Invalid host_mode: " <> s
textToOwnServers :: Text -> [ByteString]
textToOwnServers = map encodeUtf8 . T.words
runWebServer webStaticPath webHttpsParams si = do
runWebServer ini si =
case eitherToMaybe $ T.unpack <$> lookupValue "WEB" "static_path" ini of
Nothing -> logWarn "No server static path set"
Just webStaticPath -> do
let onionHost =
either (const Nothing) (find isOnion) $
strDecode @(L.NonEmpty TransportHost) . encodeUtf8 =<< lookupValue "TRANSPORT" "host" ini
webHttpPort = eitherToMaybe $ read . T.unpack <$> lookupValue "WEB" "http" ini
webHttpsParams =
eitherToMaybe $ do
port <- read . T.unpack <$> lookupValue "WEB" "https" ini
cert <- T.unpack <$> lookupValue "WEB" "cert" ini
key <- T.unpack <$> lookupValue "WEB" "key" ini
pure WebHttpsParams {port, cert, key}
generateSite si onionHost webStaticPath
when (isJust webHttpPort || isJust webHttpsParams) $
serveStaticFiles EmbeddedWebParams {webStaticPath, webHttpPort, webHttpsParams}
where
isOnion = \case THOnionHost _ -> True; _ -> False
webHttpsParams' =
eitherToMaybe $ do
port <- read . T.unpack <$> lookupValue "WEB" "https" ini
cert <- T.unpack <$> lookupValue "WEB" "cert" ini
key <- T.unpack <$> lookupValue "WEB" "key" ini
pure WebHttpsParams {port, cert, key}
webStaticPath' = eitherToMaybe $ T.unpack <$> lookupValue "WEB" "static_path" ini
data EmbeddedWebParams = EmbeddedWebParams
{ webStaticPath :: FilePath,
@@ -415,31 +367,28 @@ getServerSourceCode =
simplexmqSource :: String
simplexmqSource = "https://github.com/simplex-chat/simplexmq"
defaultControlPort :: Int
defaultControlPort = 5224
informationIniContent :: InitOptions -> Text
informationIniContent InitOptions {sourceCode, serverInfo} =
informationIniContent :: Maybe Text -> Text
informationIniContent sourceCode_ =
"[INFORMATION]\n\
\# AGPLv3 license requires that you make any source code modifications\n\
\# available to the end users of the server.\n\
\# LICENSE: https://github.com/simplex-chat/simplexmq/blob/stable/LICENSE\n\
\# Include correct source code URI in case the server source code is modified in any way.\n\
\# If any other information fields are present, source code property also MUST be present.\n\n"
<> (optDisabled sourceCode <> "source_code: " <> fromMaybe "URI" sourceCode)
<> "\n\n\
\# Declaring all below information is optional, any of these fields can be omitted.\n\
<> (maybe "# source_code: URI" ("source_code: " <>) sourceCode_ <> "\n\n")
<> "# Declaring all below information is optional, any of these fields can be omitted.\n\
\\n\
\# Server usage conditions and amendments.\n\
\# It is recommended to use standard conditions with any amendments in a separate document.\n\
\# usage_conditions: https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md\n\
\# condition_amendments: link\n\
\\n\
\# Server location and operator.\n"
<> countryStr "server" serverCountry
<> enitiyStrs "operator" operator
<> (optDisabled website <> "website: " <> fromMaybe "" website)
<> "\n\n\
\# Server location and operator.\n\
\# server_country: ISO-3166 2-letter code\n\
\# operator: entity (organization or person name)\n\
\# operator_country: ISO-3166 2-letter code\n\
\# website:\n\
\\n\
\# Administrative contacts.\n\
\# admin_simplex: SimpleX address\n\
\# admin_email:\n\
@@ -452,20 +401,9 @@ informationIniContent InitOptions {sourceCode, serverInfo} =
\# complaints_pgp:\n\
\# complaints_pgp_fingerprint:\n\
\\n\
\# Hosting provider.\n"
<> enitiyStrs "hosting" hosting
<> "\n\
\# Hosting type can be `virtual`, `dedicated`, `colocation`, `owned`\n"
<> ("hosting_type: " <> maybe "virtual" (decodeLatin1 . strEncode) hostingType <> "\n\n")
where
ServerPublicInfo {operator, website, hosting, hostingType, serverCountry}= serverInfo
countryStr optName country = optDisabled country <> optName <> "_country: " <> fromMaybe "ISO-3166 2-letter code" country <> "\n"
enitiyStrs optName entity =
optDisabled entity
<> optName <> ": "
<> maybe ("entity (organization or person name)") name entity
<> "\n"
<> countryStr optName (country =<< entity)
\# Hosting provider.\n\
\# hosting: entity (organization or person name)\n\
\# hosting_country: ISO-3166 2-letter code\n\n"
serverPublicInfo :: Ini -> Maybe ServerPublicInfo
serverPublicInfo ini = serverInfo <$!> infoValue "source_code"
@@ -481,14 +419,15 @@ serverPublicInfo ini = serverInfo <$!> infoValue "source_code"
website = infoValue "website",
adminContacts = iniContacts "admin_simplex" "admin_email" "admin_pgp" "admin_pgp_fingerprint",
complaintsContacts = iniContacts "complaints_simplex" "complaints_email" "complaints_pgp" "complaints_pgp_fingerprint",
hosting = iniEntity "hosting" "hosting_country",
hostingType = either error id <$!> strDecodeIni "INFORMATION" "hosting_type" ini
hosting = iniEntity "hosting" "hosting_country"
}
infoValue name = eitherToMaybe $ lookupValue "INFORMATION" name ini
iniEntity nameField countryField =
(\name -> Entity {name, country = countryValue countryField})
<$!> infoValue nameField
countryValue field = (either error id . validCountryValue (T.unpack field) . T.unpack) <$!> infoValue field
countryValue field =
(\cs -> if T.length cs == 2 && T.all (\c -> isAscii c && isAlpha c) cs then T.map toUpper cs else error $ "Use ISO3166 2-letter code for " <> T.unpack field)
<$!> infoValue field
iniContacts simplexField emailField pgpKeyUriField pgpKeyFingerprintField =
let simplex = either error id . parseAll (connReqUriP' Nothing) . encodeUtf8 <$!> eitherToMaybe (lookupValue "INFORMATION" simplexField ini)
email = infoValue emailField
@@ -499,14 +438,6 @@ serverPublicInfo ini = serverInfo <$!> infoValue "source_code"
(Nothing, Nothing, _, Nothing) -> Nothing
(_, _, pkURI, pkFingerprint) -> Just ServerContactAddress {simplex, email, pgp = PGPKey <$> pkURI <*> pkFingerprint}
optDisabled :: Maybe a -> Text
optDisabled p = if isNothing p then "# " else ""
validCountryValue :: String -> String -> Either String Text
validCountryValue field s
| length s == 2 && all (\c -> isAscii c && isAlpha c) s = Right $ T.pack $ map toUpper s
| otherwise = Left $ "Use ISO3166 2-letter code for " <> field
printSourceCode :: Maybe Text -> IO ()
printSourceCode = \case
Just sourceCode -> T.putStrLn $ "Server source code: " <> sourceCode
@@ -527,13 +458,7 @@ data InitOptions = InitOptions
ip :: HostName,
fqdn :: Maybe HostName,
password :: Maybe ServerPassword,
controlPort :: Maybe Int,
socksProxy :: Maybe SocksProxy,
ownDomains :: Maybe (L.NonEmpty TransportHost),
sourceCode :: Maybe Text,
serverInfo :: ServerPublicInfo,
operatorCountry :: Maybe Text,
hostingCountry :: Maybe Text,
webStaticPath :: Maybe FilePath,
disableWeb :: Bool,
scripted :: Bool
@@ -602,46 +527,11 @@ cliCommandP cfgPath logPath iniFile =
<> help "Set password to create new messaging queues"
<> value SPRandom
)
controlPort <-
flag' (Just defaultControlPort) (long "control-port" <> help ("Enable control port on " <> show defaultControlPort))
<|> option strParse (long "control-port" <> help "Enable control port" <> metavar "PORT" <> value Nothing)
socksProxy <-
flag' (Just defaultSocksProxy) (long "socks-proxy" <> help "Outgoing SOCKS proxy on port 9050")
<|>
option
strParse
( long "socks-proxy"
<> metavar "PROXY"
<> help "Outgoing SOCKS proxy to forward messages to onion-only servers"
<> value Nothing
)
ownDomains :: Maybe (L.NonEmpty TransportHost) <-
option
strParse
( long "own-domains"
<> metavar "DOMAINS"
<> help "Own server domain names (comma-separated)"
<> value Nothing
)
sourceCode <-
flag' (Just simplexmqSource) (long "source-code" <> help ("Server source code (default: " <> simplexmqSource <> ")"))
<|> (optional . strOption) (long "source-code" <> metavar "URI" <> help "Server source code")
operator_ <- entityP "operator" "OPERATOR" "Server operator"
hosting_ <- entityP "hosting" "HOSTING" "Hosting provider"
hostingType <-
option
strParse
( long "hosting-type"
<> metavar "HOSTING_TYPE"
<> help "Hosting type: virtual, dedicated, colocation, owned"
<> value Nothing
)
serverCountry <- countryP "server" "SERVER" "Server datacenter"
website <-
(optional . strOption)
( long "operator-website"
<> help "Operator public website"
<> metavar "WEBSITE"
( long "source-code"
<> help "Server source code will be communicated to the users"
<> metavar "URI"
)
webStaticPath <-
(optional . strOption)
@@ -668,46 +558,10 @@ cliCommandP cfgPath logPath iniFile =
ip,
fqdn,
password,
controlPort,
socksProxy,
ownDomains,
sourceCode = T.pack <$> sourceCode,
serverInfo =
ServerPublicInfo
{ sourceCode = T.pack simplexmqSource,
usageConditions = Nothing,
operator = fst operator_,
website,
adminContacts = Nothing,
complaintsContacts = Nothing,
hosting = fst hosting_,
hostingType,
serverCountry
},
operatorCountry = snd operator_,
hostingCountry = snd hosting_,
sourceCode,
webStaticPath,
disableWeb,
scripted
}
parseBasicAuth :: ReadM ServerPassword
parseBasicAuth = eitherReader $ fmap ServerPassword . strDecode . B.pack
entityP :: String -> String -> String -> Parser (Maybe Entity, Maybe Text)
entityP opt' metavar' help' = do
name_ <-
(optional . strOption)
( long opt'
<> metavar (metavar' <> "_NAME")
<> help (help' <> " name")
)
country <- countryP opt' metavar' help'
pure ((\name -> Entity {name, country}) <$> name_, country)
countryP :: String -> String -> String -> Parser (Maybe Text)
countryP opt' metavar' help' =
(optional . option (eitherReader $ validCountryValue opt'))
( long (opt' <> "-country")
<> metavar (metavar' <> "_COUNTRY")
<> help (help' <> " country")
)
strParse :: StrEncoding a => ReadM a
strParse = eitherReader $ parseAll strP . encodeUtf8 . T.pack
+41 -37
View File
@@ -9,21 +9,23 @@
module Simplex.Messaging.Server.MsgStore.STM
( STMMsgStore,
MsgQueue (msgQueue),
MsgQueue (..),
newMsgStore,
getMsgQueue,
delMsgQueue,
delMsgQueueSize,
flushMsgQueue,
snapshotMsgQueue,
writeMsg,
tryPeekMsg,
tryPeekMsgIO,
peekMsg,
tryDelMsg,
tryDelPeekMsg,
deleteExpiredMsgs,
getQueueSize,
)
where
import Control.Concurrent.STM.TQueue (flushTQueue)
import qualified Data.ByteString.Char8 as B
import Data.Functor (($>))
import Data.Int (Int64)
@@ -42,17 +44,12 @@ data MsgQueue = MsgQueue
type STMMsgStore = TMap RecipientId MsgQueue
newMsgStore :: IO STMMsgStore
newMsgStore = TM.emptyIO
newMsgStore :: STM STMMsgStore
newMsgStore = TM.empty
-- The reason for double lookup is that majority of messaging queues exist,
-- because multiple messages are sent to the same queue,
-- so the first lookup without STM transaction will return the queue faster.
-- In case the queue does not exist, it needs to be looked-up again inside transaction.
getMsgQueue :: STMMsgStore -> RecipientId -> Int -> IO MsgQueue
getMsgQueue st rId quota = TM.lookupIO rId st >>= maybe (atomically maybeNewQ) pure
getMsgQueue :: STMMsgStore -> RecipientId -> Int -> STM MsgQueue
getMsgQueue st rId quota = maybe newQ pure =<< TM.lookup rId st
where
maybeNewQ = TM.lookup rId st >>= maybe newQ pure
newQ = do
msgQueue <- newTQueue
canWrite <- newTVar True
@@ -61,14 +58,25 @@ getMsgQueue st rId quota = TM.lookupIO rId st >>= maybe (atomically maybeNewQ) p
TM.insert rId q st
pure q
delMsgQueue :: STMMsgStore -> RecipientId -> IO ()
delMsgQueue st rId = atomically $ TM.delete rId st
delMsgQueue :: STMMsgStore -> RecipientId -> STM ()
delMsgQueue st rId = TM.delete rId st
delMsgQueueSize :: STMMsgStore -> RecipientId -> IO Int
delMsgQueueSize st rId = atomically (TM.lookupDelete rId st) >>= maybe (pure 0) (\MsgQueue {size} -> readTVarIO size)
delMsgQueueSize :: STMMsgStore -> RecipientId -> STM Int
delMsgQueueSize st rId = TM.lookupDelete rId st >>= maybe (pure 0) (\MsgQueue {size} -> readTVar size)
writeMsg :: MsgQueue -> Message -> IO (Maybe (Message, Bool))
writeMsg MsgQueue {msgQueue = q, quota, canWrite, size} !msg = atomically $ do
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)
where
snapshotTQueue q = do
msgs <- flushTQueue q
mapM_ (writeTQueue q) msgs
pure msgs
writeMsg :: MsgQueue -> Message -> STM (Maybe (Message, Bool))
writeMsg MsgQueue {msgQueue = q, quota, canWrite, size} !msg = do
canWrt <- readTVar canWrite
empty <- isEmptyTQueue q
if canWrt || empty
@@ -83,47 +91,43 @@ writeMsg MsgQueue {msgQueue = q, quota, canWrite, size} !msg = atomically $ do
where
msgQuota = MessageQuota {msgId = msgId msg, msgTs = msgTs msg}
tryPeekMsgIO :: MsgQueue -> IO (Maybe Message)
tryPeekMsgIO = atomically . tryPeekTQueue . msgQueue
{-# INLINE tryPeekMsgIO #-}
-- TODO remove once deliverToSub is split
tryPeekMsg :: MsgQueue -> STM (Maybe Message)
tryPeekMsg = tryPeekTQueue . msgQueue
{-# INLINE tryPeekMsg #-}
tryDelMsg :: MsgQueue -> MsgId -> IO (Maybe Message)
tryDelMsg mq msgId' = atomically $
peekMsg :: MsgQueue -> STM Message
peekMsg = peekTQueue . msgQueue
{-# INLINE peekMsg #-}
tryDelMsg :: MsgQueue -> MsgId -> STM (Maybe Message)
tryDelMsg mq msgId' =
tryPeekMsg mq >>= \case
msg_@(Just msg)
| msgId msg == msgId' || B.null msgId' -> tryDeleteMsg_ mq >> pure msg_
| msgId msg == msgId' || B.null msgId' -> tryDeleteMsg mq >> pure msg_
| otherwise -> pure Nothing
_ -> pure Nothing
-- atomic delete (== read) last and peek next message if available
tryDelPeekMsg :: MsgQueue -> MsgId -> IO (Maybe Message, Maybe Message)
tryDelPeekMsg mq msgId' = atomically $
tryDelPeekMsg :: MsgQueue -> MsgId -> STM (Maybe Message, Maybe Message)
tryDelPeekMsg mq msgId' =
tryPeekMsg mq >>= \case
msg_@(Just msg)
| msgId msg == msgId' || B.null msgId' -> (msg_,) <$> (tryDeleteMsg_ mq >> tryPeekMsg mq)
| msgId msg == msgId' || B.null msgId' -> (msg_,) <$> (tryDeleteMsg mq >> tryPeekMsg mq)
| otherwise -> pure (Nothing, msg_)
_ -> pure (Nothing, Nothing)
deleteExpiredMsgs :: MsgQueue -> Int64 -> IO Int
deleteExpiredMsgs mq old = atomically $ loop 0
deleteExpiredMsgs :: MsgQueue -> Int64 -> STM Int
deleteExpiredMsgs mq old = loop 0
where
loop dc =
tryPeekMsg mq >>= \case
Just Message {msgTs}
| systemSeconds msgTs < old ->
tryDeleteMsg_ mq >> loop (dc + 1)
tryDeleteMsg mq >> loop (dc + 1)
_ -> pure dc
tryDeleteMsg_ :: MsgQueue -> STM ()
tryDeleteMsg_ MsgQueue {msgQueue = q, size} =
tryDeleteMsg :: MsgQueue -> STM ()
tryDeleteMsg MsgQueue {msgQueue = q, size} =
tryReadTQueue q >>= \case
Just _ -> modifyTVar' size (subtract 1)
_ -> pure ()
getQueueSize :: MsgQueue -> IO Int
getQueueSize MsgQueue {size} = readTVarIO size
-67
View File
@@ -1,67 +0,0 @@
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module Simplex.Messaging.Server.NtfStore where
import Control.Concurrent.STM
import Control.Monad (foldM)
import Data.Int (Int64)
import qualified Data.Map.Strict as M
import Data.Time.Clock.System (SystemTime (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (EncNMsgMeta, MsgId, NotifierId)
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
newtype NtfStore = NtfStore (TMap NotifierId (TVar [MsgNtf]))
data MsgNtf = MsgNtf
{ ntfMsgId :: MsgId,
ntfTs :: SystemTime,
ntfNonce :: C.CbNonce,
ntfEncMeta :: EncNMsgMeta
}
storeNtf :: NtfStore -> NotifierId -> MsgNtf -> IO ()
storeNtf (NtfStore ns) nId ntf = do
TM.lookupIO nId ns >>= atomically . maybe newNtfs (`modifyTVar'` (ntf :))
-- TODO coalesce messages here once the client is updated to process multiple messages
-- for single notification.
-- when (isJust prevNtf) $ incStat $ msgNtfReplaced stats
where
newNtfs = TM.lookup nId ns >>= maybe (TM.insertM nId (newTVar [ntf]) ns) (`modifyTVar'` (ntf :))
deleteNtfs :: NtfStore -> NotifierId -> IO Int
deleteNtfs (NtfStore ns) nId = atomically (TM.lookupDelete nId ns) >>= maybe (pure 0) (fmap length . readTVarIO)
deleteExpiredNtfs :: NtfStore -> Int64 -> IO Int
deleteExpiredNtfs (NtfStore ns) old =
foldM (\expired -> fmap (expired +) . expireQueue) 0 . M.keys =<< readTVarIO ns
where
expireQueue nId = TM.lookupIO nId ns >>= maybe (pure 0) expire
expire v = readTVarIO v >>= \case
[] -> pure 0
_ ->
atomically $ readTVar v >>= \case
[] -> pure 0
-- check the last message first, it is the earliest
ntfs | systemSeconds (ntfTs $ last $ ntfs) < old -> do
let !ntfs' = filter (\MsgNtf {ntfTs = ts} -> systemSeconds ts >= old) ntfs
writeTVar v ntfs'
pure $! length ntfs - length ntfs'
_ -> pure 0
data NtfLogRecord = NLRv1 NotifierId MsgNtf
instance StrEncoding MsgNtf where
strEncode MsgNtf {ntfMsgId, ntfTs, ntfNonce, ntfEncMeta} = strEncode (ntfMsgId, ntfTs, ntfNonce, ntfEncMeta)
strP = do
(ntfMsgId, ntfTs, ntfNonce, ntfEncMeta) <- strP
pure MsgNtf {ntfMsgId, ntfTs, ntfNonce, ntfEncMeta}
instance StrEncoding NtfLogRecord where
strEncode (NLRv1 nId ntf) = strEncode (Str "v1", nId, ntf)
strP = "v1 " *> (NLRv1 <$> strP_ <*> strP)
+1 -18
View File
@@ -1,13 +1,10 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
module Simplex.Messaging.Server.QueueStore where
import Data.Int (Int64)
import Data.Time.Clock.System (SystemTime (..), getSystemTime)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol
@@ -19,8 +16,7 @@ data QueueRec = QueueRec
senderKey :: !(Maybe SndPublicAuthKey),
sndSecure :: !SenderCanSecure,
notifier :: !(Maybe NtfCreds),
status :: !ServerQueueStatus,
updatedAt :: !(Maybe RoundedSystemTime)
status :: !ServerQueueStatus
}
deriving (Show)
@@ -38,16 +34,3 @@ instance StrEncoding NtfCreds where
pure NtfCreds {notifierId, notifierKey, rcvNtfDhSecret}
data ServerQueueStatus = QueueActive | QueueOff deriving (Eq, Show)
newtype RoundedSystemTime = RoundedSystemTime Int64
deriving (Eq, Ord, Show)
instance StrEncoding RoundedSystemTime where
strEncode (RoundedSystemTime t) = strEncode t
strP = RoundedSystemTime <$> strP
getRoundedSystemTime :: Int64 -> IO RoundedSystemTime
getRoundedSystemTime prec = (\t -> RoundedSystemTime $ (systemSeconds t `div` prec) * prec) <$> getSystemTime
getSystemDate :: IO RoundedSystemTime
getSystemDate = getRoundedSystemTime 86400
+30 -38
View File
@@ -19,7 +19,6 @@ module Simplex.Messaging.Server.QueueStore.STM
addQueueNotifier,
deleteQueueNotifier,
suspendQueue,
updateQueueTime,
deleteQueue,
)
where
@@ -39,15 +38,15 @@ data QueueStore = QueueStore
notifiers :: TMap NotifierId RecipientId
}
newQueueStore :: IO QueueStore
newQueueStore :: STM QueueStore
newQueueStore = do
queues <- TM.emptyIO
senders <- TM.emptyIO
notifiers <- TM.emptyIO
queues <- TM.empty
senders <- TM.empty
notifiers <- TM.empty
pure QueueStore {queues, senders, notifiers}
addQueue :: QueueStore -> QueueRec -> IO (Either ErrorType ())
addQueue QueueStore {queues, senders} q@QueueRec {recipientId = rId, senderId = sId} = atomically $ do
addQueue :: QueueStore -> QueueRec -> STM (Either ErrorType ())
addQueue QueueStore {queues, senders} q@QueueRec {recipientId = rId, senderId = sId} = do
ifM hasId (pure $ Left DUPLICATE_) $ do
qVar <- newTVar q
TM.insert rId qVar queues
@@ -56,55 +55,48 @@ addQueue QueueStore {queues, senders} q@QueueRec {recipientId = rId, senderId =
where
hasId = (||) <$> TM.member rId queues <*> TM.member sId senders
getQueue :: DirectParty p => QueueStore -> SParty p -> QueueId -> IO (Either ErrorType QueueRec)
getQueue :: DirectParty p => QueueStore -> SParty p -> QueueId -> STM (Either ErrorType QueueRec)
getQueue QueueStore {queues, senders, notifiers} party qId =
toResult <$> (mapM readTVarIO =<< getVar)
toResult <$> (mapM readTVar =<< getVar)
where
getVar = case party of
SRecipient -> TM.lookupIO qId queues
SSender -> TM.lookupIO qId senders $>>= (`TM.lookupIO` queues)
SNotifier -> TM.lookupIO qId notifiers $>>= (`TM.lookupIO` queues)
SRecipient -> TM.lookup qId queues
SSender -> TM.lookup qId senders $>>= (`TM.lookup` queues)
SNotifier -> TM.lookup qId notifiers $>>= (`TM.lookup` queues)
secureQueue :: QueueStore -> RecipientId -> SndPublicAuthKey -> IO (Either ErrorType QueueRec)
secureQueue QueueStore {queues} rId sKey = toResult <$> do
TM.lookupIO rId queues $>>= \qVar -> atomically $
secureQueue :: QueueStore -> RecipientId -> SndPublicAuthKey -> STM (Either ErrorType QueueRec)
secureQueue QueueStore {queues} rId sKey =
withQueue rId queues $ \qVar ->
readTVar qVar >>= \q -> case senderKey q of
Just k -> pure $ if sKey == k then Just q else Nothing
_ ->
let !q' = q {senderKey = Just sKey}
in writeTVar qVar q' $> Just q'
addQueueNotifier :: QueueStore -> RecipientId -> NtfCreds -> IO (Either ErrorType (Maybe NotifierId))
addQueueNotifier :: QueueStore -> RecipientId -> NtfCreds -> STM (Either ErrorType QueueRec)
addQueueNotifier QueueStore {queues, notifiers} rId ntfCreds@NtfCreds {notifierId = nId} = do
TM.lookupIO rId queues >>= \case
Just qVar -> atomically $ ifM (TM.member nId notifiers) (pure $ Left DUPLICATE_) $ do
ifM (TM.member nId notifiers) (pure $ Left DUPLICATE_) $
withQueue rId queues $ \qVar -> do
q <- readTVar qVar
nId_ <- forM (notifier q) $ \NtfCreds {notifierId} -> TM.delete notifierId notifiers $> notifierId
let !q' = q {notifier = Just ntfCreds}
writeTVar qVar q'
forM_ (notifier q) $ (`TM.delete` notifiers) . notifierId
writeTVar qVar $! q {notifier = Just ntfCreds}
TM.insert nId rId notifiers
pure $ Right nId_
Nothing -> pure $ Left AUTH
pure $ Just q
deleteQueueNotifier :: QueueStore -> RecipientId -> IO (Either ErrorType (Maybe NotifierId))
deleteQueueNotifier :: QueueStore -> RecipientId -> STM (Either ErrorType ())
deleteQueueNotifier QueueStore {queues, notifiers} rId =
withQueue rId queues $ \qVar -> do
q <- readTVar qVar
forM (notifier q) $ \NtfCreds {notifierId} -> do
TM.delete notifierId notifiers
writeTVar qVar $! q {notifier = Nothing}
pure notifierId
forM_ (notifier q) $ \NtfCreds {notifierId} -> TM.delete notifierId notifiers
writeTVar qVar $! q {notifier = Nothing}
pure $ Just ()
suspendQueue :: QueueStore -> RecipientId -> IO (Either ErrorType ())
suspendQueue :: QueueStore -> RecipientId -> STM (Either ErrorType ())
suspendQueue QueueStore {queues} rId =
withQueue rId queues (`modifyTVar'` \q -> q {status = QueueOff})
withQueue rId queues $ \qVar -> modifyTVar' qVar (\q -> q {status = QueueOff}) $> Just ()
updateQueueTime :: QueueStore -> RecipientId -> RoundedSystemTime -> IO ()
updateQueueTime QueueStore {queues} rId t =
void $ withQueue rId queues (`modifyTVar'` \q -> q {updatedAt = Just t})
deleteQueue :: QueueStore -> RecipientId -> IO (Either ErrorType QueueRec)
deleteQueue QueueStore {queues, senders, notifiers} rId = atomically $ do
deleteQueue :: QueueStore -> RecipientId -> STM (Either ErrorType QueueRec)
deleteQueue QueueStore {queues, senders, notifiers} rId = do
TM.lookupDelete rId queues >>= \case
Just qVar ->
readTVar qVar >>= \q -> do
@@ -116,5 +108,5 @@ deleteQueue QueueStore {queues, senders, notifiers} rId = atomically $ do
toResult :: Maybe a -> Either ErrorType a
toResult = maybe (Left AUTH) Right
withQueue :: RecipientId -> TMap RecipientId (TVar QueueRec) -> (TVar QueueRec -> STM a) -> IO (Either ErrorType a)
withQueue rId queues f = toResult <$> TM.lookupIO rId queues >>= atomically . mapM f
withQueue :: RecipientId -> TMap RecipientId (TVar QueueRec) -> (TVar QueueRec -> STM (Maybe a)) -> STM (Either ErrorType a)
withQueue rId queues f = toResult <$> TM.lookup rId queues $>>= f
+174 -393
View File
@@ -4,81 +4,52 @@
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
module Simplex.Messaging.Server.Stats where
import Control.Applicative (optional, (<|>))
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Hashable (hash)
import Data.IORef
import Data.IntSet (IntSet)
import qualified Data.IntSet as IS
import Data.Set (Set)
import qualified Data.Set as S
import Data.Time.Calendar.Month (pattern MonthDay)
import Data.Time.Calendar.OrdinalDate (mondayStartWeek)
import Data.Time.Clock (UTCTime (..))
import GHC.IORef (atomicSwapIORef)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (EntityId (..))
import Simplex.Messaging.Util (atomicModifyIORef'_, unlessM)
import Simplex.Messaging.Protocol (RecipientId)
import UnliftIO.STM
data ServerStats = ServerStats
{ fromTime :: IORef UTCTime,
qCreated :: IORef Int,
qSecured :: IORef Int,
qDeletedAll :: IORef Int,
qDeletedAllB :: IORef Int,
qDeletedNew :: IORef Int,
qDeletedSecured :: IORef Int,
qSub :: IORef Int, -- only includes subscriptions when there were pending messages
-- qSubNoMsg :: IORef Int, -- this stat creates too many STM transactions
qSubAllB :: IORef Int, -- count of all subscription batches (with and without pending messages)
qSubAuth :: IORef Int,
qSubDuplicate :: IORef Int,
qSubProhibited :: IORef Int,
qSubEnd :: IORef Int,
qSubEndB :: IORef Int,
ntfCreated :: IORef Int,
ntfDeleted :: IORef Int,
ntfDeletedB :: IORef Int,
ntfSub :: IORef Int,
ntfSubB :: IORef Int,
ntfSubAuth :: IORef Int,
ntfSubDuplicate :: IORef Int,
msgSent :: IORef Int,
msgSentAuth :: IORef Int,
msgSentQuota :: IORef Int,
msgSentLarge :: IORef Int,
msgRecv :: IORef Int,
msgRecvGet :: IORef Int,
msgGet :: IORef Int,
msgGetNoMsg :: IORef Int,
msgGetAuth :: IORef Int,
msgGetDuplicate :: IORef Int,
msgGetProhibited :: IORef Int,
msgExpired :: IORef Int,
activeQueues :: PeriodStats,
-- subscribedQueues :: PeriodStats, -- this stat uses too much memory
msgSentNtf :: IORef Int, -- sent messages with NTF flag
msgRecvNtf :: IORef Int, -- received messages with NTF flag
activeQueuesNtf :: PeriodStats,
msgNtfs :: IORef Int, -- messages notications delivered to NTF server (<= msgSentNtf)
msgNtfsB :: IORef Int, -- messages notication batches delivered to NTF server
msgNtfNoSub :: IORef Int, -- no subscriber to notifications (e.g., NTF server not connected)
msgNtfLost :: IORef Int, -- notification is lost because NTF delivery queue is full
msgNtfExpired :: IORef Int, -- expired
{ fromTime :: TVar UTCTime,
qCreated :: TVar Int,
qSecured :: TVar Int,
qDeletedAll :: TVar Int,
qDeletedNew :: TVar Int,
qDeletedSecured :: TVar Int,
qSub :: TVar Int,
qSubAuth :: TVar Int,
qSubDuplicate :: TVar Int,
qSubProhibited :: TVar Int,
msgSent :: TVar Int,
msgSentAuth :: TVar Int,
msgSentQuota :: TVar Int,
msgSentLarge :: TVar Int,
msgRecv :: TVar Int,
msgExpired :: TVar Int,
activeQueues :: PeriodStats RecipientId,
msgSentNtf :: TVar Int, -- sent messages with NTF flag
msgRecvNtf :: TVar Int, -- received messages with NTF flag
activeQueuesNtf :: PeriodStats RecipientId,
msgNtfs :: TVar Int, -- messages notications delivered to NTF server (<= msgSentNtf)
msgNtfNoSub :: TVar Int, -- no subscriber to notifications (e.g., NTF server not connected)
msgNtfLost :: TVar Int, -- notification is lost because NTF delivery queue is full
pRelays :: ProxyStats,
pRelaysOwn :: ProxyStats,
pMsgFwds :: ProxyStats,
pMsgFwdsOwn :: ProxyStats,
pMsgFwdsRecv :: IORef Int,
qCount :: IORef Int,
msgCount :: IORef Int,
ntfCount :: IORef Int
pMsgFwdsRecv :: TVar Int,
qCount :: TVar Int,
msgCount :: TVar Int
}
data ServerStatsData = ServerStatsData
@@ -86,320 +57,199 @@ data ServerStatsData = ServerStatsData
_qCreated :: Int,
_qSecured :: Int,
_qDeletedAll :: Int,
_qDeletedAllB :: Int,
_qDeletedNew :: Int,
_qDeletedSecured :: Int,
_qSub :: Int,
_qSubAllB :: Int,
_qSubAuth :: Int,
_qSubDuplicate :: Int,
_qSubProhibited :: Int,
_qSubEnd :: Int,
_qSubEndB :: Int,
_ntfCreated :: Int,
_ntfDeleted :: Int,
_ntfDeletedB :: Int,
_ntfSub :: Int,
_ntfSubB :: Int,
_ntfSubAuth :: Int,
_ntfSubDuplicate :: Int,
_msgSent :: Int,
_msgSentAuth :: Int,
_msgSentQuota :: Int,
_msgSentLarge :: Int,
_msgRecv :: Int,
_msgRecvGet :: Int,
_msgGet :: Int,
_msgGetNoMsg :: Int,
_msgGetAuth :: Int,
_msgGetDuplicate :: Int,
_msgGetProhibited :: Int,
_msgExpired :: Int,
_activeQueues :: PeriodStatsData,
_activeQueues :: PeriodStatsData RecipientId,
_msgSentNtf :: Int,
_msgRecvNtf :: Int,
_activeQueuesNtf :: PeriodStatsData,
_activeQueuesNtf :: PeriodStatsData RecipientId,
_msgNtfs :: Int,
_msgNtfsB :: Int,
_msgNtfNoSub :: Int,
_msgNtfLost :: Int,
_msgNtfExpired :: Int,
_pRelays :: ProxyStatsData,
_pRelaysOwn :: ProxyStatsData,
_pMsgFwds :: ProxyStatsData,
_pMsgFwdsOwn :: ProxyStatsData,
_pMsgFwdsRecv :: Int,
_qCount :: Int,
_msgCount :: Int,
_ntfCount :: Int
_msgCount :: Int
}
deriving (Show)
newServerStats :: UTCTime -> IO ServerStats
newServerStats :: UTCTime -> STM ServerStats
newServerStats ts = do
fromTime <- newIORef ts
qCreated <- newIORef 0
qSecured <- newIORef 0
qDeletedAll <- newIORef 0
qDeletedAllB <- newIORef 0
qDeletedNew <- newIORef 0
qDeletedSecured <- newIORef 0
qSub <- newIORef 0
qSubAllB <- newIORef 0
qSubAuth <- newIORef 0
qSubDuplicate <- newIORef 0
qSubProhibited <- newIORef 0
qSubEnd <- newIORef 0
qSubEndB <- newIORef 0
ntfCreated <- newIORef 0
ntfDeleted <- newIORef 0
ntfDeletedB <- newIORef 0
ntfSub <- newIORef 0
ntfSubB <- newIORef 0
ntfSubAuth <- newIORef 0
ntfSubDuplicate <- newIORef 0
msgSent <- newIORef 0
msgSentAuth <- newIORef 0
msgSentQuota <- newIORef 0
msgSentLarge <- newIORef 0
msgRecv <- newIORef 0
msgRecvGet <- newIORef 0
msgGet <- newIORef 0
msgGetNoMsg <- newIORef 0
msgGetAuth <- newIORef 0
msgGetDuplicate <- newIORef 0
msgGetProhibited <- newIORef 0
msgExpired <- newIORef 0
fromTime <- newTVar ts
qCreated <- newTVar 0
qSecured <- newTVar 0
qDeletedAll <- newTVar 0
qDeletedNew <- newTVar 0
qDeletedSecured <- newTVar 0
qSub <- newTVar 0
qSubAuth <- newTVar 0
qSubDuplicate <- newTVar 0
qSubProhibited <- newTVar 0
msgSent <- newTVar 0
msgSentAuth <- newTVar 0
msgSentQuota <- newTVar 0
msgSentLarge <- newTVar 0
msgRecv <- newTVar 0
msgExpired <- newTVar 0
activeQueues <- newPeriodStats
msgSentNtf <- newIORef 0
msgRecvNtf <- newIORef 0
msgSentNtf <- newTVar 0
msgRecvNtf <- newTVar 0
activeQueuesNtf <- newPeriodStats
msgNtfs <- newIORef 0
msgNtfsB <- newIORef 0
msgNtfNoSub <- newIORef 0
msgNtfLost <- newIORef 0
msgNtfExpired <- newIORef 0
msgNtfs <- newTVar 0
msgNtfNoSub <- newTVar 0
msgNtfLost <- newTVar 0
pRelays <- newProxyStats
pRelaysOwn <- newProxyStats
pMsgFwds <- newProxyStats
pMsgFwdsOwn <- newProxyStats
pMsgFwdsRecv <- newIORef 0
qCount <- newIORef 0
msgCount <- newIORef 0
ntfCount <- newIORef 0
pMsgFwdsRecv <- newTVar 0
qCount <- newTVar 0
msgCount <- newTVar 0
pure
ServerStats
{ fromTime,
qCreated,
qSecured,
qDeletedAll,
qDeletedAllB,
qDeletedNew,
qDeletedSecured,
qSub,
qSubAllB,
qSubAuth,
qSubDuplicate,
qSubProhibited,
qSubEnd,
qSubEndB,
ntfCreated,
ntfDeleted,
ntfDeletedB,
ntfSub,
ntfSubB,
ntfSubAuth,
ntfSubDuplicate,
msgSent,
msgSentAuth,
msgSentQuota,
msgSentLarge,
msgRecv,
msgRecvGet,
msgGet,
msgGetNoMsg,
msgGetAuth,
msgGetDuplicate,
msgGetProhibited,
msgExpired,
activeQueues,
msgSentNtf,
msgRecvNtf,
activeQueuesNtf,
msgNtfs,
msgNtfsB,
msgNtfNoSub,
msgNtfLost,
msgNtfExpired,
pRelays,
pRelaysOwn,
pMsgFwds,
pMsgFwdsOwn,
pMsgFwdsRecv,
qCount,
msgCount,
ntfCount
msgCount
}
getServerStatsData :: ServerStats -> IO ServerStatsData
getServerStatsData :: ServerStats -> STM ServerStatsData
getServerStatsData s = do
_fromTime <- readIORef $ fromTime s
_qCreated <- readIORef $ qCreated s
_qSecured <- readIORef $ qSecured s
_qDeletedAll <- readIORef $ qDeletedAll s
_qDeletedAllB <- readIORef $ qDeletedAllB s
_qDeletedNew <- readIORef $ qDeletedNew s
_qDeletedSecured <- readIORef $ qDeletedSecured s
_qSub <- readIORef $ qSub s
_qSubAllB <- readIORef $ qSubAllB s
_qSubAuth <- readIORef $ qSubAuth s
_qSubDuplicate <- readIORef $ qSubDuplicate s
_qSubProhibited <- readIORef $ qSubProhibited s
_qSubEnd <- readIORef $ qSubEnd s
_qSubEndB <- readIORef $ qSubEndB s
_ntfCreated <- readIORef $ ntfCreated s
_ntfDeleted <- readIORef $ ntfDeleted s
_ntfDeletedB <- readIORef $ ntfDeletedB s
_ntfSub <- readIORef $ ntfSub s
_ntfSubB <- readIORef $ ntfSubB s
_ntfSubAuth <- readIORef $ ntfSubAuth s
_ntfSubDuplicate <- readIORef $ ntfSubDuplicate s
_msgSent <- readIORef $ msgSent s
_msgSentAuth <- readIORef $ msgSentAuth s
_msgSentQuota <- readIORef $ msgSentQuota s
_msgSentLarge <- readIORef $ msgSentLarge s
_msgRecv <- readIORef $ msgRecv s
_msgRecvGet <- readIORef $ msgRecvGet s
_msgGet <- readIORef $ msgGet s
_msgGetNoMsg <- readIORef $ msgGetNoMsg s
_msgGetAuth <- readIORef $ msgGetAuth s
_msgGetDuplicate <- readIORef $ msgGetDuplicate s
_msgGetProhibited <- readIORef $ msgGetProhibited s
_msgExpired <- readIORef $ msgExpired s
_fromTime <- readTVar $ fromTime s
_qCreated <- readTVar $ qCreated s
_qSecured <- readTVar $ qSecured s
_qDeletedAll <- readTVar $ qDeletedAll s
_qDeletedNew <- readTVar $ qDeletedNew s
_qDeletedSecured <- readTVar $ qDeletedSecured s
_qSub <- readTVar $ qSub s
_qSubAuth <- readTVar $ qSubAuth s
_qSubDuplicate <- readTVar $ qSubDuplicate s
_qSubProhibited <- readTVar $ qSubProhibited s
_msgSent <- readTVar $ msgSent s
_msgSentAuth <- readTVar $ msgSentAuth s
_msgSentQuota <- readTVar $ msgSentQuota s
_msgSentLarge <- readTVar $ msgSentLarge s
_msgRecv <- readTVar $ msgRecv s
_msgExpired <- readTVar $ msgExpired s
_activeQueues <- getPeriodStatsData $ activeQueues s
_msgSentNtf <- readIORef $ msgSentNtf s
_msgRecvNtf <- readIORef $ msgRecvNtf s
_msgSentNtf <- readTVar $ msgSentNtf s
_msgRecvNtf <- readTVar $ msgRecvNtf s
_activeQueuesNtf <- getPeriodStatsData $ activeQueuesNtf s
_msgNtfs <- readIORef $ msgNtfs s
_msgNtfsB <- readIORef $ msgNtfsB s
_msgNtfNoSub <- readIORef $ msgNtfNoSub s
_msgNtfLost <- readIORef $ msgNtfLost s
_msgNtfExpired <- readIORef $ msgNtfExpired s
_msgNtfs <- readTVar $ msgNtfs s
_msgNtfNoSub <- readTVar $ msgNtfNoSub s
_msgNtfLost <- readTVar $ msgNtfLost s
_pRelays <- getProxyStatsData $ pRelays s
_pRelaysOwn <- getProxyStatsData $ pRelaysOwn s
_pMsgFwds <- getProxyStatsData $ pMsgFwds s
_pMsgFwdsOwn <- getProxyStatsData $ pMsgFwdsOwn s
_pMsgFwdsRecv <- readIORef $ pMsgFwdsRecv s
_qCount <- readIORef $ qCount s
_msgCount <- readIORef $ msgCount s
_ntfCount <- readIORef $ ntfCount s
_pMsgFwdsRecv <- readTVar $ pMsgFwdsRecv s
_qCount <- readTVar $ qCount s
_msgCount <- readTVar $ msgCount s
pure
ServerStatsData
{ _fromTime,
_qCreated,
_qSecured,
_qDeletedAll,
_qDeletedAllB,
_qDeletedNew,
_qDeletedSecured,
_qSub,
_qSubAllB,
_qSubAuth,
_qSubDuplicate,
_qSubProhibited,
_qSubEnd,
_qSubEndB,
_ntfCreated,
_ntfDeleted,
_ntfDeletedB,
_ntfSub,
_ntfSubB,
_ntfSubAuth,
_ntfSubDuplicate,
_msgSent,
_msgSentAuth,
_msgSentQuota,
_msgSentLarge,
_msgRecv,
_msgRecvGet,
_msgGet,
_msgGetNoMsg,
_msgGetAuth,
_msgGetDuplicate,
_msgGetProhibited,
_msgExpired,
_activeQueues,
_msgSentNtf,
_msgRecvNtf,
_activeQueuesNtf,
_msgNtfs,
_msgNtfsB,
_msgNtfNoSub,
_msgNtfLost,
_msgNtfExpired,
_pRelays,
_pRelaysOwn,
_pMsgFwds,
_pMsgFwdsOwn,
_pMsgFwdsRecv,
_qCount,
_msgCount,
_ntfCount
_msgCount
}
-- this function is not thread safe, it is used on server start only
setServerStats :: ServerStats -> ServerStatsData -> IO ()
setServerStats :: ServerStats -> ServerStatsData -> STM ()
setServerStats s d = do
writeIORef (fromTime s) $! _fromTime d
writeIORef (qCreated s) $! _qCreated d
writeIORef (qSecured s) $! _qSecured d
writeIORef (qDeletedAll s) $! _qDeletedAll d
writeIORef (qDeletedAllB s) $! _qDeletedAllB d
writeIORef (qDeletedNew s) $! _qDeletedNew d
writeIORef (qDeletedSecured s) $! _qDeletedSecured d
writeIORef (qSub s) $! _qSub d
writeIORef (qSubAllB s) $! _qSubAllB d
writeIORef (qSubAuth s) $! _qSubAuth d
writeIORef (qSubDuplicate s) $! _qSubDuplicate d
writeIORef (qSubProhibited s) $! _qSubProhibited d
writeIORef (qSubEnd s) $! _qSubEnd d
writeIORef (qSubEndB s) $! _qSubEndB d
writeIORef (ntfCreated s) $! _ntfCreated d
writeIORef (ntfDeleted s) $! _ntfDeleted d
writeIORef (ntfDeletedB s) $! _ntfDeletedB d
writeIORef (ntfSub s) $! _ntfSub d
writeIORef (ntfSubB s) $! _ntfSubB d
writeIORef (ntfSubAuth s) $! _ntfSubAuth d
writeIORef (ntfSubDuplicate s) $! _ntfSubDuplicate d
writeIORef (msgSent s) $! _msgSent d
writeIORef (msgSentAuth s) $! _msgSentAuth d
writeIORef (msgSentQuota s) $! _msgSentQuota d
writeIORef (msgSentLarge s) $! _msgSentLarge d
writeIORef (msgRecv s) $! _msgRecv d
writeIORef (msgRecvGet s) $! _msgRecvGet d
writeIORef (msgGet s) $! _msgGet d
writeIORef (msgGetNoMsg s) $! _msgGetNoMsg d
writeIORef (msgGetAuth s) $! _msgGetAuth d
writeIORef (msgGetDuplicate s) $! _msgGetDuplicate d
writeIORef (msgGetProhibited s) $! _msgGetProhibited d
writeIORef (msgExpired s) $! _msgExpired d
writeTVar (fromTime s) $! _fromTime d
writeTVar (qCreated s) $! _qCreated d
writeTVar (qSecured s) $! _qSecured d
writeTVar (qDeletedAll s) $! _qDeletedAll d
writeTVar (qDeletedNew s) $! _qDeletedNew d
writeTVar (qDeletedSecured s) $! _qDeletedSecured d
writeTVar (qSub s) $! _qSub d
writeTVar (qSubAuth s) $! _qSubAuth d
writeTVar (qSubDuplicate s) $! _qSubDuplicate d
writeTVar (qSubProhibited s) $! _qSubProhibited d
writeTVar (msgSent s) $! _msgSent d
writeTVar (msgSentAuth s) $! _msgSentAuth d
writeTVar (msgSentQuota s) $! _msgSentQuota d
writeTVar (msgSentLarge s) $! _msgSentLarge d
writeTVar (msgRecv s) $! _msgRecv d
writeTVar (msgExpired s) $! _msgExpired d
setPeriodStats (activeQueues s) (_activeQueues d)
writeIORef (msgSentNtf s) $! _msgSentNtf d
writeIORef (msgRecvNtf s) $! _msgRecvNtf d
writeTVar (msgSentNtf s) $! _msgSentNtf d
writeTVar (msgRecvNtf s) $! _msgRecvNtf d
setPeriodStats (activeQueuesNtf s) (_activeQueuesNtf d)
writeIORef (msgNtfs s) $! _msgNtfs d
writeIORef (msgNtfsB s) $! _msgNtfsB d
writeIORef (msgNtfNoSub s) $! _msgNtfNoSub d
writeIORef (msgNtfLost s) $! _msgNtfLost d
writeIORef (msgNtfExpired s) $! _msgNtfExpired d
writeTVar (msgNtfs s) $! _msgNtfs d
writeTVar (msgNtfNoSub s) $! _msgNtfNoSub d
writeTVar (msgNtfLost s) $! _msgNtfLost d
setProxyStats (pRelays s) $! _pRelays d
setProxyStats (pRelaysOwn s) $! _pRelaysOwn d
setProxyStats (pMsgFwds s) $! _pMsgFwds d
setProxyStats (pMsgFwdsOwn s) $! _pMsgFwdsOwn d
writeIORef (pMsgFwdsRecv s) $! _pMsgFwdsRecv d
writeIORef (qCount s) $! _qCount d
writeIORef (msgCount s) $! _msgCount d
writeIORef (ntfCount s) $! _ntfCount d
writeTVar (pMsgFwdsRecv s) $! _pMsgFwdsRecv d
writeTVar (qCount s) $! _qCount d
writeTVar (msgCount s) $! _msgCount d
instance StrEncoding ServerStatsData where
strEncode d =
@@ -410,41 +260,22 @@ instance StrEncoding ServerStatsData where
"qDeletedAll=" <> strEncode (_qDeletedAll d),
"qDeletedNew=" <> strEncode (_qDeletedNew d),
"qDeletedSecured=" <> strEncode (_qDeletedSecured d),
"qDeletedAllB=" <> strEncode (_qDeletedAllB d),
"qCount=" <> strEncode (_qCount d),
"qSub=" <> strEncode (_qSub d),
"qSubAllB=" <> strEncode (_qSubAllB d),
"qSubAuth=" <> strEncode (_qSubAuth d),
"qSubDuplicate=" <> strEncode (_qSubDuplicate d),
"qSubProhibited=" <> strEncode (_qSubProhibited d),
"qSubEnd=" <> strEncode (_qSubEnd d),
"qSubEndB=" <> strEncode (_qSubEndB d),
"ntfCreated=" <> strEncode (_ntfCreated d),
"ntfDeleted=" <> strEncode (_ntfDeleted d),
"ntfDeletedB=" <> strEncode (_ntfDeletedB d),
"ntfSub=" <> strEncode (_ntfSub d),
"ntfSubB=" <> strEncode (_ntfSubB d),
"ntfSubAuth=" <> strEncode (_ntfSubAuth d),
"ntfSubDuplicate=" <> strEncode (_ntfSubDuplicate d),
"msgSent=" <> strEncode (_msgSent d),
"msgSentAuth=" <> strEncode (_msgSentAuth d),
"msgSentQuota=" <> strEncode (_msgSentQuota d),
"msgSentLarge=" <> strEncode (_msgSentLarge d),
"msgRecv=" <> strEncode (_msgRecv d),
"msgRecvGet=" <> strEncode (_msgRecvGet d),
"msgGet=" <> strEncode (_msgGet d),
"msgGetNoMsg=" <> strEncode (_msgGetNoMsg d),
"msgGetAuth=" <> strEncode (_msgGetAuth d),
"msgGetDuplicate=" <> strEncode (_msgGetDuplicate d),
"msgGetProhibited=" <> strEncode (_msgGetProhibited d),
"msgExpired=" <> strEncode (_msgExpired d),
"msgSentNtf=" <> strEncode (_msgSentNtf d),
"msgRecvNtf=" <> strEncode (_msgRecvNtf d),
"msgNtfs=" <> strEncode (_msgNtfs d),
"msgNtfsB=" <> strEncode (_msgNtfsB d),
"msgNtfNoSub=" <> strEncode (_msgNtfNoSub d),
"msgNtfLost=" <> strEncode (_msgNtfLost d),
"msgNtfExpired=" <> strEncode (_msgNtfExpired d),
"activeQueues:",
strEncode (_activeQueues d),
"activeQueuesNtf:",
@@ -466,42 +297,22 @@ instance StrEncoding ServerStatsData where
(_qDeletedAll, _qDeletedNew, _qDeletedSecured) <-
(,0,0) <$> ("qDeleted=" *> strP <* A.endOfLine)
<|> ((,,) <$> ("qDeletedAll=" *> strP <* A.endOfLine) <*> ("qDeletedNew=" *> strP <* A.endOfLine) <*> ("qDeletedSecured=" *> strP <* A.endOfLine))
_qDeletedAllB <- opt "qDeletedAllB="
_qCount <- opt "qCount="
_qSub <- opt "qSub="
_qSubNoMsg <- skipInt "qSubNoMsg=" -- skipping it for backward compatibility
_qSubAllB <- opt "qSubAllB="
_qSubAuth <- opt "qSubAuth="
_qSubDuplicate <- opt "qSubDuplicate="
_qSubProhibited <- opt "qSubProhibited="
_qSubEnd <- opt "qSubEnd="
_qSubEndB <- opt "qSubEndB="
_ntfCreated <- opt "ntfCreated="
_ntfDeleted <- opt "ntfDeleted="
_ntfDeletedB <- opt "ntfDeletedB="
_ntfSub <- opt "ntfSub="
_ntfSubB <- opt "ntfSubB="
_ntfSubAuth <- opt "ntfSubAuth="
_ntfSubDuplicate <- opt "ntfSubDuplicate="
_msgSent <- "msgSent=" *> strP <* A.endOfLine
_msgSentAuth <- opt "msgSentAuth="
_msgSentQuota <- opt "msgSentQuota="
_msgSentLarge <- opt "msgSentLarge="
_msgRecv <- "msgRecv=" *> strP <* A.endOfLine
_msgRecvGet <- opt "msgRecvGet="
_msgGet <- opt "msgGet="
_msgGetNoMsg <- opt "msgGetNoMsg="
_msgGetAuth <- opt "msgGetAuth="
_msgGetDuplicate <- opt "msgGetDuplicate="
_msgGetProhibited <- opt "msgGetProhibited="
_msgExpired <- opt "msgExpired="
_msgSentNtf <- opt "msgSentNtf="
_msgRecvNtf <- opt "msgRecvNtf="
_msgNtfs <- opt "msgNtfs="
_msgNtfsB <- opt "msgNtfsB="
_msgNtfNoSub <- opt "msgNtfNoSub="
_msgNtfLost <- opt "msgNtfLost="
_msgNtfExpired <- opt "msgNtfExpired="
_activeQueues <-
optional ("activeQueues:" <* A.endOfLine) >>= \case
Just _ -> strP <* optional A.endOfLine
@@ -510,10 +321,6 @@ instance StrEncoding ServerStatsData where
_week <- "weekMsgQueues=" *> strP <* A.endOfLine
_month <- "monthMsgQueues=" *> strP <* optional A.endOfLine
pure PeriodStatsData {_day, _week, _month}
_subscribedQueues <-
optional ("subscribedQueues:" <* A.endOfLine) >>= \case
Just _ -> newPeriodStatsData <$ (strP @PeriodStatsData <* optional A.endOfLine)
_ -> pure newPeriodStatsData
_activeQueuesNtf <-
optional ("activeQueuesNtf:" <* A.endOfLine) >>= \case
Just _ -> strP <* optional A.endOfLine
@@ -529,42 +336,23 @@ instance StrEncoding ServerStatsData where
_qCreated,
_qSecured,
_qDeletedAll,
_qDeletedAllB,
_qDeletedNew,
_qDeletedSecured,
_qSub,
_qSubAllB,
_qSubAuth,
_qSubDuplicate,
_qSubProhibited,
_qSubEnd,
_qSubEndB,
_ntfCreated,
_ntfDeleted,
_ntfDeletedB,
_ntfSub,
_ntfSubB,
_ntfSubAuth,
_ntfSubDuplicate,
_msgSent,
_msgSentAuth,
_msgSentQuota,
_msgSentLarge,
_msgRecv,
_msgRecvGet,
_msgGet,
_msgGetNoMsg,
_msgGetAuth,
_msgGetDuplicate,
_msgGetProhibited,
_msgExpired,
_msgSentNtf,
_msgRecvNtf,
_msgNtfs,
_msgNtfsB,
_msgNtfNoSub,
_msgNtfLost,
_msgNtfExpired,
_activeQueues,
_activeQueuesNtf,
_pRelays,
@@ -573,64 +361,59 @@ instance StrEncoding ServerStatsData where
_pMsgFwdsOwn,
_pMsgFwdsRecv,
_qCount,
_msgCount = 0,
_ntfCount = 0
_msgCount = 0
}
where
opt s = A.string s *> strP <* A.endOfLine <|> pure 0
skipInt s = (0 :: Int) <$ optional (A.string s *> strP @Int *> A.endOfLine)
proxyStatsP key =
optional (A.string key >> A.endOfLine) >>= \case
Just _ -> strP <* optional A.endOfLine
_ -> pure newProxyStatsData
data PeriodStats = PeriodStats
{ day :: IORef IntSet,
week :: IORef IntSet,
month :: IORef IntSet
data PeriodStats a = PeriodStats
{ day :: TVar (Set a),
week :: TVar (Set a),
month :: TVar (Set a)
}
newPeriodStats :: IO PeriodStats
newPeriodStats :: STM (PeriodStats a)
newPeriodStats = do
day <- newIORef IS.empty
week <- newIORef IS.empty
month <- newIORef IS.empty
day <- newTVar S.empty
week <- newTVar S.empty
month <- newTVar S.empty
pure PeriodStats {day, week, month}
data PeriodStatsData = PeriodStatsData
{ _day :: IntSet,
_week :: IntSet,
_month :: IntSet
data PeriodStatsData a = PeriodStatsData
{ _day :: Set a,
_week :: Set a,
_month :: Set a
}
deriving (Show)
newPeriodStatsData :: PeriodStatsData
newPeriodStatsData = PeriodStatsData {_day = IS.empty, _week = IS.empty, _month = IS.empty}
newPeriodStatsData :: PeriodStatsData a
newPeriodStatsData = PeriodStatsData {_day = S.empty, _week = S.empty, _month = S.empty}
getPeriodStatsData :: PeriodStats -> IO PeriodStatsData
getPeriodStatsData :: PeriodStats a -> STM (PeriodStatsData a)
getPeriodStatsData s = do
_day <- readIORef $ day s
_week <- readIORef $ week s
_month <- readIORef $ month s
_day <- readTVar $ day s
_week <- readTVar $ week s
_month <- readTVar $ month s
pure PeriodStatsData {_day, _week, _month}
-- this function is not thread safe, it is used on server start only
setPeriodStats :: PeriodStats -> PeriodStatsData -> IO ()
setPeriodStats :: PeriodStats a -> PeriodStatsData a -> STM ()
setPeriodStats s d = do
writeIORef (day s) $! _day d
writeIORef (week s) $! _week d
writeIORef (month s) $! _month d
writeTVar (day s) $! _day d
writeTVar (week s) $! _week d
writeTVar (month s) $! _month d
instance StrEncoding PeriodStatsData where
instance (Ord a, StrEncoding a) => StrEncoding (PeriodStatsData a) where
strEncode PeriodStatsData {_day, _week, _month} =
"dayHashes=" <> strEncode _day <> "\nweekHashes=" <> strEncode _week <> "\nmonthHashes=" <> strEncode _month
"day=" <> strEncode _day <> "\nweek=" <> strEncode _week <> "\nmonth=" <> strEncode _month
strP = do
_day <- ("day=" *> bsSetP <|> "dayHashes=" *> strP) <* A.endOfLine
_week <- ("week=" *> bsSetP <|> "weekHashes=" *> strP) <* A.endOfLine
_month <- "month=" *> bsSetP <|> "monthHashes=" *> strP
_day <- "day=" *> strP <* A.endOfLine
_week <- "week=" *> strP <* A.endOfLine
_month <- "month=" *> strP
pure PeriodStatsData {_day, _week, _month}
where
bsSetP = S.foldl' (\s -> (`IS.insert` s) . hash) IS.empty <$> strP @(Set ByteString)
data PeriodStatCounts = PeriodStatCounts
{ dayCount :: String,
@@ -638,7 +421,7 @@ data PeriodStatCounts = PeriodStatCounts
monthCount :: String
}
periodStatCounts :: PeriodStats -> UTCTime -> IO PeriodStatCounts
periodStatCounts :: forall a. PeriodStats a -> UTCTime -> STM PeriodStatCounts
periodStatCounts ps ts = do
let d = utctDay ts
(_, wDay) = mondayStartWeek d
@@ -648,34 +431,33 @@ periodStatCounts ps ts = do
monthCount <- periodCount mDay $ month ps
pure PeriodStatCounts {dayCount, weekCount, monthCount}
where
periodCount :: Int -> IORef IntSet -> IO String
periodCount 1 ref = show . IS.size <$> atomicSwapIORef ref IS.empty
periodCount :: Int -> TVar (Set a) -> STM String
periodCount 1 pVar = show . S.size <$> swapTVar pVar S.empty
periodCount _ _ = pure ""
updatePeriodStats :: PeriodStats -> EntityId -> IO ()
updatePeriodStats ps (EntityId pId) = do
updatePeriod $ day ps
updatePeriod $ week ps
updatePeriod $ month ps
updatePeriodStats :: Ord a => PeriodStats a -> a -> STM ()
updatePeriodStats stats pId = do
updatePeriod day
updatePeriod week
updatePeriod month
where
ph = hash pId
updatePeriod ref = unlessM (IS.member ph <$> readIORef ref) $ atomicModifyIORef'_ ref $ IS.insert ph
updatePeriod pSel = modifyTVar' (pSel stats) (S.insert pId)
data ProxyStats = ProxyStats
{ pRequests :: IORef Int,
pSuccesses :: IORef Int, -- includes destination server error responses that will be forwarded to the client
pErrorsConnect :: IORef Int,
pErrorsCompat :: IORef Int,
pErrorsOther :: IORef Int
{ pRequests :: TVar Int,
pSuccesses :: TVar Int, -- includes destination server error responses that will be forwarded to the client
pErrorsConnect :: TVar Int,
pErrorsCompat :: TVar Int,
pErrorsOther :: TVar Int
}
newProxyStats :: IO ProxyStats
newProxyStats :: STM ProxyStats
newProxyStats = do
pRequests <- newIORef 0
pSuccesses <- newIORef 0
pErrorsConnect <- newIORef 0
pErrorsCompat <- newIORef 0
pErrorsOther <- newIORef 0
pRequests <- newTVar 0
pSuccesses <- newTVar 0
pErrorsConnect <- newTVar 0
pErrorsCompat <- newTVar 0
pErrorsOther <- newTVar 0
pure ProxyStats {pRequests, pSuccesses, pErrorsConnect, pErrorsCompat, pErrorsOther}
data ProxyStatsData = ProxyStatsData
@@ -690,32 +472,31 @@ data ProxyStatsData = ProxyStatsData
newProxyStatsData :: ProxyStatsData
newProxyStatsData = ProxyStatsData {_pRequests = 0, _pSuccesses = 0, _pErrorsConnect = 0, _pErrorsCompat = 0, _pErrorsOther = 0}
getProxyStatsData :: ProxyStats -> IO ProxyStatsData
getProxyStatsData :: ProxyStats -> STM ProxyStatsData
getProxyStatsData s = do
_pRequests <- readIORef $ pRequests s
_pSuccesses <- readIORef $ pSuccesses s
_pErrorsConnect <- readIORef $ pErrorsConnect s
_pErrorsCompat <- readIORef $ pErrorsCompat s
_pErrorsOther <- readIORef $ pErrorsOther s
_pRequests <- readTVar $ pRequests s
_pSuccesses <- readTVar $ pSuccesses s
_pErrorsConnect <- readTVar $ pErrorsConnect s
_pErrorsCompat <- readTVar $ pErrorsCompat s
_pErrorsOther <- readTVar $ pErrorsOther s
pure ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther}
getResetProxyStatsData :: ProxyStats -> IO ProxyStatsData
getResetProxyStatsData :: ProxyStats -> STM ProxyStatsData
getResetProxyStatsData s = do
_pRequests <- atomicSwapIORef (pRequests s) 0
_pSuccesses <- atomicSwapIORef (pSuccesses s) 0
_pErrorsConnect <- atomicSwapIORef (pErrorsConnect s) 0
_pErrorsCompat <- atomicSwapIORef (pErrorsCompat s) 0
_pErrorsOther <- atomicSwapIORef (pErrorsOther s) 0
_pRequests <- swapTVar (pRequests s) 0
_pSuccesses <- swapTVar (pSuccesses s) 0
_pErrorsConnect <- swapTVar (pErrorsConnect s) 0
_pErrorsCompat <- swapTVar (pErrorsCompat s) 0
_pErrorsOther <- swapTVar (pErrorsOther s) 0
pure ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther}
-- this function is not thread safe, it is used on server start only
setProxyStats :: ProxyStats -> ProxyStatsData -> IO ()
setProxyStats :: ProxyStats -> ProxyStatsData -> STM ()
setProxyStats s d = do
writeIORef (pRequests s) $! _pRequests d
writeIORef (pSuccesses s) $! _pSuccesses d
writeIORef (pErrorsConnect s) $! _pErrorsConnect d
writeIORef (pErrorsCompat s) $! _pErrorsCompat d
writeIORef (pErrorsOther s) $! _pErrorsOther d
writeTVar (pRequests s) $! _pRequests d
writeTVar (pSuccesses s) $! _pSuccesses d
writeTVar (pErrorsConnect s) $! _pErrorsConnect d
writeTVar (pErrorsCompat s) $! _pErrorsCompat d
writeTVar (pErrorsOther s) $! _pErrorsOther d
instance StrEncoding ProxyStatsData where
strEncode ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther} =
+18 -66
View File
@@ -9,7 +9,6 @@
module Simplex.Messaging.Server.StoreLog
( StoreLog, -- constructors are not exported
StoreLogRecord (..), -- used in tests
openWriteStoreLog,
openReadStoreLog,
storeLogFilePath,
@@ -21,14 +20,12 @@ module Simplex.Messaging.Server.StoreLog
logSuspendQueue,
logDeleteQueue,
logDeleteNotifier,
logUpdateQueueTime,
readWriteStoreLog,
)
where
import Control.Applicative (optional, (<|>))
import Control.Monad (foldM, unless, when)
import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Functor (($>))
@@ -36,10 +33,10 @@ import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol
import Simplex.Messaging.Server.QueueStore
import Simplex.Messaging.Server.QueueStore (NtfCreds (..), QueueRec (..), ServerQueueStatus (..))
import Simplex.Messaging.Transport.Buffer (trimCR)
import Simplex.Messaging.Util (ifM)
import System.Directory (doesFileExist, renameFile)
import System.Directory (doesFileExist)
import System.IO
-- | opaque container for file handle with a type-safe IOMode
@@ -55,20 +52,9 @@ data StoreLogRecord
| SuspendQueue QueueId
| DeleteQueue QueueId
| DeleteNotifier QueueId
| UpdateTime QueueId RoundedSystemTime
deriving (Show)
data SLRTag
= CreateQueue_
| SecureQueue_
| AddNotifier_
| SuspendQueue_
| DeleteQueue_
| DeleteNotifier_
| UpdateTime_
instance StrEncoding QueueRec where
strEncode QueueRec {recipientId, recipientKey, rcvDhSecret, senderId, senderKey, sndSecure, notifier, updatedAt} =
strEncode QueueRec {recipientId, recipientKey, rcvDhSecret, senderId, senderKey, sndSecure, notifier} =
B.unwords
[ "rid=" <> strEncode recipientId,
"rk=" <> strEncode recipientKey,
@@ -76,13 +62,10 @@ instance StrEncoding QueueRec where
"sid=" <> strEncode senderId,
"sk=" <> strEncode senderKey
]
<> sndSecureStr
<> if sndSecure then " sndSecure=" <> strEncode sndSecure else ""
<> maybe "" notifierStr notifier
<> maybe "" updatedAtStr updatedAt
where
sndSecureStr = if sndSecure then " sndSecure=" <> strEncode sndSecure else ""
notifierStr ntfCreds = " notifier=" <> strEncode ntfCreds
updatedAtStr t = " updated_at=" <> strEncode t
strP = do
recipientId <- "rid=" *> strP_
@@ -92,49 +75,24 @@ instance StrEncoding QueueRec where
senderKey <- "sk=" *> strP
sndSecure <- (" sndSecure=" *> strP) <|> pure False
notifier <- optional $ " notifier=" *> strP
updatedAt <- optional $ " updated_at=" *> strP
pure QueueRec {recipientId, recipientKey, rcvDhSecret, senderId, senderKey, sndSecure, notifier, status = QueueActive, updatedAt}
instance StrEncoding SLRTag where
strEncode = \case
CreateQueue_ -> "CREATE"
SecureQueue_ -> "SECURE"
AddNotifier_ -> "NOTIFIER"
SuspendQueue_ -> "SUSPEND"
DeleteQueue_ -> "DELETE"
DeleteNotifier_ -> "NDELETE"
UpdateTime_ -> "TIME"
strP =
A.takeTill (== ' ') >>= \case
"CREATE" -> pure CreateQueue_
"SECURE" -> pure SecureQueue_
"NOTIFIER" -> pure AddNotifier_
"SUSPEND" -> pure SuspendQueue_
"DELETE" -> pure DeleteQueue_
"NDELETE" -> pure DeleteNotifier_
"TIME" -> pure UpdateTime_
s -> fail $ "invalid log record tag: " <> B.unpack s
pure QueueRec {recipientId, recipientKey, rcvDhSecret, senderId, senderKey, sndSecure, notifier, status = QueueActive}
instance StrEncoding StoreLogRecord where
strEncode = \case
CreateQueue q -> strEncode (CreateQueue_, q)
SecureQueue rId sKey -> strEncode (SecureQueue_, rId, sKey)
AddNotifier rId ntfCreds -> strEncode (AddNotifier_, rId, ntfCreds)
SuspendQueue rId -> strEncode (SuspendQueue_, rId)
DeleteQueue rId -> strEncode (DeleteQueue_, rId)
DeleteNotifier rId -> strEncode (DeleteNotifier_, rId)
UpdateTime rId t -> strEncode (UpdateTime_, rId, t)
CreateQueue q -> strEncode (Str "CREATE", q)
SecureQueue rId sKey -> strEncode (Str "SECURE", rId, sKey)
AddNotifier rId ntfCreds -> strEncode (Str "NOTIFIER", rId, ntfCreds)
SuspendQueue rId -> strEncode (Str "SUSPEND", rId)
DeleteQueue rId -> strEncode (Str "DELETE", rId)
DeleteNotifier rId -> strEncode (Str "NDELETE", rId)
strP =
strP_ >>= \case
CreateQueue_ -> CreateQueue <$> strP
SecureQueue_ -> SecureQueue <$> strP_ <*> strP
AddNotifier_ -> AddNotifier <$> strP_ <*> strP
SuspendQueue_ -> SuspendQueue <$> strP
DeleteQueue_ -> DeleteQueue <$> strP
DeleteNotifier_ -> DeleteNotifier <$> strP
UpdateTime_ -> UpdateTime <$> strP_ <*> strP
"CREATE " *> (CreateQueue <$> strP)
<|> "SECURE " *> (SecureQueue <$> strP_ <*> strP)
<|> "NOTIFIER " *> (AddNotifier <$> strP_ <*> strP)
<|> "SUSPEND " *> (SuspendQueue <$> strP)
<|> "DELETE " *> (DeleteQueue <$> strP)
<|> "NDELETE " *> (DeleteNotifier <$> strP)
openWriteStoreLog :: FilePath -> IO (StoreLog 'WriteMode)
openWriteStoreLog f = do
@@ -180,17 +138,12 @@ logDeleteQueue s = writeStoreLogRecord s . DeleteQueue
logDeleteNotifier :: StoreLog 'WriteMode -> QueueId -> IO ()
logDeleteNotifier s = writeStoreLogRecord s . DeleteNotifier
logUpdateQueueTime :: StoreLog 'WriteMode -> QueueId -> RoundedSystemTime -> IO ()
logUpdateQueueTime s qId t = writeStoreLogRecord s $ UpdateTime qId t
readWriteStoreLog :: FilePath -> IO (Map RecipientId QueueRec, StoreLog 'WriteMode)
readWriteStoreLog f = do
qs <- ifM (doesFileExist f) readQS (pure M.empty)
qs <- ifM (doesFileExist f) (readQueues f) (pure M.empty)
s <- openWriteStoreLog f
writeQueues s qs
pure (qs, s)
where
readQS = readQueues f <* renameFile f (f <> ".bak")
writeQueues :: StoreLog 'WriteMode -> Map RecipientId QueueRec -> IO ()
writeQueues s = mapM_ $ \q -> when (active q) $ logCreateQueue s q
@@ -214,6 +167,5 @@ readQueues f = foldM processLine M.empty . LB.lines =<< LB.readFile f
SuspendQueue qId -> M.adjust (\q -> q {status = QueueOff}) qId m
DeleteQueue qId -> M.delete qId m
DeleteNotifier qId -> M.adjust (\q -> q {notifier = Nothing}) qId m
UpdateTime qId t -> M.adjust (\q -> q {updatedAt = Just t}) qId m
printError :: String -> IO ()
printError e = B.putStrLn $ "Error parsing log: " <> B.pack e <> " - " <> s
+10 -3
View File
@@ -5,6 +5,9 @@
module Simplex.Messaging.Session where
import Control.Concurrent.STM
import Control.Monad
import Data.Composition ((.:.))
import Data.Functor (($>))
import Data.Time (UTCTime)
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
@@ -28,10 +31,14 @@ getSessVar sessSeq sessKey vs sessionVarTs = maybe (Left <$> newSessionVar) (pur
pure v
removeSessVar :: Ord k => SessionVar a -> k -> TMap k (SessionVar a) -> STM ()
removeSessVar v sessKey vs =
removeSessVar = void .:. removeSessVar'
{-# INLINE removeSessVar #-}
removeSessVar' :: Ord k => SessionVar a -> k -> TMap k (SessionVar a) -> STM Bool
removeSessVar' v sessKey vs =
TM.lookup sessKey vs >>= \case
Just v' | sessionVarId v == sessionVarId v' -> TM.delete sessKey vs
_ -> pure ()
Just v' | sessionVarId v == sessionVarId v' -> TM.delete sessKey vs $> True
_ -> pure False
tryReadSessVar :: Ord k => k -> TMap k (SessionVar a) -> STM (Maybe a)
tryReadSessVar sessKey vs = TM.lookup sessKey vs $>>= (tryReadTMVar . sessionVar)
+4 -19
View File
@@ -1,15 +1,12 @@
module Simplex.Messaging.TMap
( TMap,
emptyIO,
empty,
singleton,
clear,
Simplex.Messaging.TMap.null,
Simplex.Messaging.TMap.lookup,
lookupIO,
member,
memberIO,
insert,
insertM,
delete,
lookupInsert,
lookupDelete,
@@ -27,9 +24,9 @@ import qualified Data.Map.Strict as M
type TMap k a = TVar (Map k a)
emptyIO :: IO (TMap k a)
emptyIO = newTVarIO M.empty
{-# INLINE emptyIO #-}
empty :: STM (TMap k a)
empty = newTVar M.empty
{-# INLINE empty #-}
singleton :: k -> a -> STM (TMap k a)
singleton k v = newTVar $ M.singleton k v
@@ -47,26 +44,14 @@ lookup :: Ord k => k -> TMap k a -> STM (Maybe a)
lookup k m = M.lookup k <$> readTVar m
{-# INLINE lookup #-}
lookupIO :: Ord k => k -> TMap k a -> IO (Maybe a)
lookupIO k m = M.lookup k <$> readTVarIO m
{-# INLINE lookupIO #-}
member :: Ord k => k -> TMap k a -> STM Bool
member k m = M.member k <$> readTVar m
{-# INLINE member #-}
memberIO :: Ord k => k -> TMap k a -> IO Bool
memberIO k m = M.member k <$> readTVarIO m
{-# INLINE memberIO #-}
insert :: Ord k => k -> a -> TMap k a -> STM ()
insert k v m = modifyTVar' m $ M.insert k v
{-# INLINE insert #-}
insertM :: Ord k => k -> STM a -> TMap k a -> STM ()
insertM k f m = modifyTVar' m . M.insert k =<< f
{-# INLINE insertM #-}
delete :: Ord k => k -> TMap k a -> STM ()
delete k m = modifyTVar' m $ M.delete k
{-# INLINE delete #-}
+30 -101
View File
@@ -47,8 +47,6 @@ module Simplex.Messaging.Transport
authCmdsSMPVersion,
sendingProxySMPVersion,
sndAuthKeySMPVersion,
deletedEventSMPVersion,
encryptedBlockSMPVersion,
simplexMQVersion,
smpBlockSize,
TransportConfig (..),
@@ -66,15 +64,13 @@ module Simplex.Messaging.Transport
ALPN,
connectTLS,
closeTLS,
defaultSupportedParams,
defaultSupportedParamsHTTPS,
supportedParameters,
withTlsUnique,
-- * SMP transport
THandle (..),
THandleParams (..),
THandleAuth (..),
TSbChainKeys (..),
TransportError (..),
HandshakeError (..),
smpServerHandshake,
@@ -88,10 +84,8 @@ module Simplex.Messaging.Transport
where
import Control.Applicative (optional)
import Control.Concurrent.STM
import Control.Monad (forM, (<$!>))
import Control.Monad.Except
import Control.Monad.IO.Class
import Control.Monad.Trans.Except (throwE)
import qualified Data.Aeson.TH as J
import Data.Attoparsec.ByteString.Char8 (Parser)
@@ -103,8 +97,6 @@ import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Default (def)
import Data.Functor (($>))
import Data.Tuple (swap)
import Data.Typeable (Typeable)
import Data.Version (showVersion)
import Data.Word (Word16)
import qualified Data.X509 as X
@@ -121,9 +113,9 @@ import Simplex.Messaging.Transport.Buffer
import Simplex.Messaging.Util (bshow, catchAll, catchAll_, liftEitherWith)
import Simplex.Messaging.Version
import Simplex.Messaging.Version.Internal
import System.IO.Error (isEOFError)
import UnliftIO.Exception (Exception)
import qualified UnliftIO.Exception as E
import UnliftIO.STM
-- * Transport parameters
@@ -138,10 +130,6 @@ smpBlockSize = 16384
-- 5 - basic auth for SMP servers (11/12/2022)
-- 6 - allow creating queues without subscribing (9/10/2023)
-- 7 - support authenticated encryption to verify senders' commands, imply but do NOT send session ID in signed part (4/30/2024)
-- 8 - SMP proxy for sender commands
-- 9 - faster handshake: SKEY command for sender to secure queue
-- 10 - DELD event to subscriber when queue is deleted via another connnection
-- 11 - additional encryption of transport blocks with forward secrecy (9/14/2024)
data SMPVersion
@@ -172,20 +160,14 @@ sendingProxySMPVersion = VersionSMP 8
sndAuthKeySMPVersion :: VersionSMP
sndAuthKeySMPVersion = VersionSMP 9
deletedEventSMPVersion :: VersionSMP
deletedEventSMPVersion = VersionSMP 10
encryptedBlockSMPVersion :: VersionSMP
encryptedBlockSMPVersion = VersionSMP 11
currentClientSMPRelayVersion :: VersionSMP
currentClientSMPRelayVersion = VersionSMP 11
currentClientSMPRelayVersion = VersionSMP 9
legacyServerSMPRelayVersion :: VersionSMP
legacyServerSMPRelayVersion = VersionSMP 6
currentServerSMPRelayVersion :: VersionSMP
currentServerSMPRelayVersion = VersionSMP 11
currentServerSMPRelayVersion = VersionSMP 9
-- Max SMP protocol version to be used in e2e encrypted
-- connection between client and server, as defined by SMP proxy.
@@ -223,7 +205,7 @@ data TransportConfig = TransportConfig
transportTimeout :: Maybe Int
}
class Typeable c => Transport c where
class Transport c where
transport :: ATransport
transport = ATransport (TProxy @c)
@@ -303,7 +285,7 @@ getTLS :: TransportPeer -> TransportConfig -> X.CertificateChain -> T.Context ->
getTLS tlsPeer cfg tlsServerCerts cxt = withTlsUnique tlsPeer cxt newTLS
where
newTLS tlsUniq = do
tlsBuffer <- newTBuffer
tlsBuffer <- atomically newTBuffer
tlsALPN <- T.getNegotiatedProtocol cxt
pure TLS {tlsContext = cxt, tlsALPN, tlsTransportConfig = cfg, tlsServerCerts, tlsPeer, tlsUniq, tlsBuffer}
@@ -321,8 +303,8 @@ closeTLS ctx =
`E.finally` T.contextClose ctx
`catchAll_` pure ()
defaultSupportedParams :: T.Supported
defaultSupportedParams =
supportedParameters :: T.Supported
supportedParameters =
def
{ T.supportedVersions = [T.TLS13, T.TLS12],
T.supportedCiphers =
@@ -330,29 +312,8 @@ defaultSupportedParams =
TE.cipher_ECDHE_ECDSA_CHACHA20POLY1305_SHA256 -- for TLS12
],
T.supportedHashSignatures = [(T.HashIntrinsic, T.SignatureEd448), (T.HashIntrinsic, T.SignatureEd25519)],
T.supportedGroups = [T.X448, T.X25519],
T.supportedSecureRenegotiation = False
}
-- | A selection of extra parameters to accomodate browser chains
defaultSupportedParamsHTTPS :: T.Supported
defaultSupportedParamsHTTPS =
defaultSupportedParams
{ T.supportedCiphers = TE.ciphersuite_strong,
T.supportedGroups = [T.X25519, T.X448, T.FFDHE4096, T.FFDHE6144, T.FFDHE8192, T.P521],
T.supportedHashSignatures =
[ (T.HashIntrinsic, T.SignatureEd448),
(T.HashIntrinsic, T.SignatureEd25519),
(T.HashSHA256, T.SignatureECDSA),
(T.HashSHA384, T.SignatureECDSA),
(T.HashSHA512, T.SignatureECDSA),
(T.HashIntrinsic, T.SignatureRSApssRSAeSHA512),
(T.HashIntrinsic, T.SignatureRSApssRSAeSHA384),
(T.HashIntrinsic, T.SignatureRSApssRSAeSHA256),
(T.HashSHA512, T.SignatureRSA),
(T.HashSHA384, T.SignatureRSA),
(T.HashSHA256, T.SignatureRSA)
]
T.supportedSecureRenegotiation = False,
T.supportedGroups = [T.X448, T.X25519]
}
instance Transport TLS where
@@ -378,12 +339,11 @@ instance Transport TLS where
getLn :: TLS -> IO ByteString
getLn TLS {tlsContext, tlsBuffer} = do
getLnBuffered tlsBuffer (T.recvData tlsContext) `E.catches` [E.Handler handleTlsEOF, E.Handler handleEOF]
getLnBuffered tlsBuffer (T.recvData tlsContext) `E.catch` handleEOF
where
handleTlsEOF = \case
T.PostHandshake T.Error_EOF -> E.throwIO TEBadBlock
handleEOF = \case
T.Error_EOF -> E.throwIO TEBadBlock
e -> E.throwIO e
handleEOF e = if isEOFError e then E.throwIO TEBadBlock else E.throwIO e
-- * SMP transport
@@ -407,8 +367,6 @@ data THandleParams v p = THandleParams
-- | do NOT send session ID in transmission, but include it into signed message
-- based on protocol version
implySessId :: Bool,
-- -- | additional block encryption
encryptBlock :: Maybe TSbChainKeys,
-- | send multiple transmissions in a single block
-- based on protocol version
batch :: Bool
@@ -427,11 +385,6 @@ data THandleAuth (p :: TransportPeer) where
} ->
THandleAuth 'TServer
data TSbChainKeys = TSbChainKeys
{ sndKey :: TVar C.SbChainKey,
rcvKey :: TVar C.SbChainKey
}
-- | TLS-unique channel binding
type SessionId = ByteString
@@ -534,26 +487,16 @@ instance Encoding TransportError where
-- | Pad and send block to SMP transport.
tPutBlock :: Transport c => THandle v c p -> ByteString -> IO (Either TransportError ())
tPutBlock THandle {connection = c, params = THandleParams {blockSize, encryptBlock}} block = do
block_ <- case encryptBlock of
Just TSbChainKeys {sndKey} -> do
(sk, nonce) <- atomically $ stateTVar sndKey C.sbcHkdf
pure $ C.sbEncrypt sk nonce block (blockSize - 16)
Nothing -> pure $ C.pad block blockSize
bimapM (const $ pure TELargeMsg) (cPut c) block_
tPutBlock THandle {connection = c, params = THandleParams {blockSize}} block =
bimapM (const $ pure TELargeMsg) (cPut c) $
C.pad block blockSize
-- | Receive block from SMP transport.
tGetBlock :: Transport c => THandle v c p -> IO (Either TransportError ByteString)
tGetBlock THandle {connection = c, params = THandleParams {blockSize, encryptBlock}} = do
tGetBlock THandle {connection = c, params = THandleParams {blockSize}} = do
msg <- cGet c blockSize
if B.length msg == blockSize
then
first (const TELargeMsg) <$>
case encryptBlock of
Just TSbChainKeys {rcvKey} -> do
(sk, nonce) <- atomically $ stateTVar rcvKey C.sbcHkdf
pure $ C.sbDecrypt sk nonce msg
Nothing -> pure $ C.unPad msg
then pure . first (const TELargeMsg) $ C.unPad msg
else ioe_EOF
-- | Server SMP transport handshake.
@@ -572,7 +515,7 @@ smpServerHandshake serverSignKey c (k, pk) kh smpVRange = do
throwE $ TEHandshake IDENTITY
| otherwise ->
case compatibleVRange' smpVersionRange v of
Just (Compatible vr) -> liftIO $ smpTHandleServer th v vr pk k'
Just (Compatible vr) -> pure $ smpTHandleServer th v vr pk k'
Nothing -> throwE TEVersion
-- | Client SMP transport handshake.
@@ -596,36 +539,23 @@ smpClientHandshake c ks_ keyHash@(C.KeyHash kh) smpVRange = do
(,certKey) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey)
let v = maxVersion vr
sendHandshake th $ ClientHandshake {smpVersion = v, keyHash, authPubKey = fst <$> ks_}
liftIO $ smpTHandleClient th v vr (snd <$> ks_) ck_
pure $ smpTHandleClient th v vr (snd <$> ks_) ck_
Nothing -> throwE TEVersion
smpTHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> VersionRangeSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> IO (THandleSMP c 'TServer)
smpTHandleServer th v vr pk k_ = do
let thAuth = Just THAuthServer {serverPrivKey = pk, sessSecret' = (`C.dh'` pk) <$!> k_}
be <- blockEncryption th v thAuth
pure $ smpTHandle_ th v vr thAuth $ uncurry TSbChainKeys <$> be
smpTHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> VersionRangeSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c 'TServer
smpTHandleServer th v vr pk k_ =
let thAuth = THAuthServer {serverPrivKey = pk, sessSecret' = (`C.dh'` pk) <$!> k_}
in smpTHandle_ th v vr (Just thAuth)
smpTHandleClient :: forall c. THandleSMP c 'TClient -> VersionSMP -> VersionRangeSMP -> Maybe C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> IO (THandleSMP c 'TClient)
smpTHandleClient th v vr pk_ ck_ = do
smpTHandleClient :: forall c. THandleSMP c 'TClient -> VersionSMP -> VersionRangeSMP -> Maybe C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleSMP c 'TClient
smpTHandleClient th v vr pk_ ck_ =
let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = forceCertChain ck, sessSecret = C.dh' k <$!> pk_}) <$!> ck_
be <- blockEncryption th v thAuth
-- swap is needed to use client's sndKey as server's rcvKey and vice versa
pure $ smpTHandle_ th v vr thAuth $ uncurry TSbChainKeys . swap <$> be
in smpTHandle_ th v vr thAuth
blockEncryption :: THandleSMP c p -> VersionSMP -> Maybe (THandleAuth p) -> IO (Maybe (TVar C.SbChainKey, TVar C.SbChainKey))
blockEncryption THandle {params = THandleParams {sessionId}} v = \case
Just thAuth | v >= encryptedBlockSMPVersion -> case thAuth of
THAuthClient {sessSecret} -> be sessSecret
THAuthServer {sessSecret'} -> be sessSecret'
_ -> pure Nothing
where
be :: Maybe C.DhSecretX25519 -> IO (Maybe (TVar C.SbChainKey, TVar C.SbChainKey))
be = mapM $ \(C.DhSecretX25519 secret) -> bimapM newTVarIO newTVarIO $ C.sbcInit sessionId secret
smpTHandle_ :: forall c p. THandleSMP c p -> VersionSMP -> VersionRangeSMP -> Maybe (THandleAuth p) -> Maybe TSbChainKeys -> THandleSMP c p
smpTHandle_ th@THandle {params} v vr thAuth encryptBlock =
smpTHandle_ :: forall c p. THandleSMP c p -> VersionSMP -> VersionRangeSMP -> Maybe (THandleAuth p) -> THandleSMP c p
smpTHandle_ th@THandle {params} v vr thAuth =
-- TODO drop SMP v6: make thAuth non-optional
let params' = params {thVersion = v, thServerVRange = vr, thAuth, implySessId = v >= authCmdsSMPVersion, encryptBlock}
let params' = params {thVersion = v, thServerVRange = vr, thAuth, implySessId = v >= authCmdsSMPVersion}
in (th :: THandleSMP c p) {params = params'}
{-# INLINE forceCertChain #-}
@@ -657,7 +587,6 @@ smpTHandle c = THandle {connection = c, params}
thVersion = v,
thAuth = Nothing,
implySessId = False,
encryptBlock = Nothing,
batch = True
}
+3 -3
View File
@@ -17,10 +17,10 @@ data TBuffer = TBuffer
getLock :: TMVar ()
}
newTBuffer :: IO TBuffer
newTBuffer :: STM TBuffer
newTBuffer = do
buffer <- newTVarIO ""
getLock <- newTMVarIO ()
buffer <- newTVar ""
getLock <- newTMVar ()
pure TBuffer {buffer, getLock}
withBufferLock :: TBuffer -> IO a -> IO a
+28 -67
View File
@@ -13,13 +13,9 @@ module Simplex.Messaging.Transport.Client
defaultSMPPort,
defaultTcpConnectTimeout,
defaultTransportClientConfig,
defaultSocksProxyWithAuth,
defaultSocksProxy,
defaultSocksHost,
TransportClientConfig (..),
SocksProxy (..),
SocksProxyWithAuth (..),
SocksAuth (..),
SocksProxy,
TransportHost (..),
TransportHosts (..),
TransportHosts_ (..),
@@ -27,7 +23,7 @@ module Simplex.Messaging.Transport.Client
)
where
import Control.Applicative (optional, (<|>))
import Control.Applicative (optional)
import Control.Logger.Simple (logError)
import Control.Monad (when)
import Data.Aeson (FromJSON (..), ToJSON (..))
@@ -83,7 +79,7 @@ instance StrEncoding TransportHost where
strP =
A.choice
[ THIPv4 <$> ((,,,) <$> ipNum <*> ipNum <*> ipNum <*> A.decimal),
maybe (Left "bad IPv6") (Right . THIPv6 . fromIPv6w) . readMaybe . B.unpack <$?> ipv6StrP,
maybe (Left "bad IPv6") (Right . THIPv6 . fromIPv6w) . readMaybe . B.unpack <$?> A.takeWhile1 (\c -> isHexDigit c || c == ':'),
THOnionHost <$> ((<>) <$> A.takeWhile (\c -> isAsciiLower c || isDigit c) <*> A.string ".onion"),
THDomainName . B.unpack <$> (notOnion <$?> A.takeWhile1 (A.notInClass ":#,;/ \n\r\t"))
]
@@ -91,9 +87,6 @@ instance StrEncoding TransportHost where
ipNum = validIP <$?> (A.decimal <* A.char '.')
validIP :: Int -> Either String Word8
validIP n = if 0 <= n && n <= 255 then Right $ fromIntegral n else Left "invalid IP address"
ipv6StrP =
A.char '[' *> A.takeWhile1 (/= ']') <* A.char ']'
<|> A.takeWhile1 (\c -> isHexDigit c || c == ':')
notOnion s = if ".onion" `B.isSuffixOf` s then Left "invalid onion host" else Right s
instance ToJSON TransportHost where
@@ -125,8 +118,7 @@ data TransportClientConfig = TransportClientConfig
tcpKeepAlive :: Maybe KeepAliveOpts,
logTLSErrors :: Bool,
clientCredentials :: Maybe (X.CertificateChain, T.PrivKey),
alpn :: Maybe [ALPN],
useSNI :: Bool
alpn :: Maybe [ALPN]
}
deriving (Eq, Show)
@@ -135,23 +127,23 @@ defaultTcpConnectTimeout :: Int
defaultTcpConnectTimeout = 25_000_000
defaultTransportClientConfig :: TransportClientConfig
defaultTransportClientConfig = TransportClientConfig Nothing defaultTcpConnectTimeout (Just defaultKeepAliveOpts) True Nothing Nothing True
defaultTransportClientConfig = TransportClientConfig Nothing defaultTcpConnectTimeout (Just defaultKeepAliveOpts) True Nothing Nothing
clientTransportConfig :: TransportClientConfig -> TransportConfig
clientTransportConfig TransportClientConfig {logTLSErrors} =
TransportConfig {logTLSErrors, transportTimeout = Nothing}
-- | Connect to passed TCP host:port and pass handle to the client.
runTransportClient :: Transport c => TransportClientConfig -> Maybe SocksCredentials -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> IO a) -> IO a
runTransportClient = runTLSTransportClient defaultSupportedParams Nothing
runTransportClient :: Transport c => TransportClientConfig -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> IO a) -> IO a
runTransportClient = runTLSTransportClient supportedParameters Nothing
runTLSTransportClient :: Transport c => T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> Maybe SocksCredentials -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> IO a) -> IO a
runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy, tcpKeepAlive, clientCredentials, alpn, useSNI} socksCreds host port keyHash client = do
runTLSTransportClient :: Transport c => T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> IO a) -> IO a
runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy, tcpKeepAlive, clientCredentials, alpn} proxyUsername host port keyHash client = do
serverCert <- newEmptyTMVarIO
let hostName = B.unpack $ strEncode host
clientParams = mkTLSClientParams tlsParams caStore_ hostName port keyHash clientCredentials alpn useSNI serverCert
clientParams = mkTLSClientParams tlsParams caStore_ hostName port keyHash clientCredentials alpn serverCert
connectTCP = case socksProxy of
Just proxy -> connectSocksClient proxy socksCreds (hostAddr host)
Just proxy -> connectSocksClient proxy proxyUsername (hostAddr host)
_ -> connectTCPClient hostName
c <- do
sock <- connectTCP port
@@ -199,76 +191,45 @@ connectTCPClient host port = withSocketsDo $ resolve >>= tryOpen err
defaultSMPPort :: PortNumber
defaultSMPPort = 5223
connectSocksClient :: SocksProxy -> Maybe SocksCredentials -> SocksHostAddress -> ServiceName -> IO Socket
connectSocksClient (SocksProxy addr) socksCreds hostAddr _port = do
connectSocksClient :: SocksProxy -> Maybe ByteString -> SocksHostAddress -> ServiceName -> IO Socket
connectSocksClient (SocksProxy addr) proxyUsername hostAddr _port = do
let port = if null _port then defaultSMPPort else fromMaybe defaultSMPPort $ readMaybe _port
fst <$> case socksCreds of
Just creds -> socksConnectAuth (defaultSocksConf addr) (SocksAddress hostAddr port) creds
fst <$> case proxyUsername of
Just username -> socksConnectAuth (defaultSocksConf addr) (SocksAddress hostAddr port) (SocksCredentials username "")
_ -> socksConnect (defaultSocksConf addr) (SocksAddress hostAddr port)
defaultSocksHost :: (Word8, Word8, Word8, Word8)
defaultSocksHost = (127, 0, 0, 1)
defaultSocksProxyWithAuth :: SocksProxyWithAuth
defaultSocksProxyWithAuth = SocksProxyWithAuth SocksIsolateByAuth defaultSocksProxy
defaultSocksHost :: HostAddress
defaultSocksHost = tupleToHostAddress (127, 0, 0, 1)
defaultSocksProxy :: SocksProxy
defaultSocksProxy = SocksProxy $ SockAddrInet 9050 $ tupleToHostAddress defaultSocksHost
defaultSocksProxy = SocksProxy $ SockAddrInet 9050 defaultSocksHost
newtype SocksProxy = SocksProxy SockAddr
deriving (Eq)
data SocksProxyWithAuth = SocksProxyWithAuth SocksAuth SocksProxy
deriving (Eq, Show)
data SocksAuth
= SocksAuthUsername {username :: ByteString, password :: ByteString}
| SocksAuthNull
| SocksIsolateByAuth -- this is default
deriving (Eq, Show)
instance Show SocksProxy where show (SocksProxy addr) = show addr
instance StrEncoding SocksProxy where
strEncode = B.pack . show
strP = do
host <- fromMaybe (THIPv4 defaultSocksHost) <$> optional strP
host <- maybe defaultSocksHost tupleToHostAddress <$> optional ipv4P
port <- fromMaybe 9050 <$> optional (A.char ':' *> (fromInteger <$> A.decimal))
SocksProxy <$> socksAddr port host
pure . SocksProxy $ SockAddrInet port host
where
socksAddr port = \case
THIPv4 addr -> pure $ SockAddrInet port $ tupleToHostAddress addr
THIPv6 addr -> pure $ SockAddrInet6 port 0 addr 0
_ -> fail "SOCKS5 host should be IPv4 or IPv6 address"
ipv4P = (,,,) <$> ipNum <*> ipNum <*> ipNum <*> A.decimal
ipNum = A.decimal <* A.char '.'
instance StrEncoding SocksProxyWithAuth where
strEncode (SocksProxyWithAuth auth proxy) = strEncode auth <> strEncode proxy
strP = SocksProxyWithAuth <$> strP <*> strP
instance ToJSON SocksProxyWithAuth where
instance ToJSON SocksProxy where
toJSON = strToJSON
toEncoding = strToJEncoding
instance FromJSON SocksProxyWithAuth where
parseJSON = strParseJSON "SocksProxyWithAuth"
instance FromJSON SocksProxy where
parseJSON = strParseJSON "SocksProxy"
instance StrEncoding SocksAuth where
strEncode = \case
SocksAuthUsername {username, password} -> username <> ":" <> password <> "@"
SocksAuthNull -> "@"
SocksIsolateByAuth -> ""
strP = usernameP <|> (SocksAuthNull <$ A.char '@') <|> pure SocksIsolateByAuth
where
usernameP = do
username <- A.takeTill (== ':') <* A.char ':'
password <- A.takeTill (== '@') <* A.char '@'
pure SocksAuthUsername {username, password}
mkTLSClientParams :: T.Supported -> Maybe XS.CertificateStore -> HostName -> ServiceName -> Maybe C.KeyHash -> Maybe (X.CertificateChain, T.PrivKey) -> Maybe [ALPN] -> Bool -> TMVar X.CertificateChain -> T.ClientParams
mkTLSClientParams supported caStore_ host port cafp_ clientCreds_ alpn_ sni serverCerts =
mkTLSClientParams :: T.Supported -> Maybe XS.CertificateStore -> HostName -> ServiceName -> Maybe C.KeyHash -> Maybe (X.CertificateChain, T.PrivKey) -> Maybe [ALPN] -> TMVar X.CertificateChain -> T.ClientParams
mkTLSClientParams supported caStore_ host port cafp_ clientCreds_ alpn_ serverCerts =
(T.defaultParamsClient host p)
{ T.clientUseServerNameIndication = sni,
T.clientShared = def {T.sharedCAStore = fromMaybe (T.sharedCAStore def) caStore_},
{ T.clientShared = def {T.sharedCAStore = fromMaybe (T.sharedCAStore def) caStore_},
T.clientHooks =
def
{ T.onServerCertificate = onServerCert,
@@ -32,8 +32,8 @@ import qualified Time.System as Hourglass
-- leaf <- genCredentials (Just ca) (0, 1) "Entity" -- session-signing cert
-- pure $ tlsCredentials (leaf :| [ca])
-- @
tlsCredentials :: NonEmpty Credentials -> (C.KeyHash, TLS.Credential)
tlsCredentials credentials = (C.KeyHash rootFP, (X509.CertificateChain certs, privateToTls $ snd leafKey))
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
+2 -1
View File
@@ -2,6 +2,7 @@
module Simplex.Messaging.Transport.HTTP2 where
import Control.Concurrent.STM
import qualified Control.Exception as E
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
@@ -74,7 +75,7 @@ instance HTTP2BodyChunk HS.Request where
getHTTP2Body :: HTTP2BodyChunk a => a -> Int -> IO HTTP2Body
getHTTP2Body r n = do
bodyBuffer <- newTBuffer
bodyBuffer <- atomically newTBuffer
let getPart n' = getBuffered bodyBuffer n' Nothing $ getBodyChunk r
bodyHead <- getPart n
let bodySize = fromMaybe 0 $ getBodySize r
+12 -13
View File
@@ -11,6 +11,7 @@ import Control.Concurrent.Async
import Control.Exception (IOException, try)
import qualified Control.Exception as E
import Control.Monad
import Data.ByteString.Char8 (ByteString)
import Data.Functor (($>))
import Data.Time (UTCTime, getCurrentTime)
import qualified Data.X509 as X
@@ -19,7 +20,6 @@ import Network.HPACK (BufferSize)
import Network.HTTP2.Client (ClientConfig (..), Request, Response)
import qualified Network.HTTP2.Client as H
import Network.Socket (HostName, ServiceName)
import Network.Socks5 (SocksCredentials)
import qualified Network.TLS as T
import Numeric.Natural (Natural)
import qualified Simplex.Messaging.Crypto as C
@@ -78,8 +78,7 @@ defaultHTTP2ClientConfig =
tcpKeepAlive = Nothing,
logTLSErrors = True,
clientCredentials = Nothing,
alpn = Nothing,
useSNI = False
alpn = Nothing
},
bufferSize = defaultHTTP2BufferSize,
bodyHeadSize = 16384,
@@ -92,10 +91,10 @@ data HTTP2ClientError = HCResponseTimeout | HCNetworkError | HCIOError IOExcepti
getHTTP2Client :: HostName -> ServiceName -> Maybe XS.CertificateStore -> HTTP2ClientConfig -> IO () -> IO (Either HTTP2ClientError HTTP2Client)
getHTTP2Client host port = getVerifiedHTTP2Client Nothing (THDomainName host) port Nothing
getVerifiedHTTP2Client :: Maybe SocksCredentials -> TransportHost -> ServiceName -> Maybe C.KeyHash -> Maybe XS.CertificateStore -> HTTP2ClientConfig -> IO () -> IO (Either HTTP2ClientError HTTP2Client)
getVerifiedHTTP2Client socksCreds host port keyHash caStore config disconnected = getVerifiedHTTP2ClientWith config host port disconnected setup
getVerifiedHTTP2Client :: Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> Maybe XS.CertificateStore -> HTTP2ClientConfig -> IO () -> IO (Either HTTP2ClientError HTTP2Client)
getVerifiedHTTP2Client proxyUsername host port keyHash caStore config disconnected = getVerifiedHTTP2ClientWith config host port disconnected setup
where
setup = runHTTP2Client (suportedTLSParams config) caStore (transportConfig config) (bufferSize config) socksCreds host port keyHash
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
@@ -105,13 +104,13 @@ attachHTTP2Client config host port disconnected bufferSize tls = getVerifiedHTTP
getVerifiedHTTP2ClientWith :: HTTP2ClientConfig -> TransportHost -> ServiceName -> IO () -> ((TLS -> H.Client HTTP2Response) -> IO HTTP2Response) -> IO (Either HTTP2ClientError HTTP2Client)
getVerifiedHTTP2ClientWith config host port disconnected setup =
(mkHTTPS2Client >>= runClient)
(atomically mkHTTPS2Client >>= runClient)
`E.catch` \(e :: IOException) -> pure . Left $ HCIOError e
where
mkHTTPS2Client :: IO HClient
mkHTTPS2Client :: STM HClient
mkHTTPS2Client = do
connected <- newTVarIO False
reqQ <- newTBQueueIO $ qSize config
connected <- newTVar False
reqQ <- newTBQueue $ qSize config
pure HClient {connected, disconnected, host, port, config, reqQ}
runClient :: HClient -> IO (Either HTTP2ClientError HTTP2Client)
@@ -179,11 +178,11 @@ sendRequestDirect HTTP2Client {client_ = HClient {config, disconnected}, sendReq
http2RequestTimeout :: HTTP2ClientConfig -> Maybe Int -> Int
http2RequestTimeout HTTP2ClientConfig {connTimeout} = maybe connTimeout (connTimeout +)
runHTTP2Client :: forall a. T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> BufferSize -> Maybe SocksCredentials -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (TLS -> H.Client a) -> IO a
runHTTP2Client tlsParams caStore tcConfig bufferSize socksCreds host port keyHash = runHTTP2ClientWith bufferSize host setup
runHTTP2Client :: forall a. T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> BufferSize -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (TLS -> H.Client a) -> IO a
runHTTP2Client tlsParams caStore tcConfig bufferSize proxyUsername host port keyHash = runHTTP2ClientWith bufferSize host setup
where
setup :: (TLS -> IO a) -> IO a
setup = runTLSTransportClient tlsParams caStore tcConfig socksCreds host port keyHash
setup = runTLSTransportClient tlsParams caStore tcConfig proxyUsername host port keyHash
runHTTP2ClientWith :: forall a. BufferSize -> TransportHost -> ((TLS -> IO a) -> IO a) -> (TLS -> H.Client a) -> IO a
runHTTP2ClientWith bufferSize host setup client = setup $ \tls -> withHTTP2 bufferSize (run tls) (pure ()) tls
@@ -15,7 +15,7 @@ import Numeric.Natural (Natural)
import Simplex.Messaging.Server.Expiration
import Simplex.Messaging.Transport (ALPN, SessionId, TLS, closeConnection, tlsALPN, tlsUniq)
import Simplex.Messaging.Transport.HTTP2
import Simplex.Messaging.Transport.Server (ServerCredentials, TransportServerConfig (..), loadServerCredential, runTransportServer)
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), loadSupportedTLSServerParams, runTransportServer)
import Simplex.Messaging.Util (threadDelay')
import UnliftIO (finally)
import UnliftIO.Concurrent (forkIO, killThread)
@@ -28,7 +28,9 @@ data HTTP2ServerConfig = HTTP2ServerConfig
bufferSize :: BufferSize,
bodyHeadSize :: Int,
serverSupported :: T.Supported,
https2Credentials :: ServerCredentials,
caCertificateFile :: FilePath,
privateKeyFile :: FilePath,
certificateFile :: FilePath,
transportConfig :: TransportServerConfig
}
deriving (Show)
@@ -47,13 +49,13 @@ data HTTP2Server = HTTP2Server
}
-- This server is for testing only, it processes all requests in a single queue.
getHTTP2Server :: HTTP2ServerConfig -> Maybe [ALPN] -> IO HTTP2Server
getHTTP2Server HTTP2ServerConfig {qSize, http2Port, bufferSize, bodyHeadSize, serverSupported, https2Credentials, transportConfig} alpn_ = do
srvCreds <- loadServerCredential https2Credentials
getHTTP2Server :: HTTP2ServerConfig -> IO HTTP2Server
getHTTP2Server HTTP2ServerConfig {qSize, http2Port, bufferSize, bodyHeadSize, serverSupported, caCertificateFile, certificateFile, privateKeyFile, transportConfig} = do
tlsServerParams <- loadSupportedTLSServerParams serverSupported caCertificateFile certificateFile privateKeyFile (alpn transportConfig)
started <- newEmptyTMVarIO
reqQ <- newTBQueueIO qSize
action <- async $
runHTTP2Server started http2Port bufferSize serverSupported srvCreds alpn_ transportConfig Nothing (const $ pure ()) $ \sessionId sessionALPN r sendResponse -> do
runHTTP2Server started http2Port bufferSize tlsServerParams transportConfig Nothing (const $ pure ()) $ \sessionId sessionALPN r sendResponse -> do
reqBody <- getHTTP2Body r bodyHeadSize
atomically $ writeTBQueue reqQ HTTP2Request {sessionId, sessionALPN, request = r, reqBody, sendResponse}
void . atomically $ takeTMVar started
@@ -62,10 +64,10 @@ getHTTP2Server HTTP2ServerConfig {qSize, http2Port, bufferSize, bodyHeadSize, se
closeHTTP2Server :: HTTP2Server -> IO ()
closeHTTP2Server = uninterruptibleCancel . action
runHTTP2Server :: TMVar Bool -> ServiceName -> BufferSize -> T.Supported -> T.Credential -> Maybe [ALPN] -> TransportServerConfig -> Maybe ExpirationConfig -> (SessionId -> IO ()) -> HTTP2ServerFunc -> IO ()
runHTTP2Server started port bufferSize srvSupported srvCreds alpn_ transportConfig expCfg_ clientFinished = runHTTP2ServerWith_ expCfg_ clientFinished bufferSize setup
runHTTP2Server :: TMVar Bool -> ServiceName -> BufferSize -> T.ServerParams -> TransportServerConfig -> Maybe ExpirationConfig -> (SessionId -> IO ()) -> HTTP2ServerFunc -> IO ()
runHTTP2Server started port bufferSize serverParams transportConfig expCfg_ clientFinished = runHTTP2ServerWith_ expCfg_ clientFinished bufferSize setup
where
setup = runTransportServer started port srvSupported srvCreds alpn_ transportConfig
setup = runTransportServer started port serverParams transportConfig
runHTTP2ServerWith :: BufferSize -> ((TLS -> IO ()) -> a) -> HTTP2ServerFunc -> a
runHTTP2ServerWith = runHTTP2ServerWith_ Nothing (\_sessId -> pure ())
+63 -81
View File
@@ -6,24 +6,20 @@
module Simplex.Messaging.Transport.Server
( TransportServerConfig (..),
ServerCredentials (..),
AddHTTP,
defaultTransportServerConfig,
runTransportServerState,
runTransportServerState_,
SocketState,
newSocketState,
runTransportServer,
runTransportServerSocket,
runLocalTCPServer,
runTCPServer,
runTCPServerSocket,
startTCPServer,
loadServerCredential,
supportedTLSServerParams,
supportedTLSServerParams_,
loadSupportedTLSServerParams,
loadTLSServerParams,
loadFingerprint,
loadFileFingerprint,
smpServerHandshake,
tlsServerCredentials,
)
where
@@ -35,7 +31,7 @@ import Data.Default (def)
import Data.IntMap.Strict (IntMap)
import qualified Data.IntMap.Strict as IM
import Data.List (find)
import Data.Maybe (fromJust, fromMaybe, maybeToList)
import Data.Maybe (fromJust, fromMaybe)
import qualified Data.X509 as X
import Data.X509.Validation (Fingerprint (..))
import qualified Data.X509.Validation as XV
@@ -56,25 +52,18 @@ import UnliftIO.STM
data TransportServerConfig = TransportServerConfig
{ logTLSErrors :: Bool,
tlsSetupTimeout :: Int,
transportTimeout :: Int
transportTimeout :: Int,
alpn :: Maybe [ALPN]
}
deriving (Eq, Show)
data ServerCredentials = ServerCredentials
{ caCertificateFile :: Maybe FilePath, -- CA certificate private key is not needed for initialization
privateKeyFile :: FilePath,
certificateFile :: FilePath
}
deriving (Show)
type AddHTTP = Bool
defaultTransportServerConfig :: TransportServerConfig
defaultTransportServerConfig =
TransportServerConfig
{ logTLSErrors = True,
tlsSetupTimeout = 60000000,
transportTimeout = 40000000
transportTimeout = 40000000,
alpn = Nothing
}
serverTransportConfig :: TransportServerConfig -> TransportConfig
@@ -85,54 +74,50 @@ serverTransportConfig TransportServerConfig {logTLSErrors} =
-- | Run transport server (plain TCP or WebSockets) on passed TCP port and signal when server started and stopped via passed TMVar.
--
-- All accepted connections are passed to the passed function.
runTransportServer :: forall c. Transport c => TMVar Bool -> ServiceName -> T.Supported -> T.Credential -> Maybe [ALPN] -> TransportServerConfig -> (c -> IO ()) -> IO ()
runTransportServer started port srvSupported srvCreds alpn_ cfg server = do
ss <- newSocketState
runTransportServerState ss started port srvSupported srvCreds alpn_ cfg server
runTransportServer :: forall c. Transport c => TMVar Bool -> ServiceName -> T.ServerParams -> TransportServerConfig -> (c -> IO ()) -> IO ()
runTransportServer started port params cfg server = do
ss <- atomically newSocketState
runTransportServerState ss started port params cfg server
runTransportServerState :: forall c . Transport c => SocketState -> TMVar Bool -> ServiceName -> T.Supported -> T.Credential -> Maybe [ALPN] -> TransportServerConfig -> (c -> IO ()) -> IO ()
runTransportServerState ss started port srvSupported srvCreds alpn_ cfg server = runTransportServerState_ ss started port srvSupported (const srvCreds) alpn_ cfg (const server)
runTransportServerState_ :: forall c . Transport c => SocketState -> TMVar Bool -> ServiceName -> T.Supported -> (Maybe HostName -> T.Credential) -> Maybe [ALPN] -> TransportServerConfig -> (Socket -> c -> IO ()) -> IO ()
runTransportServerState_ ss started port = runTransportServerSocketState ss started (startTCPServer started Nothing port) (transportName (TProxy :: TProxy c))
runTransportServerState :: forall c . Transport c => SocketState -> TMVar Bool -> ServiceName -> T.ServerParams -> TransportServerConfig -> (c -> IO ()) -> IO ()
runTransportServerState ss started port = runTransportServerSocketState ss started (startTCPServer started port) (transportName (TProxy :: TProxy c))
-- | Run a transport server with provided connection setup and handler.
runTransportServerSocket :: Transport a => TMVar Bool -> IO Socket -> String -> T.Credential -> T.ServerParams -> TransportServerConfig -> (a -> IO ()) -> IO ()
runTransportServerSocket started getSocket threadLabel srvCreds srvParams cfg server = do
ss <- newSocketState
runTransportServerSocketState_ ss started getSocket threadLabel (const srvCreds) srvParams cfg (const server)
runTransportServerSocketState :: Transport a => SocketState -> TMVar Bool -> IO Socket -> String -> T.Supported -> (Maybe HostName -> T.Credential) -> Maybe [ALPN] -> TransportServerConfig -> (Socket -> a -> IO ()) -> IO ()
runTransportServerSocketState ss started getSocket threadLabel srvSupported srvCreds alpn_ =
runTransportServerSocketState_ ss started getSocket threadLabel srvCreds srvParams
where
srvParams = supportedTLSServerParams_ srvSupported srvCreds alpn_
runTransportServerSocket :: Transport a => TMVar Bool -> IO Socket -> String -> T.ServerParams -> TransportServerConfig -> (a -> IO ()) -> IO ()
runTransportServerSocket started getSocket threadLabel serverParams cfg server = do
ss <- atomically newSocketState
runTransportServerSocketState ss started getSocket threadLabel serverParams cfg server
-- | Run a transport server with provided connection setup and handler.
runTransportServerSocketState_ :: Transport a => SocketState -> TMVar Bool -> IO Socket -> String -> (Maybe HostName -> (X.CertificateChain, X.PrivKey)) -> T.ServerParams -> TransportServerConfig -> (Socket -> a -> IO ()) -> IO ()
runTransportServerSocketState_ ss started getSocket threadLabel srvCreds srvParams cfg server = do
runTransportServerSocketState :: Transport a => SocketState -> TMVar Bool -> IO Socket -> String -> T.ServerParams -> TransportServerConfig -> (a -> IO ()) -> IO ()
runTransportServerSocketState ss started getSocket threadLabel serverParams cfg server = do
labelMyThread $ "transport server for " <> threadLabel
runTCPServerSocket ss started getSocket $ \conn ->
E.bracket (setup conn >>= maybe (fail "tls setup timeout") pure) closeConnection (server conn)
E.bracket (setup conn >>= maybe (fail "tls setup timeout") pure) closeConnection server
where
tCfg = serverTransportConfig cfg
setup conn = timeout (tlsSetupTimeout cfg) $ do
labelMyThread $ threadLabel <> "/setup"
tls <- connectTLS Nothing tCfg srvParams conn
getServerConnection tCfg (fst $ srvCreds Nothing) tls
tls <- connectTLS Nothing tCfg serverParams conn
getServerConnection tCfg (fst $ tlsServerCredentials serverParams) tls
tlsServerCredentials :: T.ServerParams -> (X.CertificateChain, X.PrivKey)
tlsServerCredentials serverParams = case T.sharedCredentials $ T.serverShared serverParams of
T.Credentials [creds] -> creds
_ -> error "server has more than one key"
-- | Run TCP server without TLS
runLocalTCPServer :: TMVar Bool -> ServiceName -> (Socket -> IO ()) -> IO ()
runLocalTCPServer started port server = do
ss <- newSocketState
runTCPServerSocket ss started (startTCPServer started (Just "127.0.0.1") port) server
runTCPServer :: TMVar Bool -> ServiceName -> (Socket -> IO ()) -> IO ()
runTCPServer started port server = do
ss <- atomically newSocketState
runTCPServerSocket ss started (startTCPServer started port) server
-- | Wrap socket provider in a TCP server bracket.
runTCPServerSocket :: SocketState -> TMVar Bool -> IO Socket -> (Socket -> IO ()) -> IO ()
runTCPServerSocket (accepted, gracefullyClosed, clients) started getSocket server =
E.bracket getSocket (closeServer started clients) $ \sock ->
forever . E.bracketOnError (safeAccept sock) (close . fst) $ \(conn, _peer) -> do
cId <- atomically $ stateTVar accepted $ \cId -> let cId' = cId + 1 in cId' `seq` (cId', cId')
cId <- atomically $ stateTVar accepted $ \cId -> let cId' = cId + 1 in cId `seq` (cId', cId')
let closeConn _ = do
atomically $ modifyTVar' clients $ IM.delete cId
gracefulClose conn 5000 `catchAll_` pure () -- catchAll_ is needed here in case the connection was closed earlier
@@ -163,21 +148,21 @@ safeAccept sock =
type SocketState = (TVar Int, TVar Int, TVar (IntMap (Weak ThreadId)))
newSocketState :: IO SocketState
newSocketState = (,,) <$> newTVarIO 0 <*> newTVarIO 0 <*> newTVarIO mempty
newSocketState :: STM SocketState
newSocketState = (,,) <$> newTVar 0 <*> newTVar 0 <*> newTVar mempty
closeServer :: TMVar Bool -> TVar (IntMap (Weak ThreadId)) -> Socket -> IO ()
closeServer started clients sock = do
close sock
readTVarIO clients >>= mapM_ (deRefWeak >=> mapM_ killThread)
close sock
void . atomically $ tryPutTMVar started False
startTCPServer :: TMVar Bool -> Maybe HostName -> ServiceName -> IO Socket
startTCPServer started host port = withSocketsDo $ resolve >>= open >>= setStarted
startTCPServer :: TMVar Bool -> ServiceName -> IO Socket
startTCPServer started port = withSocketsDo $ resolve >>= open >>= setStarted
where
resolve =
let hints = defaultHints {addrFlags = [AI_PASSIVE], addrSocketType = Stream}
in select <$> getAddrInfo (Just hints) host (Just port)
in select <$> getAddrInfo (Just hints) Nothing (Just port)
select as = fromJust $ family AF_INET6 <|> family AF_INET
where
family f = find ((== f) . addrFamily) as
@@ -191,33 +176,30 @@ startTCPServer started host port = withSocketsDo $ resolve >>= open >>= setStart
pure sock
setStarted sock = atomically (tryPutTMVar started True) >> pure sock
loadServerCredential :: ServerCredentials -> IO T.Credential
loadServerCredential ServerCredentials {caCertificateFile, certificateFile, privateKeyFile} =
T.credentialLoadX509Chain certificateFile (maybeToList caCertificateFile) privateKeyFile >>= \case
Right credential -> pure credential
Left _ -> putStrLn "invalid credential" >> exitFailure
loadTLSServerParams :: FilePath -> FilePath -> FilePath -> Maybe [ALPN] -> IO T.ServerParams
loadTLSServerParams = loadSupportedTLSServerParams supportedParameters
supportedTLSServerParams :: T.Credential -> Maybe [ALPN] -> T.ServerParams
supportedTLSServerParams = supportedTLSServerParams_ defaultSupportedParams . const
loadSupportedTLSServerParams :: T.Supported -> FilePath -> FilePath -> FilePath -> Maybe [ALPN] -> IO T.ServerParams
loadSupportedTLSServerParams serverSupported caCertificateFile certificateFile privateKeyFile alpn_ = do
tlsServerParams <- fromCredential <$> loadServerCredential
pure tlsServerParams {T.serverHooks = maybe def alpnHooks alpn_}
where
loadServerCredential :: IO T.Credential
loadServerCredential =
T.credentialLoadX509Chain certificateFile [caCertificateFile] privateKeyFile >>= \case
Right credential -> pure credential
Left _ -> putStrLn "invalid credential" >> exitFailure
fromCredential :: T.Credential -> T.ServerParams
fromCredential credential =
def
{ T.serverWantClientCert = False,
T.serverShared = def {T.sharedCredentials = T.Credentials [credential]},
T.serverHooks = def,
T.serverSupported = serverSupported
}
alpnHooks supported = def {T.onALPNClientSuggest = Just $ pure . fromMaybe "" . find (`elem` supported)}
supportedTLSServerParams_ :: T.Supported -> (Maybe HostName -> T.Credential) -> Maybe [ALPN] -> T.ServerParams
supportedTLSServerParams_ serverSupported creds alpn_ =
def
{ T.serverWantClientCert = False,
T.serverHooks =
def
{ T.onServerNameIndication = \host_ -> pure $ T.Credentials [creds host_],
T.onALPNClientSuggest = (\alpn -> pure . fromMaybe "" . find (`elem` alpn)) <$> alpn_
},
T.serverSupported = serverSupported
}
loadFingerprint :: ServerCredentials -> IO Fingerprint
loadFingerprint ServerCredentials {caCertificateFile} = case caCertificateFile of
Just certificateFile -> loadFileFingerprint certificateFile
Nothing -> error "CA file must be used in protocol credentials"
loadFileFingerprint :: FilePath -> IO Fingerprint
loadFileFingerprint certificateFile = do
loadFingerprint :: FilePath -> IO Fingerprint
loadFingerprint certificateFile = do
(cert : _) <- SX.readSignedObject certificateFile
pure $ XV.getFingerprint (cert :: X.SignedExact X.Certificate) X.HashSHA256

Some files were not shown because too many files have changed in this diff Show More