mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-08-31 00:58:22 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5e666ae4f | ||
|
|
62133ceb24 | ||
|
|
3b50e1fb7d | ||
|
|
7c25b3b1e0 | ||
|
|
a70bd02c67 | ||
|
|
990dcec348 | ||
|
|
75712641ee | ||
|
|
946e16339e | ||
|
|
092ed088ca | ||
|
|
dab1980d79 | ||
|
|
344a295845 | ||
|
|
67d38090ed | ||
|
|
a9e8d02593 | ||
|
|
d859f27999 | ||
|
|
e86338d555 | ||
|
|
137ebc1cad | ||
|
|
d84a49b85a | ||
|
|
d5efe3406a | ||
|
|
d559a66145 | ||
|
|
ce6777b68d | ||
|
|
655e7ad7d5 | ||
|
|
9596a03139 | ||
|
|
2e7e476f81 | ||
|
|
2c9ad74599 | ||
|
|
aa60bd6770 | ||
|
|
16cf5c8628 | ||
|
|
56986f82c8 | ||
|
|
53e829a21c | ||
|
|
ab4acadcc1 | ||
|
|
325e679bc4 | ||
|
|
d81cae712e | ||
|
|
1d22608f86 | ||
|
|
39f3cf3082 | ||
|
|
5be81c562f | ||
|
|
2bdda1ff1f | ||
|
|
ac930dff30 | ||
|
|
1cbf8c0015 | ||
|
|
571d148bdf | ||
|
|
32a150eb40 | ||
|
|
5ad6e5f2f3 | ||
|
|
f229e135e3 | ||
|
|
d15d87c111 | ||
|
|
0dd52dc69f | ||
|
|
47dd0c4c64 | ||
|
|
7762b4d364 | ||
|
|
7d8457263b | ||
|
|
fc6b9c0e1b | ||
|
|
a76e15fd77 | ||
|
|
a017047c52 | ||
|
|
f4d1a33ed8 | ||
|
|
03ea151be5 | ||
|
|
83f8622b23 | ||
|
|
3753379ae4 | ||
|
|
2de16cfae8 | ||
|
|
5fa3c149e9 | ||
|
|
9093c7b120 | ||
|
|
47ae921b86 | ||
|
|
c605156302 | ||
|
|
051bf38bc7 | ||
|
|
8423c636a8 | ||
|
|
cc5732f41f | ||
|
|
7565ddd91c | ||
|
|
0de596dbcf | ||
|
|
e59a098e66 | ||
|
|
8d56b0ba85 | ||
|
|
b40d55c358 | ||
|
|
784d36d691 | ||
|
|
1bdfc8ae00 | ||
|
|
291039159f | ||
|
|
d4fa0af350 | ||
|
|
492d2f86bc | ||
|
|
8dd54ced0e | ||
|
|
e56bd0b47b | ||
|
|
ff2b00a029 | ||
|
|
017469b2de | ||
|
|
26979ff6b5 | ||
|
|
21abc5cabe | ||
|
|
6e76221e07 | ||
|
|
743676421d | ||
|
|
9d0774a58e | ||
|
|
ce732c0efb | ||
|
|
1ac0068d22 | ||
|
|
ae8e1c5e9a | ||
|
|
e371a4a62d | ||
|
|
369efc24c2 |
@@ -1,3 +1,83 @@
|
||||
# 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:
|
||||
|
||||
@@ -150,7 +150,7 @@ 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 '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
|
||||
&& if echo 'c90886104cd640b2ed64921dba80e90691db36788e8d6dcc13d8f33f92f0ea54 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
|
||||
|
||||
@@ -15,7 +15,7 @@ logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
setLogLevel LogDebug -- change to LogError in production
|
||||
setLogLevel LogInfo
|
||||
cfgPath <- getEnvPath "NTF_SERVER_CFG_PATH" defaultCfgPath
|
||||
logPath <- getEnvPath "NTF_SERVER_LOG_PATH" defaultLogPath
|
||||
withGlobalLogging logCfg $ ntfServerCLI cfgPath logPath
|
||||
|
||||
+92
-18
@@ -3,8 +3,7 @@ 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"
|
||||
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}')"
|
||||
|
||||
scripts="https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/scripts/main"
|
||||
scripts_systemd_smp="$scripts/smp-server.service"
|
||||
@@ -26,6 +25,8 @@ 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"
|
||||
@@ -65,7 +66,13 @@ ${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}ENTER${NC} to continue or ${RED}Ctrl+C${NC} to cancel installation"
|
||||
Press:
|
||||
- ${GRN}ENTER${NC} to continue installing both xftp and smp servers
|
||||
- ${GRN}1${NC} to install only smp server
|
||||
- ${GRN}2${NC} to install only xftp server
|
||||
- ${RED}Ctrl+C${NC} to cancel installation
|
||||
|
||||
Selection: "
|
||||
|
||||
end="Installtion is complete!
|
||||
|
||||
@@ -76,27 +83,64 @@ Please checkout our server guides:
|
||||
To uninstall with full clean-up, simply run: ${YLW}sudo /usr/local/bin/simplex-servers-uninstall${NC}
|
||||
"
|
||||
|
||||
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() {
|
||||
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"
|
||||
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
|
||||
}
|
||||
|
||||
setup_users() {
|
||||
useradd -M "$user_smp" 2> /dev/null || true
|
||||
useradd -M "$user_xftp" 2> /dev/null || true
|
||||
eval "user=\$user_${1}"
|
||||
|
||||
useradd -M "$user" 2> /dev/null || true
|
||||
|
||||
unset user
|
||||
}
|
||||
|
||||
setup_dirs() {
|
||||
# Unquoted varibles, so field splitting can occur
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
setup_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"
|
||||
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
|
||||
}
|
||||
|
||||
setup_scripts() {
|
||||
@@ -110,32 +154,62 @@ checks() {
|
||||
printf "This script is intended to be run with root privileges. Please re-run script using sudo."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
os_test
|
||||
|
||||
mkdir -p $path_conf_info
|
||||
}
|
||||
|
||||
main() {
|
||||
checks
|
||||
|
||||
printf "%b\n%b\n" "${BLU}$logo${NC}" "$welcome"
|
||||
printf "%b\n%b" "${BLU}$logo${NC}" "$welcome"
|
||||
read ans
|
||||
|
||||
if [ "$ans" = '1' ]; then
|
||||
setup='smp'
|
||||
elif [ "$ans" = '2' ]; then
|
||||
setup='xftp'
|
||||
else
|
||||
setup='smp xftp'
|
||||
fi
|
||||
|
||||
printf "Installing binaries..."
|
||||
setup_bins
|
||||
|
||||
for i in $setup; do
|
||||
setup_bins "$i"
|
||||
done
|
||||
|
||||
printf "${GRN} Done!${NC}\n"
|
||||
|
||||
printf "Creating users..."
|
||||
setup_users
|
||||
|
||||
for i in $setup; do
|
||||
setup_users "$i"
|
||||
done
|
||||
|
||||
printf "${GRN} Done!${NC}\n"
|
||||
|
||||
printf "Creating directories..."
|
||||
setup_dirs
|
||||
|
||||
for i in $setup; do
|
||||
setup_dirs "$i"
|
||||
done
|
||||
|
||||
printf "${GRN} Done!${NC}\n"
|
||||
|
||||
printf "Creating systemd services..."
|
||||
setup_systemd
|
||||
|
||||
for i in $setup; do
|
||||
setup_systemd "$i"
|
||||
done
|
||||
|
||||
printf "${GRN} Done!${NC}\n"
|
||||
|
||||
printf "Installing stopscript, update and uninstallation script..."
|
||||
|
||||
setup_scripts
|
||||
|
||||
printf "${GRN} Done!${NC}\n"
|
||||
|
||||
printf "%b" "$end"
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
name: simplexmq
|
||||
version: 5.8.2.0
|
||||
version: 6.0.4.0
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: |
|
||||
This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
@@ -47,6 +47,7 @@ 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
|
||||
@@ -69,7 +70,7 @@ dependencies:
|
||||
- temporary == 1.3.*
|
||||
- time == 1.12.*
|
||||
- time-manager == 0.0.*
|
||||
- tls >= 1.7.0 && < 1.8
|
||||
- tls >= 1.9.0 && < 1.10
|
||||
- transformers == 0.6.*
|
||||
- unliftio == 0.2.*
|
||||
- unliftio-core == 0.2.*
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# 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.
|
||||
@@ -13,11 +13,15 @@ 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 stop smp-server
|
||||
systemctl stop xftp-server
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
userdel smp && userdel xftp
|
||||
userdel smp 2>/dev/null || true
|
||||
userdel xftp 2>/dev/null || true
|
||||
|
||||
printf "Uninstallation is complete! Thanks for trying out SimpleX!\n"
|
||||
|
||||
@@ -3,8 +3,6 @@ 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"
|
||||
@@ -33,6 +31,9 @@ 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'
|
||||
@@ -40,8 +41,40 @@ RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Currently, XFTP default to v0.1.0, so it doesn't make sense to check its version
|
||||
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}')"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
update_scripts() {
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_update" -o "$path_tmp_bin_update" && chmod +x "$path_tmp_bin_update"
|
||||
@@ -56,6 +89,7 @@ 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"
|
||||
@@ -64,6 +98,7 @@ 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"
|
||||
@@ -71,75 +106,84 @@ 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() {
|
||||
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"
|
||||
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"
|
||||
|
||||
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"
|
||||
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"
|
||||
else
|
||||
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"
|
||||
printf -- "- Updating %s service..." "$service"
|
||||
mv "$path_tmp_systemd" "$path_systemd"
|
||||
systemctl daemon-reload
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
fi
|
||||
|
||||
unset service scripts_systemd path_systemd path_tmp_systemd
|
||||
}
|
||||
|
||||
update_bins() {
|
||||
if [ "$local_version" != "$remote_version" ]; then
|
||||
if systemctl is-active --quiet smp-server; then
|
||||
printf -- "- Stopping smp-server service..."
|
||||
systemctl stop smp-server
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
service="${1}-server"
|
||||
eval "bin=\$bin_${1}"
|
||||
eval "path_bin=\$path_bin_${1}"
|
||||
|
||||
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 smp-server service..."
|
||||
systemctl start smp-server
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
else
|
||||
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"
|
||||
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}')"
|
||||
|
||||
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 systemctl is-active --quiet xftp-server; then
|
||||
printf -- "- Stopping xftp-server service..."
|
||||
systemctl stop xftp-server
|
||||
if [ "$local_version" != "$remote_version" ]; then
|
||||
if systemctl is-active --quiet "$service"; then
|
||||
printf -- "- Stopping %s service..." "$service"
|
||||
systemctl stop "$service"
|
||||
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 -- "- Updating %s to %s..." "$service" "$remote_version"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$bin" -o "$path_bin" && chmod +x "$path_bin"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
|
||||
printf -- "- Starting xftp-server service..."
|
||||
systemctl start xftp-server
|
||||
printf -- "- Starting %s service..." "$service"
|
||||
systemctl start "$service"
|
||||
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 -- "- Updating %s to %s..." "$service" "$remote_version"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$bin" -o "$path_bin" && chmod +x "$path_bin"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
fi
|
||||
else
|
||||
printf -- "- ${YLW}smp-server and xftp-server binaries is up-to-date${NC}.\n"
|
||||
printf -- "- ${YLW}%s is up-to-date${NC}.\n" "$service"
|
||||
fi
|
||||
|
||||
set_ver "$1"
|
||||
|
||||
unset service bin path_bin local_version
|
||||
}
|
||||
|
||||
checks() {
|
||||
@@ -147,6 +191,11 @@ checks() {
|
||||
printf "This script is intended to be run with root privileges. Please re-run script using sudo.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
os_test
|
||||
installed_test
|
||||
|
||||
mkdir -p $path_conf_info
|
||||
}
|
||||
|
||||
main() {
|
||||
@@ -163,10 +212,14 @@ main() {
|
||||
fi
|
||||
|
||||
printf "Updating systemd services...\n"
|
||||
update_systemd
|
||||
for i in $apps; do
|
||||
update_systemd "$i"
|
||||
done
|
||||
|
||||
printf "Updating simplex server binaries...\n"
|
||||
update_bins
|
||||
printf "Updating simplex servers...\n"
|
||||
for i in $apps; do
|
||||
update_bins "$i"
|
||||
done
|
||||
|
||||
rm -rf "$path_tmp_bin"
|
||||
}
|
||||
|
||||
+14
-8
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplexmq
|
||||
version: 5.8.2.0
|
||||
version: 6.0.4.0
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
<./docs/Simplex-Messaging-Client.html client> and
|
||||
@@ -133,8 +133,8 @@ library
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240223_connections_wait_delivery
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240225_ratchet_kem
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240518_servers_stats
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240624_snd_secure
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240702_servers_stats
|
||||
Simplex.Messaging.Agent.TRcvQueues
|
||||
Simplex.Messaging.Client
|
||||
Simplex.Messaging.Client.Agent
|
||||
@@ -236,6 +236,7 @@ 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
|
||||
@@ -258,7 +259,7 @@ library
|
||||
, temporary ==1.3.*
|
||||
, time ==1.12.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.7.0 && <1.8
|
||||
, tls >=1.9.0 && <1.10
|
||||
, transformers ==0.6.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
@@ -310,6 +311,7 @@ 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
|
||||
@@ -333,7 +335,7 @@ executable ntf-server
|
||||
, temporary ==1.3.*
|
||||
, time ==1.12.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.7.0 && <1.8
|
||||
, tls >=1.9.0 && <1.10
|
||||
, transformers ==0.6.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
@@ -389,6 +391,7 @@ 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
|
||||
@@ -412,7 +415,7 @@ executable smp-server
|
||||
, temporary ==1.3.*
|
||||
, time ==1.12.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.7.0 && <1.8
|
||||
, tls >=1.9.0 && <1.10
|
||||
, transformers ==0.6.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
@@ -467,6 +470,7 @@ 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
|
||||
@@ -490,7 +494,7 @@ executable xftp
|
||||
, temporary ==1.3.*
|
||||
, time ==1.12.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.7.0 && <1.8
|
||||
, tls >=1.9.0 && <1.10
|
||||
, transformers ==0.6.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
@@ -542,6 +546,7 @@ 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
|
||||
@@ -565,7 +570,7 @@ executable xftp-server
|
||||
, temporary ==1.3.*
|
||||
, time ==1.12.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.7.0 && <1.8
|
||||
, tls >=1.9.0 && <1.10
|
||||
, transformers ==0.6.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
@@ -653,6 +658,7 @@ test-suite simplexmq-test
|
||||
, directory ==1.3.*
|
||||
, filepath ==1.4.*
|
||||
, generic-random ==1.5.*
|
||||
, hashable ==1.4.*
|
||||
, hourglass ==0.2.*
|
||||
, hspec ==2.11.*
|
||||
, hspec-core ==2.11.*
|
||||
@@ -681,7 +687,7 @@ test-suite simplexmq-test
|
||||
, time ==1.12.*
|
||||
, time-manager ==0.0.*
|
||||
, timeit ==2.0.*
|
||||
, tls >=1.7.0 && <1.8
|
||||
, tls >=1.9.0 && <1.10
|
||||
, transformers ==0.6.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
module Simplex.FileTransfer.Agent
|
||||
( startXFTPWorkers,
|
||||
startXFTPSndWorkers,
|
||||
closeXFTPAgent,
|
||||
toFSFilePath,
|
||||
-- Receiving files
|
||||
@@ -42,13 +43,14 @@ 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 (Map)
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (mapMaybe)
|
||||
import Data.Maybe (fromMaybe, mapMaybe)
|
||||
import qualified Data.Set as S
|
||||
import Data.Text (Text)
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import Data.Time.Format (defaultTimeLocale, formatTime)
|
||||
import Simplex.FileTransfer.Chunks (toKB)
|
||||
import Simplex.FileTransfer.Client (XFTPChunkSpec (..))
|
||||
import Simplex.FileTransfer.Client.Main
|
||||
import Simplex.FileTransfer.Crypto
|
||||
@@ -72,7 +74,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 (EntityId, ProtocolServer, ProtocolType (..), XFTPServer)
|
||||
import Simplex.Messaging.Protocol (ProtocolServer, ProtocolType (..), XFTPServer)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Util (catchAll_, liftError, tshow, unlessM, whenM)
|
||||
import System.FilePath (takeFileName, (</>))
|
||||
@@ -81,13 +83,21 @@ import UnliftIO.Directory
|
||||
import qualified UnliftIO.Exception as E
|
||||
|
||||
startXFTPWorkers :: AgentClient -> Maybe FilePath -> AM ()
|
||||
startXFTPWorkers c workDir = do
|
||||
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
|
||||
wd <- asks $ xftpWorkDir . xftpAgent
|
||||
atomically $ writeTVar wd workDir
|
||||
cfg <- asks config
|
||||
startRcvFiles cfg
|
||||
when allWorkers $ startRcvFiles cfg
|
||||
startSndFiles cfg
|
||||
startDelFiles cfg
|
||||
when allWorkers $ startDelFiles cfg
|
||||
where
|
||||
startRcvFiles :: AgentConfig -> AM ()
|
||||
startRcvFiles AgentConfig {rcvFilesTTL} = do
|
||||
@@ -174,16 +184,18 @@ runXFTPRcvWorker c srv Worker {doWork} = do
|
||||
cfg <- asks config
|
||||
forever $ do
|
||||
lift $ waitForWork doWork
|
||||
atomically $ assertAgentForeground c
|
||||
liftIO $ 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 = []}, _) -> 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
|
||||
(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
|
||||
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
|
||||
@@ -191,25 +203,26 @@ runXFTPRcvWorker c srv Worker {doWork} = do
|
||||
where
|
||||
retryLoop loop e replicaDelay = do
|
||||
flip catchAgentError (\_ -> pure ()) $ do
|
||||
when (serverHostError e) $ notify c rcvFileEntityId $ RFWARN e
|
||||
when (serverHostError e) $ notify c (fromMaybe rcvFileEntityId redirectEntityId_) (RFWARN e)
|
||||
liftIO $ closeXFTPServerClient c userId server digest
|
||||
withStore' c $ \db -> updateRcvChunkReplicaDelay db rcvChunkReplicaId replicaDelay
|
||||
atomically $ assertAgentForeground c
|
||||
liftIO $ assertAgentForeground c
|
||||
loop
|
||||
retryDone e = do
|
||||
atomically . incXFTPServerStat c userId srv $ case e of
|
||||
XFTP _ XFTP.AUTH -> downloadAuthErrs
|
||||
_ -> downloadErrs
|
||||
rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) e
|
||||
rcvWorkerInternalError c rcvFileId rcvFileEntityId redirectEntityId_ (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
|
||||
fsFileTmpPath <- lift $ toFSFilePath fileTmpPath
|
||||
chunkPath <- uniqueCombine fsFileTmpPath $ show chunkNo
|
||||
let chunkSpec = XFTPRcvChunkSpec chunkPath (unFileSize chunkSize) (unFileDigest digest)
|
||||
let chSize = unFileSize chunkSize
|
||||
chunkSpec = XFTPRcvChunkSpec chunkPath chSize (unFileDigest digest)
|
||||
relChunkPath = fileTmpPath </> takeFileName chunkPath
|
||||
agentXFTPDownloadChunk c userId digest replica chunkSpec
|
||||
atomically $ waitUntilForeground c
|
||||
liftIO $ 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
|
||||
@@ -221,13 +234,14 @@ runXFTPRcvWorker c srv Worker {doWork} = do
|
||||
liftIO . when complete $ updateRcvFileStatus db rcvFileId RFSReceived
|
||||
pure (entityId, complete, RFPROG rcvd total)
|
||||
atomically $ incXFTPServerStat c userId srv downloads
|
||||
atomically $ incXFTPServerSizeStat c userId srv downloadsSize (fromIntegral $ toKB chSize)
|
||||
notify c entityId progress
|
||||
when complete . lift . void $
|
||||
getXFTPRcvWorker True c Nothing
|
||||
where
|
||||
ipAddressProtected' :: AM Bool
|
||||
ipAddressProtected' = do
|
||||
cfg <- liftIO $ getNetworkConfig' c
|
||||
cfg <- liftIO $ getFastNetworkConfig c
|
||||
pure $ ipAddressProtected cfg srv
|
||||
receivedSize :: [RcvFileChunk] -> Int64
|
||||
receivedSize = foldl' (\sz ch -> sz + receivedChunkSize ch) 0
|
||||
@@ -249,25 +263,25 @@ retryOnError name loop done e = do
|
||||
then loop
|
||||
else done
|
||||
|
||||
rcvWorkerInternalError :: AgentClient -> DBRcvFileId -> RcvFileId -> Maybe FilePath -> AgentErrorType -> AM ()
|
||||
rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath err = do
|
||||
rcvWorkerInternalError :: AgentClient -> DBRcvFileId -> RcvFileId -> Maybe RcvFileId -> Maybe FilePath -> AgentErrorType -> AM ()
|
||||
rcvWorkerInternalError c rcvFileId rcvFileEntityId redirectEntityId_ tmpPath err = do
|
||||
lift $ forM_ tmpPath (removePath <=< toFSFilePath)
|
||||
withStore' c $ \db -> updateRcvFileError db rcvFileId (show err)
|
||||
notify c rcvFileEntityId $ RFERR err
|
||||
notify c (fromMaybe rcvFileEntityId redirectEntityId_) (RFERR err)
|
||||
|
||||
runXFTPRcvLocalWorker :: AgentClient -> Worker -> AM ()
|
||||
runXFTPRcvLocalWorker c Worker {doWork} = do
|
||||
cfg <- asks config
|
||||
forever $ do
|
||||
lift $ waitForWork doWork
|
||||
atomically $ assertAgentForeground c
|
||||
liftIO $ assertAgentForeground c
|
||||
runXFTPOperation cfg
|
||||
where
|
||||
runXFTPOperation :: AgentConfig -> AM ()
|
||||
runXFTPOperation AgentConfig {rcvFilesTTL} =
|
||||
withWork c doWork (`getNextRcvFileToDecrypt` rcvFilesTTL) $
|
||||
\f@RcvFile {rcvFileId, rcvFileEntityId, tmpPath} ->
|
||||
decryptFile f `catchAgentError` rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath
|
||||
\f@RcvFile {rcvFileId, rcvFileEntityId, tmpPath, redirect} ->
|
||||
decryptFile f `catchAgentError` rcvWorkerInternalError c rcvFileId rcvFileEntityId (redirectEntityId <$> redirect) tmpPath
|
||||
decryptFile :: RcvFile -> AM ()
|
||||
decryptFile RcvFile {rcvFileId, rcvFileEntityId, size, digest, key, nonce, tmpPath, saveFile, status, chunks, redirect} = do
|
||||
let CryptoFile savePath cfArgs = saveFile
|
||||
@@ -286,12 +300,12 @@ runXFTPRcvLocalWorker c Worker {doWork} = do
|
||||
Nothing -> do
|
||||
notify c rcvFileEntityId $ RFDONE fsSavePath
|
||||
lift $ forM_ tmpPath (removePath <=< toFSFilePath)
|
||||
atomically $ waitUntilForeground c
|
||||
liftIO $ waitUntilForeground c
|
||||
withStore' c (`updateRcvFileComplete` rcvFileId)
|
||||
Just RcvFileRedirect {redirectFileInfo, redirectDbId} -> do
|
||||
let RedirectFileInfo {size = redirectSize, digest = redirectDigest} = redirectFileInfo
|
||||
lift $ forM_ tmpPath (removePath <=< toFSFilePath)
|
||||
atomically $ waitUntilForeground c
|
||||
liftIO $ 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)
|
||||
@@ -333,7 +347,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 -> EntityId -> AEvent e -> m ()
|
||||
notify :: forall m e. (MonadIO m, AEntityI e) => AgentClient -> AEntityId -> AEvent e -> m ()
|
||||
notify c entId cmd = atomically $ writeTBQueue (subQ c) ("", entId, AEvt (sAEntity @e) cmd)
|
||||
|
||||
xftpSendFile' :: AgentClient -> UserId -> CryptoFile -> Int -> AM SndFileId
|
||||
@@ -379,7 +393,7 @@ runXFTPSndPrepareWorker c Worker {doWork} = do
|
||||
cfg <- asks config
|
||||
forever $ do
|
||||
lift $ waitForWork doWork
|
||||
atomically $ assertAgentForeground c
|
||||
liftIO $ assertAgentForeground c
|
||||
runXFTPOperation cfg
|
||||
where
|
||||
runXFTPOperation :: AgentConfig -> AM ()
|
||||
@@ -441,7 +455,7 @@ runXFTPSndPrepareWorker c Worker {doWork} = do
|
||||
SndFileChunkReplica {server} : _ -> Right server
|
||||
createChunk :: Int -> SndFileChunk -> AM (ProtocolServer 'PXFTP)
|
||||
createChunk numRecipients' ch = do
|
||||
atomically $ assertAgentForeground c
|
||||
liftIO $ assertAgentForeground c
|
||||
(replica, ProtoServerWithAuth srv _) <- tryCreate
|
||||
withStore' c $ \db -> createSndFileReplica db ch replica
|
||||
pure srv
|
||||
@@ -449,8 +463,9 @@ runXFTPSndPrepareWorker c Worker {doWork} = do
|
||||
tryCreate = do
|
||||
usedSrvs <- newTVarIO ([] :: [XFTPServer])
|
||||
let AgentClient {xftpServers} = c
|
||||
userSrvCount <- length <$> atomically (TM.lookup userId xftpServers)
|
||||
userSrvCount <- liftIO $ length <$> TM.lookupIO userId xftpServers
|
||||
withRetryIntervalCount (riFast ri) $ \n _ loop -> do
|
||||
liftIO $ waitWhileSuspended c
|
||||
liftIO $ waitForUserNetwork c
|
||||
let triedAllSrvs = n > userSrvCount
|
||||
createWithNextSrv usedSrvs
|
||||
@@ -460,7 +475,7 @@ runXFTPSndPrepareWorker c Worker {doWork} = do
|
||||
retryLoop loop triedAllSrvs e = do
|
||||
flip catchAgentError (\_ -> pure ()) $ do
|
||||
when (triedAllSrvs && serverHostError e) $ notify c sndFileEntityId $ SFWARN e
|
||||
atomically $ assertAgentForeground c
|
||||
liftIO $ assertAgentForeground c
|
||||
loop
|
||||
createWithNextSrv usedSrvs = do
|
||||
deleted <- withStore' c $ \db -> getSndFileDeleted db sndFileId
|
||||
@@ -480,7 +495,7 @@ runXFTPSndWorker c srv Worker {doWork} = do
|
||||
cfg <- asks config
|
||||
forever $ do
|
||||
lift $ waitForWork doWork
|
||||
atomically $ assertAgentForeground c
|
||||
liftIO $ assertAgentForeground c
|
||||
runXFTPOperation cfg
|
||||
where
|
||||
runXFTPOperation :: AgentConfig -> AM ()
|
||||
@@ -490,6 +505,7 @@ 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
|
||||
@@ -500,20 +516,20 @@ 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
|
||||
atomically $ assertAgentForeground c
|
||||
liftIO $ assertAgentForeground c
|
||||
loop
|
||||
retryDone e = do
|
||||
atomically $ incXFTPServerStat c userId srv uploadErrs
|
||||
sndWorkerInternalError c sndFileId sndFileEntityId (Just filePrefixPath) e
|
||||
uploadFileChunk :: AgentConfig -> SndFileChunk -> SndFileChunkReplica -> AM ()
|
||||
uploadFileChunk AgentConfig {xftpMaxRecipientsPerRequest = maxRecipients} sndFileChunk@SndFileChunk {sndFileId, userId, chunkSpec = chunkSpec@XFTPChunkSpec {filePath}, digest = chunkDigest} replica = do
|
||||
uploadFileChunk AgentConfig {xftpMaxRecipientsPerRequest = maxRecipients} sndFileChunk@SndFileChunk {sndFileId, userId, chunkSpec = chunkSpec@XFTPChunkSpec {filePath, chunkSize = chSize}, digest = chunkDigest} replica = do
|
||||
replica'@SndFileChunkReplica {sndChunkReplicaId} <- addRecipients sndFileChunk replica
|
||||
fsFilePath <- lift $ toFSFilePath filePath
|
||||
unlessM (doesFileExist fsFilePath) $ throwE $ FILE NO_FILE
|
||||
let chunkSpec' = chunkSpec {filePath = fsFilePath} :: XFTPChunkSpec
|
||||
atomically $ assertAgentForeground c
|
||||
liftIO $ assertAgentForeground c
|
||||
agentXFTPUploadChunk c userId chunkDigest replica' chunkSpec'
|
||||
atomically $ waitUntilForeground c
|
||||
liftIO $ waitUntilForeground c
|
||||
sf@SndFile {sndFileEntityId, prefixPath, chunks} <- withStore c $ \db -> do
|
||||
updateSndChunkReplicaStatus db sndChunkReplicaId SFRSUploaded
|
||||
getSndFile db sndFileId
|
||||
@@ -521,6 +537,7 @@ runXFTPSndWorker c srv Worker {doWork} = do
|
||||
total = totalSize chunks
|
||||
complete = all chunkUploaded chunks
|
||||
atomically $ incXFTPServerStat c userId srv uploads
|
||||
atomically $ incXFTPServerSizeStat c userId srv uploadsSize (fromIntegral $ toKB chSize)
|
||||
notify c sndFileEntityId $ SFPROG uploaded total
|
||||
when complete $ do
|
||||
(sndDescr, rcvDescrs) <- sndFileToDescrs sf
|
||||
@@ -650,7 +667,7 @@ runXFTPDelWorker c srv Worker {doWork} = do
|
||||
cfg <- asks config
|
||||
forever $ do
|
||||
lift $ waitForWork doWork
|
||||
atomically $ assertAgentForeground c
|
||||
liftIO $ assertAgentForeground c
|
||||
runXFTPOperation cfg
|
||||
where
|
||||
runXFTPOperation :: AgentConfig -> AM ()
|
||||
@@ -661,6 +678,7 @@ 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
|
||||
@@ -671,7 +689,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
|
||||
atomically $ assertAgentForeground c
|
||||
liftIO $ assertAgentForeground c
|
||||
loop
|
||||
retryDone e = do
|
||||
atomically $ incXFTPServerStat c userId srv deleteErrs
|
||||
@@ -686,7 +704,7 @@ delWorkerInternalError c deletedSndChunkReplicaId e = do
|
||||
withStore' c $ \db -> deleteDeletedSndChunkReplica db deletedSndChunkReplicaId
|
||||
notify c "" $ SFERR e
|
||||
|
||||
assertAgentForeground :: AgentClient -> STM ()
|
||||
assertAgentForeground :: AgentClient -> IO ()
|
||||
assertAgentForeground c = do
|
||||
throwWhenInactive c
|
||||
waitUntilForeground c
|
||||
|
||||
@@ -26,6 +26,10 @@ kb :: Integral a => a -> a
|
||||
kb n = 1024 * n
|
||||
{-# INLINE kb #-}
|
||||
|
||||
toKB :: Integral a => a -> a
|
||||
toKB n = n `div` 1024
|
||||
{-# INLINE toKB #-}
|
||||
|
||||
mb :: Integral a => a -> a
|
||||
mb n = 1024 * kb n
|
||||
{-# INLINE mb #-}
|
||||
|
||||
@@ -50,6 +50,7 @@ import Simplex.Messaging.Protocol
|
||||
ProtocolServer (..),
|
||||
RecipientId,
|
||||
SenderId,
|
||||
pattern NoEntity,
|
||||
)
|
||||
import Simplex.Messaging.Transport (ALPN, HandshakeError (..), THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), supportedParameters)
|
||||
import Simplex.Messaging.Transport.Client (TransportClientConfig, TransportHost, alpn)
|
||||
@@ -222,7 +223,7 @@ createXFTPChunk ::
|
||||
Maybe BasicAuth ->
|
||||
ExceptT XFTPClientError IO (SenderId, NonEmpty RecipientId)
|
||||
createXFTPChunk c spKey file rcps auth_ =
|
||||
sendXFTPCommand c spKey "" (FNEW file rcps auth_) Nothing >>= \case
|
||||
sendXFTPCommand c spKey NoEntity (FNEW file rcps auth_) Nothing >>= \case
|
||||
(FRSndIds sId rIds, body) -> noFile body (sId, rIds)
|
||||
(r, _) -> throwE $ unexpectedResponse r
|
||||
|
||||
@@ -278,7 +279,7 @@ pingXFTP :: XFTPClient -> ExceptT XFTPClientError IO ()
|
||||
pingXFTP c@XFTPClient {thParams} = do
|
||||
t <-
|
||||
liftEither . first PCETransportError $
|
||||
xftpEncodeTransmission thParams ("", "", FileCmd SFRecipient PING)
|
||||
xftpEncodeTransmission thParams ("", NoEntity, FileCmd SFRecipient PING)
|
||||
(r, _) <- sendXFTPTransmission c t Nothing
|
||||
case r of
|
||||
FRPong -> pure ()
|
||||
|
||||
@@ -53,9 +53,9 @@ defaultXFTPClientAgentConfig =
|
||||
data XFTPClientAgentError = XFTPClientAgentError XFTPServer XFTPClientError
|
||||
deriving (Show, Exception)
|
||||
|
||||
newXFTPAgent :: XFTPClientAgentConfig -> STM XFTPClientAgent
|
||||
newXFTPAgent :: XFTPClientAgentConfig -> IO XFTPClientAgent
|
||||
newXFTPAgent config = do
|
||||
xftpClients <- TM.empty
|
||||
xftpClients <- TM.emptyIO
|
||||
pure XFTPClientAgent {xftpClients, config}
|
||||
|
||||
type ME a = ExceptT XFTPClientAgentError IO a
|
||||
|
||||
@@ -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 (Map)
|
||||
import qualified Data.Map as M
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict 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 <- atomically $ newXFTPAgent defaultXFTPClientAgentConfig
|
||||
a <- liftIO $ 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 <- atomically $ newXFTPAgent defaultXFTPClientAgentConfig
|
||||
a <- liftIO $ 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 <- atomically $ newXFTPAgent defaultXFTPClientAgentConfig
|
||||
a <- liftIO $ newXFTPAgent defaultXFTPClientAgentConfig
|
||||
forM_ chunks $ deleteFileChunk a
|
||||
liftIO $ do
|
||||
printNoNewLine "File deleted!"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DerivingStrategies #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
@@ -52,8 +54,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 (Map)
|
||||
import qualified Data.Map as M
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.String
|
||||
import Data.Text (Text)
|
||||
@@ -139,12 +141,9 @@ data FileChunkReplica = FileChunkReplica
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
newtype ChunkReplicaId = ChunkReplicaId {unChunkReplicaId :: ByteString}
|
||||
newtype ChunkReplicaId = ChunkReplicaId {unChunkReplicaId :: XFTPFileId}
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding ChunkReplicaId where
|
||||
strEncode (ChunkReplicaId fid) = strEncode fid
|
||||
strP = ChunkReplicaId <$> strP
|
||||
deriving newtype (StrEncoding)
|
||||
|
||||
instance FromJSON ChunkReplicaId where
|
||||
parseJSON = strParseJSON "ChunkReplicaId"
|
||||
@@ -153,10 +152,6 @@ 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,
|
||||
|
||||
@@ -41,6 +41,7 @@ import Simplex.Messaging.Protocol
|
||||
ProtocolType (..),
|
||||
RcvPublicAuthKey,
|
||||
RcvPublicDhKey,
|
||||
EntityId (..),
|
||||
RecipientId,
|
||||
SenderId,
|
||||
SentRawTransmission,
|
||||
@@ -170,7 +171,7 @@ data FileInfo = FileInfo
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
type XFTPFileId = ByteString
|
||||
type XFTPFileId = EntityId
|
||||
|
||||
instance FilePartyI p => ProtocolEncoding XFTPVersion XFTPErrorType (FileCommand p) where
|
||||
type Tag (FileCommand p) = FileCommandTag p
|
||||
@@ -191,7 +192,7 @@ instance FilePartyI p => ProtocolEncoding XFTPVersion XFTPErrorType (FileCommand
|
||||
fromProtocolError = fromProtocolError @XFTPVersion @XFTPErrorType @FileResponse
|
||||
{-# INLINE fromProtocolError #-}
|
||||
|
||||
checkCredentials (auth, _, fileId, _) cmd = case cmd of
|
||||
checkCredentials (auth, _, EntityId fileId, _) cmd = case cmd of
|
||||
-- FNEW must not have signature and chunk ID
|
||||
FNEW {}
|
||||
| isNothing auth -> Left $ CMD NO_AUTH
|
||||
@@ -301,7 +302,7 @@ instance ProtocolEncoding XFTPVersion XFTPErrorType FileResponse where
|
||||
PEBlock -> BLOCK
|
||||
{-# INLINE fromProtocolError #-}
|
||||
|
||||
checkCredentials (_, _, entId, _) cmd = case cmd of
|
||||
checkCredentials (_, _, EntityId entId, _) cmd = case cmd of
|
||||
FRSndIds {} -> noEntity
|
||||
-- ERR response does not always have entity ID
|
||||
FRErr _ -> Right cmd
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
@@ -32,11 +33,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
|
||||
@@ -52,9 +53,10 @@ 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 (..), RcvPublicAuthKey, RcvPublicDhKey, RecipientId, TransmissionAuth)
|
||||
import Simplex.Messaging.Protocol (CorrId (..), EntityId (..), RcvPublicAuthKey, RcvPublicDhKey, RecipientId, TransmissionAuth, pattern NoEntity)
|
||||
import Simplex.Messaging.Server (dummyVerifyCmd, verifyCmdAuthorization)
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Server.QueueStore (RoundedSystemTime, getRoundedSystemTime)
|
||||
import Simplex.Messaging.Server.Stats
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
@@ -63,7 +65,7 @@ import Simplex.Messaging.Transport.Buffer (trimCR)
|
||||
import Simplex.Messaging.Transport.HTTP2
|
||||
import Simplex.Messaging.Transport.HTTP2.File (fileBlockSize)
|
||||
import Simplex.Messaging.Transport.HTTP2.Server
|
||||
import Simplex.Messaging.Transport.Server (runTCPServer, tlsServerCredentials)
|
||||
import Simplex.Messaging.Transport.Server (runLocalTCPServer, tlsServerCredentials)
|
||||
import Simplex.Messaging.Util
|
||||
import Simplex.Messaging.Version
|
||||
import System.Exit (exitFailure)
|
||||
@@ -112,7 +114,7 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
|
||||
Right pk' -> pure pk'
|
||||
Left e -> putStrLn ("servers has no valid key: " <> show e) >> exitFailure
|
||||
env <- ask
|
||||
sessions <- atomically TM.empty
|
||||
sessions <- liftIO TM.emptyIO
|
||||
let cleanup sessionId = atomically $ TM.delete sessionId sessions
|
||||
liftIO . runHTTP2Server started xftpPort defaultHTTP2BufferSize serverParams transportConfig inactiveClientExpiration cleanup $ \sessionId sessionALPN r sendResponse -> do
|
||||
reqBody <- getHTTP2Body r xftpBlockSize
|
||||
@@ -207,17 +209,17 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
|
||||
withFile statsFilePath AppendMode $ \h -> liftIO $ do
|
||||
hSetBuffering h LineBuffering
|
||||
ts <- getCurrentTime
|
||||
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
|
||||
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
|
||||
hPutStrLn h $
|
||||
intercalate
|
||||
","
|
||||
@@ -247,7 +249,7 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
|
||||
u <- askUnliftIO
|
||||
liftIO $ do
|
||||
labelMyThread "control port server"
|
||||
runTCPServer cpStarted port $ runCPClient u
|
||||
runLocalTCPServer cpStarted port $ runCPClient u
|
||||
where
|
||||
runCPClient :: UnliftIO (ReaderT XFTPEnv IO) -> Socket -> IO ()
|
||||
runCPClient u sock = do
|
||||
@@ -309,7 +311,7 @@ data ServerFile = ServerFile
|
||||
|
||||
processRequest :: XFTPTransportRequest -> M ()
|
||||
processRequest XFTPTransportRequest {thParams, reqBody = body@HTTP2Body {bodyHead}, sendResponse}
|
||||
| B.length bodyHead /= xftpBlockSize = sendXFTPResponse ("", "", FRErr BLOCK) Nothing
|
||||
| B.length bodyHead /= xftpBlockSize = sendXFTPResponse ("", NoEntity, FRErr BLOCK) Nothing
|
||||
| otherwise = do
|
||||
case xftpDecodeTransmission thParams bodyHead of
|
||||
Right (sig_, signed, (corrId, fId, cmdOrErr)) ->
|
||||
@@ -322,7 +324,7 @@ processRequest XFTPTransportRequest {thParams, reqBody = body@HTTP2Body {bodyHea
|
||||
Left e -> send (FRErr e) Nothing
|
||||
where
|
||||
send resp = sendXFTPResponse (corrId, fId, resp)
|
||||
Left e -> sendXFTPResponse ("", "", FRErr e) Nothing
|
||||
Left e -> sendXFTPResponse ("", NoEntity, FRErr e) Nothing
|
||||
where
|
||||
sendXFTPResponse (corrId, fId, resp) serverFile_ = do
|
||||
let t_ = xftpEncodeTransmission thParams (corrId, fId, resp)
|
||||
@@ -397,7 +399,7 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
|
||||
r <- runExceptT $ do
|
||||
sizes <- asks $ allowedChunkSizes . config
|
||||
unless (size file `elem` sizes) $ throwE SIZE
|
||||
ts <- liftIO getSystemTime
|
||||
ts <- liftIO getFileTime
|
||||
-- TODO validate body empty
|
||||
sId <- ExceptT $ addFileRetry st file 3 ts
|
||||
rcps <- mapM (ExceptT . addRecipientRetry st 3 sId) rks
|
||||
@@ -405,12 +407,12 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
|
||||
logAddFile sl sId file ts
|
||||
logAddRecipients sl sId rcps
|
||||
stats <- asks serverStats
|
||||
atomically $ modifyTVar' (filesCreated stats) (+ 1)
|
||||
atomically $ modifyTVar' (fileRecipients stats) (+ length rks)
|
||||
lift $ incFileStat filesCreated
|
||||
liftIO $ atomicModifyIORef'_ (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 -> SystemTime -> M (Either XFTPErrorType XFTPFileId)
|
||||
addFileRetry :: FileStore -> FileInfo -> Int -> RoundedSystemTime -> M (Either XFTPErrorType XFTPFileId)
|
||||
addFileRetry st file n ts =
|
||||
retryAdd n $ \sId -> runExceptT $ do
|
||||
ExceptT $ addFile st sId file ts
|
||||
@@ -435,7 +437,7 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
|
||||
rcps <- mapM (ExceptT . addRecipientRetry st 3 sId) rks
|
||||
lift $ withFileLog $ \sl -> logAddRecipients sl sId rcps
|
||||
stats <- asks serverStats
|
||||
atomically $ modifyTVar' (fileRecipients stats) (+ length rks)
|
||||
liftIO $ atomicModifyIORef'_ (fileRecipients stats) (+ length rks)
|
||||
let rIds = L.map (\(FileRecipient rId _) -> rId) rcps
|
||||
pure $ FRRcvIds rIds
|
||||
pure $ either FRErr id r
|
||||
@@ -463,19 +465,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 senderId)
|
||||
let fPath = path </> B.unpack (B64.encode $ unEntityId senderId)
|
||||
receiveChunk (XFTPRcvChunkSpec fPath size digest) >>= \case
|
||||
Right () -> do
|
||||
stats <- asks serverStats
|
||||
withFileLog $ \sl -> logPutFile sl senderId fPath
|
||||
atomically $ writeTVar filePath (Just fPath)
|
||||
atomically $ modifyTVar' (filesUploaded stats) (+ 1)
|
||||
atomically $ modifyTVar' (filesCount stats) (+ 1)
|
||||
atomically $ modifyTVar' (filesSize stats) (+ fromIntegral size)
|
||||
incFileStat filesUploaded
|
||||
incFileStat filesCount
|
||||
liftIO $ atomicModifyIORef'_ (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
|
||||
@@ -494,8 +496,8 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
|
||||
case LC.cbInit dhSecret cbNonce of
|
||||
Right sbState -> do
|
||||
stats <- asks serverStats
|
||||
atomically $ modifyTVar' (fileDownloads stats) (+ 1)
|
||||
atomically $ updatePeriodStats (filesDownloaded stats) senderId
|
||||
incFileStat fileDownloads
|
||||
liftIO $ updatePeriodStats (filesDownloaded stats) senderId
|
||||
pure (FRFile sDhKey cbNonce, Just ServerFile {filePath = path, fileSize = size, sbState})
|
||||
_ -> pure (FRErr INTERNAL, Nothing)
|
||||
_ -> pure (FRErr NO_FILE, Nothing)
|
||||
@@ -511,8 +513,7 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
|
||||
withFileLog (`logAckFile` rId)
|
||||
st <- asks store
|
||||
atomically $ deleteRecipient st rId fr
|
||||
stats <- asks serverStats
|
||||
atomically $ modifyTVar' (fileDownloadAcks stats) (+ 1)
|
||||
incFileStat fileDownloadAcks
|
||||
pure FROk
|
||||
|
||||
deleteServerFile_ :: FileRec -> M (Either XFTPErrorType ())
|
||||
@@ -524,11 +525,14 @@ 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
|
||||
atomically $ modifyTVar' (filesDeleted stats) (+ 1)
|
||||
lift $ incFileStat filesDeleted
|
||||
where
|
||||
deletedStats stats = do
|
||||
atomically $ modifyTVar' (filesCount stats) (subtract 1)
|
||||
atomically $ modifyTVar' (filesSize stats) (subtract $ fromIntegral $ size fileInfo)
|
||||
liftIO $ atomicModifyIORef'_ (filesCount stats) (subtract 1)
|
||||
liftIO $ atomicModifyIORef'_ (filesSize stats) (subtract $ fromIntegral $ size fileInfo)
|
||||
|
||||
getFileTime :: IO RoundedSystemTime
|
||||
getFileTime = getRoundedSystemTime fileTimePrecision
|
||||
|
||||
expireServerFiles :: Maybe Int -> ExpirationConfig -> M ()
|
||||
expireServerFiles itemDelay expCfg = do
|
||||
@@ -554,29 +558,26 @@ 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
|
||||
FileServerStats {filesExpired} <- asks serverStats
|
||||
atomically $ modifyTVar' filesExpired (+ 1)
|
||||
incFileStat filesExpired
|
||||
|
||||
randomId :: Int -> M ByteString
|
||||
randomId n = atomically . C.randomBytes n =<< asks random
|
||||
|
||||
getFileId :: M XFTPFileId
|
||||
getFileId = do
|
||||
size <- asks (fileIdSize . config)
|
||||
atomically . C.randomBytes size =<< asks random
|
||||
getFileId = fmap EntityId . randomId =<< asks (fileIdSize . config)
|
||||
|
||||
withFileLog :: (StoreLog 'WriteMode -> IO a) -> M ()
|
||||
withFileLog action = liftIO . mapM_ action =<< asks storeLog
|
||||
|
||||
incFileStat :: (FileServerStats -> TVar Int) -> M ()
|
||||
incFileStat :: (FileServerStats -> IORef Int) -> M ()
|
||||
incFileStat statSel = do
|
||||
stats <- asks serverStats
|
||||
atomically $ modifyTVar (statSel stats) (+ 1)
|
||||
liftIO $ atomicModifyIORef'_ (statSel stats) (+ 1)
|
||||
|
||||
saveServerStats :: M ()
|
||||
saveServerStats =
|
||||
asks (serverStatsBackupFile . config)
|
||||
>>= mapM_ (\f -> asks serverStats >>= atomically . getFileServerStatsData >>= liftIO . saveStats f)
|
||||
>>= mapM_ (\f -> asks serverStats >>= liftIO . getFileServerStatsData >>= liftIO . saveStats f)
|
||||
where
|
||||
saveStats f stats = do
|
||||
logInfo $ "saving server stats to file " <> T.pack f
|
||||
@@ -594,7 +595,7 @@ restoreServerStats = asks (serverStatsBackupFile . config) >>= mapM_ restoreStat
|
||||
FileStore {files, usedStorage} <- asks store
|
||||
_filesCount <- M.size <$> readTVarIO files
|
||||
_filesSize <- readTVarIO usedStorage
|
||||
atomically $ setFileServerStats s d {_filesCount, _filesSize}
|
||||
liftIO $ 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,7 +4,7 @@
|
||||
module Simplex.FileTransfer.Server.Control where
|
||||
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString (ByteString)
|
||||
import Simplex.FileTransfer.Protocol (XFTPFileId)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (BasicAuth)
|
||||
|
||||
@@ -13,7 +13,7 @@ data CPClientRole = CPRNone | CPRUser | CPRAdmin
|
||||
data ControlProtocol
|
||||
= CPAuth BasicAuth
|
||||
| CPStatsRTS
|
||||
| CPDelete ByteString
|
||||
| CPDelete XFTPFileId
|
||||
| CPHelp
|
||||
| CPQuit
|
||||
| CPSkip
|
||||
|
||||
@@ -11,7 +11,6 @@ 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)
|
||||
@@ -105,17 +104,17 @@ supportedXFTPhandshakes = ["xftp/1"]
|
||||
|
||||
newXFTPServerEnv :: XFTPServerConfig -> IO XFTPEnv
|
||||
newXFTPServerEnv config@XFTPServerConfig {storeLogFile, fileSizeQuota, caCertificateFile, certificateFile, privateKeyFile, transportConfig} = do
|
||||
random <- liftIO C.newRandom
|
||||
store <- atomically newFileStore
|
||||
storeLog <- liftIO $ mapM (`readWriteFileStore` store) storeLogFile
|
||||
random <- C.newRandom
|
||||
store <- newFileStore
|
||||
storeLog <- 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!"
|
||||
tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig)
|
||||
Fingerprint fp <- liftIO $ loadFingerprint caCertificateFile
|
||||
serverStats <- atomically . newFileServerStats =<< liftIO getCurrentTime
|
||||
tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig)
|
||||
Fingerprint fp <- loadFingerprint caCertificateFile
|
||||
serverStats <- newFileServerStats =<< getCurrentTime
|
||||
pure XFTPEnv {config, store, storeLog, random, tlsServerParams, serverIdentity = C.KeyHash fp, serverStats}
|
||||
|
||||
countUsedStorage :: M.Map k FileRec -> Int64
|
||||
|
||||
@@ -7,25 +7,24 @@ 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 :: 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
|
||||
{ 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
|
||||
}
|
||||
|
||||
data FileServerStatsData = FileServerStatsData
|
||||
@@ -35,7 +34,7 @@ data FileServerStatsData = FileServerStatsData
|
||||
_filesUploaded :: Int,
|
||||
_filesExpired :: Int,
|
||||
_filesDeleted :: Int,
|
||||
_filesDownloaded :: PeriodStatsData SenderId,
|
||||
_filesDownloaded :: PeriodStatsData,
|
||||
_fileDownloads :: Int,
|
||||
_fileDownloadAcks :: Int,
|
||||
_filesCount :: Int,
|
||||
@@ -43,49 +42,50 @@ data FileServerStatsData = FileServerStatsData
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
newFileServerStats :: UTCTime -> STM FileServerStats
|
||||
newFileServerStats :: UTCTime -> IO FileServerStats
|
||||
newFileServerStats ts = do
|
||||
fromTime <- newTVar ts
|
||||
filesCreated <- newTVar 0
|
||||
fileRecipients <- newTVar 0
|
||||
filesUploaded <- newTVar 0
|
||||
filesExpired <- newTVar 0
|
||||
filesDeleted <- newTVar 0
|
||||
fromTime <- newIORef ts
|
||||
filesCreated <- newIORef 0
|
||||
fileRecipients <- newIORef 0
|
||||
filesUploaded <- newIORef 0
|
||||
filesExpired <- newIORef 0
|
||||
filesDeleted <- newIORef 0
|
||||
filesDownloaded <- newPeriodStats
|
||||
fileDownloads <- newTVar 0
|
||||
fileDownloadAcks <- newTVar 0
|
||||
filesCount <- newTVar 0
|
||||
filesSize <- newTVar 0
|
||||
fileDownloads <- newIORef 0
|
||||
fileDownloadAcks <- newIORef 0
|
||||
filesCount <- newIORef 0
|
||||
filesSize <- newIORef 0
|
||||
pure FileServerStats {fromTime, filesCreated, fileRecipients, filesUploaded, filesExpired, filesDeleted, filesDownloaded, fileDownloads, fileDownloadAcks, filesCount, filesSize}
|
||||
|
||||
getFileServerStatsData :: FileServerStats -> STM FileServerStatsData
|
||||
getFileServerStatsData :: FileServerStats -> IO FileServerStatsData
|
||||
getFileServerStatsData s = do
|
||||
_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
|
||||
_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
|
||||
_filesDownloaded <- getPeriodStatsData $ filesDownloaded s
|
||||
_fileDownloads <- readTVar $ fileDownloads s
|
||||
_fileDownloadAcks <- readTVar $ fileDownloadAcks s
|
||||
_filesCount <- readTVar $ filesCount s
|
||||
_filesSize <- readTVar $ filesSize s
|
||||
_fileDownloads <- readIORef $ fileDownloads s
|
||||
_fileDownloadAcks <- readIORef $ fileDownloadAcks s
|
||||
_filesCount <- readIORef $ filesCount s
|
||||
_filesSize <- readIORef $ filesSize s
|
||||
pure FileServerStatsData {_fromTime, _filesCreated, _fileRecipients, _filesUploaded, _filesExpired, _filesDeleted, _filesDownloaded, _fileDownloads, _fileDownloadAcks, _filesCount, _filesSize}
|
||||
|
||||
setFileServerStats :: FileServerStats -> FileServerStatsData -> STM ()
|
||||
-- this function is not thread safe, it is used on server start only
|
||||
setFileServerStats :: FileServerStats -> FileServerStatsData -> IO ()
|
||||
setFileServerStats s d = do
|
||||
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
|
||||
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
|
||||
setPeriodStats (filesDownloaded s) $! _filesDownloaded d
|
||||
writeTVar (fileDownloads s) $! _fileDownloads d
|
||||
writeTVar (fileDownloadAcks s) $! _fileDownloadAcks d
|
||||
writeTVar (filesCount s) $! _filesCount d
|
||||
writeTVar (filesSize s) $! _filesSize d
|
||||
writeIORef (fileDownloads s) $! _fileDownloads d
|
||||
writeIORef (fileDownloadAcks s) $! _fileDownloadAcks d
|
||||
writeIORef (filesCount s) $! _filesCount d
|
||||
writeIORef (filesSize s) $! _filesSize d
|
||||
|
||||
instance StrEncoding FileServerStatsData where
|
||||
strEncode FileServerStatsData {_fromTime, _filesCreated, _fileRecipients, _filesUploaded, _filesExpired, _filesDeleted, _filesDownloaded, _fileDownloads, _fileDownloadAcks, _filesCount, _filesSize} =
|
||||
|
||||
@@ -17,6 +17,7 @@ module Simplex.FileTransfer.Server.Store
|
||||
expiredFilePath,
|
||||
getFile,
|
||||
ackFile,
|
||||
fileTimePrecision,
|
||||
)
|
||||
where
|
||||
|
||||
@@ -25,12 +26,12 @@ import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Int (Int64)
|
||||
import Data.Set (Set)
|
||||
import qualified Data.Set as S
|
||||
import Data.Time.Clock.System (SystemTime (..))
|
||||
import Simplex.FileTransfer.Protocol (FileInfo (..), SFileParty (..), XFTPFileId)
|
||||
import Simplex.FileTransfer.Transport (XFTPErrorType (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (RcvPublicAuthKey, RecipientId, SenderId)
|
||||
import Simplex.Messaging.Server.QueueStore (RoundedSystemTime (..))
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Util (ifM, ($>>=))
|
||||
@@ -46,30 +47,33 @@ data FileRec = FileRec
|
||||
fileInfo :: FileInfo,
|
||||
filePath :: TVar (Maybe FilePath),
|
||||
recipientIds :: TVar (Set RecipientId),
|
||||
createdAt :: SystemTime
|
||||
createdAt :: RoundedSystemTime
|
||||
}
|
||||
|
||||
fileTimePrecision :: Int64
|
||||
fileTimePrecision = 3600 -- truncate creation time to 1 hour
|
||||
|
||||
data FileRecipient = FileRecipient RecipientId RcvPublicAuthKey
|
||||
|
||||
instance StrEncoding FileRecipient where
|
||||
strEncode (FileRecipient rId rKey) = strEncode rId <> ":" <> strEncode rKey
|
||||
strP = FileRecipient <$> strP <* A.char ':' <*> strP
|
||||
|
||||
newFileStore :: STM FileStore
|
||||
newFileStore :: IO FileStore
|
||||
newFileStore = do
|
||||
files <- TM.empty
|
||||
recipients <- TM.empty
|
||||
usedStorage <- newTVar 0
|
||||
files <- TM.emptyIO
|
||||
recipients <- TM.emptyIO
|
||||
usedStorage <- newTVarIO 0
|
||||
pure FileStore {files, recipients, usedStorage}
|
||||
|
||||
addFile :: FileStore -> SenderId -> FileInfo -> SystemTime -> STM (Either XFTPErrorType ())
|
||||
addFile :: FileStore -> SenderId -> FileInfo -> RoundedSystemTime -> STM (Either XFTPErrorType ())
|
||||
addFile FileStore {files} sId fileInfo createdAt =
|
||||
ifM (TM.member sId files) (pure $ Left DUPLICATE_) $ do
|
||||
f <- newFileRec sId fileInfo createdAt
|
||||
TM.insert sId f files
|
||||
pure $ Right ()
|
||||
|
||||
newFileRec :: SenderId -> FileInfo -> SystemTime -> STM FileRec
|
||||
newFileRec :: SenderId -> FileInfo -> RoundedSystemTime -> STM FileRec
|
||||
newFileRec senderId fileInfo createdAt = do
|
||||
recipientIds <- newTVar S.empty
|
||||
filePath <- newTVar Nothing
|
||||
@@ -120,8 +124,8 @@ getFile st party fId = case party of
|
||||
expiredFilePath :: FileStore -> XFTPFileId -> Int64 -> STM (Maybe (Maybe FilePath))
|
||||
expiredFilePath FileStore {files} sId old =
|
||||
TM.lookup sId files
|
||||
$>>= \FileRec {filePath, createdAt} ->
|
||||
if systemSeconds createdAt < old
|
||||
$>>= \FileRec {filePath, createdAt = RoundedSystemTime createdAt} ->
|
||||
if createdAt + fileTimePrecision < old
|
||||
then Just <$> readTVar filePath
|
||||
else pure Nothing
|
||||
|
||||
|
||||
@@ -28,18 +28,18 @@ import Data.List.NonEmpty (NonEmpty)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Time.Clock.System (SystemTime)
|
||||
import Simplex.FileTransfer.Protocol (FileInfo (..))
|
||||
import Simplex.FileTransfer.Server.Store
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (RcvPublicAuthKey, RecipientId, SenderId)
|
||||
import Simplex.Messaging.Server.QueueStore (RoundedSystemTime)
|
||||
import Simplex.Messaging.Server.StoreLog
|
||||
import Simplex.Messaging.Util (bshow, whenM)
|
||||
import System.Directory (doesFileExist, renameFile)
|
||||
import System.IO
|
||||
|
||||
data FileStoreLogRecord
|
||||
= AddFile SenderId FileInfo SystemTime
|
||||
= AddFile SenderId FileInfo RoundedSystemTime
|
||||
| PutFile SenderId FilePath
|
||||
| AddRecipients SenderId (NonEmpty FileRecipient)
|
||||
| DeleteFile SenderId
|
||||
@@ -64,7 +64,7 @@ instance StrEncoding FileStoreLogRecord where
|
||||
logFileStoreRecord :: StoreLog 'WriteMode -> FileStoreLogRecord -> IO ()
|
||||
logFileStoreRecord = writeStoreLogRecord
|
||||
|
||||
logAddFile :: StoreLog 'WriteMode -> SenderId -> FileInfo -> SystemTime -> IO ()
|
||||
logAddFile :: StoreLog 'WriteMode -> SenderId -> FileInfo -> RoundedSystemTime -> IO ()
|
||||
logAddFile s = logFileStoreRecord s .:. AddFile
|
||||
|
||||
logPutFile :: StoreLog 'WriteMode -> SenderId -> FilePath -> IO ()
|
||||
|
||||
@@ -25,9 +25,9 @@ import Simplex.Messaging.Parsers
|
||||
import Simplex.Messaging.Protocol (XFTPServer)
|
||||
import System.FilePath ((</>))
|
||||
|
||||
type RcvFileId = ByteString
|
||||
type RcvFileId = ByteString -- Agent entity ID
|
||||
|
||||
type SndFileId = ByteString
|
||||
type SndFileId = ByteString -- Agent entity ID
|
||||
|
||||
authTagSize :: Int64
|
||||
authTagSize = fromIntegral C.authTagSize
|
||||
|
||||
+299
-198
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
|
||||
|
||||
@@ -15,7 +16,12 @@ module Simplex.Messaging.Agent.Env.SQLite
|
||||
AM,
|
||||
AgentConfig (..),
|
||||
InitialAgentServers (..),
|
||||
ServerCfg (..),
|
||||
UserServers (..),
|
||||
NetworkConfig (..),
|
||||
presetServerCfg,
|
||||
enabledServerCfg,
|
||||
mkUserServers,
|
||||
defaultAgentConfig,
|
||||
defaultReconnectInterval,
|
||||
tryAgentError,
|
||||
@@ -35,14 +41,19 @@ module Simplex.Messaging.Agent.Env.SQLite
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent (ThreadId)
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Monad.Reader
|
||||
import Crypto.Random
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import qualified Data.Aeson.TH as JQ
|
||||
import Data.ByteArray (ScrubbedBytes)
|
||||
import Data.Int (Int64)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import Data.Map (Map)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map.Strict (Map)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Time.Clock (NominalDiffTime, nominalDay)
|
||||
import Data.Time.Clock.System (SystemTime (..))
|
||||
import Data.Word (Word16)
|
||||
@@ -59,14 +70,16 @@ import Simplex.Messaging.Crypto.Ratchet (VersionRangeE2E, supportedE2EEncryptVRa
|
||||
import Simplex.Messaging.Notifications.Client (defaultNTFClientConfig)
|
||||
import Simplex.Messaging.Notifications.Transport (NTFVersion)
|
||||
import Simplex.Messaging.Notifications.Types
|
||||
import Simplex.Messaging.Protocol (NtfServer, VersionRangeSMPC, XFTPServer, XFTPServerWithAuth, supportedSMPClientVRange)
|
||||
import Simplex.Messaging.Parsers (defaultJSON)
|
||||
import Simplex.Messaging.Protocol (NtfServer, ProtoServerWithAuth, ProtocolServer, ProtocolType (..), ProtocolTypeI, VersionRangeSMPC, XFTPServer, supportedSMPClientVRange)
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport (SMPVersion, TLS, Transport (..))
|
||||
import Simplex.Messaging.Transport.Client (defaultSMPPort)
|
||||
import Simplex.Messaging.Util (allFinally, catchAllErrors, catchAllErrors', tryAllErrors, tryAllErrors')
|
||||
import System.Mem.Weak (Weak)
|
||||
import System.Random (StdGen, newStdGen)
|
||||
import UnliftIO (Async, SomeException)
|
||||
import UnliftIO (SomeException)
|
||||
import UnliftIO.STM
|
||||
|
||||
type AM' a = ReaderT Env IO a
|
||||
@@ -74,12 +87,38 @@ type AM' a = ReaderT Env IO a
|
||||
type AM a = ExceptT AgentErrorType (ReaderT Env IO) a
|
||||
|
||||
data InitialAgentServers = InitialAgentServers
|
||||
{ smp :: Map UserId (NonEmpty SMPServerWithAuth),
|
||||
{ smp :: Map UserId (NonEmpty (ServerCfg 'PSMP)),
|
||||
ntf :: [NtfServer],
|
||||
xftp :: Map UserId (NonEmpty XFTPServerWithAuth),
|
||||
xftp :: Map UserId (NonEmpty (ServerCfg 'PXFTP)),
|
||||
netCfg :: NetworkConfig
|
||||
}
|
||||
|
||||
data ServerCfg p = ServerCfg
|
||||
{ server :: ProtoServerWithAuth p,
|
||||
preset :: Bool,
|
||||
tested :: Maybe Bool,
|
||||
enabled :: Bool
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
enabledServerCfg :: ProtoServerWithAuth p -> ServerCfg p
|
||||
enabledServerCfg server = ServerCfg {server, preset = False, tested = Nothing, enabled = True}
|
||||
|
||||
presetServerCfg :: Bool -> ProtoServerWithAuth p -> ServerCfg p
|
||||
presetServerCfg enabled server = ServerCfg {server, preset = True, tested = Nothing, enabled}
|
||||
|
||||
data UserServers p = UserServers
|
||||
{ enabledSrvs :: NonEmpty (ProtoServerWithAuth p),
|
||||
knownSrvs :: NonEmpty (ProtocolServer p)
|
||||
}
|
||||
|
||||
-- This function sets all servers as enabled in case all passed servers are disabled.
|
||||
mkUserServers :: NonEmpty (ServerCfg p) -> UserServers p
|
||||
mkUserServers srvs = UserServers {enabledSrvs, knownSrvs}
|
||||
where
|
||||
enabledSrvs = L.map (\ServerCfg {server} -> server) $ fromMaybe srvs $ L.nonEmpty $ L.filter (\ServerCfg {enabled} -> enabled) srvs
|
||||
knownSrvs = L.map (\ServerCfg {server = ProtoServerWithAuth srv _} -> srv) srvs
|
||||
|
||||
data AgentConfig = AgentConfig
|
||||
{ tcpPort :: Maybe ServiceName,
|
||||
rcvAuthAlg :: C.AuthAlg,
|
||||
@@ -111,10 +150,7 @@ data AgentConfig = AgentConfig
|
||||
xftpMaxRecipientsPerRequest :: Int,
|
||||
deleteErrorCount :: Int,
|
||||
ntfCron :: Word16,
|
||||
ntfWorkerDelay :: Int,
|
||||
ntfSMPWorkerDelay :: Int,
|
||||
ntfSubCheckInterval :: NominalDiffTime,
|
||||
ntfMaxMessages :: Int,
|
||||
caCertificateFile :: FilePath,
|
||||
privateKeyFile :: FilePath,
|
||||
certificateFile :: FilePath,
|
||||
@@ -128,7 +164,7 @@ defaultReconnectInterval =
|
||||
RetryInterval
|
||||
{ initialInterval = 2_000000,
|
||||
increaseAfter = 10_000000,
|
||||
maxInterval = 60_000000
|
||||
maxInterval = 180_000000
|
||||
}
|
||||
|
||||
defaultMessageRetryInterval :: RetryInterval2
|
||||
@@ -138,7 +174,7 @@ defaultMessageRetryInterval =
|
||||
RetryInterval
|
||||
{ initialInterval = 2_000000,
|
||||
increaseAfter = 10_000000,
|
||||
maxInterval = 60_000000
|
||||
maxInterval = 120_000000
|
||||
},
|
||||
riSlow =
|
||||
RetryInterval
|
||||
@@ -183,10 +219,7 @@ defaultAgentConfig =
|
||||
xftpMaxRecipientsPerRequest = 200,
|
||||
deleteErrorCount = 10,
|
||||
ntfCron = 20, -- minutes
|
||||
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",
|
||||
@@ -211,8 +244,8 @@ newSMPAgentEnv :: AgentConfig -> SQLiteStore -> IO Env
|
||||
newSMPAgentEnv config store = do
|
||||
random <- C.newRandom
|
||||
randomServer <- newTVarIO =<< liftIO newStdGen
|
||||
ntfSupervisor <- atomically . newNtfSubSupervisor $ tbqSize config
|
||||
xftpAgent <- atomically newXFTPAgent
|
||||
ntfSupervisor <- newNtfSubSupervisor $ tbqSize config
|
||||
xftpAgent <- newXFTPAgent
|
||||
multicastSubscribers <- newTMVarIO 0
|
||||
pure Env {config, store, random, randomServer, ntfSupervisor, xftpAgent, multicastSubscribers}
|
||||
|
||||
@@ -229,12 +262,12 @@ data NtfSupervisor = NtfSupervisor
|
||||
data NtfSupervisorCommand = NSCCreate | NSCDelete | NSCSmpDelete | NSCNtfWorker NtfServer | NSCNtfSMPWorker SMPServer
|
||||
deriving (Show)
|
||||
|
||||
newNtfSubSupervisor :: Natural -> STM NtfSupervisor
|
||||
newNtfSubSupervisor :: Natural -> IO NtfSupervisor
|
||||
newNtfSubSupervisor qSize = do
|
||||
ntfTkn <- newTVar Nothing
|
||||
ntfSubQ <- newTBQueue qSize
|
||||
ntfWorkers <- TM.empty
|
||||
ntfSMPWorkers <- TM.empty
|
||||
ntfTkn <- newTVarIO Nothing
|
||||
ntfSubQ <- newTBQueueIO qSize
|
||||
ntfWorkers <- TM.emptyIO
|
||||
ntfSMPWorkers <- TM.emptyIO
|
||||
pure NtfSupervisor {ntfTkn, ntfSubQ, ntfWorkers, ntfSMPWorkers}
|
||||
|
||||
data XFTPAgent = XFTPAgent
|
||||
@@ -245,12 +278,12 @@ data XFTPAgent = XFTPAgent
|
||||
xftpDelWorkers :: TMap XFTPServer Worker
|
||||
}
|
||||
|
||||
newXFTPAgent :: STM XFTPAgent
|
||||
newXFTPAgent :: IO XFTPAgent
|
||||
newXFTPAgent = do
|
||||
xftpWorkDir <- newTVar Nothing
|
||||
xftpRcvWorkers <- TM.empty
|
||||
xftpSndWorkers <- TM.empty
|
||||
xftpDelWorkers <- TM.empty
|
||||
xftpWorkDir <- newTVarIO Nothing
|
||||
xftpRcvWorkers <- TM.emptyIO
|
||||
xftpSndWorkers <- TM.emptyIO
|
||||
xftpDelWorkers <- TM.emptyIO
|
||||
pure XFTPAgent {xftpWorkDir, xftpRcvWorkers, xftpSndWorkers, xftpDelWorkers}
|
||||
|
||||
tryAgentError :: AM a -> AM (Either AgentErrorType a)
|
||||
@@ -281,7 +314,7 @@ mkInternal = INTERNAL . show
|
||||
data Worker = Worker
|
||||
{ workerId :: Int,
|
||||
doWork :: TMVar (),
|
||||
action :: TMVar (Maybe (Async ())),
|
||||
action :: TMVar (Maybe (Weak ThreadId)),
|
||||
restarts :: TVar RestartCount
|
||||
}
|
||||
|
||||
@@ -294,3 +327,12 @@ updateRestartCount :: SystemTime -> RestartCount -> RestartCount
|
||||
updateRestartCount t (RestartCount minute count) = do
|
||||
let min' = systemSeconds t `div` 60
|
||||
in RestartCount min' $ if minute == min' then count + 1 else 1
|
||||
|
||||
$(pure [])
|
||||
|
||||
instance ProtocolTypeI p => ToJSON (ServerCfg p) where
|
||||
toEncoding = $(JQ.mkToEncoding defaultJSON ''ServerCfg)
|
||||
toJSON = $(JQ.mkToJSON defaultJSON ''ServerCfg)
|
||||
|
||||
instance ProtocolTypeI p => FromJSON (ServerCfg p) where
|
||||
parseJSON = $(JQ.mkParseJSON defaultJSON ''ServerCfg)
|
||||
|
||||
@@ -20,8 +20,8 @@ where
|
||||
|
||||
import Control.Logger.Simple (logError, logInfo)
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Reader
|
||||
import Control.Monad.Trans.Except
|
||||
import Data.Bifunctor (first)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Text (Text)
|
||||
@@ -31,6 +31,7 @@ import Simplex.Messaging.Agent.Client
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Agent.Protocol (AEvent (..), AEvt (..), AgentErrorType (..), BrokerErrorType (..), ConnId, NotificationsMode (..), SAEntity (..))
|
||||
import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Agent.Stats
|
||||
import Simplex.Messaging.Agent.Store
|
||||
import Simplex.Messaging.Agent.Store.SQLite
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
@@ -40,7 +41,7 @@ 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, threadDelay)
|
||||
import UnliftIO.Concurrent (forkIO)
|
||||
import qualified UnliftIO.Exception as E
|
||||
|
||||
runNtfSupervisor :: AgentClient -> AM' ()
|
||||
@@ -64,7 +65,7 @@ processNtfSub c (connId, cmd) = do
|
||||
logInfo $ "processNtfSub - connId = " <> tshow connId <> " - cmd = " <> tshow cmd
|
||||
case cmd of
|
||||
NSCCreate -> do
|
||||
(a, RcvQueue {server = smpServer, clientNtfCreds}) <- withStore c $ \db -> runExceptT $ do
|
||||
(a, RcvQueue {userId, server = smpServer, clientNtfCreds}) <- withStore c $ \db -> runExceptT $ do
|
||||
a <- liftIO $ getNtfSubscription db connId
|
||||
q <- ExceptT $ getPrimaryRcvQueue db connId
|
||||
pure (a, q)
|
||||
@@ -74,12 +75,12 @@ processNtfSub c (connId, cmd) = do
|
||||
withTokenServer $ \ntfServer -> do
|
||||
case clientNtfCreds of
|
||||
Just ClientNtfCreds {notifierId} -> do
|
||||
let newSub = newNtfSubscription connId smpServer (Just notifierId) ntfServer NASKey
|
||||
withStore c $ \db -> createNtfSubscription db newSub $ NtfSubNTFAction NSACreate
|
||||
let newSub = newNtfSubscription userId connId smpServer (Just notifierId) ntfServer NASKey
|
||||
withStore c $ \db -> createNtfSubscription db newSub $ NSANtf NSACreate
|
||||
lift . void $ getNtfNTFWorker True c ntfServer
|
||||
Nothing -> do
|
||||
let newSub = newNtfSubscription connId smpServer Nothing ntfServer NASNew
|
||||
withStore c $ \db -> createNtfSubscription db newSub $ NtfSubSMPAction NSASmpKey
|
||||
let newSub = newNtfSubscription userId connId smpServer Nothing ntfServer NASNew
|
||||
withStore c $ \db -> createNtfSubscription db newSub $ NSASMP NSASmpKey
|
||||
lift . void $ getNtfSMPWorker True c smpServer
|
||||
(Just (sub@NtfSubscription {ntfSubStatus, ntfServer = subNtfServer, smpServer = smpServer', ntfQueueId}, action_)) -> do
|
||||
case (clientNtfCreds, ntfQueueId) of
|
||||
@@ -99,24 +100,24 @@ processNtfSub c (connId, cmd) = do
|
||||
if ntfSubStatus == NASNew || ntfSubStatus == NASOff || ntfSubStatus == NASDeleted
|
||||
then resetSubscription
|
||||
else withTokenServer $ \ntfServer -> do
|
||||
withStore' c $ \db -> supervisorUpdateNtfSub db sub {ntfServer} (NtfSubNTFAction NSACreate)
|
||||
withStore' c $ \db -> supervisorUpdateNtfSub db sub {ntfServer} (NSANtf 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
|
||||
NSANtf _ -> lift . void $ getNtfNTFWorker True c subNtfServer
|
||||
NSASMP _ -> lift . void $ getNtfSMPWorker True c smpServer
|
||||
rotate :: AM ()
|
||||
rotate = do
|
||||
withStore' c $ \db -> supervisorUpdateNtfSub db sub (NtfSubNTFAction NSARotate)
|
||||
withStore' c $ \db -> supervisorUpdateNtfSub db sub (NSANtf NSARotate)
|
||||
lift . void $ getNtfNTFWorker True c subNtfServer
|
||||
resetSubscription :: AM ()
|
||||
resetSubscription =
|
||||
withTokenServer $ \ntfServer -> do
|
||||
let sub' = sub {ntfQueueId = Nothing, ntfServer, ntfSubId = Nothing, ntfSubStatus = NASNew}
|
||||
withStore' c $ \db -> supervisorUpdateNtfSub db sub' (NtfSubSMPAction NSASmpKey)
|
||||
withStore' c $ \db -> supervisorUpdateNtfSub db sub' (NSASMP NSASmpKey)
|
||||
lift . void $ getNtfSMPWorker True c smpServer
|
||||
NSCDelete -> do
|
||||
sub_ <- withStore' c $ \db -> do
|
||||
supervisorUpdateNtfAction db connId (NtfSubNTFAction NSADelete)
|
||||
supervisorUpdateNtfAction db connId (NSANtf NSADelete)
|
||||
getNtfSubscription db connId
|
||||
logInfo $ "processNtfSub, NSCDelete - sub_ = " <> tshow sub_
|
||||
case sub_ of
|
||||
@@ -126,7 +127,7 @@ processNtfSub c (connId, cmd) = do
|
||||
withStore' c (`getPrimaryRcvQueue` connId) >>= \case
|
||||
Right rq@RcvQueue {server = smpServer} -> do
|
||||
logInfo $ "processNtfSub, NSCSmpDelete - rq = " <> tshow rq
|
||||
withStore' c $ \db -> supervisorUpdateNtfAction db connId (NtfSubSMPAction NSASmpDelete)
|
||||
withStore' c $ \db -> supervisorUpdateNtfAction db connId (NSASMP NSASmpDelete)
|
||||
lift . void $ getNtfSMPWorker True c smpServer
|
||||
_ -> notifyInternalError c connId "NSCSmpDelete - no rcv queue"
|
||||
NSCNtfWorker ntfServer -> lift . void $ getNtfNTFWorker True c ntfServer
|
||||
@@ -146,12 +147,10 @@ withTokenServer :: (NtfServer -> AM ()) -> AM ()
|
||||
withTokenServer action = lift getNtfToken >>= mapM_ (\NtfToken {ntfServer} -> action ntfServer)
|
||||
|
||||
runNtfWorker :: AgentClient -> NtfServer -> Worker -> AM ()
|
||||
runNtfWorker c srv Worker {doWork} = do
|
||||
delay <- asks $ ntfWorkerDelay . config
|
||||
runNtfWorker c srv Worker {doWork} =
|
||||
forever $ do
|
||||
waitForWork doWork
|
||||
ExceptT $ agentOperationBracket c AONtfNetwork throwWhenInactive $ runExceptT runNtfOperation
|
||||
threadDelay delay
|
||||
where
|
||||
runNtfOperation :: AM ()
|
||||
runNtfOperation =
|
||||
@@ -160,70 +159,73 @@ runNtfWorker c srv Worker {doWork} = do
|
||||
logInfo $ "runNtfWorker, nextSub " <> tshow nextSub
|
||||
ri <- asks $ reconnectInterval . config
|
||||
withRetryInterval ri $ \_ loop -> do
|
||||
liftIO $ waitWhileSuspended c
|
||||
liftIO $ waitForUserNetwork c
|
||||
processSub nextSub
|
||||
`catchAgentError` retryOnError c "NtfWorker" loop (workerInternalError c connId . show)
|
||||
processSub :: (NtfSubscription, NtfSubNTFAction, NtfActionTs) -> AM ()
|
||||
processSub (sub@NtfSubscription {connId, smpServer, ntfSubId}, action, actionTs) = do
|
||||
processSub (sub@NtfSubscription {userId, connId, smpServer, ntfSubId}, action, actionTs) = do
|
||||
ts <- liftIO getCurrentTime
|
||||
unlessM (lift $ rescheduleAction doWork ts actionTs) $
|
||||
case action of
|
||||
NSACreate ->
|
||||
lift getNtfToken >>= \case
|
||||
Just tkn@NtfToken {ntfTokenId = Just tknId, ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
|
||||
Just tkn@NtfToken {ntfServer, ntfTokenId = Just tknId, ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
|
||||
RcvQueue {clientNtfCreds} <- withStore c (`getPrimaryRcvQueue` connId)
|
||||
case clientNtfCreds of
|
||||
Just ClientNtfCreds {ntfPrivateKey, notifierId} -> do
|
||||
atomically $ incNtfServerStat c userId ntfServer ntfCreateAttempts
|
||||
nSubId <- agentNtfCreateSubscription c tknId tkn (SMPQueueNtf smpServer notifierId) ntfPrivateKey
|
||||
atomically $ incNtfServerStat c userId ntfServer ntfCreated
|
||||
-- possible improvement: smaller retry until Active, less frequently (daily?) once Active
|
||||
let actionTs' = addUTCTime 30 ts
|
||||
withStore' c $ \db ->
|
||||
updateNtfSubscription db sub {ntfSubId = Just nSubId, ntfSubStatus = NASCreated NSNew} (NtfSubNTFAction NSACheck) actionTs'
|
||||
updateNtfSubscription db sub {ntfSubId = Just nSubId, ntfSubStatus = NASCreated NSNew} (NSANtf NSACheck) actionTs'
|
||||
_ -> workerInternalError c connId "NSACreate - no notifier queue credentials"
|
||||
_ -> workerInternalError c connId "NSACreate - no active token"
|
||||
NSACheck ->
|
||||
lift getNtfToken >>= \case
|
||||
Just tkn ->
|
||||
Just tkn@NtfToken {ntfServer} ->
|
||||
case ntfSubId of
|
||||
Just nSubId ->
|
||||
Just nSubId -> do
|
||||
atomically $ incNtfServerStat c userId ntfServer ntfCheckAttempts
|
||||
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"
|
||||
withStore' c $ \db ->
|
||||
updateNtfSubscription db sub {ntfServer, ntfQueueId = Nothing, ntfSubId = Nothing, ntfSubStatus = NASNew} (NSASMP NSASmpKey) ts
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ writeTBQueue (ntfSubQ ns) (connId, NSCNtfSMPWorker smpServer)
|
||||
status -> updateSubNextCheck ts status
|
||||
atomically $ incNtfServerStat c userId ntfServer ntfChecked
|
||||
Nothing -> workerInternalError c connId "NSACheck - no subscription ID"
|
||||
_ -> workerInternalError c connId "NSACheck - no active token"
|
||||
NSADelete -> 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)
|
||||
NSADelete ->
|
||||
deleteNtfSub $ do
|
||||
let sub' = sub {ntfSubId = Nothing, ntfSubStatus = NASOff}
|
||||
withStore' c $ \db -> updateNtfSubscription db sub' (NSASMP NSASmpDelete) ts
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ writeTBQueue (ntfSubQ ns) (connId, NSCNtfSMPWorker smpServer)
|
||||
NSARotate ->
|
||||
deleteNtfSub $ do
|
||||
withStore' c $ \db -> deleteNtfSubscription db connId
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ writeTBQueue (ntfSubQ ns) (connId, NSCCreate)
|
||||
where
|
||||
deleteNtfSub continue = case ntfSubId of
|
||||
Just nSubId ->
|
||||
lift getNtfToken >>= \case
|
||||
Just tkn@NtfToken {ntfServer} -> do
|
||||
atomically $ incNtfServerStat c userId ntfServer ntfDelAttempts
|
||||
tryAgentError (agentNtfDeleteSubscription c nSubId tkn) >>= \case
|
||||
Left e | temporaryOrHostError e -> throwE e
|
||||
_ -> continue
|
||||
atomically $ incNtfServerStat c userId ntfServer ntfDeleted
|
||||
Nothing -> continue
|
||||
_ -> continue
|
||||
updateSubNextCheck ts toStatus = do
|
||||
checkInterval <- asks $ ntfSubCheckInterval . config
|
||||
let nextCheckTs = addUTCTime checkInterval ts
|
||||
updateSub (NASCreated toStatus) (NtfSubNTFAction NSACheck) nextCheckTs
|
||||
updateSub (NASCreated toStatus) (NSANtf NSACheck) nextCheckTs
|
||||
updateSub toStatus toAction actionTs' =
|
||||
withStore' c $ \db ->
|
||||
updateNtfSubscription db sub {ntfSubStatus = toStatus} toAction actionTs'
|
||||
@@ -231,12 +233,10 @@ runNtfWorker c srv Worker {doWork} = do
|
||||
runNtfSMPWorker :: AgentClient -> SMPServer -> Worker -> AM ()
|
||||
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 =
|
||||
withWork c doWork (`getNextNtfSubSMPAction` srv) $
|
||||
@@ -244,6 +244,7 @@ runNtfSMPWorker c srv Worker {doWork} = do
|
||||
logInfo $ "runNtfSMPWorker, nextSub " <> tshow nextSub
|
||||
ri <- asks $ reconnectInterval . config
|
||||
withRetryInterval ri $ \_ loop -> do
|
||||
liftIO $ waitWhileSuspended c
|
||||
liftIO $ waitForUserNetwork c
|
||||
processSub nextSub
|
||||
`catchAgentError` retryOnError c "NtfSMPWorker" loop (workerInternalError c connId . show)
|
||||
@@ -264,11 +265,12 @@ runNtfSMPWorker c srv Worker {doWork} = do
|
||||
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
|
||||
updateNtfSubscription db sub {ntfQueueId = Just notifierId, ntfSubStatus = NASKey} (NSANtf NSACreate) ts
|
||||
ns <- asks ntfSupervisor
|
||||
atomically $ sendNtfSubCommand ns (connId, NSCNtfWorker ntfServer)
|
||||
_ -> workerInternalError c connId "NSASmpKey - no active token"
|
||||
NSASmpDelete -> do
|
||||
-- TODO should we remove it after successful removal from the server?
|
||||
rq_ <- withStore' c $ \db -> do
|
||||
setRcvQueueNtfCreds db connId Nothing
|
||||
getPrimaryRcvQueue db connId
|
||||
@@ -295,7 +297,7 @@ retryOnError c name loop done e = do
|
||||
where
|
||||
retryLoop = do
|
||||
atomically $ endAgentOperation c AONtfNetwork
|
||||
atomically $ throwWhenInactive c
|
||||
liftIO $ throwWhenInactive c
|
||||
atomically $ beginAgentOperation c AONtfNetwork
|
||||
loop
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ module Simplex.Messaging.Agent.Protocol
|
||||
deliveryRcptsSMPAgentVersion,
|
||||
pqdrSMPAgentVersion,
|
||||
sndAuthKeySMPAgentVersion,
|
||||
ratchetOnConfSMPAgentVersion,
|
||||
currentSMPAgentVersion,
|
||||
supportedSMPAgentVRange,
|
||||
e2eEncConnInfoLength,
|
||||
@@ -49,6 +50,8 @@ module Simplex.Messaging.Agent.Protocol
|
||||
|
||||
-- * SMP agent protocol types
|
||||
ConnInfo,
|
||||
SndQueueSecured,
|
||||
AEntityId,
|
||||
ACommand (..),
|
||||
AEvent (..),
|
||||
AEvt (..),
|
||||
@@ -153,8 +156,8 @@ import Data.Int (Int64)
|
||||
import Data.Kind (Type)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map (Map)
|
||||
import qualified Data.Map as M
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (fromMaybe, isJust)
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||||
@@ -188,7 +191,6 @@ import Simplex.Messaging.Parsers
|
||||
import Simplex.Messaging.Protocol
|
||||
( AProtocolType,
|
||||
BrokerErrorType (..),
|
||||
EntityId,
|
||||
ErrorType,
|
||||
MsgBody,
|
||||
MsgFlags,
|
||||
@@ -257,11 +259,14 @@ pqdrSMPAgentVersion = VersionSMPA 5
|
||||
sndAuthKeySMPAgentVersion :: VersionSMPA
|
||||
sndAuthKeySMPAgentVersion = VersionSMPA 6
|
||||
|
||||
ratchetOnConfSMPAgentVersion :: VersionSMPA
|
||||
ratchetOnConfSMPAgentVersion = VersionSMPA 7
|
||||
|
||||
minSupportedSMPAgentVersion :: VersionSMPA
|
||||
minSupportedSMPAgentVersion = duplexHandshakeSMPAgentVersion
|
||||
|
||||
currentSMPAgentVersion :: VersionSMPA
|
||||
currentSMPAgentVersion = VersionSMPA 6
|
||||
currentSMPAgentVersion = VersionSMPA 7
|
||||
|
||||
supportedSMPAgentVRange :: VersionRangeSMPA
|
||||
supportedSMPAgentVRange = mkVersionRange minSupportedSMPAgentVersion currentSMPAgentVersion
|
||||
@@ -282,10 +287,12 @@ e2eEncAgentMsgLength v = \case
|
||||
_ -> 15856
|
||||
|
||||
-- | SMP agent event
|
||||
type ATransmission = (ACorrId, EntityId, AEvt)
|
||||
type ATransmission = (ACorrId, AEntityId, AEvt)
|
||||
|
||||
type UserId = Int64
|
||||
|
||||
type AEntityId = ByteString
|
||||
|
||||
type ACorrId = ByteString
|
||||
|
||||
data AEntity = AEConn | AERcvFile | AESndFile | AENone
|
||||
@@ -327,6 +334,8 @@ 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
|
||||
@@ -335,6 +344,7 @@ data AEvent (e :: AEntity) where
|
||||
INFO :: PQSupport -> ConnInfo -> AEvent AEConn
|
||||
CON :: PQEncryption -> AEvent AEConn -- notification that connection is established
|
||||
END :: AEvent AEConn
|
||||
DELD :: AEvent AEConn
|
||||
CONNECT :: AProtocolType -> TransportHost -> AEvent AENone
|
||||
DISCONNECT :: AProtocolType -> TransportHost -> AEvent AENone
|
||||
DOWN :: SMPServer -> [ConnId] -> AEvent AENone
|
||||
@@ -354,6 +364,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
|
||||
SUSPENDED :: AEvent AENone
|
||||
RFPROG :: Int64 -> Int64 -> AEvent AERcvFile
|
||||
@@ -403,6 +414,7 @@ data AEventTag (e :: AEntity) where
|
||||
INFO_ :: AEventTag AEConn
|
||||
CON_ :: AEventTag AEConn
|
||||
END_ :: AEventTag AEConn
|
||||
DELD_ :: AEventTag AEConn
|
||||
CONNECT_ :: AEventTag AENone
|
||||
DISCONNECT_ :: AEventTag AENone
|
||||
DOWN_ :: AEventTag AENone
|
||||
@@ -422,6 +434,7 @@ data AEventTag (e :: AEntity) where
|
||||
DEL_USER_ :: AEventTag AENone
|
||||
STAT_ :: AEventTag AEConn
|
||||
OK_ :: AEventTag AEConn
|
||||
JOINED_ :: AEventTag AEConn
|
||||
ERR_ :: AEventTag AEConn
|
||||
SUSPENDED_ :: AEventTag AENone
|
||||
-- XFTP commands and responses
|
||||
@@ -455,6 +468,7 @@ aEventTag = \case
|
||||
INFO {} -> INFO_
|
||||
CON _ -> CON_
|
||||
END -> END_
|
||||
DELD -> DELD_
|
||||
CONNECT {} -> CONNECT_
|
||||
DISCONNECT {} -> DISCONNECT_
|
||||
DOWN {} -> DOWN_
|
||||
@@ -474,6 +488,7 @@ aEventTag = \case
|
||||
DEL_USER _ -> DEL_USER_
|
||||
STAT _ -> STAT_
|
||||
OK -> OK_
|
||||
JOINED _ -> JOINED_
|
||||
ERR _ -> ERR_
|
||||
SUSPENDED -> SUSPENDED_
|
||||
RFPROG {} -> RFPROG_
|
||||
@@ -1326,6 +1341,8 @@ 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
|
||||
|
||||
@@ -9,6 +9,7 @@ module Simplex.Messaging.Agent.RetryInterval
|
||||
RI2State (..),
|
||||
withRetryInterval,
|
||||
withRetryIntervalCount,
|
||||
withRetryForeground,
|
||||
withRetryLock2,
|
||||
updateRetryInterval2,
|
||||
nextRetryDelay,
|
||||
@@ -16,10 +17,11 @@ 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', whenM)
|
||||
import Simplex.Messaging.Util (threadDelay', unlessM, whenM)
|
||||
import UnliftIO.STM
|
||||
|
||||
data RetryInterval = RetryInterval
|
||||
@@ -63,6 +65,27 @@ 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 =
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
{-# 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.Map (Map)
|
||||
import Data.Int (Int64)
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
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)
|
||||
import Simplex.Messaging.Protocol (SMPServer, XFTPServer, NtfServer)
|
||||
import Simplex.Messaging.Util (decodeJSON, encodeJSON)
|
||||
import UnliftIO.STM
|
||||
|
||||
@@ -29,13 +33,26 @@ data AgentSMPServerStats = AgentSMPServerStats
|
||||
recvDuplicates :: TVar Int, -- duplicate messages received
|
||||
recvCryptoErrs :: TVar Int, -- message decryption errors
|
||||
recvErrs :: TVar Int, -- receive errors
|
||||
connCreated :: TVar Int,
|
||||
connSecured :: TVar Int,
|
||||
connCompleted :: TVar Int,
|
||||
connDeleted :: TVar Int,
|
||||
ackMsgs :: TVar Int, -- total messages acknowledged
|
||||
ackAttempts :: TVar Int, -- acknowledgement attempts
|
||||
ackNoMsgErrs :: TVar Int, -- NO_MSG ack errors
|
||||
ackOtherErrs :: TVar Int, -- other permanent ack errors (temporary accounted for in attempts)
|
||||
-- conn stats are accounted for rcv queue server
|
||||
connCreated :: TVar Int, -- total connections created
|
||||
connSecured :: TVar Int, -- connections secured
|
||||
connCompleted :: TVar Int, -- connections completed
|
||||
connDeleted :: TVar Int, -- total connections deleted
|
||||
connDelAttempts :: TVar Int, -- total connection deletion attempts
|
||||
connDelErrs :: TVar Int, -- permanent connection deletion errors (temporary accounted for in attempts)
|
||||
connSubscribed :: TVar Int, -- total successful subscription
|
||||
connSubAttempts :: TVar Int, -- subscription attempts
|
||||
connSubErrs :: TVar Int -- permanent subscription errors (temporary accounted for in 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
|
||||
}
|
||||
|
||||
data AgentSMPServerStatsData = AgentSMPServerStatsData
|
||||
@@ -53,16 +70,30 @@ data AgentSMPServerStatsData = AgentSMPServerStatsData
|
||||
_recvDuplicates :: Int,
|
||||
_recvCryptoErrs :: Int,
|
||||
_recvErrs :: Int,
|
||||
_ackMsgs :: Int,
|
||||
_ackAttempts :: Int,
|
||||
_ackNoMsgErrs :: Int,
|
||||
_ackOtherErrs :: Int,
|
||||
_connCreated :: Int,
|
||||
_connSecured :: Int,
|
||||
_connCompleted :: Int,
|
||||
_connDeleted :: Int,
|
||||
_connDelAttempts :: Int,
|
||||
_connDelErrs :: Int,
|
||||
_connSubscribed :: Int,
|
||||
_connSubAttempts :: Int,
|
||||
_connSubErrs :: Int
|
||||
_connSubIgnored :: Int,
|
||||
_connSubErrs :: Int,
|
||||
_ntfKey :: OptionalInt,
|
||||
_ntfKeyAttempts :: OptionalInt,
|
||||
_ntfKeyDeleted :: OptionalInt,
|
||||
_ntfKeyDeleteAttempts :: OptionalInt
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
newtype OptionalInt = OInt {toInt :: Int}
|
||||
deriving (Num, Show, ToJSON)
|
||||
|
||||
newAgentSMPServerStats :: STM AgentSMPServerStats
|
||||
newAgentSMPServerStats = do
|
||||
sentDirect <- newTVar 0
|
||||
@@ -79,13 +110,24 @@ newAgentSMPServerStats = do
|
||||
recvDuplicates <- newTVar 0
|
||||
recvCryptoErrs <- newTVar 0
|
||||
recvErrs <- newTVar 0
|
||||
ackMsgs <- newTVar 0
|
||||
ackAttempts <- newTVar 0
|
||||
ackNoMsgErrs <- newTVar 0
|
||||
ackOtherErrs <- newTVar 0
|
||||
connCreated <- newTVar 0
|
||||
connSecured <- newTVar 0
|
||||
connCompleted <- newTVar 0
|
||||
connDeleted <- newTVar 0
|
||||
connDelAttempts <- newTVar 0
|
||||
connDelErrs <- newTVar 0
|
||||
connSubscribed <- newTVar 0
|
||||
connSubAttempts <- newTVar 0
|
||||
connSubIgnored <- newTVar 0
|
||||
connSubErrs <- newTVar 0
|
||||
ntfKey <- newTVar 0
|
||||
ntfKeyAttempts <- newTVar 0
|
||||
ntfKeyDeleted <- newTVar 0
|
||||
ntfKeyDeleteAttempts <- newTVar 0
|
||||
pure
|
||||
AgentSMPServerStats
|
||||
{ sentDirect,
|
||||
@@ -102,15 +144,63 @@ newAgentSMPServerStats = do
|
||||
recvDuplicates,
|
||||
recvCryptoErrs,
|
||||
recvErrs,
|
||||
ackMsgs,
|
||||
ackAttempts,
|
||||
ackNoMsgErrs,
|
||||
ackOtherErrs,
|
||||
connCreated,
|
||||
connSecured,
|
||||
connCompleted,
|
||||
connDeleted,
|
||||
connDelAttempts,
|
||||
connDelErrs,
|
||||
connSubscribed,
|
||||
connSubAttempts,
|
||||
connSubErrs
|
||||
connSubIgnored,
|
||||
connSubErrs,
|
||||
ntfKey,
|
||||
ntfKeyAttempts,
|
||||
ntfKeyDeleted,
|
||||
ntfKeyDeleteAttempts
|
||||
}
|
||||
|
||||
newAgentSMPServerStatsData :: AgentSMPServerStatsData
|
||||
newAgentSMPServerStatsData =
|
||||
AgentSMPServerStatsData
|
||||
{ _sentDirect = 0,
|
||||
_sentViaProxy = 0,
|
||||
_sentProxied = 0,
|
||||
_sentDirectAttempts = 0,
|
||||
_sentViaProxyAttempts = 0,
|
||||
_sentProxiedAttempts = 0,
|
||||
_sentAuthErrs = 0,
|
||||
_sentQuotaErrs = 0,
|
||||
_sentExpiredErrs = 0,
|
||||
_sentOtherErrs = 0,
|
||||
_recvMsgs = 0,
|
||||
_recvDuplicates = 0,
|
||||
_recvCryptoErrs = 0,
|
||||
_recvErrs = 0,
|
||||
_ackMsgs = 0,
|
||||
_ackAttempts = 0,
|
||||
_ackNoMsgErrs = 0,
|
||||
_ackOtherErrs = 0,
|
||||
_connCreated = 0,
|
||||
_connSecured = 0,
|
||||
_connCompleted = 0,
|
||||
_connDeleted = 0,
|
||||
_connDelAttempts = 0,
|
||||
_connDelErrs = 0,
|
||||
_connSubscribed = 0,
|
||||
_connSubAttempts = 0,
|
||||
_connSubIgnored = 0,
|
||||
_connSubErrs = 0,
|
||||
_ntfKey = 0,
|
||||
_ntfKeyAttempts = 0,
|
||||
_ntfKeyDeleted = 0,
|
||||
_ntfKeyDeleteAttempts = 0
|
||||
}
|
||||
|
||||
newAgentSMPServerStats' :: AgentSMPServerStatsData -> STM AgentSMPServerStats
|
||||
newAgentSMPServerStats' s = do
|
||||
sentDirect <- newTVar $ _sentDirect s
|
||||
@@ -127,13 +217,24 @@ newAgentSMPServerStats' s = do
|
||||
recvDuplicates <- newTVar $ _recvDuplicates s
|
||||
recvCryptoErrs <- newTVar $ _recvCryptoErrs s
|
||||
recvErrs <- newTVar $ _recvErrs s
|
||||
ackMsgs <- newTVar $ _ackMsgs s
|
||||
ackAttempts <- newTVar $ _ackAttempts s
|
||||
ackNoMsgErrs <- newTVar $ _ackNoMsgErrs s
|
||||
ackOtherErrs <- newTVar $ _ackOtherErrs s
|
||||
connCreated <- newTVar $ _connCreated s
|
||||
connSecured <- newTVar $ _connSecured s
|
||||
connCompleted <- newTVar $ _connCompleted s
|
||||
connDeleted <- newTVar $ _connDeleted s
|
||||
connDelAttempts <- newTVar $ _connDelAttempts s
|
||||
connDelErrs <- newTVar $ _connDelErrs s
|
||||
connSubscribed <- newTVar $ _connSubscribed s
|
||||
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,
|
||||
@@ -150,13 +251,24 @@ newAgentSMPServerStats' s = do
|
||||
recvDuplicates,
|
||||
recvCryptoErrs,
|
||||
recvErrs,
|
||||
ackMsgs,
|
||||
ackAttempts,
|
||||
ackNoMsgErrs,
|
||||
ackOtherErrs,
|
||||
connCreated,
|
||||
connSecured,
|
||||
connCompleted,
|
||||
connDeleted,
|
||||
connDelAttempts,
|
||||
connDelErrs,
|
||||
connSubscribed,
|
||||
connSubAttempts,
|
||||
connSubErrs
|
||||
connSubIgnored,
|
||||
connSubErrs,
|
||||
ntfKey,
|
||||
ntfKeyAttempts,
|
||||
ntfKeyDeleted,
|
||||
ntfKeyDeleteAttempts
|
||||
}
|
||||
|
||||
-- as this is used to periodically update stats in db,
|
||||
@@ -177,13 +289,24 @@ getAgentSMPServerStats s = do
|
||||
_recvDuplicates <- readTVarIO $ recvDuplicates s
|
||||
_recvCryptoErrs <- readTVarIO $ recvCryptoErrs s
|
||||
_recvErrs <- readTVarIO $ recvErrs s
|
||||
_ackMsgs <- readTVarIO $ ackMsgs s
|
||||
_ackAttempts <- readTVarIO $ ackAttempts s
|
||||
_ackNoMsgErrs <- readTVarIO $ ackNoMsgErrs s
|
||||
_ackOtherErrs <- readTVarIO $ ackOtherErrs s
|
||||
_connCreated <- readTVarIO $ connCreated s
|
||||
_connSecured <- readTVarIO $ connSecured s
|
||||
_connCompleted <- readTVarIO $ connCompleted s
|
||||
_connDeleted <- readTVarIO $ connDeleted s
|
||||
_connDelAttempts <- readTVarIO $ connDelAttempts s
|
||||
_connDelErrs <- readTVarIO $ connDelErrs s
|
||||
_connSubscribed <- readTVarIO $ connSubscribed s
|
||||
_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,
|
||||
@@ -200,20 +323,70 @@ getAgentSMPServerStats s = do
|
||||
_recvDuplicates,
|
||||
_recvCryptoErrs,
|
||||
_recvErrs,
|
||||
_ackMsgs,
|
||||
_ackAttempts,
|
||||
_ackNoMsgErrs,
|
||||
_ackOtherErrs,
|
||||
_connCreated,
|
||||
_connSecured,
|
||||
_connCompleted,
|
||||
_connDeleted,
|
||||
_connDelAttempts,
|
||||
_connDelErrs,
|
||||
_connSubscribed,
|
||||
_connSubAttempts,
|
||||
_connSubErrs
|
||||
_connSubIgnored,
|
||||
_connSubErrs,
|
||||
_ntfKey,
|
||||
_ntfKeyAttempts,
|
||||
_ntfKeyDeleted,
|
||||
_ntfKeyDeleteAttempts
|
||||
}
|
||||
|
||||
addSMPStatsData :: AgentSMPServerStatsData -> AgentSMPServerStatsData -> AgentSMPServerStatsData
|
||||
addSMPStatsData sd1 sd2 =
|
||||
AgentSMPServerStatsData
|
||||
{ _sentDirect = _sentDirect sd1 + _sentDirect sd2,
|
||||
_sentViaProxy = _sentViaProxy sd1 + _sentViaProxy sd2,
|
||||
_sentProxied = _sentProxied sd1 + _sentProxied sd2,
|
||||
_sentDirectAttempts = _sentDirectAttempts sd1 + _sentDirectAttempts sd2,
|
||||
_sentViaProxyAttempts = _sentViaProxyAttempts sd1 + _sentViaProxyAttempts sd2,
|
||||
_sentProxiedAttempts = _sentProxiedAttempts sd1 + _sentProxiedAttempts sd2,
|
||||
_sentAuthErrs = _sentAuthErrs sd1 + _sentAuthErrs sd2,
|
||||
_sentQuotaErrs = _sentQuotaErrs sd1 + _sentQuotaErrs sd2,
|
||||
_sentExpiredErrs = _sentExpiredErrs sd1 + _sentExpiredErrs sd2,
|
||||
_sentOtherErrs = _sentOtherErrs sd1 + _sentOtherErrs sd2,
|
||||
_recvMsgs = _recvMsgs sd1 + _recvMsgs sd2,
|
||||
_recvDuplicates = _recvDuplicates sd1 + _recvDuplicates sd2,
|
||||
_recvCryptoErrs = _recvCryptoErrs sd1 + _recvCryptoErrs sd2,
|
||||
_recvErrs = _recvErrs sd1 + _recvErrs sd2,
|
||||
_ackMsgs = _ackMsgs sd1 + _ackMsgs sd2,
|
||||
_ackAttempts = _ackAttempts sd1 + _ackAttempts sd2,
|
||||
_ackNoMsgErrs = _ackNoMsgErrs sd1 + _ackNoMsgErrs sd2,
|
||||
_ackOtherErrs = _ackOtherErrs sd1 + _ackOtherErrs sd2,
|
||||
_connCreated = _connCreated sd1 + _connCreated sd2,
|
||||
_connSecured = _connSecured sd1 + _connSecured sd2,
|
||||
_connCompleted = _connCompleted sd1 + _connCompleted sd2,
|
||||
_connDeleted = _connDeleted sd1 + _connDeleted sd2,
|
||||
_connDelAttempts = _connDelAttempts sd1 + _connDelAttempts sd2,
|
||||
_connDelErrs = _connDelErrs sd1 + _connDelErrs 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
|
||||
}
|
||||
|
||||
data AgentXFTPServerStats = AgentXFTPServerStats
|
||||
{ uploads :: TVar Int, -- total replicas uploaded to server
|
||||
uploadsSize :: TVar Int64, -- total size of uploaded replicas in KB
|
||||
uploadAttempts :: TVar Int, -- upload attempts
|
||||
uploadErrs :: TVar Int, -- upload errors
|
||||
downloads :: TVar Int, -- total replicas downloaded from server
|
||||
downloadsSize :: TVar Int64, -- total size of downloaded replicas in KB
|
||||
downloadAttempts :: TVar Int, -- download attempts
|
||||
downloadAuthErrs :: TVar Int, -- download AUTH errors
|
||||
downloadErrs :: TVar Int, -- other download errors (excluding above)
|
||||
@@ -224,9 +397,11 @@ data AgentXFTPServerStats = AgentXFTPServerStats
|
||||
|
||||
data AgentXFTPServerStatsData = AgentXFTPServerStatsData
|
||||
{ _uploads :: Int,
|
||||
_uploadsSize :: Int64,
|
||||
_uploadAttempts :: Int,
|
||||
_uploadErrs :: Int,
|
||||
_downloads :: Int,
|
||||
_downloadsSize :: Int64,
|
||||
_downloadAttempts :: Int,
|
||||
_downloadAuthErrs :: Int,
|
||||
_downloadErrs :: Int,
|
||||
@@ -239,9 +414,11 @@ data AgentXFTPServerStatsData = AgentXFTPServerStatsData
|
||||
newAgentXFTPServerStats :: STM AgentXFTPServerStats
|
||||
newAgentXFTPServerStats = do
|
||||
uploads <- newTVar 0
|
||||
uploadsSize <- newTVar 0
|
||||
uploadAttempts <- newTVar 0
|
||||
uploadErrs <- newTVar 0
|
||||
downloads <- newTVar 0
|
||||
downloadsSize <- newTVar 0
|
||||
downloadAttempts <- newTVar 0
|
||||
downloadAuthErrs <- newTVar 0
|
||||
downloadErrs <- newTVar 0
|
||||
@@ -251,9 +428,11 @@ newAgentXFTPServerStats = do
|
||||
pure
|
||||
AgentXFTPServerStats
|
||||
{ uploads,
|
||||
uploadsSize,
|
||||
uploadAttempts,
|
||||
uploadErrs,
|
||||
downloads,
|
||||
downloadsSize,
|
||||
downloadAttempts,
|
||||
downloadAuthErrs,
|
||||
downloadErrs,
|
||||
@@ -262,12 +441,31 @@ newAgentXFTPServerStats = do
|
||||
deleteErrs
|
||||
}
|
||||
|
||||
newAgentXFTPServerStatsData :: AgentXFTPServerStatsData
|
||||
newAgentXFTPServerStatsData =
|
||||
AgentXFTPServerStatsData
|
||||
{ _uploads = 0,
|
||||
_uploadsSize = 0,
|
||||
_uploadAttempts = 0,
|
||||
_uploadErrs = 0,
|
||||
_downloads = 0,
|
||||
_downloadsSize = 0,
|
||||
_downloadAttempts = 0,
|
||||
_downloadAuthErrs = 0,
|
||||
_downloadErrs = 0,
|
||||
_deletions = 0,
|
||||
_deleteAttempts = 0,
|
||||
_deleteErrs = 0
|
||||
}
|
||||
|
||||
newAgentXFTPServerStats' :: AgentXFTPServerStatsData -> STM AgentXFTPServerStats
|
||||
newAgentXFTPServerStats' s = do
|
||||
uploads <- newTVar $ _uploads s
|
||||
uploadsSize <- newTVar $ _uploadsSize s
|
||||
uploadAttempts <- newTVar $ _uploadAttempts s
|
||||
uploadErrs <- newTVar $ _uploadErrs s
|
||||
downloads <- newTVar $ _downloads s
|
||||
downloadsSize <- newTVar $ _downloadsSize s
|
||||
downloadAttempts <- newTVar $ _downloadAttempts s
|
||||
downloadAuthErrs <- newTVar $ _downloadAuthErrs s
|
||||
downloadErrs <- newTVar $ _downloadErrs s
|
||||
@@ -277,9 +475,11 @@ newAgentXFTPServerStats' s = do
|
||||
pure
|
||||
AgentXFTPServerStats
|
||||
{ uploads,
|
||||
uploadsSize,
|
||||
uploadAttempts,
|
||||
uploadErrs,
|
||||
downloads,
|
||||
downloadsSize,
|
||||
downloadAttempts,
|
||||
downloadAuthErrs,
|
||||
downloadErrs,
|
||||
@@ -293,9 +493,11 @@ newAgentXFTPServerStats' s = do
|
||||
getAgentXFTPServerStats :: AgentXFTPServerStats -> IO AgentXFTPServerStatsData
|
||||
getAgentXFTPServerStats s = do
|
||||
_uploads <- readTVarIO $ uploads s
|
||||
_uploadsSize <- readTVarIO $ uploadsSize s
|
||||
_uploadAttempts <- readTVarIO $ uploadAttempts s
|
||||
_uploadErrs <- readTVarIO $ uploadErrs s
|
||||
_downloads <- readTVarIO $ downloads s
|
||||
_downloadsSize <- readTVarIO $ downloadsSize s
|
||||
_downloadAttempts <- readTVarIO $ downloadAttempts s
|
||||
_downloadAuthErrs <- readTVarIO $ downloadAuthErrs s
|
||||
_downloadErrs <- readTVarIO $ downloadErrs s
|
||||
@@ -305,9 +507,11 @@ getAgentXFTPServerStats s = do
|
||||
pure
|
||||
AgentXFTPServerStatsData
|
||||
{ _uploads,
|
||||
_uploadsSize,
|
||||
_uploadAttempts,
|
||||
_uploadErrs,
|
||||
_downloads,
|
||||
_downloadsSize,
|
||||
_downloadAttempts,
|
||||
_downloadAuthErrs,
|
||||
_downloadErrs,
|
||||
@@ -316,18 +520,144 @@ getAgentXFTPServerStats s = do
|
||||
_deleteErrs
|
||||
}
|
||||
|
||||
addXFTPStatsData :: AgentXFTPServerStatsData -> AgentXFTPServerStatsData -> AgentXFTPServerStatsData
|
||||
addXFTPStatsData sd1 sd2 =
|
||||
AgentXFTPServerStatsData
|
||||
{ _uploads = _uploads sd1 + _uploads sd2,
|
||||
_uploadsSize = _uploadsSize sd1 + _uploadsSize sd2,
|
||||
_uploadAttempts = _uploadAttempts sd1 + _uploadAttempts sd2,
|
||||
_uploadErrs = _uploadErrs sd1 + _uploadErrs sd2,
|
||||
_downloads = _downloads sd1 + _downloads sd2,
|
||||
_downloadsSize = _downloadsSize sd1 + _downloadsSize sd2,
|
||||
_downloadAttempts = _downloadAttempts sd1 + _downloadAttempts sd2,
|
||||
_downloadAuthErrs = _downloadAuthErrs sd1 + _downloadAuthErrs sd2,
|
||||
_downloadErrs = _downloadErrs sd1 + _downloadErrs sd2,
|
||||
_deletions = _deletions sd1 + _deletions sd2,
|
||||
_deleteAttempts = _deleteAttempts sd1 + _deleteAttempts 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
|
||||
xftpServersStats :: Map (UserId, XFTPServer) AgentXFTPServerStatsData,
|
||||
ntfServersStats :: OptionalMap (UserId, NtfServer) AgentNtfServerStatsData
|
||||
}
|
||||
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
|
||||
|
||||
@@ -175,6 +175,12 @@ instance SMPQueue RcvQueue where
|
||||
queueId RcvQueue {rcvId} = rcvId
|
||||
{-# INLINE queueId #-}
|
||||
|
||||
instance SMPQueue NewRcvQueue where
|
||||
qServer RcvQueue {server} = server
|
||||
{-# INLINE qServer #-}
|
||||
queueId RcvQueue {rcvId} = rcvId
|
||||
{-# INLINE queueId #-}
|
||||
|
||||
instance SMPQueue SndQueue where
|
||||
qServer SndQueue {server} = server
|
||||
{-# INLINE qServer #-}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
{-# LANGUAGE ConstraintKinds #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DerivingStrategies #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE InstanceSigs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
@@ -15,6 +17,7 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
@@ -57,6 +60,7 @@ module Simplex.Messaging.Agent.Store.SQLite
|
||||
getDeletedConns,
|
||||
getConnData,
|
||||
setConnDeleted,
|
||||
setConnUserId,
|
||||
setConnAgentVersion,
|
||||
setConnPQSupport,
|
||||
getDeletedConnIds,
|
||||
@@ -110,6 +114,7 @@ module Simplex.Messaging.Agent.Store.SQLite
|
||||
getSndMsgViaRcpt,
|
||||
updateSndMsgRcpt,
|
||||
getPendingQueueMsg,
|
||||
getConnectionsForDelivery,
|
||||
updatePendingMsgRIState,
|
||||
deletePendingMsgs,
|
||||
getExpiredSndMessages,
|
||||
@@ -135,6 +140,7 @@ module Simplex.Messaging.Agent.Store.SQLite
|
||||
createCommand,
|
||||
getPendingCommandServers,
|
||||
getPendingServerCommand,
|
||||
updateCommandServer,
|
||||
deleteCommand,
|
||||
-- Notification device token persistence
|
||||
createNtfToken,
|
||||
@@ -220,7 +226,7 @@ module Simplex.Messaging.Agent.Store.SQLite
|
||||
-- * utilities
|
||||
withConnection,
|
||||
withTransaction,
|
||||
withTransactionCtx,
|
||||
withTransactionPriority,
|
||||
firstRow,
|
||||
firstRow',
|
||||
maybeFirstRow,
|
||||
@@ -392,10 +398,10 @@ connectSQLiteStore dbFilePath key keepKey = do
|
||||
dbNew <- not <$> doesFileExist dbFilePath
|
||||
dbConn <- dbBusyLoop (connectDB dbFilePath key)
|
||||
dbConnection <- newMVar dbConn
|
||||
atomically $ do
|
||||
dbKey <- newTVar $! storeKey key keepKey
|
||||
dbClosed <- newTVar False
|
||||
pure SQLiteStore {dbFilePath, dbKey, dbConnection, dbNew, dbClosed}
|
||||
dbKey <- newTVarIO $! storeKey key keepKey
|
||||
dbClosed <- newTVarIO False
|
||||
dbSem <- newTVarIO 0
|
||||
pure SQLiteStore {dbFilePath, dbKey, dbSem, dbConnection, dbNew, dbClosed}
|
||||
|
||||
connectDB :: FilePath -> ScrubbedBytes -> IO DB.Connection
|
||||
connectDB path key = do
|
||||
@@ -965,12 +971,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 (InternalId, InternalSndId, PrevSndMsgHash)
|
||||
updateSndIds db connId = do
|
||||
(lastInternalId, lastInternalSndId, prevSndHash) <- retrieveLastIdsAndHashSnd_ db connId
|
||||
updateSndIds :: DB.Connection -> ConnId -> IO (Either StoreError (InternalId, InternalSndId, PrevSndMsgHash))
|
||||
updateSndIds db connId = runExceptT $ do
|
||||
(lastInternalId, lastInternalSndId, prevSndHash) <- ExceptT $ retrieveLastIdsAndHashSnd_ db connId
|
||||
let internalId = InternalId $ unId lastInternalId + 1
|
||||
internalSndId = InternalSndId $ unSndId lastInternalSndId + 1
|
||||
updateLastIdsSnd_ db connId internalId internalSndId
|
||||
liftIO $ updateLastIdsSnd_ db connId internalId internalSndId
|
||||
pure (internalId, internalSndId, prevSndHash)
|
||||
|
||||
createSndMsg :: DB.Connection -> ConnId -> SndMsgData -> IO ()
|
||||
@@ -1008,6 +1014,10 @@ 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
|
||||
@@ -1317,38 +1327,39 @@ getPendingCommandServers db connId = do
|
||||
where
|
||||
smpServer (host, port, keyHash) = SMPServer <$> host <*> port <*> keyHash
|
||||
|
||||
getPendingServerCommand :: DB.Connection -> Maybe SMPServer -> IO (Either StoreError (Maybe PendingCommand))
|
||||
getPendingServerCommand db srv_ = getWorkItem "command" getCmdId getCommand markCommandFailed
|
||||
getPendingServerCommand :: DB.Connection -> ConnId -> Maybe SMPServer -> IO (Either StoreError (Maybe PendingCommand))
|
||||
getPendingServerCommand db connId 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 host IS NULL AND port IS NULL AND failed = 0
|
||||
WHERE conn_id = ? AND 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 host = ? AND port = ? AND failed = 0
|
||||
WHERE conn_id = ? AND host = ? AND port = ? AND failed = 0
|
||||
ORDER BY created_at ASC, command_id ASC
|
||||
LIMIT 1
|
||||
|]
|
||||
(host, port)
|
||||
(connId, 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.conn_id, c.command
|
||||
SELECT c.corr_id, cs.user_id, c.command
|
||||
FROM commands c
|
||||
JOIN connections cs USING (conn_id)
|
||||
WHERE c.command_id = ?
|
||||
@@ -1356,9 +1367,22 @@ getPendingServerCommand db srv_ = getWorkItem "command" getCmdId getCommand mark
|
||||
(Only cmdId)
|
||||
where
|
||||
err = SEInternal $ "command " <> bshow cmdId <> " returned []"
|
||||
pendingCommand (corrId, userId, connId, command) = PendingCommand {cmdId, corrId, userId, connId, command}
|
||||
pendingCommand (corrId, userId, 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)
|
||||
@@ -1457,23 +1481,24 @@ getNtfSubscription db connId =
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT s.host, s.port, COALESCE(nsb.smp_server_key_hash, s.key_hash), ns.ntf_host, ns.ntf_port, ns.ntf_key_hash,
|
||||
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,
|
||||
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 (smpHost, smpPort, smpKeyHash, ntfHost, ntfPort, ntfKeyHash, ntfQueueId, ntfSubId, ntfSubStatus, ntfAction_, smpAction_, actionTs_) =
|
||||
ntfSubscription ((userId, smpHost, smpPort, smpKeyHash, ntfHost, ntfPort, ntfKeyHash ) :. (ntfQueueId, ntfSubId, ntfSubStatus, ntfAction_, smpAction_, actionTs_)) =
|
||||
let smpServer = SMPServer smpHost smpPort smpKeyHash
|
||||
ntfServer = NtfServer ntfHost ntfPort ntfKeyHash
|
||||
action = case (ntfAction_, smpAction_, actionTs_) of
|
||||
(Just ntfAction, Nothing, Just actionTs) -> Just (NtfSubNTFAction ntfAction, actionTs)
|
||||
(Nothing, Just smpAction, Just actionTs) -> Just (NtfSubSMPAction smpAction, actionTs)
|
||||
(Just ntfAction, Nothing, Just actionTs) -> Just (NSANtf ntfAction, actionTs)
|
||||
(Nothing, Just smpAction, Just actionTs) -> Just (NSASMP smpAction, actionTs)
|
||||
_ -> Nothing
|
||||
in (NtfSubscription {connId, smpServer, ntfQueueId, ntfServer, ntfSubId, ntfSubStatus}, action)
|
||||
in (NtfSubscription {userId, connId, smpServer, ntfQueueId, ntfServer, ntfSubId, ntfSubStatus}, action)
|
||||
|
||||
createNtfSubscription :: DB.Connection -> NtfSubscription -> NtfSubAction -> IO (Either StoreError ())
|
||||
createNtfSubscription db ntfSubscription action = runExceptT $ do
|
||||
@@ -1607,18 +1632,19 @@ getNextNtfSubNTFAction db ntfServer@(NtfServer ntfHost ntfPort _) =
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT s.host, s.port, COALESCE(ns.smp_server_key_hash, s.key_hash),
|
||||
SELECT c.user_id, 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 (smpHost, smpPort, smpKeyHash, ntfQueueId, ntfSubId, ntfSubStatus, actionTs, action) =
|
||||
ntfSubAction (userId, smpHost, smpPort, smpKeyHash, ntfQueueId, ntfSubId, ntfSubStatus, actionTs, action) =
|
||||
let smpServer = SMPServer smpHost smpPort smpKeyHash
|
||||
ntfSubscription = NtfSubscription {connId, smpServer, ntfQueueId, ntfServer, ntfSubId, ntfSubStatus}
|
||||
ntfSubscription = NtfSubscription {userId, connId, smpServer, ntfQueueId, ntfServer, ntfSubId, ntfSubStatus}
|
||||
in (ntfSubscription, action, actionTs)
|
||||
|
||||
markNtfSubActionNtfFailed_ :: DB.Connection -> ConnId -> IO ()
|
||||
@@ -1650,18 +1676,19 @@ getNextNtfSubSMPAction db smpServer@(SMPServer smpHost smpPort _) =
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT s.ntf_host, s.ntf_port, s.ntf_key_hash,
|
||||
SELECT c.user_id, s.ntf_host, s.ntf_port, s.ntf_key_hash,
|
||||
ns.smp_ntf_id, ns.ntf_sub_id, ns.ntf_sub_status, ns.ntf_sub_action_ts, ns.ntf_sub_smp_action
|
||||
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 (ntfHost, ntfPort, ntfKeyHash, ntfQueueId, ntfSubId, ntfSubStatus, actionTs, action) =
|
||||
ntfSubAction (userId, ntfHost, ntfPort, ntfKeyHash, ntfQueueId, ntfSubId, ntfSubStatus, actionTs, action) =
|
||||
let ntfServer = NtfServer ntfHost ntfPort ntfKeyHash
|
||||
ntfSubscription = NtfSubscription {connId, smpServer, ntfQueueId, ntfServer, ntfSubId, ntfSubStatus}
|
||||
ntfSubscription = NtfSubscription {userId, connId, smpServer, ntfQueueId, ntfServer, ntfSubId, ntfSubStatus}
|
||||
in (ntfSubscription, action, actionTs)
|
||||
|
||||
markNtfSubActionSMPFailed_ :: DB.Connection -> ConnId -> IO ()
|
||||
@@ -1790,6 +1817,14 @@ 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
|
||||
@@ -1906,9 +1941,11 @@ 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 =
|
||||
@@ -1929,9 +1966,11 @@ 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
|
||||
@@ -1964,6 +2003,10 @@ 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)
|
||||
@@ -2176,9 +2219,9 @@ updateRcvMsgHash db connId sndMsgId internalRcvId internalHash =
|
||||
|
||||
-- * updateSndIds helpers
|
||||
|
||||
retrieveLastIdsAndHashSnd_ :: DB.Connection -> ConnId -> IO (InternalId, InternalSndId, PrevSndMsgHash)
|
||||
retrieveLastIdsAndHashSnd_ :: DB.Connection -> ConnId -> IO (Either StoreError (InternalId, InternalSndId, PrevSndMsgHash))
|
||||
retrieveLastIdsAndHashSnd_ dbConn connId = do
|
||||
[(lastInternalId, lastInternalSndId, lastSndHash)] <-
|
||||
firstRow id SEConnNotFound $
|
||||
DB.queryNamed
|
||||
dbConn
|
||||
[sql|
|
||||
@@ -2187,7 +2230,6 @@ 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 =
|
||||
@@ -2272,8 +2314,8 @@ randomId :: TVar ChaChaDRG -> Int -> IO ByteString
|
||||
randomId gVar n = atomically $ U.encode <$> C.randomBytes n gVar
|
||||
|
||||
ntfSubAndSMPAction :: NtfSubAction -> (Maybe NtfSubNTFAction, Maybe NtfSubSMPAction)
|
||||
ntfSubAndSMPAction (NtfSubNTFAction action) = (Just action, Nothing)
|
||||
ntfSubAndSMPAction (NtfSubSMPAction action) = (Nothing, Just action)
|
||||
ntfSubAndSMPAction (NSANtf action) = (Just action, Nothing)
|
||||
ntfSubAndSMPAction (NSASMP action) = (Nothing, Just action)
|
||||
|
||||
createXFTPServer_ :: DB.Connection -> XFTPServer -> IO Int64
|
||||
createXFTPServer_ db newSrv@ProtocolServer {host, port, keyHash} =
|
||||
@@ -2483,7 +2525,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)))
|
||||
getNextRcvChunkToDownload :: DB.Connection -> XFTPServer -> NominalDiffTime -> IO (Either StoreError (Maybe (RcvFileChunk, Bool, Maybe RcvFileId)))
|
||||
getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = do
|
||||
getWorkItem "rcv_file_download" getReplicaId getChunkData (markRcvFileFailed db . snd)
|
||||
where
|
||||
@@ -2507,7 +2549,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))
|
||||
getChunkData :: (Int64, DBRcvFileId) -> IO (Either StoreError (RcvFileChunk, Bool, Maybe RcvFileId))
|
||||
getChunkData (rcvFileChunkReplicaId, _fileId) =
|
||||
firstRow toChunk SEFileNotFound $
|
||||
DB.query
|
||||
@@ -2516,7 +2558,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.approved_relays, f.redirect_entity_id
|
||||
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
|
||||
@@ -2525,8 +2567,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) :. Only Bool) -> (RcvFileChunk, Bool)
|
||||
toChunk ((rcvFileId, rcvFileEntityId, userId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath, chunkTmpPath) :. (rcvChunkReplicaId, replicaId, replicaKey, received, delay, retries) :. (Only approvedRelays)) =
|
||||
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_)) =
|
||||
( RcvFileChunk
|
||||
{ rcvFileId,
|
||||
rcvFileEntityId,
|
||||
@@ -2539,7 +2581,8 @@ getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = d
|
||||
chunkTmpPath,
|
||||
replicas = [RcvFileChunkReplica {rcvChunkReplicaId, server, replicaId, replicaKey, received, delay, retries}]
|
||||
},
|
||||
approvedRelays
|
||||
approvedRelays,
|
||||
redirectEntityId_
|
||||
)
|
||||
|
||||
getNextRcvFileToDecrypt :: DB.Connection -> NominalDiffTime -> IO (Either StoreError (Maybe RcvFile))
|
||||
@@ -3041,10 +3084,9 @@ getServersStats db =
|
||||
firstRow id SEServersStatsNotFound $
|
||||
DB.query_ db "SELECT started_at, servers_stats FROM servers_stats WHERE servers_stats_id = 1"
|
||||
|
||||
resetServersStats :: DB.Connection -> IO ()
|
||||
resetServersStats db = do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute db "UPDATE servers_stats SET servers_stats = NULL, started_at = ?, updated_at = ? WHERE servers_stats_id = 1" (currentTs, currentTs)
|
||||
resetServersStats :: DB.Connection -> UTCTime -> IO ()
|
||||
resetServersStats db startedAt =
|
||||
DB.execute db "UPDATE servers_stats SET servers_stats = NULL, started_at = ?, updated_at = ? WHERE servers_stats_id = 1" (startedAt, startedAt)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''UpMigration)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
@@ -8,20 +9,20 @@ module Simplex.Messaging.Agent.Store.SQLite.Common
|
||||
withConnection',
|
||||
withTransaction,
|
||||
withTransaction',
|
||||
withTransactionCtx,
|
||||
withTransactionPriority,
|
||||
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 (diffToMilliseconds)
|
||||
import Simplex.Messaging.Util (ifM, unlessM)
|
||||
import qualified UnliftIO.Exception as E
|
||||
import UnliftIO.MVar
|
||||
import UnliftIO.STM
|
||||
@@ -32,35 +33,40 @@ 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 SQLiteStore {dbConnection} = withMVar dbConnection
|
||||
withConnection st = withConnectionPriority st False
|
||||
|
||||
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
|
||||
|
||||
withTransactionCtx :: Maybe String -> SQLiteStore -> (DB.Connection -> IO a) -> IO a
|
||||
withTransactionCtx ctx_ st action = withConnection st $ dbBusyLoop . transactionWithCtx
|
||||
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
|
||||
where
|
||||
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
|
||||
transaction db@DB.Connection {conn} = SQL.withImmediateTransaction conn $ action db
|
||||
|
||||
dbBusyLoop :: forall a. IO a -> IO a
|
||||
dbBusyLoop action = loop 500 3000000
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE StrictData #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
@@ -20,15 +21,19 @@ 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)
|
||||
import Simplex.Messaging.Util (diffToMilliseconds, tshow)
|
||||
|
||||
data Connection = Connection
|
||||
{ conn :: SQL.Connection,
|
||||
@@ -38,33 +43,41 @@ data Connection = Connection
|
||||
data SlowQueryStats = SlowQueryStats
|
||||
{ count :: Int64,
|
||||
timeMax :: Int64,
|
||||
timeAvg :: Int64
|
||||
timeAvg :: Int64,
|
||||
errs :: Map Text Int
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
timeIt :: TMap Query SlowQueryStats -> Query -> IO a -> IO a
|
||||
timeIt slow sql a = do
|
||||
t <- getCurrentTime
|
||||
r <- a
|
||||
r <- a `catch` \e -> do
|
||||
atomically $ TM.alter (Just . updateQueryErrors e) sql slow
|
||||
throwIO e
|
||||
t' <- getCurrentTime
|
||||
let diff = diffToMilliseconds $ diffUTCTime t' t
|
||||
atomically $ when (diff > 5) $ TM.alter (updateQueryStats diff) sql slow
|
||||
when (diff > 1) $ atomically $ 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
|
||||
updateQueryStats diff (Just SlowQueryStats {count, timeMax, timeAvg}) =
|
||||
updateQueryStats diff Nothing = Just $ SlowQueryStats 1 diff diff M.empty
|
||||
updateQueryStats diff (Just SlowQueryStats {count, timeMax, timeAvg, errs}) =
|
||||
Just $
|
||||
SlowQueryStats
|
||||
{ count = count + 1,
|
||||
timeMax = max timeMax diff,
|
||||
timeAvg = (timeAvg * count + diff) `div` (count + 1)
|
||||
timeAvg = (timeAvg * count + diff) `div` (count + 1),
|
||||
errs
|
||||
}
|
||||
|
||||
open :: String -> IO Connection
|
||||
open f = do
|
||||
conn <- SQL.open f
|
||||
slow <- atomically $ TM.empty
|
||||
slow <- TM.emptyIO
|
||||
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 as M
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (isNothing, mapMaybe)
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (decodeLatin1)
|
||||
@@ -73,6 +73,7 @@ import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240223_connections_wai
|
||||
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.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||
@@ -114,7 +115,8 @@ schemaMigrations =
|
||||
("m20240223_connections_wait_delivery", m20240223_connections_wait_delivery, Just down_m20240223_connections_wait_delivery),
|
||||
("m20240225_ratchet_kem", m20240225_ratchet_kem, Just down_m20240225_ratchet_kem),
|
||||
("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)
|
||||
("m20240624_snd_secure", m20240624_snd_secure, Just down_m20240624_snd_secure),
|
||||
("m20240702_servers_stats", m20240702_servers_stats, Just down_m20240702_servers_stats)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240518_servers_stats where
|
||||
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240702_servers_stats where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
@@ -8,8 +8,8 @@ import Database.SQLite.Simple.QQ (sql)
|
||||
-- servers_stats_id: dummy id, there should always only be one record with servers_stats_id = 1
|
||||
-- servers_stats: overall accumulated stats, past and session, reset to null on stats reset
|
||||
-- started_at: starting point of tracking stats, reset on stats reset
|
||||
m20240518_servers_stats :: Query
|
||||
m20240518_servers_stats =
|
||||
m20240702_servers_stats :: Query
|
||||
m20240702_servers_stats =
|
||||
[sql|
|
||||
CREATE TABLE servers_stats(
|
||||
servers_stats_id INTEGER PRIMARY KEY,
|
||||
@@ -22,8 +22,8 @@ CREATE TABLE servers_stats(
|
||||
INSERT INTO servers_stats (servers_stats_id) VALUES (1);
|
||||
|]
|
||||
|
||||
down_m20240518_servers_stats :: Query
|
||||
down_m20240518_servers_stats =
|
||||
down_m20240702_servers_stats :: Query
|
||||
down_m20240702_servers_stats =
|
||||
[sql|
|
||||
DROP TABLE servers_stats;
|
||||
|]
|
||||
@@ -396,6 +396,13 @@ CREATE TABLE processed_ratchet_key_hashes(
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE TABLE servers_stats(
|
||||
servers_stats_id INTEGER PRIMARY KEY,
|
||||
servers_stats TEXT,
|
||||
started_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_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);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
|
||||
module Simplex.Messaging.Agent.TRcvQueues
|
||||
( TRcvQueues (getRcvQueues, getConnections),
|
||||
Queue (..),
|
||||
empty,
|
||||
clear,
|
||||
deleteConn,
|
||||
@@ -9,9 +11,9 @@ module Simplex.Messaging.Agent.TRcvQueues
|
||||
addQueue,
|
||||
batchAddQueues,
|
||||
deleteQueue,
|
||||
hasSessQueues,
|
||||
getSessQueues,
|
||||
getDelSessQueues,
|
||||
qKey,
|
||||
)
|
||||
where
|
||||
|
||||
@@ -25,46 +27,51 @@ 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 = TRcvQueues
|
||||
{ getRcvQueues :: TMap (UserId, SMPServer, RecipientId) RcvQueue,
|
||||
data TRcvQueues q = TRcvQueues
|
||||
{ getRcvQueues :: TMap (UserId, SMPServer, RecipientId) q,
|
||||
getConnections :: TMap ConnId (NonEmpty (UserId, SMPServer, RecipientId))
|
||||
}
|
||||
|
||||
empty :: STM TRcvQueues
|
||||
empty = TRcvQueues <$> TM.empty <*> TM.empty
|
||||
empty :: IO (TRcvQueues q)
|
||||
empty = TRcvQueues <$> TM.emptyIO <*> TM.emptyIO
|
||||
|
||||
clear :: TRcvQueues -> STM ()
|
||||
clear :: TRcvQueues q -> STM ()
|
||||
clear (TRcvQueues qs cs) = TM.clear qs >> TM.clear cs
|
||||
|
||||
deleteConn :: ConnId -> TRcvQueues -> STM ()
|
||||
deleteConn :: ConnId -> TRcvQueues q -> 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 -> STM Bool
|
||||
hasConn :: ConnId -> TRcvQueues q -> STM Bool
|
||||
hasConn cId (TRcvQueues _ cs) = TM.member cId cs
|
||||
|
||||
addQueue :: RcvQueue -> TRcvQueues -> STM ()
|
||||
addQueue :: Queue q => q -> TRcvQueues q -> 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 => TRcvQueues -> t RcvQueue -> STM ()
|
||||
-- Save time by aggregating modifyTVar'
|
||||
batchAddQueues :: (Foldable t, Queue q) => TRcvQueues q -> t q -> 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 -> STM ()
|
||||
deleteQueue :: RcvQueue -> TRcvQueues RcvQueue -> STM ()
|
||||
deleteQueue rq (TRcvQueues qs cs) = do
|
||||
TM.delete k qs
|
||||
TM.update delQ (connId rq) cs
|
||||
@@ -72,21 +79,25 @@ deleteQueue rq (TRcvQueues qs cs) = do
|
||||
delQ = L.nonEmpty . L.filter (/= k)
|
||||
k = qKey rq
|
||||
|
||||
getSessQueues :: (UserId, SMPServer, Maybe ConnId) -> TRcvQueues -> STM [RcvQueue]
|
||||
getSessQueues tSess (TRcvQueues qs _) = M.foldl' addQ [] <$> readTVar qs
|
||||
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
|
||||
where
|
||||
addQ qs' rq = if rq `isSession` tSess then rq : qs' else qs'
|
||||
|
||||
getDelSessQueues :: (UserId, SMPServer, Maybe ConnId) -> TRcvQueues -> STM ([RcvQueue], [ConnId])
|
||||
getDelSessQueues tSess (TRcvQueues qs cs) = do
|
||||
getDelSessQueues :: (UserId, SMPServer, Maybe ConnId) -> SessionId -> TRcvQueues (SessionId, RcvQueue) -> STM ([RcvQueue], [ConnId])
|
||||
getDelSessQueues tSess sessId' (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') rq
|
||||
| rq `isSession` tSess = (rq : removed, M.delete (qKey rq) qs')
|
||||
delQ acc@(removed, qs') (sessId, rq)
|
||||
| rq `isSession` tSess && sessId == sessId' = (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
|
||||
@@ -100,5 +111,10 @@ isSession :: RcvQueue -> (UserId, SMPServer, Maybe ConnId) -> Bool
|
||||
isSession rq (uId, srv, connId_) =
|
||||
userId rq == uId && server rq == srv && maybe True (connId rq ==) connId_
|
||||
|
||||
qKey :: RcvQueue -> (UserId, SMPServer, ConnId)
|
||||
qKey rq = (userId rq, server rq, connId rq)
|
||||
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
|
||||
|
||||
@@ -130,7 +130,7 @@ import Numeric.Natural
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON)
|
||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol
|
||||
import Simplex.Messaging.Server.QueueStore.QueueInfo
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
@@ -170,17 +170,17 @@ data PClient v err msg = PClient
|
||||
msgQ :: Maybe (TBQueue (ServerTransmissionBatch v err msg))
|
||||
}
|
||||
|
||||
smpClientStub :: TVar ChaChaDRG -> ByteString -> VersionSMP -> Maybe (THandleAuth 'TClient) -> STM SMPClient
|
||||
smpClientStub :: TVar ChaChaDRG -> ByteString -> VersionSMP -> Maybe (THandleAuth 'TClient) -> IO SMPClient
|
||||
smpClientStub g sessionId thVersion thAuth = do
|
||||
let ts = UTCTime (read "2024-03-31") 0
|
||||
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
|
||||
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
|
||||
return
|
||||
ProtocolClient
|
||||
{ action = Nothing,
|
||||
@@ -240,10 +240,20 @@ data SocksMode
|
||||
= -- | always use SOCKS proxy when enabled
|
||||
SMAlways
|
||||
| -- | use SOCKS proxy only for .onion hosts when no public host is available
|
||||
-- This mode is used in SMP proxy to minimize SOCKS proxy usage.
|
||||
-- This mode is used in SMP proxy and in notifications server to minimize SOCKS proxy usage.
|
||||
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
|
||||
@@ -431,7 +441,8 @@ transportSession' = transportSession . client_
|
||||
type UserId = Int64
|
||||
|
||||
-- | Transport session key - includes entity ID if `sessionMode = TSMEntity`.
|
||||
type TransportSession msg = (UserId, ProtoServer msg, Maybe EntityId)
|
||||
-- Please note that for SMP connection ID is used as entity ID, not queue ID.
|
||||
type TransportSession msg = (UserId, ProtoServer msg, Maybe ByteString)
|
||||
|
||||
-- | Connects to 'ProtocolServer' using passed client configuration
|
||||
-- and queue for messages and notifications.
|
||||
@@ -442,21 +453,21 @@ getProtocolClient :: forall v err msg. Protocol v err msg => TVar ChaChaDRG -> T
|
||||
getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, clientALPN, serverVRange, agreeSecret} msgQ disconnected = do
|
||||
case chooseTransportHost networkConfig (host srv) of
|
||||
Right useHost ->
|
||||
(getCurrentTime >>= atomically . mkProtocolClient useHost >>= runClient useTransport useHost)
|
||||
(getCurrentTime >>= mkProtocolClient useHost >>= runClient useTransport useHost)
|
||||
`catch` \(e :: IOException) -> pure . Left $ PCEIOError e
|
||||
Left e -> pure $ Left e
|
||||
where
|
||||
NetworkConfig {tcpConnectTimeout, tcpTimeout, smpPingInterval} = networkConfig
|
||||
mkProtocolClient :: TransportHost -> UTCTime -> STM (PClient v err msg)
|
||||
mkProtocolClient :: TransportHost -> UTCTime -> IO (PClient v err msg)
|
||||
mkProtocolClient transportHost ts = do
|
||||
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
|
||||
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
|
||||
return
|
||||
PClient
|
||||
{ connected,
|
||||
@@ -534,7 +545,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 "" (protocolPing @v @err @msg)
|
||||
whenM (readTVarIO sendPings) $ void . runExceptT $ sendProtocolCommand c Nothing NoEntity (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
|
||||
@@ -555,7 +566,7 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
|
||||
processMsg ProtocolClient {client_ = PClient {sentCommands}} (_, _, (corrId, entId, respOrErr))
|
||||
| B.null $ bs corrId = sendMsg $ STEvent clientResp
|
||||
| otherwise =
|
||||
atomically (TM.lookup corrId sentCommands) >>= \case
|
||||
TM.lookupIO corrId sentCommands >>= \case
|
||||
Nothing -> sendMsg $ STUnexpectedError unexpected
|
||||
Just Request {entityId, command, pending, responseVar} -> do
|
||||
wasPending <-
|
||||
@@ -660,7 +671,7 @@ createSMPQueue ::
|
||||
Bool ->
|
||||
ExceptT SMPClientError IO QueueIdsKeys
|
||||
createSMPQueue c (rKey, rpKey) dhKey auth subMode sndSecure =
|
||||
sendSMPCommand c (Just rpKey) "" (NEW rKey dhKey auth subMode sndSecure) >>= \case
|
||||
sendSMPCommand c (Just rpKey) NoEntity (NEW rKey dhKey auth subMode sndSecure) >>= \case
|
||||
IDS qik -> pure qik
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
@@ -819,11 +830,11 @@ 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 "" (Cmd SProxiedClient (PRXY relayServ proxyAuth)) >>= \case
|
||||
sendProtocolCommand_ c Nothing tOut Nothing NoEntity (Cmd SProxiedClient (PRXY relayServ proxyAuth)) >>= \case
|
||||
PKEY sId vr (chain, key) ->
|
||||
case supportedClientSMPRelayVRange `compatibleVersion` vr of
|
||||
Nothing -> throwE $ transportErr TEVersion
|
||||
Just (Compatible v) -> liftEitherWith (const $ transportErr $ TEHandshake IDENTITY) $ ProxiedRelay sId v <$> validateRelay chain key
|
||||
Just (Compatible v) -> liftEitherWith (const $ transportErr $ TEHandshake IDENTITY) $ ProxiedRelay sId v proxyAuth <$> validateRelay chain key
|
||||
r -> throwE $ unexpectedResponse r
|
||||
| otherwise = throwE $ PCETransportError TEVersion
|
||||
where
|
||||
@@ -842,16 +853,17 @@ connectSMPProxiedRelay c@ProtocolClient {client_ = PClient {tcpConnectTimeout, t
|
||||
data ProxiedRelay = ProxiedRelay
|
||||
{ prSessionId :: SessionId,
|
||||
prVersion :: VersionSMP,
|
||||
prBasicAuth :: Maybe BasicAuth, -- auth is included here to allow reconnecting via the same proxy after NO_SESSION error
|
||||
prServerKey :: C.PublicKeyX25519
|
||||
}
|
||||
|
||||
data ProxyClientError
|
||||
= -- | protocol error response from proxy
|
||||
ProxyProtocolError ErrorType
|
||||
ProxyProtocolError {protocolErr :: ErrorType}
|
||||
| -- | unexpexted response
|
||||
ProxyUnexpectedResponse String
|
||||
ProxyUnexpectedResponse {responseStr :: String}
|
||||
| -- | error between proxy and server
|
||||
ProxyResponseError ErrorType
|
||||
ProxyResponseError {responseErr :: ErrorType}
|
||||
deriving (Eq, Show, Exception)
|
||||
|
||||
instance StrEncoding ProxyClientError where
|
||||
@@ -902,7 +914,7 @@ proxySMPCommand ::
|
||||
SenderId ->
|
||||
Command 'Sender ->
|
||||
ExceptT SMPClientError IO (Either ProxyClientError ())
|
||||
proxySMPCommand c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g, tcpTimeout}} (ProxiedRelay sessionId v serverKey) spKey sId command = do
|
||||
proxySMPCommand c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g, tcpTimeout}} (ProxiedRelay sessionId v _ serverKey) spKey sId command = do
|
||||
-- prepare params
|
||||
let serverThAuth = (\ta -> ta {serverPeerPubKey = serverKey}) <$> thAuth proxyThParams
|
||||
serverThParams = smpTHParamsSetVersion v proxyThParams {sessionId, thAuth = serverThAuth}
|
||||
@@ -920,7 +932,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 sessionId (Cmd SProxiedClient (PFWD v cmdPubKey et))) >>= \case
|
||||
tryE (sendProtocolCommand_ c (Just nonce) tOut Nothing (EntityId sessionId) (Cmd SProxiedClient (PFWD v cmdPubKey et))) >>= \case
|
||||
Right r -> case r of
|
||||
PRES (EncResponse er) -> do
|
||||
-- server interaction errors are thrown directly
|
||||
@@ -956,7 +968,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 "" (Cmd SSender (RFWD eft)) >>= \case
|
||||
sendProtocolCommand_ c (Just nonce) Nothing Nothing NoEntity (Cmd SSender (RFWD eft)) >>= \case
|
||||
RRES (EncFwdResponse efr) -> do
|
||||
-- unwrap
|
||||
r' <- liftEitherWith PCECryptoError $ C.cbDecryptNoPad sessSecret (C.reverseNonce nonce) efr
|
||||
@@ -1004,7 +1016,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 "" $ Left $ PCETransportError TEBadBlock)
|
||||
pure $ L.fromList $ rs <> replicate diff (Response NoEntity $ Left $ PCETransportError TEBadBlock)
|
||||
| otherwise = do
|
||||
putStrLn "send error: more responses than expected"
|
||||
pure $ L.fromList $ take (L.length cs) rs
|
||||
@@ -1078,13 +1090,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 <- atomically $ mkRequest (CorrId corrId)
|
||||
r <- mkRequest (CorrId corrId)
|
||||
pure ((,tToSend) <$> auth, r)
|
||||
where
|
||||
mkRequest :: CorrId -> STM (Request err msg)
|
||||
mkRequest :: CorrId -> IO (Request err msg)
|
||||
mkRequest corrId = do
|
||||
pending <- newTVar True
|
||||
responseVar <- newEmptyTMVar
|
||||
pending <- newTVarIO True
|
||||
responseVar <- newEmptyTMVarIO
|
||||
let r =
|
||||
Request
|
||||
{ corrId,
|
||||
@@ -1093,7 +1105,7 @@ mkTransmission_ ProtocolClient {thParams, client_ = PClient {clientCorrId, sentC
|
||||
pending,
|
||||
responseVar
|
||||
}
|
||||
TM.insert corrId r sentCommands
|
||||
atomically $ TM.insert corrId r sentCommands
|
||||
pure r
|
||||
|
||||
authTransmission :: Maybe (THandleAuth 'TClient) -> Maybe C.APrivateAuthKey -> C.CbNonce -> ByteString -> Either TransportError (Maybe TransmissionAuth)
|
||||
@@ -1139,6 +1151,6 @@ $(J.deriveJSON (enumJSON $ dropPrefix "SPF") ''SMPProxyFallback)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''NetworkConfig)
|
||||
|
||||
$(J.deriveJSON (enumJSON $ dropPrefix "Proxy") ''ProxyClientError)
|
||||
$(J.deriveJSON (sumTypeJSON $ dropPrefix "Proxy") ''ProxyClientError)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''TBQueueInfo)
|
||||
|
||||
@@ -20,17 +20,15 @@ import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Monad.Trans.Except
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import Data.Bifunctor (bimap, first)
|
||||
import Data.Bifunctor (first)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Either (partitionEithers)
|
||||
import Data.List (partition)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (listToMaybe)
|
||||
import Data.Set (Set)
|
||||
import qualified Data.Set as S
|
||||
import Data.Text.Encoding
|
||||
import Data.Time.Clock (NominalDiffTime, UTCTime, addUTCTime, getCurrentTime)
|
||||
import Data.Tuple (swap)
|
||||
@@ -55,8 +53,8 @@ type SMPClientVar = SessionVar (Either (SMPClientError, Maybe UTCTime) (OwnServe
|
||||
data SMPClientAgentEvent
|
||||
= CAConnected SMPServer
|
||||
| CADisconnected SMPServer (Set SMPSub)
|
||||
| CAResubscribed SMPServer (NonEmpty SMPSub)
|
||||
| CASubError SMPServer (NonEmpty (SMPSub, SMPClientError))
|
||||
| CASubscribed SMPServer SMPSubParty (NonEmpty QueueId)
|
||||
| CASubError SMPServer SMPSubParty (NonEmpty (QueueId, SMPClientError))
|
||||
|
||||
data SMPSubParty = SPRecipient | SPNotifier
|
||||
deriving (Eq, Ord, Show)
|
||||
@@ -86,9 +84,9 @@ defaultSMPClientAgentConfig =
|
||||
maxInterval = 10 * second
|
||||
},
|
||||
persistErrorInterval = 30, -- seconds
|
||||
msgQSize = 256,
|
||||
agentQSize = 256,
|
||||
agentSubsBatchSize = 900,
|
||||
msgQSize = 1024,
|
||||
agentQSize = 1024,
|
||||
agentSubsBatchSize = 1360,
|
||||
ownServerDomains = []
|
||||
}
|
||||
where
|
||||
@@ -102,7 +100,7 @@ data SMPClientAgent = SMPClientAgent
|
||||
randomDrg :: TVar ChaChaDRG,
|
||||
smpClients :: TMap SMPServer SMPClientVar,
|
||||
smpSessions :: TMap SessionId (OwnServer, SMPClient),
|
||||
srvSubs :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey),
|
||||
srvSubs :: TMap SMPServer (TMap SMPSub (SessionId, C.APrivateAuthKey)),
|
||||
pendingSrvSubs :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey),
|
||||
smpSubWorkers :: TMap SMPServer (SessionVar (Async ())),
|
||||
workerSeq :: TVar Int
|
||||
@@ -110,17 +108,17 @@ data SMPClientAgent = SMPClientAgent
|
||||
|
||||
type OwnServer = Bool
|
||||
|
||||
newSMPClientAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> STM SMPClientAgent
|
||||
newSMPClientAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> IO SMPClientAgent
|
||||
newSMPClientAgent agentCfg@SMPClientAgentConfig {msgQSize, agentQSize} randomDrg = do
|
||||
active <- newTVar True
|
||||
msgQ <- newTBQueue msgQSize
|
||||
agentQ <- newTBQueue agentQSize
|
||||
smpClients <- TM.empty
|
||||
smpSessions <- TM.empty
|
||||
srvSubs <- TM.empty
|
||||
pendingSrvSubs <- TM.empty
|
||||
smpSubWorkers <- TM.empty
|
||||
workerSeq <- newTVar 0
|
||||
active <- newTVarIO True
|
||||
msgQ <- newTBQueueIO msgQSize
|
||||
agentQ <- newTBQueueIO agentQSize
|
||||
smpClients <- TM.emptyIO
|
||||
smpSessions <- TM.emptyIO
|
||||
srvSubs <- TM.emptyIO
|
||||
pendingSrvSubs <- TM.emptyIO
|
||||
smpSubWorkers <- TM.emptyIO
|
||||
workerSeq <- newTVarIO 0
|
||||
pure
|
||||
SMPClientAgent
|
||||
{ agentCfg,
|
||||
@@ -192,7 +190,7 @@ getSMPServerClient'' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, worke
|
||||
isOwnServer :: SMPClientAgent -> SMPServer -> OwnServer
|
||||
isOwnServer SMPClientAgent {agentCfg} ProtocolServer {host} =
|
||||
let srv = strEncode $ L.head host
|
||||
in any (\s -> s == srv || (B.cons '.' s) `B.isSuffixOf` srv) (ownServerDomains agentCfg)
|
||||
in any (\s -> s == srv || B.cons '.' s `B.isSuffixOf` srv) (ownServerDomains agentCfg)
|
||||
|
||||
-- | Run an SMP client for SMPClientVar
|
||||
connectClient :: SMPClientAgent -> SMPServer -> SMPClientVar -> IO (Either SMPClientError SMPClient)
|
||||
@@ -206,20 +204,17 @@ 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.delete (sessionId $ thParams smp) smpSessions
|
||||
TM.lookupDelete srv (srvSubs ca) >>= mapM updateSubs
|
||||
TM.lookup srv (srvSubs ca) >>= mapM updateSubs
|
||||
where
|
||||
sessId = sessionId $ thParams smp
|
||||
updateSubs sVar = do
|
||||
ss <- readTVar sVar
|
||||
addPendingSubs sVar ss
|
||||
pure ss
|
||||
|
||||
addPendingSubs sVar ss = do
|
||||
let ps = pendingSrvSubs ca
|
||||
TM.lookup srv ps >>= \case
|
||||
Just ss' -> TM.union ss ss'
|
||||
_ -> TM.insert srv sVar ps
|
||||
-- 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
|
||||
|
||||
serverDown :: Map SMPSub C.APrivateAuthKey -> IO ()
|
||||
serverDown ss = unless (M.null ss) $ do
|
||||
@@ -234,7 +229,7 @@ reconnectClient ca@SMPClientAgent {active, agentCfg, smpSubWorkers, workerSeq} s
|
||||
where
|
||||
getWorkerVar ts =
|
||||
ifM
|
||||
(null <$> getPending)
|
||||
(noPending)
|
||||
(pure Nothing) -- prevent race with cleanup and adding pending queues in another call
|
||||
(Just <$> getSessVar workerSeq srv smpSubWorkers ts)
|
||||
newSubWorker :: SessionVar (Async ()) -> IO ()
|
||||
@@ -243,12 +238,13 @@ reconnectClient ca@SMPClientAgent {active, agentCfg, smpSubWorkers, workerSeq} s
|
||||
atomically $ putTMVar (sessionVar v) a
|
||||
runSubWorker =
|
||||
withRetryInterval (reconnectInterval agentCfg) $ \_ loop -> do
|
||||
pending <- atomically getPending
|
||||
forM_ pending $ \cs -> whenM (readTVarIO active) $ do
|
||||
void $ tcpConnectTimeout `timeout` runExceptT (reconnectSMPClient ca srv cs)
|
||||
pending <- liftIO getPending
|
||||
unless (null pending) $ whenM (readTVarIO active) $ do
|
||||
void $ tcpConnectTimeout `timeout` runExceptT (reconnectSMPClient ca srv pending)
|
||||
loop
|
||||
ProtocolClientConfig {networkConfig = NetworkConfig {tcpConnectTimeout}} = smpCfg agentCfg
|
||||
getPending = mapM readTVar =<< TM.lookup srv (pendingSrvSubs ca)
|
||||
noPending = maybe (pure True) (fmap M.null . readTVar) =<< TM.lookup srv (pendingSrvSubs ca)
|
||||
getPending = maybe (pure M.empty) readTVarIO =<< TM.lookupIO 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,32 +254,22 @@ 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 -> do
|
||||
subs' <- filterM (fmap not . atomically . hasSub (srvSubs ca) srv . fst) $ M.assocs cs
|
||||
let (nSubs, rSubs) = partition (isNotifier . fst . fst) subs'
|
||||
withSMP ca srv $ \smp -> liftIO $ do
|
||||
currSubs <- maybe (pure M.empty) readTVarIO =<< TM.lookupIO srv (srvSubs ca)
|
||||
let (nSubs, rSubs) = foldr (groupSub currSubs) ([], []) $ M.assocs cs
|
||||
subscribe_ smp SPNotifier nSubs
|
||||
subscribe_ smp SPRecipient rSubs
|
||||
where
|
||||
isNotifier = \case
|
||||
SPNotifier -> True
|
||||
SPRecipient -> False
|
||||
subscribe_ :: SMPClient -> SMPSubParty -> [(SMPSub, C.APrivateAuthKey)] -> ExceptT SMPClientError IO ()
|
||||
subscribe_ smp party = mapM_ subscribeBatch . toChunks (agentSubsBatchSize agentCfg)
|
||||
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
|
||||
| otherwise = case party of
|
||||
SPNotifier -> (s' : nSubs, rSubs)
|
||||
SPRecipient -> (nSubs, s' : rSubs)
|
||||
where
|
||||
subscribeBatch subs' = do
|
||||
let subs'' :: (NonEmpty (QueueId, C.APrivateAuthKey)) = L.map (first snd) subs'
|
||||
rs <- liftIO $ smpSubscribeQueues party ca smp srv subs''
|
||||
let rs' :: (NonEmpty ((SMPSub, C.APrivateAuthKey), Either SMPClientError ())) =
|
||||
L.zipWith (first . const) subs' rs
|
||||
rs'' :: [Either (SMPSub, SMPClientError) (SMPSub, C.APrivateAuthKey)] =
|
||||
map (\(sub, r) -> bimap (fst sub,) (const sub) r) $ L.toList rs'
|
||||
(errs, oks) = partitionEithers rs''
|
||||
(tempErrs, finalErrs) = partition (temporaryClientError . snd) errs
|
||||
mapM_ (atomically . addSubscription ca srv) oks
|
||||
mapM_ (notify ca . CAResubscribed srv) $ L.nonEmpty $ map fst oks
|
||||
mapM_ (atomically . removePendingSubscription ca srv . fst) finalErrs
|
||||
mapM_ (notify ca . CASubError srv) $ L.nonEmpty finalErrs
|
||||
mapM_ (throwE . snd) $ listToMaybe tempErrs
|
||||
s' = (qId, k)
|
||||
subscribe_ :: SMPClient -> SMPSubParty -> [(QueueId, C.APrivateAuthKey)] -> IO ()
|
||||
subscribe_ smp party = mapM_ (smpSubscribeQueues party ca smp srv) . toChunks (agentSubsBatchSize agentCfg)
|
||||
|
||||
notify :: MonadIO m => SMPClientAgent -> SMPClientAgentEvent -> m ()
|
||||
notify ca evt = atomically $ writeTBQueue (agentQ ca) evt
|
||||
@@ -297,14 +283,15 @@ getConnectedSMPServerClient SMPClientAgent {smpClients} srv =
|
||||
$>>= \case
|
||||
(_, Right r) -> pure $ Just $ Right r
|
||||
(v, Left (e, ts_)) ->
|
||||
pure ts_ $>>= \ts -> -- proxy will create a new connection if ts_ is Nothing
|
||||
pure ts_ $>>= \ts ->
|
||||
-- proxy will create a new connection if ts_ is Nothing
|
||||
ifM
|
||||
((ts <) <$> liftIO getCurrentTime) -- error persistence interval period expired?
|
||||
(Nothing <$ atomically (removeSessVar v srv smpClients)) -- proxy will create a new connection
|
||||
(pure $ Just $ Left e) -- not expired, returning error
|
||||
|
||||
lookupSMPServerClient :: SMPClientAgent -> SessionId -> STM (Maybe (OwnServer, SMPClient))
|
||||
lookupSMPServerClient SMPClientAgent {smpSessions} sessId = TM.lookup sessId smpSessions
|
||||
lookupSMPServerClient :: SMPClientAgent -> SessionId -> IO (Maybe (OwnServer, SMPClient))
|
||||
lookupSMPServerClient SMPClientAgent {smpSessions} sessId = TM.lookupIO sessId smpSessions
|
||||
|
||||
closeSMPClientAgent :: SMPClientAgent -> IO ()
|
||||
closeSMPClientAgent c = do
|
||||
@@ -334,86 +321,102 @@ withSMP ca srv action = (getSMPServerClient' ca srv >>= action) `catchE` logSMPE
|
||||
liftIO $ putStrLn $ "SMP error (" <> show srv <> "): " <> show e
|
||||
throwE e
|
||||
|
||||
subscribeQueue :: SMPClientAgent -> SMPServer -> (SMPSub, C.APrivateAuthKey) -> ExceptT SMPClientError IO ()
|
||||
subscribeQueue ca srv sub = do
|
||||
atomically $ addPendingSubscription ca srv sub
|
||||
withSMP ca srv $ \smp -> subscribe_ smp `catchE` handleErr
|
||||
where
|
||||
subscribe_ smp = do
|
||||
smpSubscribe smp sub
|
||||
atomically $ addSubscription ca srv sub
|
||||
|
||||
handleErr e = do
|
||||
atomically . when (e /= PCENetworkError && e /= PCEResponseTimeout) $
|
||||
removePendingSubscription ca srv (fst sub)
|
||||
throwE e
|
||||
|
||||
subscribeQueuesSMP :: SMPClientAgent -> SMPServer -> NonEmpty (RecipientId, RcvPrivateAuthKey) -> IO (NonEmpty (RecipientId, Either SMPClientError ()))
|
||||
subscribeQueuesSMP :: SMPClientAgent -> SMPServer -> NonEmpty (RecipientId, RcvPrivateAuthKey) -> IO ()
|
||||
subscribeQueuesSMP = subscribeQueues_ SPRecipient
|
||||
|
||||
subscribeQueuesNtfs :: SMPClientAgent -> SMPServer -> NonEmpty (NotifierId, NtfPrivateAuthKey) -> IO (NonEmpty (NotifierId, Either SMPClientError ()))
|
||||
subscribeQueuesNtfs :: SMPClientAgent -> SMPServer -> NonEmpty (NotifierId, NtfPrivateAuthKey) -> IO ()
|
||||
subscribeQueuesNtfs = subscribeQueues_ SPNotifier
|
||||
|
||||
subscribeQueues_ :: SMPSubParty -> SMPClientAgent -> SMPServer -> NonEmpty (QueueId, C.APrivateAuthKey) -> IO (NonEmpty (QueueId, Either SMPClientError ()))
|
||||
subscribeQueues_ :: SMPSubParty -> SMPClientAgent -> SMPServer -> NonEmpty (QueueId, C.APrivateAuthKey) -> IO ()
|
||||
subscribeQueues_ party ca srv subs = do
|
||||
atomically $ forM_ subs $ addPendingSubscription ca srv . first (party,)
|
||||
atomically $ addPendingSubs ca srv party $ L.toList subs
|
||||
runExceptT (getSMPServerClient' ca srv) >>= \case
|
||||
Left e -> pure $ L.map ((,Left e) . fst) subs
|
||||
Right smp -> smpSubscribeQueues party ca smp srv subs
|
||||
Left _ -> pure () -- no call to reconnectClient - failing getSMPServerClient' does that
|
||||
|
||||
smpSubscribeQueues :: SMPSubParty -> SMPClientAgent -> SMPClient -> SMPServer -> NonEmpty (QueueId, C.APrivateAuthKey) -> IO (NonEmpty (QueueId, Either SMPClientError ()))
|
||||
smpSubscribeQueues :: SMPSubParty -> SMPClientAgent -> SMPClient -> SMPServer -> NonEmpty (QueueId, C.APrivateAuthKey) -> IO ()
|
||||
smpSubscribeQueues party ca smp srv subs = do
|
||||
rs <- L.zip subs <$> subscribe smp (L.map swap subs)
|
||||
atomically $ forM rs $ \(sub, r) ->
|
||||
(fst sub,) <$> case r of
|
||||
Right () -> do
|
||||
addSubscription ca srv $ first (party,) sub
|
||||
pure $ Right ()
|
||||
Left e -> do
|
||||
when (e /= PCENetworkError && e /= PCEResponseTimeout) $
|
||||
removePendingSubscription ca srv (party, fst sub)
|
||||
pure $ Left e
|
||||
rs <- subscribe smp $ L.map swap subs
|
||||
rs' <-
|
||||
atomically $
|
||||
ifM
|
||||
(activeClientSession ca smp srv)
|
||||
(Just <$> processSubscriptions rs)
|
||||
(pure Nothing)
|
||||
case rs' of
|
||||
Just (tempErrs, finalErrs, oks, _) -> do
|
||||
notify_ CASubscribed $ map fst oks
|
||||
notify_ CASubError finalErrs
|
||||
when tempErrs $ reconnectClient ca srv
|
||||
Nothing -> reconnectClient ca srv
|
||||
where
|
||||
processSubscriptions :: NonEmpty (Either SMPClientError ()) -> STM (Bool, [(QueueId, SMPClientError)], [(QueueId, (SessionId, 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
|
||||
Right ()
|
||||
| M.member (party, qId) pending -> (tempErrs, finalErrs, (qId, (sessId, pk)) : oks, qId : notPending)
|
||||
| otherwise -> acc
|
||||
Left e
|
||||
| temporaryClientError e -> (True, finalErrs, oks, notPending)
|
||||
| otherwise -> (tempErrs, (qId, e) : finalErrs, oks, qId : notPending)
|
||||
subscribe = case party of
|
||||
SPRecipient -> subscribeSMPQueues
|
||||
SPNotifier -> subscribeSMPQueuesNtfs
|
||||
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
|
||||
|
||||
showServer :: SMPServer -> ByteString
|
||||
showServer ProtocolServer {host, port} =
|
||||
strEncode host <> B.pack (if null port then "" else ':' : port)
|
||||
|
||||
smpSubscribe :: SMPClient -> (SMPSub, C.APrivateAuthKey) -> ExceptT SMPClientError IO ()
|
||||
smpSubscribe smp ((party, queueId), privKey) = subscribe_ smp privKey queueId
|
||||
addSubscriptions :: SMPClientAgent -> SMPServer -> SMPSubParty -> [(QueueId, (SessionId, C.APrivateAuthKey))] -> STM ()
|
||||
addSubscriptions = addSubsList_ . srvSubs
|
||||
{-# INLINE addSubscriptions #-}
|
||||
|
||||
addPendingSubs :: SMPClientAgent -> SMPServer -> SMPSubParty -> [(QueueId, C.APrivateAuthKey)] -> STM ()
|
||||
addPendingSubs = addSubsList_ . pendingSrvSubs
|
||||
{-# INLINE addPendingSubs #-}
|
||||
|
||||
addSubsList_ :: TMap SMPServer (TMap SMPSub s) -> SMPServer -> SMPSubParty -> [(QueueId, s)] -> STM ()
|
||||
addSubsList_ subs srv party ss = addSubs_ subs srv ss'
|
||||
where
|
||||
subscribe_ = case party of
|
||||
SPRecipient -> subscribeSMPQueue
|
||||
SPNotifier -> subscribeSMPQueueNotifications
|
||||
ss' = M.fromList $ map (first (party,)) ss
|
||||
|
||||
addSubscription :: SMPClientAgent -> SMPServer -> (SMPSub, C.APrivateAuthKey) -> STM ()
|
||||
addSubscription ca srv sub = do
|
||||
addSub_ (srvSubs ca) srv sub
|
||||
removePendingSubscription ca srv $ fst sub
|
||||
|
||||
addPendingSubscription :: SMPClientAgent -> SMPServer -> (SMPSub, C.APrivateAuthKey) -> STM ()
|
||||
addPendingSubscription = addSub_ . pendingSrvSubs
|
||||
|
||||
addSub_ :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey) -> SMPServer -> (SMPSub, C.APrivateAuthKey) -> STM ()
|
||||
addSub_ subs srv (s, key) =
|
||||
addSubs_ :: TMap SMPServer (TMap SMPSub s) -> SMPServer -> Map SMPSub s -> STM ()
|
||||
addSubs_ subs srv ss =
|
||||
TM.lookup srv subs >>= \case
|
||||
Just m -> TM.insert s key m
|
||||
_ -> TM.singleton s key >>= \v -> TM.insert srv v subs
|
||||
Just m -> TM.union ss m
|
||||
_ -> newTVar ss >>= \v -> TM.insert srv v subs
|
||||
|
||||
removeSubscription :: SMPClientAgent -> SMPServer -> SMPSub -> STM ()
|
||||
removeSubscription = removeSub_ . srvSubs
|
||||
{-# INLINE removeSubscription #-}
|
||||
|
||||
removePendingSubscription :: SMPClientAgent -> SMPServer -> SMPSub -> STM ()
|
||||
removePendingSubscription = removeSub_ . pendingSrvSubs
|
||||
|
||||
removeSub_ :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey) -> SMPServer -> SMPSub -> STM ()
|
||||
removeSub_ :: TMap SMPServer (TMap SMPSub s) -> SMPServer -> SMPSub -> STM ()
|
||||
removeSub_ subs srv s = TM.lookup srv subs >>= mapM_ (TM.delete s)
|
||||
|
||||
getSubKey :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey) -> SMPServer -> SMPSub -> STM (Maybe C.APrivateAuthKey)
|
||||
getSubKey subs srv s = TM.lookup srv subs $>>= TM.lookup s
|
||||
removePendingSubs :: SMPClientAgent -> SMPServer -> SMPSubParty -> [QueueId] -> STM ()
|
||||
removePendingSubs = removeSubs_ . pendingSrvSubs
|
||||
{-# INLINE removePendingSubs #-}
|
||||
|
||||
hasSub :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey) -> SMPServer -> SMPSub -> STM Bool
|
||||
hasSub subs srv s = maybe (pure False) (TM.member s) =<< TM.lookup srv subs
|
||||
removeSubs_ :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey) -> SMPServer -> SMPSubParty -> [QueueId] -> STM ()
|
||||
removeSubs_ subs srv party qs = TM.lookup srv subs >>= mapM_ (`modifyTVar'` (`M.withoutKeys` ss))
|
||||
where
|
||||
ss = S.fromList $ map (party,) qs
|
||||
|
||||
@@ -42,10 +42,12 @@ 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
|
||||
|
||||
@@ -28,6 +28,8 @@ 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
|
||||
@@ -39,7 +41,7 @@ import Data.Time.Format.ISO8601
|
||||
import Data.Word (Word16, Word32)
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
import Simplex.Messaging.Util ((<$?>))
|
||||
import Simplex.Messaging.Util (bshow, (<$?>))
|
||||
|
||||
class TextEncoding a where
|
||||
textEncode :: a -> Text
|
||||
@@ -53,14 +55,20 @@ 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
|
||||
@@ -119,15 +127,15 @@ instance StrEncoding Bool where
|
||||
{-# INLINE strP #-}
|
||||
|
||||
instance StrEncoding Int where
|
||||
strEncode = B.pack . show
|
||||
strEncode = bshow
|
||||
{-# INLINE strEncode #-}
|
||||
strP = A.decimal
|
||||
strP = A.signed A.decimal
|
||||
{-# INLINE strP #-}
|
||||
|
||||
instance StrEncoding Int64 where
|
||||
strEncode = B.pack . show
|
||||
strEncode = bshow
|
||||
{-# INLINE strEncode #-}
|
||||
strP = A.decimal
|
||||
strP = A.signed A.decimal
|
||||
{-# INLINE strP #-}
|
||||
|
||||
instance StrEncoding SystemTime where
|
||||
@@ -154,6 +162,10 @@ 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')
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
|
||||
module Simplex.Messaging.Notifications.Client where
|
||||
|
||||
@@ -11,7 +12,7 @@ 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)
|
||||
import Simplex.Messaging.Protocol (ErrorType, pattern NoEntity)
|
||||
|
||||
type NtfClient = ProtocolClient NTFVersion ErrorType NtfResponse
|
||||
|
||||
@@ -22,7 +23,7 @@ defaultNTFClientConfig = defaultClientConfig (Just supportedNTFHandshakes) suppo
|
||||
|
||||
ntfRegisterToken :: NtfClient -> C.APrivateAuthKey -> NewNtfEntity 'Token -> ExceptT NtfClientError IO (NtfTokenId, C.PublicKeyX25519)
|
||||
ntfRegisterToken c pKey newTkn =
|
||||
sendNtfCommand c (Just pKey) "" (TNEW newTkn) >>= \case
|
||||
sendNtfCommand c (Just pKey) NoEntity (TNEW newTkn) >>= \case
|
||||
NRTknId tknId dhKey -> pure (tknId, dhKey)
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
@@ -46,7 +47,7 @@ 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) "" (SNEW newSub) >>= \case
|
||||
sendNtfCommand c (Just pKey) NoEntity (SNEW newSub) >>= \case
|
||||
NRSubId subId -> pure subId
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ instance NtfEntityI e => ProtocolEncoding NTFVersion ErrorType (NtfCommand e) wh
|
||||
fromProtocolError = fromProtocolError @NTFVersion @ErrorType @NtfResponse
|
||||
{-# INLINE fromProtocolError #-}
|
||||
|
||||
checkCredentials (auth, _, entityId, _) cmd = case cmd of
|
||||
checkCredentials (auth, _, EntityId 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 (_, _, entId, _) cmd = case cmd of
|
||||
checkCredentials (_, _, EntityId 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 = ByteString
|
||||
type NtfEntityId = EntityId
|
||||
|
||||
type NtfSubscriptionId = NtfEntityId
|
||||
|
||||
@@ -443,6 +443,8 @@ data NtfSubStatus
|
||||
NSInactive
|
||||
| -- | END received
|
||||
NSEnd
|
||||
| -- | DELD received (connection was deleted)
|
||||
NSDeleted
|
||||
| -- | SMP AUTH error
|
||||
NSAuth
|
||||
| -- | SMP error other than AUTH
|
||||
@@ -456,6 +458,7 @@ ntfShouldSubscribe = \case
|
||||
NSActive -> True
|
||||
NSInactive -> True
|
||||
NSEnd -> False
|
||||
NSDeleted -> False
|
||||
NSAuth -> False
|
||||
NSErr _ -> False
|
||||
|
||||
@@ -466,6 +469,7 @@ instance Encoding NtfSubStatus where
|
||||
NSActive -> "ACTIVE"
|
||||
NSInactive -> "INACTIVE"
|
||||
NSEnd -> "END"
|
||||
NSDeleted -> "DELETED"
|
||||
NSAuth -> "AUTH"
|
||||
NSErr err -> "ERR " <> err
|
||||
smpP =
|
||||
@@ -475,6 +479,7 @@ instance Encoding NtfSubStatus where
|
||||
"ACTIVE" -> pure NSActive
|
||||
"INACTIVE" -> pure NSInactive
|
||||
"END" -> pure NSEnd
|
||||
"DELETED" -> pure NSDeleted
|
||||
"AUTH" -> pure NSAuth
|
||||
"ERR" -> NSErr <$> (A.space *> A.takeByteString)
|
||||
_ -> fail "bad NtfSubStatus"
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
@@ -19,6 +20,7 @@ import Control.Monad.Reader
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Functor (($>))
|
||||
import Data.IORef
|
||||
import Data.Int (Int64)
|
||||
import Data.List (intercalate, sort)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
@@ -30,6 +32,7 @@ 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 Network.Socket (ServiceName)
|
||||
import Simplex.Messaging.Client (ProtocolClientError (..), SMPClientError, ServerTransmission (..))
|
||||
import Simplex.Messaging.Client.Agent
|
||||
@@ -42,7 +45,7 @@ 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 (ErrorType (..), ProtocolServer (host), SMPServer, SignedTransmission, Transmission, encodeTransmission, tGet, tPut)
|
||||
import Simplex.Messaging.Protocol (EntityId (..), ErrorType (..), ProtocolServer (host), SMPServer, SignedTransmission, Transmission, pattern NoEntity, encodeTransmission, tGet, tPut)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Server
|
||||
import Simplex.Messaging.Server.Stats
|
||||
@@ -118,16 +121,16 @@ ntfServer cfg@NtfServerConfig {transports, transportConfig = tCfg} started = do
|
||||
withFile statsFilePath AppendMode $ \h -> liftIO $ do
|
||||
hSetBuffering h LineBuffering
|
||||
ts <- getCurrentTime
|
||||
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
|
||||
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
|
||||
hPutStrLn h $
|
||||
intercalate
|
||||
","
|
||||
@@ -176,10 +179,10 @@ ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAge
|
||||
|
||||
getSMPSubscriber :: SMPServer -> M SMPSubscriber
|
||||
getSMPSubscriber smpServer =
|
||||
atomically (TM.lookup smpServer smpSubscribers) >>= maybe createSMPSubscriber pure
|
||||
liftIO (TM.lookupIO smpServer smpSubscribers) >>= maybe createSMPSubscriber pure
|
||||
where
|
||||
createSMPSubscriber = do
|
||||
sub@SMPSubscriber {subThreadId} <- atomically newSMPSubscriber
|
||||
sub@SMPSubscriber {subThreadId} <- liftIO newSMPSubscriber
|
||||
atomically $ TM.insert smpServer sub smpSubscribers
|
||||
tId <- mkWeakThreadId =<< forkIO (runSMPSubscriber sub)
|
||||
atomically . writeTVar subThreadId $ Just tId
|
||||
@@ -188,39 +191,22 @@ ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAge
|
||||
runSMPSubscriber :: SMPSubscriber -> M ()
|
||||
runSMPSubscriber SMPSubscriber {newSubQ = subscriberSubQ} =
|
||||
forever $ do
|
||||
subs <- atomically (peekTQueue subscriberSubQ)
|
||||
subs <- atomically $ readTQueue subscriberSubQ
|
||||
let subs' = L.map (\(NtfSub sub) -> sub) subs
|
||||
srv = server $ L.head subs
|
||||
logSubStatus srv "subscribing" $ length subs
|
||||
mapM_ (\NtfSubData {smpQueue} -> updateSubStatus smpQueue NSPending) subs'
|
||||
rs <- liftIO $ subscribeQueues srv subs'
|
||||
(subs'', oks, errs) <- foldM process ([], 0, []) rs
|
||||
atomically $ do
|
||||
void $ readTQueue subscriberSubQ
|
||||
mapM_ (writeTQueue subscriberSubQ . L.map NtfSub) $ L.nonEmpty subs''
|
||||
logSubStatus srv "retrying" $ length subs''
|
||||
logSubStatus srv "subscribed" oks
|
||||
logSubErrors srv errs
|
||||
where
|
||||
process :: ([NtfSubData], Int, [NtfSubStatus]) -> (NtfSubData, Either SMPClientError ()) -> M ([NtfSubData], Int, [NtfSubStatus])
|
||||
process (subs, oks, errs) (sub@NtfSubData {smpQueue}, r) = case r of
|
||||
Right _ -> updateSubStatus smpQueue NSActive $> (subs, oks + 1, errs)
|
||||
Left e -> update <$> handleSubError smpQueue e
|
||||
where
|
||||
update = \case
|
||||
Just err -> (subs, oks, err : errs) -- permanent error, log and don't retry subscription
|
||||
Nothing -> (sub : subs, oks, errs) -- temporary error, retry subscription
|
||||
liftIO $ subscribeQueues srv subs'
|
||||
|
||||
-- \| Subscribe to queues. The list of results can have a different order.
|
||||
subscribeQueues :: SMPServer -> NonEmpty NtfSubData -> IO (NonEmpty (NtfSubData, Either SMPClientError ()))
|
||||
subscribeQueues srv subs =
|
||||
L.zipWith (\s r -> (s, snd r)) subs <$> subscribeQueuesNtfs ca srv (L.map sub subs)
|
||||
subscribeQueues :: SMPServer -> NonEmpty NtfSubData -> IO ()
|
||||
subscribeQueues srv subs = subscribeQueuesNtfs ca srv (L.map sub subs)
|
||||
where
|
||||
sub NtfSubData {smpQueue = SMPQueueNtf {notifierId}, notifierKey} = (notifierId, notifierKey)
|
||||
|
||||
receiveSMP :: M ()
|
||||
receiveSMP = forever $ do
|
||||
((_, srv, _), _, _, ts) <- atomically $ readTBQueue msgQ
|
||||
((_, srv, _), _thVersion, sessionId, 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
|
||||
@@ -232,14 +218,17 @@ ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAge
|
||||
st <- asks store
|
||||
NtfPushServer {pushQ} <- asks pushServer
|
||||
stats <- asks serverStats
|
||||
atomically $ updatePeriodStats (activeSubs stats) ntfId
|
||||
liftIO $ updatePeriodStats (activeSubs stats) ntfId
|
||||
atomically $
|
||||
findNtfSubscriptionToken st smpQueue
|
||||
>>= mapM_ (\tkn -> writeTBQueue pushQ (tkn, PNMessage PNMessageData {smpQueue, ntfTs, nmsgNonce, encNMsgMeta}))
|
||||
>>= mapM_ (\tkn -> writeTBQueue pushQ (tkn, PNMessage (PNMessageData {smpQueue, ntfTs, nmsgNonce, encNMsgMeta} :| [])))
|
||||
incNtfStat ntfReceived
|
||||
Right SMP.END -> updateSubStatus smpQueue NSEnd
|
||||
Right SMP.END ->
|
||||
whenM (atomically $ activeClientSession' ca sessionId srv) $
|
||||
updateSubStatus smpQueue NSEnd
|
||||
Right SMP.DELD -> updateSubStatus smpQueue NSDeleted
|
||||
Right (SMP.ERR e) -> logError $ "SMP server error: " <> tshow e
|
||||
Right _ -> logError $ "SMP server unexpected response"
|
||||
Right _ -> logError "SMP server unexpected response"
|
||||
Left e -> logError $ "SMP client error: " <> tshow e
|
||||
|
||||
receiveAgent =
|
||||
@@ -252,11 +241,11 @@ ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAge
|
||||
forM_ subs $ \(_, ntfId) -> do
|
||||
let smpQueue = SMPQueueNtf srv ntfId
|
||||
updateSubStatus smpQueue NSInactive
|
||||
CAResubscribed srv subs -> do
|
||||
forM_ subs $ \(_, ntfId) -> updateSubStatus (SMPQueueNtf srv ntfId) NSActive
|
||||
logSubStatus srv "resubscribed" $ length subs
|
||||
CASubError srv errs ->
|
||||
forM errs (\((_, ntfId), err) -> handleSubError (SMPQueueNtf srv ntfId) err)
|
||||
CASubscribed srv _ subs -> do
|
||||
forM_ subs $ \ntfId -> updateSubStatus (SMPQueueNtf srv ntfId) NSActive
|
||||
logSubStatus srv "subscribed" $ length subs
|
||||
CASubError srv _ errs ->
|
||||
forM errs (\(ntfId, err) -> handleSubError (SMPQueueNtf srv ntfId) err)
|
||||
>>= logSubErrors srv . catMaybes . L.toList
|
||||
|
||||
logSubStatus srv event n =
|
||||
@@ -314,7 +303,7 @@ ntfPush s@NtfPushServer {pushQ} = forever $ do
|
||||
void $ deliverNotification pp tkn ntf
|
||||
PNMessage {} -> checkActiveTkn status $ do
|
||||
stats <- asks serverStats
|
||||
atomically $ updatePeriodStats (activeTokens stats) ntfTknId
|
||||
liftIO $ updatePeriodStats (activeTokens stats) ntfTknId
|
||||
void $ deliverNotification pp tkn ntf
|
||||
incNtfStat ntfDelivered
|
||||
where
|
||||
@@ -350,7 +339,7 @@ runNtfClientTransport :: Transport c => THandleNTF c 'TServer -> M ()
|
||||
runNtfClientTransport th@THandle {params} = do
|
||||
qSize <- asks $ clientQSize . config
|
||||
ts <- liftIO getSystemTime
|
||||
c <- atomically $ newNtfServerClient qSize params ts
|
||||
c <- liftIO $ newNtfServerClient qSize params ts
|
||||
s <- asks subscriber
|
||||
ps <- asks pushServer
|
||||
expCfg <- asks $ inactiveClientExpiration . config
|
||||
@@ -461,7 +450,7 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
|
||||
atomically $ writeTBQueue pushQ (tkn, PNVerification regCode)
|
||||
withNtfLog (`logCreateToken` tkn)
|
||||
incNtfStatT token tknCreated
|
||||
pure (corrId, "", NRTknId tknId srvDhPubKey)
|
||||
pure (corrId, NoEntity, 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
|
||||
@@ -524,7 +513,7 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
|
||||
| otherwise -> do
|
||||
logDebug "TCRN"
|
||||
atomically $ writeTVar tknCronInterval int
|
||||
atomically (TM.lookup tknId intervalNotifiers) >>= \case
|
||||
liftIO (TM.lookupIO tknId intervalNotifiers) >>= \case
|
||||
Nothing -> runIntervalNotifier int
|
||||
Just IntervalNotifier {interval, action} ->
|
||||
unless (interval == int) $ do
|
||||
@@ -552,7 +541,7 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
|
||||
_ -> pure $ NRErr AUTH
|
||||
withNtfLog (`logCreateSubscription` sub)
|
||||
incNtfStat subCreated
|
||||
pure (corrId, "", resp)
|
||||
pure (corrId, NoEntity, resp)
|
||||
NtfReqCmd SSubscription (NtfSub NtfSubData {smpQueue = SMPQueueNtf {smpServer, notifierId}, notifierKey = registeredNKey, subStatus}) (corrId, subId, cmd) -> do
|
||||
status <- readTVarIO subStatus
|
||||
(corrId,subId,) <$> case cmd of
|
||||
@@ -577,7 +566,7 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
|
||||
PING -> pure NRPong
|
||||
NtfReqPing corrId entId -> pure (corrId, entId, NRPong)
|
||||
getId :: M NtfEntityId
|
||||
getId = randomBytes =<< asks (subIdBytes . config)
|
||||
getId = fmap EntityId . randomBytes =<< asks (subIdBytes . config)
|
||||
getRegCode :: M NtfRegCode
|
||||
getRegCode = NtfRegCode <$> (randomBytes =<< asks (regCodeBytes . config))
|
||||
randomBytes :: Int -> M ByteString
|
||||
@@ -590,19 +579,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 -> TVar Int) -> M ()
|
||||
incNtfStatT :: DeviceToken -> (NtfServerStats -> IORef Int) -> M ()
|
||||
incNtfStatT (DeviceToken PPApnsNull _) _ = pure ()
|
||||
incNtfStatT _ statSel = incNtfStat statSel
|
||||
|
||||
incNtfStat :: (NtfServerStats -> TVar Int) -> M ()
|
||||
incNtfStat :: (NtfServerStats -> IORef Int) -> M ()
|
||||
incNtfStat statSel = do
|
||||
stats <- asks serverStats
|
||||
atomically $ modifyTVar' (statSel stats) (+ 1)
|
||||
liftIO $ atomicModifyIORef'_ (statSel stats) (+ 1)
|
||||
|
||||
saveServerStats :: M ()
|
||||
saveServerStats =
|
||||
asks (serverStatsBackupFile . config)
|
||||
>>= mapM_ (\f -> asks serverStats >>= atomically . getNtfServerStatsData >>= liftIO . saveStats f)
|
||||
>>= mapM_ (\f -> asks serverStats >>= liftIO . getNtfServerStatsData >>= liftIO . saveStats f)
|
||||
where
|
||||
saveStats f stats = do
|
||||
logInfo $ "saving server stats to file " <> T.pack f
|
||||
@@ -617,7 +606,7 @@ restoreServerStats = asks (serverStatsBackupFile . config) >>= mapM_ restoreStat
|
||||
liftIO (strDecode <$> B.readFile f) >>= \case
|
||||
Right d -> do
|
||||
s <- asks serverStats
|
||||
atomically $ setNtfServerStats s d
|
||||
liftIO $ setNtfServerStats s d
|
||||
renameFile f $ f <> ".bak"
|
||||
logInfo "server stats restored"
|
||||
Left e -> do
|
||||
|
||||
@@ -10,7 +10,6 @@ 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)
|
||||
@@ -85,16 +84,16 @@ data NtfEnv = NtfEnv
|
||||
|
||||
newNtfServerEnv :: NtfServerConfig -> IO NtfEnv
|
||||
newNtfServerEnv config@NtfServerConfig {subQSize, pushQSize, smpAgentCfg, apnsConfig, storeLogFile, caCertificateFile, certificateFile, privateKeyFile, transportConfig} = do
|
||||
random <- liftIO C.newRandom
|
||||
store <- atomically newNtfStore
|
||||
random <- C.newRandom
|
||||
store <- newNtfStore
|
||||
logInfo "restoring subscriptions..."
|
||||
storeLog <- liftIO $ mapM (`readWriteNtfStore` store) storeLogFile
|
||||
storeLog <- mapM (`readWriteNtfStore` store) storeLogFile
|
||||
logInfo "restored subscriptions"
|
||||
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
|
||||
subscriber <- newNtfSubscriber subQSize smpAgentCfg random
|
||||
pushServer <- newNtfPushServer pushQSize apnsConfig
|
||||
tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig)
|
||||
Fingerprint fp <- loadFingerprint caCertificateFile
|
||||
serverStats <- newNtfServerStats =<< getCurrentTime
|
||||
pure NtfEnv {config, subscriber, pushServer, store, storeLog, random, tlsServerParams, serverIdentity = C.KeyHash fp, serverStats}
|
||||
|
||||
data NtfSubscriber = NtfSubscriber
|
||||
@@ -103,10 +102,10 @@ data NtfSubscriber = NtfSubscriber
|
||||
smpAgent :: SMPClientAgent
|
||||
}
|
||||
|
||||
newNtfSubscriber :: Natural -> SMPClientAgentConfig -> TVar ChaChaDRG -> STM NtfSubscriber
|
||||
newNtfSubscriber :: Natural -> SMPClientAgentConfig -> TVar ChaChaDRG -> IO NtfSubscriber
|
||||
newNtfSubscriber qSize smpAgentCfg random = do
|
||||
smpSubscribers <- TM.empty
|
||||
newSubQ <- newTBQueue qSize
|
||||
smpSubscribers <- TM.emptyIO
|
||||
newSubQ <- newTBQueueIO qSize
|
||||
smpAgent <- newSMPClientAgent smpAgentCfg random
|
||||
pure NtfSubscriber {smpSubscribers, newSubQ, smpAgent}
|
||||
|
||||
@@ -115,10 +114,10 @@ data SMPSubscriber = SMPSubscriber
|
||||
subThreadId :: TVar (Maybe (Weak ThreadId))
|
||||
}
|
||||
|
||||
newSMPSubscriber :: STM SMPSubscriber
|
||||
newSMPSubscriber :: IO SMPSubscriber
|
||||
newSMPSubscriber = do
|
||||
newSubQ <- newTQueue
|
||||
subThreadId <- newTVar Nothing
|
||||
newSubQ <- newTQueueIO
|
||||
subThreadId <- newTVarIO Nothing
|
||||
pure SMPSubscriber {newSubQ, subThreadId}
|
||||
|
||||
data NtfPushServer = NtfPushServer
|
||||
@@ -134,11 +133,11 @@ data IntervalNotifier = IntervalNotifier
|
||||
interval :: Word16
|
||||
}
|
||||
|
||||
newNtfPushServer :: Natural -> APNSPushClientConfig -> STM NtfPushServer
|
||||
newNtfPushServer :: Natural -> APNSPushClientConfig -> IO NtfPushServer
|
||||
newNtfPushServer qSize apnsConfig = do
|
||||
pushQ <- newTBQueue qSize
|
||||
pushClients <- TM.empty
|
||||
intervalNotifiers <- TM.empty
|
||||
pushQ <- newTBQueueIO qSize
|
||||
pushClients <- TM.emptyIO
|
||||
intervalNotifiers <- TM.emptyIO
|
||||
pure NtfPushServer {pushQ, pushClients, intervalNotifiers, apnsConfig}
|
||||
|
||||
newPushClient :: NtfPushServer -> PushProvider -> IO PushProviderClient
|
||||
@@ -151,7 +150,7 @@ newPushClient NtfPushServer {apnsConfig, pushClients} pp = do
|
||||
|
||||
getPushClient :: NtfPushServer -> PushProvider -> IO PushProviderClient
|
||||
getPushClient s@NtfPushServer {pushClients} pp =
|
||||
atomically (TM.lookup pp pushClients) >>= maybe (newPushClient s pp) pure
|
||||
TM.lookupIO pp pushClients >>= maybe (newPushClient s pp) pure
|
||||
|
||||
data NtfRequest
|
||||
= NtfReqNew CorrId ANewNtfEntity
|
||||
@@ -167,11 +166,11 @@ data NtfServerClient = NtfServerClient
|
||||
sndActiveAt :: TVar SystemTime
|
||||
}
|
||||
|
||||
newNtfServerClient :: Natural -> THandleParams NTFVersion 'TServer -> SystemTime -> STM NtfServerClient
|
||||
newNtfServerClient :: Natural -> THandleParams NTFVersion 'TServer -> SystemTime -> IO NtfServerClient
|
||||
newNtfServerClient qSize ntfThParams ts = do
|
||||
rcvQ <- newTBQueue qSize
|
||||
sndQ <- newTBQueue qSize
|
||||
connected <- newTVar True
|
||||
rcvActiveAt <- newTVar ts
|
||||
sndActiveAt <- newTVar ts
|
||||
rcvQ <- newTBQueueIO qSize
|
||||
sndQ <- newTBQueueIO qSize
|
||||
connected <- newTVarIO True
|
||||
rcvActiveAt <- newTVarIO ts
|
||||
sndActiveAt <- newTVarIO ts
|
||||
return NtfServerClient {rcvQ, sndQ, ntfThParams, connected, rcvActiveAt, sndActiveAt}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
|
||||
module Simplex.Messaging.Notifications.Server.Main where
|
||||
|
||||
import Control.Monad ((<$!>))
|
||||
import Data.Functor (($>))
|
||||
import Data.Ini (lookupValue, readIniFile)
|
||||
import Data.Maybe (fromMaybe)
|
||||
@@ -14,6 +16,7 @@ import qualified Data.Text as T
|
||||
import qualified Data.Text.IO as T
|
||||
import Network.Socket (HostName)
|
||||
import Options.Applicative
|
||||
import Simplex.Messaging.Client (HostMode (..), NetworkConfig (..), ProtocolClientConfig (..), SocksMode (..), defaultNetworkConfig)
|
||||
import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Notifications.Server (runNtfServer)
|
||||
@@ -23,6 +26,7 @@ import Simplex.Messaging.Notifications.Transport (supportedNTFHandshakes, suppor
|
||||
import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), pattern NtfServer)
|
||||
import Simplex.Messaging.Server.CLI
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Server.Main (textToHostMode)
|
||||
import Simplex.Messaging.Transport (simplexMQVersion)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig)
|
||||
@@ -87,6 +91,18 @@ ntfServerCLI cfgPath logPath =
|
||||
<> ("port: " <> T.pack defaultServerPort <> "\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\
|
||||
\[INACTIVE_CLIENTS]\n\
|
||||
\# TTL and interval to check inactive clients\n\
|
||||
\disconnect: off\n"
|
||||
@@ -115,7 +131,21 @@ ntfServerCLI cfgPath logPath =
|
||||
clientQSize = 64,
|
||||
subQSize = 512,
|
||||
pushQSize = 1048,
|
||||
smpAgentCfg = defaultSMPClientAgentConfig {persistErrorInterval = 0},
|
||||
smpAgentCfg =
|
||||
defaultSMPClientAgentConfig
|
||||
{ smpCfg =
|
||||
(smpCfg defaultSMPClientAgentConfig)
|
||||
{ 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) textToHostMode $ lookupValue "SUBSCRIBER" "host_mode" ini,
|
||||
requiredHostMode = fromMaybe False $ iniOnOff "SUBSCRIBER" "required_host_mode" ini,
|
||||
smpPingInterval = 60_000_000 -- 1 minutes
|
||||
}
|
||||
},
|
||||
persistErrorInterval = 0 -- seconds
|
||||
},
|
||||
apnsConfig = defaultAPNSPushClientConfig,
|
||||
subsBatchSize = 900,
|
||||
inactiveClientExpiration =
|
||||
|
||||
@@ -28,12 +28,16 @@ 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)
|
||||
@@ -103,11 +107,20 @@ readECPrivateKey f = do
|
||||
|
||||
data PushNotification
|
||||
= PNVerification NtfRegCode
|
||||
| PNMessage PNMessageData
|
||||
| PNMessage (NonEmpty 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,
|
||||
@@ -285,7 +298,7 @@ apnsNotification NtfTknData {tknDhSecret} nonce paddedLen = \case
|
||||
encrypt code $ \code' ->
|
||||
apn APNSBackground {contentAvailable = 1} . Just $ J.object ["nonce" .= nonce, "verification" .= code']
|
||||
PNMessage pnMessageData ->
|
||||
encrypt (strEncode pnMessageData) $ \ntfData ->
|
||||
encrypt (encodePNMessages 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,24 +7,22 @@ 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 :: 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
|
||||
{ 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
|
||||
}
|
||||
|
||||
data NtfServerStatsData = NtfServerStatsData
|
||||
@@ -36,48 +34,49 @@ data NtfServerStatsData = NtfServerStatsData
|
||||
_subDeleted :: Int,
|
||||
_ntfReceived :: Int,
|
||||
_ntfDelivered :: Int,
|
||||
_activeTokens :: PeriodStatsData NtfTokenId,
|
||||
_activeSubs :: PeriodStatsData NotifierId
|
||||
_activeTokens :: PeriodStatsData,
|
||||
_activeSubs :: PeriodStatsData
|
||||
}
|
||||
|
||||
newNtfServerStats :: UTCTime -> STM NtfServerStats
|
||||
newNtfServerStats :: UTCTime -> IO NtfServerStats
|
||||
newNtfServerStats ts = do
|
||||
fromTime <- newTVar ts
|
||||
tknCreated <- newTVar 0
|
||||
tknVerified <- newTVar 0
|
||||
tknDeleted <- newTVar 0
|
||||
subCreated <- newTVar 0
|
||||
subDeleted <- newTVar 0
|
||||
ntfReceived <- newTVar 0
|
||||
ntfDelivered <- newTVar 0
|
||||
fromTime <- newIORef ts
|
||||
tknCreated <- newIORef 0
|
||||
tknVerified <- newIORef 0
|
||||
tknDeleted <- newIORef 0
|
||||
subCreated <- newIORef 0
|
||||
subDeleted <- newIORef 0
|
||||
ntfReceived <- newIORef 0
|
||||
ntfDelivered <- newIORef 0
|
||||
activeTokens <- newPeriodStats
|
||||
activeSubs <- newPeriodStats
|
||||
pure NtfServerStats {fromTime, tknCreated, tknVerified, tknDeleted, subCreated, subDeleted, ntfReceived, ntfDelivered, activeTokens, activeSubs}
|
||||
|
||||
getNtfServerStatsData :: NtfServerStats -> STM NtfServerStatsData
|
||||
getNtfServerStatsData :: NtfServerStats -> IO NtfServerStatsData
|
||||
getNtfServerStatsData s@NtfServerStats {fromTime} = do
|
||||
_fromTime <- readTVar fromTime
|
||||
_tknCreated <- readTVar $ tknCreated s
|
||||
_tknVerified <- readTVar $ tknVerified s
|
||||
_tknDeleted <- readTVar $ tknDeleted s
|
||||
_subCreated <- readTVar $ subCreated s
|
||||
_subDeleted <- readTVar $ subDeleted s
|
||||
_ntfReceived <- readTVar $ ntfReceived s
|
||||
_ntfDelivered <- readTVar $ ntfDelivered s
|
||||
_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
|
||||
_activeTokens <- getPeriodStatsData $ activeTokens s
|
||||
_activeSubs <- getPeriodStatsData $ activeSubs s
|
||||
pure NtfServerStatsData {_fromTime, _tknCreated, _tknVerified, _tknDeleted, _subCreated, _subDeleted, _ntfReceived, _ntfDelivered, _activeTokens, _activeSubs}
|
||||
|
||||
setNtfServerStats :: NtfServerStats -> NtfServerStatsData -> STM ()
|
||||
-- this function is not thread safe, it is used on server start only
|
||||
setNtfServerStats :: NtfServerStats -> NtfServerStatsData -> IO ()
|
||||
setNtfServerStats s@NtfServerStats {fromTime} d@NtfServerStatsData {_fromTime} = do
|
||||
writeTVar fromTime $! _fromTime
|
||||
writeTVar (tknCreated s) $! _tknCreated d
|
||||
writeTVar (tknVerified s) $! _tknVerified d
|
||||
writeTVar (tknDeleted s) $! _tknDeleted d
|
||||
writeTVar (subCreated s) $! _subCreated d
|
||||
writeTVar (subDeleted s) $! _subDeleted d
|
||||
writeTVar (ntfReceived s) $! _ntfReceived d
|
||||
writeTVar (ntfDelivered s) $! _ntfDelivered d
|
||||
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
|
||||
setPeriodStats (activeTokens s) (_activeTokens d)
|
||||
setPeriodStats (activeSubs s) (_activeSubs d)
|
||||
|
||||
|
||||
@@ -33,13 +33,13 @@ data NtfStore = NtfStore
|
||||
subscriptionLookup :: TMap SMPQueueNtf NtfSubscriptionId
|
||||
}
|
||||
|
||||
newNtfStore :: STM NtfStore
|
||||
newNtfStore :: IO NtfStore
|
||||
newNtfStore = do
|
||||
tokens <- TM.empty
|
||||
tokenRegistrations <- TM.empty
|
||||
subscriptions <- TM.empty
|
||||
tokenSubscriptions <- TM.empty
|
||||
subscriptionLookup <- TM.empty
|
||||
tokens <- TM.emptyIO
|
||||
tokenRegistrations <- TM.emptyIO
|
||||
subscriptions <- TM.emptyIO
|
||||
tokenSubscriptions <- TM.emptyIO
|
||||
subscriptionLookup <- TM.emptyIO
|
||||
pure NtfStore {tokens, tokenRegistrations, subscriptions, tokenSubscriptions, subscriptionLookup}
|
||||
|
||||
data NtfTknData = NtfTknData
|
||||
@@ -77,6 +77,9 @@ 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
|
||||
|
||||
@@ -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 (..))
|
||||
import Simplex.Messaging.Agent.Protocol (ConnId, NotificationsMode (..), UserId)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
@@ -48,6 +48,7 @@ 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
|
||||
@@ -79,17 +80,17 @@ newNtfToken deviceToken ntfServer (ntfPubKey, ntfPrivKey) ntfDhKeys ntfMode =
|
||||
ntfMode
|
||||
}
|
||||
|
||||
data NtfSubAction = NtfSubNTFAction NtfSubNTFAction | NtfSubSMPAction NtfSubSMPAction
|
||||
data NtfSubAction = NSANtf NtfSubNTFAction | NSASMP NtfSubSMPAction
|
||||
deriving (Show)
|
||||
|
||||
isDeleteNtfSubAction :: NtfSubAction -> Bool
|
||||
isDeleteNtfSubAction = \case
|
||||
NtfSubNTFAction a -> case a of
|
||||
NSANtf a -> case a of
|
||||
NSACreate -> False
|
||||
NSACheck -> False
|
||||
NSADelete -> True
|
||||
NSARotate -> True
|
||||
NtfSubSMPAction a -> case a of
|
||||
NSASMP a -> case a of
|
||||
NSASmpKey -> False
|
||||
NSASmpDelete -> True
|
||||
|
||||
@@ -177,7 +178,8 @@ instance FromField NtfAgentSubStatus where fromField = fromTextField_ $ either (
|
||||
instance ToField NtfAgentSubStatus where toField = toField . decodeLatin1 . smpEncode
|
||||
|
||||
data NtfSubscription = NtfSubscription
|
||||
{ connId :: ConnId,
|
||||
{ userId :: UserId,
|
||||
connId :: ConnId,
|
||||
smpServer :: SMPServer,
|
||||
ntfQueueId :: Maybe NotifierId,
|
||||
ntfServer :: NtfServer,
|
||||
@@ -186,10 +188,11 @@ data NtfSubscription = NtfSubscription
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
newNtfSubscription :: ConnId -> SMPServer -> Maybe NotifierId -> NtfServer -> NtfAgentSubStatus -> NtfSubscription
|
||||
newNtfSubscription connId smpServer ntfQueueId ntfServer ntfSubStatus =
|
||||
newNtfSubscription :: UserId -> ConnId -> SMPServer -> Maybe NotifierId -> NtfServer -> NtfAgentSubStatus -> NtfSubscription
|
||||
newNtfSubscription userId connId smpServer ntfQueueId ntfServer ntfSubStatus =
|
||||
NtfSubscription
|
||||
{ connId,
|
||||
{ userId,
|
||||
connId,
|
||||
smpServer,
|
||||
ntfQueueId,
|
||||
ntfServer,
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
{-# 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 #-}
|
||||
@@ -99,7 +101,8 @@ module Simplex.Messaging.Protocol
|
||||
BasicAuth (..),
|
||||
SrvLoc (..),
|
||||
CorrId (..),
|
||||
EntityId,
|
||||
EntityId (..),
|
||||
pattern NoEntity,
|
||||
QueueId,
|
||||
RecipientId,
|
||||
SenderId,
|
||||
@@ -329,8 +332,8 @@ data RawTransmission = RawTransmission
|
||||
{ authenticator :: ByteString, -- signature or encrypted transmission hash
|
||||
authorized :: ByteString, -- authorized transmission
|
||||
sessId :: SessionId,
|
||||
corrId :: ByteString,
|
||||
entityId :: ByteString,
|
||||
corrId :: CorrId,
|
||||
entityId :: EntityId,
|
||||
command :: ByteString
|
||||
}
|
||||
deriving (Show)
|
||||
@@ -357,7 +360,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, SessionId, ByteString, ByteString)
|
||||
type SignedRawTransmission = (Maybe TransmissionAuth, CorrId, EntityId, ByteString)
|
||||
|
||||
-- | unparsed sent SMP transmission with signature.
|
||||
type SentRawTransmission = (Maybe TransmissionAuth, ByteString)
|
||||
@@ -374,7 +377,13 @@ type NotifierId = QueueId
|
||||
-- | SMP queue ID on the server.
|
||||
type QueueId = EntityId
|
||||
|
||||
type EntityId = ByteString
|
||||
-- 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 ""
|
||||
|
||||
-- | Parameterized type for SMP protocol commands from all clients.
|
||||
data Command (p :: Party) where
|
||||
@@ -475,6 +484,7 @@ data BrokerMsg where
|
||||
RRES :: EncFwdResponse -> BrokerMsg -- relay to proxy
|
||||
PRES :: EncResponse -> BrokerMsg -- proxy to client
|
||||
END :: BrokerMsg
|
||||
DELD :: BrokerMsg
|
||||
INFO :: QueueInfo -> BrokerMsg
|
||||
OK :: BrokerMsg
|
||||
ERR :: ErrorType -> BrokerMsg
|
||||
@@ -696,6 +706,7 @@ data BrokerMsgTag
|
||||
| RRES_
|
||||
| PRES_
|
||||
| END_
|
||||
| DELD_
|
||||
| INFO_
|
||||
| OK_
|
||||
| ERR_
|
||||
@@ -769,6 +780,7 @@ instance Encoding BrokerMsgTag where
|
||||
RRES_ -> "RRES"
|
||||
PRES_ -> "PRES"
|
||||
END_ -> "END"
|
||||
DELD_ -> "DELD"
|
||||
INFO_ -> "INFO"
|
||||
OK_ -> "OK"
|
||||
ERR_ -> "ERR"
|
||||
@@ -785,6 +797,7 @@ instance ProtocolMsgTag BrokerMsgTag where
|
||||
"RRES" -> Just RRES_
|
||||
"PRES" -> Just PRES_
|
||||
"END" -> Just END_
|
||||
"DELD" -> Just DELD_
|
||||
"INFO" -> Just INFO_
|
||||
"OK" -> Just OK_
|
||||
"ERR" -> Just ERR_
|
||||
@@ -1097,10 +1110,13 @@ serverStrP = do
|
||||
portP = show <$> (A.char ':' *> (A.decimal :: Parser Int))
|
||||
|
||||
-- | Transmission correlation ID.
|
||||
newtype CorrId = CorrId {bs :: ByteString} deriving (Eq, Ord, Show)
|
||||
newtype CorrId = CorrId {bs :: ByteString}
|
||||
deriving (Eq, Ord, Show)
|
||||
deriving newtype (Encoding)
|
||||
|
||||
instance IsString CorrId where
|
||||
fromString = CorrId . fromString
|
||||
{-# INLINE fromString #-}
|
||||
|
||||
instance StrEncoding CorrId where
|
||||
strEncode (CorrId cId) = strEncode cId
|
||||
@@ -1323,7 +1339,7 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where
|
||||
fromProtocolError = fromProtocolError @SMPVersion @ErrorType @BrokerMsg
|
||||
{-# INLINE fromProtocolError #-}
|
||||
|
||||
checkCredentials (auth, _, entId, _) cmd = case cmd of
|
||||
checkCredentials (auth, _, EntityId entId, _) cmd = case cmd of
|
||||
-- NEW must have signature but NOT queue ID
|
||||
NEW {}
|
||||
| isNothing auth -> Left $ CMD NO_AUTH
|
||||
@@ -1411,6 +1427,9 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
|
||||
RRES (EncFwdResponse encBlock) -> e (RRES_, ' ', Tail encBlock)
|
||||
PRES (EncResponse encBlock) -> e (PRES_, ' ', Tail encBlock)
|
||||
END -> e END_
|
||||
DELD
|
||||
| v >= deletedEventSMPVersion -> e DELD_
|
||||
| otherwise -> e END_
|
||||
INFO info -> e (INFO_, ' ', info)
|
||||
OK -> e OK_
|
||||
ERR err -> e (ERR_, ' ', err)
|
||||
@@ -1436,6 +1455,7 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
|
||||
RRES_ -> RRES <$> (EncFwdResponse . unTail <$> _smpP)
|
||||
PRES_ -> PRES <$> (EncResponse . unTail <$> _smpP)
|
||||
END_ -> pure END
|
||||
DELD_ -> pure DELD
|
||||
INFO_ -> INFO <$> _smpP
|
||||
OK_ -> pure OK
|
||||
ERR_ -> ERR <$> _smpP
|
||||
@@ -1448,7 +1468,7 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
|
||||
PEBlock -> BLOCK
|
||||
{-# INLINE fromProtocolError #-}
|
||||
|
||||
checkCredentials (_, _, entId, _) cmd = case cmd of
|
||||
checkCredentials (_, _, EntityId entId, _) cmd = case cmd of
|
||||
-- IDS response should not have queue ID
|
||||
IDS _ -> Right cmd
|
||||
-- ERR response does not always have queue ID
|
||||
@@ -1721,16 +1741,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 corrId, "", Left $ fromProtocolError @v @err @cmd PESession))
|
||||
| otherwise -> (Nothing, "", (corrId, NoEntity, Left $ fromProtocolError @v @err @cmd PESession))
|
||||
Left _ -> tError ""
|
||||
where
|
||||
tError :: ByteString -> SignedTransmission err cmd
|
||||
tError corrId = (Nothing, "", (CorrId corrId, "", Left $ fromProtocolError @v @err @cmd PEBlock))
|
||||
tError :: CorrId -> SignedTransmission err cmd
|
||||
tError corrId = (Nothing, "", (corrId, NoEntity, 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 corrId, entityId, cmd))
|
||||
in (sig, signed, (corrId, entityId, cmd))
|
||||
|
||||
$(J.deriveJSON defaultJSON ''MsgFlags)
|
||||
|
||||
|
||||
+632
-320
File diff suppressed because it is too large
Load Diff
@@ -4,11 +4,11 @@
|
||||
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)
|
||||
import Simplex.Messaging.Protocol (BasicAuth, SenderId)
|
||||
|
||||
data CPClientRole = CPRNone | CPRUser | CPRAdmin
|
||||
deriving (Eq)
|
||||
|
||||
data ControlProtocol
|
||||
= CPAuth BasicAuth
|
||||
@@ -20,7 +20,8 @@ data ControlProtocol
|
||||
| CPThreads
|
||||
| CPSockets
|
||||
| CPSocketThreads
|
||||
| CPDelete ByteString
|
||||
| CPServerInfo
|
||||
| CPDelete SenderId
|
||||
| CPSave
|
||||
| CPHelp
|
||||
| CPQuit
|
||||
@@ -37,6 +38,7 @@ instance StrEncoding ControlProtocol where
|
||||
CPThreads -> "threads"
|
||||
CPSockets -> "sockets"
|
||||
CPSocketThreads -> "socket-threads"
|
||||
CPServerInfo -> "server-info"
|
||||
CPDelete bs -> "delete " <> strEncode bs
|
||||
CPSave -> "save"
|
||||
CPHelp -> "help"
|
||||
@@ -53,6 +55,7 @@ instance StrEncoding ControlProtocol where
|
||||
"threads" -> pure CPThreads
|
||||
"sockets" -> pure CPSockets
|
||||
"socket-threads" -> pure CPSocketThreads
|
||||
"server-info" -> pure CPServerInfo
|
||||
"delete" -> CPDelete <$> (A.space *> strP)
|
||||
"save" -> pure CPSave
|
||||
"help" -> pure CPHelp
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StrictData #-}
|
||||
|
||||
module Simplex.Messaging.Server.Env.STM where
|
||||
|
||||
import Control.Concurrent (ThreadId)
|
||||
import Control.Monad.IO.Unlift
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import Crypto.Random
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import Data.Int (Int64)
|
||||
@@ -17,6 +19,7 @@ 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 Data.X509.Validation (Fingerprint (..))
|
||||
@@ -47,7 +50,6 @@ data ServerConfig = ServerConfig
|
||||
{ transports :: [(ServiceName, ATransport)],
|
||||
smpHandshakeTimeout :: Int,
|
||||
tbqSize :: Natural,
|
||||
-- serverTbqSize :: Natural,
|
||||
msgQueueQuota :: Int,
|
||||
queueIdBytes :: Int,
|
||||
msgIdBytes :: Int,
|
||||
@@ -74,6 +76,8 @@ data ServerConfig = ServerConfig
|
||||
serverStatsLogFile :: FilePath,
|
||||
-- | file to save and restore stats
|
||||
serverStatsBackupFile :: Maybe FilePath,
|
||||
-- | interval between sending pending END events to unsubscribed clients, seconds
|
||||
pendingENDInterval :: Int,
|
||||
-- | CA certificate private key is not needed for initialization
|
||||
caCertificateFile :: FilePath,
|
||||
privateKeyFile :: FilePath,
|
||||
@@ -105,7 +109,7 @@ defaultMessageExpiration =
|
||||
defaultInactiveClientExpiration :: ExpirationConfig
|
||||
defaultInactiveClientExpiration =
|
||||
ExpirationConfig
|
||||
{ ttl = 43200, -- seconds, 12 hours
|
||||
{ ttl = 21600, -- seconds, 6 hours
|
||||
checkInterval = 3600 -- seconds, 1 hours
|
||||
}
|
||||
|
||||
@@ -125,15 +129,23 @@ data Env = Env
|
||||
serverStats :: ServerStats,
|
||||
sockets :: SocketState,
|
||||
clientSeq :: TVar ClientId,
|
||||
clients :: TVar (IntMap Client),
|
||||
clients :: TVar (IntMap (Maybe Client)),
|
||||
proxyAgent :: ProxyAgent -- senders served on this proxy
|
||||
}
|
||||
|
||||
type Subscribed = Bool
|
||||
|
||||
data Server = Server
|
||||
{ subscribedQ :: TQueue (RecipientId, Client),
|
||||
subscribers :: TMap RecipientId Client,
|
||||
ntfSubscribedQ :: TQueue (NotifierId, Client),
|
||||
notifiers :: TMap NotifierId Client,
|
||||
{ subscribedQ :: TQueue (RecipientId, ClientId),
|
||||
deletedQ :: TQueue (RecipientId, ClientId),
|
||||
subscribers :: TMap RecipientId (TVar Client),
|
||||
ntfSubscribedQ :: TQueue (NotifierId, ClientId),
|
||||
ntfDeletedQ :: TQueue (NotifierId, ClientId),
|
||||
notifiers :: TMap NotifierId (TVar Client),
|
||||
pendingENDs :: TVar (IntMap (NonEmpty RecipientId)),
|
||||
pendingDELDs :: TVar (IntMap (NonEmpty RecipientId)),
|
||||
pendingNtfENDs :: TVar (IntMap (NonEmpty NotifierId)),
|
||||
pendingNtfDELDs :: TVar (IntMap (NonEmpty NotifierId)),
|
||||
savingLock :: Lock
|
||||
}
|
||||
|
||||
@@ -145,7 +157,7 @@ type ClientId = Int
|
||||
|
||||
data Client = Client
|
||||
{ clientId :: ClientId,
|
||||
subscriptions :: TMap RecipientId (TVar Sub),
|
||||
subscriptions :: TMap RecipientId Sub,
|
||||
ntfSubscriptions :: TMap NotifierId (),
|
||||
rcvQ :: TBQueue (NonEmpty (Maybe QueueRec, Transmission Cmd)),
|
||||
sndQ :: TBQueue (NonEmpty (Transmission BrokerMsg)),
|
||||
@@ -161,67 +173,82 @@ data Client = Client
|
||||
sndActiveAt :: TVar SystemTime
|
||||
}
|
||||
|
||||
data SubscriptionThread = NoSub | SubPending | SubThread (Weak ThreadId) | ProhibitSub
|
||||
data ServerSub = ServerSub (TVar SubscriptionThread) | ProhibitSub
|
||||
|
||||
data SubscriptionThread = NoSub | SubPending | SubThread (Weak ThreadId)
|
||||
|
||||
data Sub = Sub
|
||||
{ subThread :: SubscriptionThread,
|
||||
{ subThread :: ServerSub, -- Nothing value indicates that sub
|
||||
delivered :: TMVar MsgId
|
||||
}
|
||||
|
||||
newServer :: STM Server
|
||||
newServer :: IO Server
|
||||
newServer = do
|
||||
subscribedQ <- newTQueue
|
||||
subscribers <- TM.empty
|
||||
ntfSubscribedQ <- newTQueue
|
||||
notifiers <- TM.empty
|
||||
savingLock <- createLock
|
||||
return Server {subscribedQ, subscribers, ntfSubscribedQ, notifiers, savingLock}
|
||||
subscribedQ <- newTQueueIO
|
||||
deletedQ <- newTQueueIO
|
||||
subscribers <- TM.emptyIO
|
||||
ntfSubscribedQ <- newTQueueIO
|
||||
ntfDeletedQ <- newTQueueIO
|
||||
notifiers <- TM.emptyIO
|
||||
pendingENDs <- newTVarIO IM.empty
|
||||
pendingDELDs <- newTVarIO IM.empty
|
||||
pendingNtfENDs <- newTVarIO IM.empty
|
||||
pendingNtfDELDs <- newTVarIO IM.empty
|
||||
savingLock <- atomically createLock
|
||||
return Server {subscribedQ, deletedQ, subscribers, ntfSubscribedQ, ntfDeletedQ, notifiers, pendingENDs, pendingDELDs, pendingNtfENDs, pendingNtfDELDs, savingLock}
|
||||
|
||||
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
|
||||
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
|
||||
return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, msgQ, procThreads, endThreads, endThreadSeq, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt}
|
||||
|
||||
newSubscription :: SubscriptionThread -> STM Sub
|
||||
newSubscription subThread = do
|
||||
newSubscription st = do
|
||||
delivered <- newEmptyTMVar
|
||||
subThread <- ServerSub <$> 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 {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
|
||||
server <- newServer
|
||||
queueStore <- newQueueStore
|
||||
msgStore <- newMsgStore
|
||||
random <- C.newRandom
|
||||
storeLog <-
|
||||
forM storeLogFile $ \f -> do
|
||||
logInfo $ "restoring queues from file " <> T.pack f
|
||||
restoreQueues queueStore f
|
||||
tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig)
|
||||
Fingerprint fp <- loadFingerprint caCertificateFile
|
||||
let serverIdentity = KeyHash fp
|
||||
serverStats <- atomically . newServerStats =<< getCurrentTime
|
||||
sockets <- atomically newSocketState
|
||||
serverStats <- newServerStats =<< getCurrentTime
|
||||
sockets <- newSocketState
|
||||
clientSeq <- newTVarIO 0
|
||||
clients <- newTVarIO mempty
|
||||
proxyAgent <- atomically $ newSMPProxyAgent smpAgentCfg random
|
||||
proxyAgent <- newSMPProxyAgent smpAgentCfg random
|
||||
pure Env {config, serverInfo, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerParams, serverStats, sockets, clientSeq, clients, proxyAgent}
|
||||
where
|
||||
restoreQueues :: QueueStore -> FilePath -> IO (StoreLog 'WriteMode)
|
||||
restoreQueues QueueStore {queues, senders, notifiers} f = do
|
||||
(qs, s) <- readWriteStoreLog f
|
||||
atomically $ do
|
||||
writeTVar queues =<< mapM newTVar qs
|
||||
writeTVar senders $! M.foldr' addSender M.empty qs
|
||||
writeTVar notifiers $! M.foldr' addNotifier M.empty qs
|
||||
atomically . writeTVar queues =<< mapM newTVarIO qs
|
||||
atomically $ writeTVar senders $! M.foldr' addSender M.empty qs
|
||||
atomically $ 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)
|
||||
@@ -247,7 +274,7 @@ newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile,
|
||||
| isJust (storeMsgsFile config) = SPMMessages
|
||||
| otherwise = SPMQueues
|
||||
|
||||
newSMPProxyAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> STM ProxyAgent
|
||||
newSMPProxyAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> IO ProxyAgent
|
||||
newSMPProxyAgent smpAgentCfg random = do
|
||||
smpAgent <- newSMPClientAgent smpAgentCfg random
|
||||
pure ProxyAgent {smpAgent}
|
||||
|
||||
@@ -255,7 +255,6 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath =
|
||||
{ transports = iniTransports ini,
|
||||
smpHandshakeTimeout = 120000000,
|
||||
tbqSize = 64,
|
||||
-- serverTbqSize = 1024,
|
||||
msgQueueQuota = 128,
|
||||
queueIdBytes = 24,
|
||||
msgIdBytes = 24, -- must be at least 24 bytes, it is used as 192-bit nonce for XSalsa20
|
||||
@@ -290,6 +289,7 @@ smpServerCLI_ generateSite serveStaticFiles 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
|
||||
smpServerVRange = supportedServerSMPRelayVRange,
|
||||
transportConfig =
|
||||
defaultTransportServerConfig
|
||||
@@ -306,7 +306,7 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath =
|
||||
networkConfig =
|
||||
defaultNetworkConfig
|
||||
{ socksProxy = either error id <$!> strDecodeIni "PROXY" "socks_proxy" ini,
|
||||
socksMode = either (const SMOnion) textToSocksMode $ lookupValue "PROXY" "socks_mode" ini,
|
||||
socksMode = maybe SMOnion (either error id) $! strDecodeIni "PROXY" "socks_mode" ini,
|
||||
hostMode = either (const HMPublic) textToHostMode $ lookupValue "PROXY" "host_mode" ini,
|
||||
requiredHostMode = fromMaybe False $ iniOnOff "PROXY" "required_host_mode" ini
|
||||
}
|
||||
@@ -318,16 +318,6 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath =
|
||||
serverClientConcurrency = readIniDefault defaultProxyClientConcurrency "PROXY" "client_concurrency" ini,
|
||||
information = serverPublicInfo ini
|
||||
}
|
||||
textToSocksMode :: Text -> SocksMode
|
||||
textToSocksMode = \case
|
||||
"always" -> SMAlways
|
||||
"onion" -> SMOnion
|
||||
s -> error . T.unpack $ "Invalid socks_mode: " <> s
|
||||
textToHostMode :: Text -> HostMode
|
||||
textToHostMode = \case
|
||||
"public" -> HMPublic
|
||||
"onion" -> HMOnionViaSocks
|
||||
s -> error . T.unpack $ "Invalid host_mode: " <> s
|
||||
textToOwnServers :: Text -> [ByteString]
|
||||
textToOwnServers = map encodeUtf8 . T.words
|
||||
|
||||
@@ -351,6 +341,12 @@ smpServerCLI_ generateSite serveStaticFiles cfgPath logPath =
|
||||
where
|
||||
isOnion = \case THOnionHost _ -> True; _ -> False
|
||||
|
||||
textToHostMode :: Text -> HostMode
|
||||
textToHostMode = \case
|
||||
"public" -> HMPublic
|
||||
"onion" -> HMOnionViaSocks
|
||||
s -> error . T.unpack $ "Invalid host_mode: " <> s
|
||||
|
||||
data EmbeddedWebParams = EmbeddedWebParams
|
||||
{ webStaticPath :: FilePath,
|
||||
webHttpPort :: Maybe Int,
|
||||
|
||||
@@ -9,23 +9,21 @@
|
||||
|
||||
module Simplex.Messaging.Server.MsgStore.STM
|
||||
( STMMsgStore,
|
||||
MsgQueue (..),
|
||||
MsgQueue (msgQueue),
|
||||
newMsgStore,
|
||||
getMsgQueue,
|
||||
delMsgQueue,
|
||||
delMsgQueueSize,
|
||||
flushMsgQueue,
|
||||
snapshotMsgQueue,
|
||||
writeMsg,
|
||||
tryPeekMsg,
|
||||
peekMsg,
|
||||
tryPeekMsgIO,
|
||||
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)
|
||||
@@ -44,12 +42,17 @@ data MsgQueue = MsgQueue
|
||||
|
||||
type STMMsgStore = TMap RecipientId MsgQueue
|
||||
|
||||
newMsgStore :: STM STMMsgStore
|
||||
newMsgStore = TM.empty
|
||||
newMsgStore :: IO STMMsgStore
|
||||
newMsgStore = TM.emptyIO
|
||||
|
||||
getMsgQueue :: STMMsgStore -> RecipientId -> Int -> STM MsgQueue
|
||||
getMsgQueue st rId quota = maybe newQ pure =<< TM.lookup rId st
|
||||
-- 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
|
||||
where
|
||||
maybeNewQ = TM.lookup rId st >>= maybe newQ pure
|
||||
newQ = do
|
||||
msgQueue <- newTQueue
|
||||
canWrite <- newTVar True
|
||||
@@ -58,25 +61,14 @@ getMsgQueue st rId quota = maybe newQ pure =<< TM.lookup rId st
|
||||
TM.insert rId q st
|
||||
pure q
|
||||
|
||||
delMsgQueue :: STMMsgStore -> RecipientId -> STM ()
|
||||
delMsgQueue st rId = TM.delete rId st
|
||||
delMsgQueue :: STMMsgStore -> RecipientId -> IO ()
|
||||
delMsgQueue st rId = atomically $ TM.delete rId st
|
||||
|
||||
delMsgQueueSize :: STMMsgStore -> RecipientId -> STM Int
|
||||
delMsgQueueSize st rId = TM.lookupDelete rId st >>= maybe (pure 0) (\MsgQueue {size} -> readTVar size)
|
||||
delMsgQueueSize :: STMMsgStore -> RecipientId -> IO Int
|
||||
delMsgQueueSize st rId = atomically (TM.lookupDelete rId st) >>= maybe (pure 0) (\MsgQueue {size} -> readTVarIO size)
|
||||
|
||||
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)
|
||||
writeMsg MsgQueue {msgQueue = q, quota, canWrite, size} !msg = do
|
||||
writeMsg :: MsgQueue -> Message -> IO (Maybe (Message, Bool))
|
||||
writeMsg MsgQueue {msgQueue = q, quota, canWrite, size} !msg = atomically $ do
|
||||
canWrt <- readTVar canWrite
|
||||
empty <- isEmptyTQueue q
|
||||
if canWrt || empty
|
||||
@@ -85,49 +77,53 @@ writeMsg MsgQueue {msgQueue = q, quota, canWrite, size} !msg = do
|
||||
writeTVar canWrite $! canWrt'
|
||||
modifyTVar' size (+ 1)
|
||||
if canWrt'
|
||||
then writeTQueue q msg $> Just msg
|
||||
then writeTQueue q msg $> Just (msg, empty)
|
||||
else (writeTQueue q $! msgQuota) $> Nothing
|
||||
else pure Nothing
|
||||
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 #-}
|
||||
|
||||
peekMsg :: MsgQueue -> STM Message
|
||||
peekMsg = peekTQueue . msgQueue
|
||||
{-# INLINE peekMsg #-}
|
||||
|
||||
tryDelMsg :: MsgQueue -> MsgId -> STM (Maybe Message)
|
||||
tryDelMsg mq msgId' =
|
||||
tryDelMsg :: MsgQueue -> MsgId -> IO (Maybe Message)
|
||||
tryDelMsg mq msgId' = atomically $
|
||||
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 -> STM (Maybe Message, Maybe Message)
|
||||
tryDelPeekMsg mq msgId' =
|
||||
tryDelPeekMsg :: MsgQueue -> MsgId -> IO (Maybe Message, Maybe Message)
|
||||
tryDelPeekMsg mq msgId' = atomically $
|
||||
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 -> STM Int
|
||||
deleteExpiredMsgs mq old = loop 0
|
||||
deleteExpiredMsgs :: MsgQueue -> Int64 -> IO Int
|
||||
deleteExpiredMsgs mq old = atomically $ 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
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
{-# 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
|
||||
|
||||
@@ -16,7 +19,8 @@ data QueueRec = QueueRec
|
||||
senderKey :: !(Maybe SndPublicAuthKey),
|
||||
sndSecure :: !SenderCanSecure,
|
||||
notifier :: !(Maybe NtfCreds),
|
||||
status :: !ServerQueueStatus
|
||||
status :: !ServerQueueStatus,
|
||||
updatedAt :: !(Maybe RoundedSystemTime)
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
@@ -34,3 +38,16 @@ 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
|
||||
|
||||
@@ -19,6 +19,7 @@ module Simplex.Messaging.Server.QueueStore.STM
|
||||
addQueueNotifier,
|
||||
deleteQueueNotifier,
|
||||
suspendQueue,
|
||||
updateQueueTime,
|
||||
deleteQueue,
|
||||
)
|
||||
where
|
||||
@@ -38,15 +39,15 @@ data QueueStore = QueueStore
|
||||
notifiers :: TMap NotifierId RecipientId
|
||||
}
|
||||
|
||||
newQueueStore :: STM QueueStore
|
||||
newQueueStore :: IO QueueStore
|
||||
newQueueStore = do
|
||||
queues <- TM.empty
|
||||
senders <- TM.empty
|
||||
notifiers <- TM.empty
|
||||
queues <- TM.emptyIO
|
||||
senders <- TM.emptyIO
|
||||
notifiers <- TM.emptyIO
|
||||
pure QueueStore {queues, senders, notifiers}
|
||||
|
||||
addQueue :: QueueStore -> QueueRec -> STM (Either ErrorType ())
|
||||
addQueue QueueStore {queues, senders} q@QueueRec {recipientId = rId, senderId = sId} = do
|
||||
addQueue :: QueueStore -> QueueRec -> IO (Either ErrorType ())
|
||||
addQueue QueueStore {queues, senders} q@QueueRec {recipientId = rId, senderId = sId} = atomically $ do
|
||||
ifM hasId (pure $ Left DUPLICATE_) $ do
|
||||
qVar <- newTVar q
|
||||
TM.insert rId qVar queues
|
||||
@@ -55,48 +56,55 @@ 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 -> STM (Either ErrorType QueueRec)
|
||||
getQueue :: DirectParty p => QueueStore -> SParty p -> QueueId -> IO (Either ErrorType QueueRec)
|
||||
getQueue QueueStore {queues, senders, notifiers} party qId =
|
||||
toResult <$> (mapM readTVar =<< getVar)
|
||||
toResult <$> (mapM readTVarIO =<< getVar)
|
||||
where
|
||||
getVar = case party of
|
||||
SRecipient -> TM.lookup qId queues
|
||||
SSender -> TM.lookup qId senders $>>= (`TM.lookup` queues)
|
||||
SNotifier -> TM.lookup qId notifiers $>>= (`TM.lookup` queues)
|
||||
SRecipient -> TM.lookupIO qId queues
|
||||
SSender -> TM.lookupIO qId senders $>>= (`TM.lookupIO` queues)
|
||||
SNotifier -> TM.lookupIO qId notifiers $>>= (`TM.lookupIO` queues)
|
||||
|
||||
secureQueue :: QueueStore -> RecipientId -> SndPublicAuthKey -> STM (Either ErrorType QueueRec)
|
||||
secureQueue QueueStore {queues} rId sKey =
|
||||
withQueue rId queues $ \qVar ->
|
||||
secureQueue :: QueueStore -> RecipientId -> SndPublicAuthKey -> IO (Either ErrorType QueueRec)
|
||||
secureQueue QueueStore {queues} rId sKey = toResult <$> do
|
||||
TM.lookupIO rId queues $>>= \qVar -> atomically $
|
||||
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 -> STM (Either ErrorType QueueRec)
|
||||
addQueueNotifier :: QueueStore -> RecipientId -> NtfCreds -> IO (Either ErrorType (Maybe NotifierId))
|
||||
addQueueNotifier QueueStore {queues, notifiers} rId ntfCreds@NtfCreds {notifierId = nId} = do
|
||||
ifM (TM.member nId notifiers) (pure $ Left DUPLICATE_) $
|
||||
withQueue rId queues $ \qVar -> do
|
||||
TM.lookupIO rId queues >>= \case
|
||||
Just qVar -> atomically $ ifM (TM.member nId notifiers) (pure $ Left DUPLICATE_) $ do
|
||||
q <- readTVar qVar
|
||||
forM_ (notifier q) $ (`TM.delete` notifiers) . notifierId
|
||||
writeTVar qVar $! q {notifier = Just ntfCreds}
|
||||
nId_ <- forM (notifier q) $ \NtfCreds {notifierId} -> TM.delete notifierId notifiers $> notifierId
|
||||
let !q' = q {notifier = Just ntfCreds}
|
||||
writeTVar qVar q'
|
||||
TM.insert nId rId notifiers
|
||||
pure $ Just q
|
||||
pure $ Right nId_
|
||||
Nothing -> pure $ Left AUTH
|
||||
|
||||
deleteQueueNotifier :: QueueStore -> RecipientId -> STM (Either ErrorType ())
|
||||
deleteQueueNotifier :: QueueStore -> RecipientId -> IO (Either ErrorType (Maybe NotifierId))
|
||||
deleteQueueNotifier QueueStore {queues, notifiers} rId =
|
||||
withQueue rId queues $ \qVar -> do
|
||||
q <- readTVar qVar
|
||||
forM_ (notifier q) $ \NtfCreds {notifierId} -> TM.delete notifierId notifiers
|
||||
writeTVar qVar $! q {notifier = Nothing}
|
||||
pure $ Just ()
|
||||
forM (notifier q) $ \NtfCreds {notifierId} -> do
|
||||
TM.delete notifierId notifiers
|
||||
writeTVar qVar $! q {notifier = Nothing}
|
||||
pure notifierId
|
||||
|
||||
suspendQueue :: QueueStore -> RecipientId -> STM (Either ErrorType ())
|
||||
suspendQueue :: QueueStore -> RecipientId -> IO (Either ErrorType ())
|
||||
suspendQueue QueueStore {queues} rId =
|
||||
withQueue rId queues $ \qVar -> modifyTVar' qVar (\q -> q {status = QueueOff}) $> Just ()
|
||||
withQueue rId queues (`modifyTVar'` \q -> q {status = QueueOff})
|
||||
|
||||
deleteQueue :: QueueStore -> RecipientId -> STM (Either ErrorType QueueRec)
|
||||
deleteQueue QueueStore {queues, senders, notifiers} rId = do
|
||||
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
|
||||
TM.lookupDelete rId queues >>= \case
|
||||
Just qVar ->
|
||||
readTVar qVar >>= \q -> do
|
||||
@@ -108,5 +116,5 @@ deleteQueue QueueStore {queues, senders, notifiers} rId = do
|
||||
toResult :: Maybe a -> Either ErrorType a
|
||||
toResult = maybe (Left AUTH) Right
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
@@ -4,52 +4,78 @@
|
||||
{-# 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 (RecipientId)
|
||||
import UnliftIO.STM
|
||||
import Simplex.Messaging.Protocol (EntityId (..))
|
||||
import Simplex.Messaging.Util (atomicModifyIORef'_, unlessM)
|
||||
|
||||
data ServerStats = ServerStats
|
||||
{ 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
|
||||
{ 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)
|
||||
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
|
||||
pRelays :: ProxyStats,
|
||||
pRelaysOwn :: ProxyStats,
|
||||
pMsgFwds :: ProxyStats,
|
||||
pMsgFwdsOwn :: ProxyStats,
|
||||
pMsgFwdsRecv :: TVar Int,
|
||||
qCount :: TVar Int,
|
||||
msgCount :: TVar Int
|
||||
pMsgFwdsRecv :: IORef Int,
|
||||
qCount :: IORef Int,
|
||||
msgCount :: IORef Int
|
||||
}
|
||||
|
||||
data ServerStatsData = ServerStatsData
|
||||
@@ -57,22 +83,39 @@ 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 RecipientId,
|
||||
_activeQueues :: PeriodStatsData,
|
||||
_msgSentNtf :: Int,
|
||||
_msgRecvNtf :: Int,
|
||||
_activeQueuesNtf :: PeriodStatsData RecipientId,
|
||||
_activeQueuesNtf :: PeriodStatsData,
|
||||
_msgNtfs :: Int,
|
||||
_msgNtfNoSub :: Int,
|
||||
_msgNtfLost :: Int,
|
||||
@@ -86,55 +129,89 @@ data ServerStatsData = ServerStatsData
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
newServerStats :: UTCTime -> STM ServerStats
|
||||
newServerStats :: UTCTime -> IO ServerStats
|
||||
newServerStats ts = do
|
||||
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
|
||||
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
|
||||
activeQueues <- newPeriodStats
|
||||
msgSentNtf <- newTVar 0
|
||||
msgRecvNtf <- newTVar 0
|
||||
msgSentNtf <- newIORef 0
|
||||
msgRecvNtf <- newIORef 0
|
||||
activeQueuesNtf <- newPeriodStats
|
||||
msgNtfs <- newTVar 0
|
||||
msgNtfNoSub <- newTVar 0
|
||||
msgNtfLost <- newTVar 0
|
||||
msgNtfs <- newIORef 0
|
||||
msgNtfNoSub <- newIORef 0
|
||||
msgNtfLost <- newIORef 0
|
||||
pRelays <- newProxyStats
|
||||
pRelaysOwn <- newProxyStats
|
||||
pMsgFwds <- newProxyStats
|
||||
pMsgFwdsOwn <- newProxyStats
|
||||
pMsgFwdsRecv <- newTVar 0
|
||||
qCount <- newTVar 0
|
||||
msgCount <- newTVar 0
|
||||
pMsgFwdsRecv <- newIORef 0
|
||||
qCount <- newIORef 0
|
||||
msgCount <- newIORef 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,
|
||||
@@ -152,55 +229,89 @@ newServerStats ts = do
|
||||
msgCount
|
||||
}
|
||||
|
||||
getServerStatsData :: ServerStats -> STM ServerStatsData
|
||||
getServerStatsData :: ServerStats -> IO ServerStatsData
|
||||
getServerStatsData s = do
|
||||
_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
|
||||
_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
|
||||
_activeQueues <- getPeriodStatsData $ activeQueues s
|
||||
_msgSentNtf <- readTVar $ msgSentNtf s
|
||||
_msgRecvNtf <- readTVar $ msgRecvNtf s
|
||||
_msgSentNtf <- readIORef $ msgSentNtf s
|
||||
_msgRecvNtf <- readIORef $ msgRecvNtf s
|
||||
_activeQueuesNtf <- getPeriodStatsData $ activeQueuesNtf s
|
||||
_msgNtfs <- readTVar $ msgNtfs s
|
||||
_msgNtfNoSub <- readTVar $ msgNtfNoSub s
|
||||
_msgNtfLost <- readTVar $ msgNtfLost s
|
||||
_msgNtfs <- readIORef $ msgNtfs s
|
||||
_msgNtfNoSub <- readIORef $ msgNtfNoSub s
|
||||
_msgNtfLost <- readIORef $ msgNtfLost s
|
||||
_pRelays <- getProxyStatsData $ pRelays s
|
||||
_pRelaysOwn <- getProxyStatsData $ pRelaysOwn s
|
||||
_pMsgFwds <- getProxyStatsData $ pMsgFwds s
|
||||
_pMsgFwdsOwn <- getProxyStatsData $ pMsgFwdsOwn s
|
||||
_pMsgFwdsRecv <- readTVar $ pMsgFwdsRecv s
|
||||
_qCount <- readTVar $ qCount s
|
||||
_msgCount <- readTVar $ msgCount s
|
||||
_pMsgFwdsRecv <- readIORef $ pMsgFwdsRecv s
|
||||
_qCount <- readIORef $ qCount s
|
||||
_msgCount <- readIORef $ 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,
|
||||
@@ -218,38 +329,56 @@ getServerStatsData s = do
|
||||
_msgCount
|
||||
}
|
||||
|
||||
setServerStats :: ServerStats -> ServerStatsData -> STM ()
|
||||
-- this function is not thread safe, it is used on server start only
|
||||
setServerStats :: ServerStats -> ServerStatsData -> IO ()
|
||||
setServerStats s d = do
|
||||
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
|
||||
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
|
||||
setPeriodStats (activeQueues s) (_activeQueues d)
|
||||
writeTVar (msgSentNtf s) $! _msgSentNtf d
|
||||
writeTVar (msgRecvNtf s) $! _msgRecvNtf d
|
||||
writeIORef (msgSentNtf s) $! _msgSentNtf d
|
||||
writeIORef (msgRecvNtf s) $! _msgRecvNtf d
|
||||
setPeriodStats (activeQueuesNtf s) (_activeQueuesNtf d)
|
||||
writeTVar (msgNtfs s) $! _msgNtfs d
|
||||
writeTVar (msgNtfNoSub s) $! _msgNtfNoSub d
|
||||
writeTVar (msgNtfLost s) $! _msgNtfLost d
|
||||
writeIORef (msgNtfs s) $! _msgNtfs d
|
||||
writeIORef (msgNtfNoSub s) $! _msgNtfNoSub d
|
||||
writeIORef (msgNtfLost s) $! _msgNtfLost d
|
||||
setProxyStats (pRelays s) $! _pRelays d
|
||||
setProxyStats (pRelaysOwn s) $! _pRelaysOwn d
|
||||
setProxyStats (pMsgFwds s) $! _pMsgFwds d
|
||||
setProxyStats (pMsgFwdsOwn s) $! _pMsgFwdsOwn d
|
||||
writeTVar (pMsgFwdsRecv s) $! _pMsgFwdsRecv d
|
||||
writeTVar (qCount s) $! _qCount d
|
||||
writeTVar (msgCount s) $! _msgCount d
|
||||
writeIORef (pMsgFwdsRecv s) $! _pMsgFwdsRecv d
|
||||
writeIORef (qCount s) $! _qCount d
|
||||
writeIORef (msgCount s) $! _msgCount d
|
||||
|
||||
instance StrEncoding ServerStatsData where
|
||||
strEncode d =
|
||||
@@ -260,16 +389,33 @@ 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),
|
||||
@@ -297,16 +443,34 @@ 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="
|
||||
@@ -321,6 +485,10 @@ 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
|
||||
@@ -336,17 +504,34 @@ 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,
|
||||
@@ -365,55 +550,59 @@ instance StrEncoding ServerStatsData where
|
||||
}
|
||||
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 a = PeriodStats
|
||||
{ day :: TVar (Set a),
|
||||
week :: TVar (Set a),
|
||||
month :: TVar (Set a)
|
||||
data PeriodStats = PeriodStats
|
||||
{ day :: IORef IntSet,
|
||||
week :: IORef IntSet,
|
||||
month :: IORef IntSet
|
||||
}
|
||||
|
||||
newPeriodStats :: STM (PeriodStats a)
|
||||
newPeriodStats :: IO PeriodStats
|
||||
newPeriodStats = do
|
||||
day <- newTVar S.empty
|
||||
week <- newTVar S.empty
|
||||
month <- newTVar S.empty
|
||||
day <- newIORef IS.empty
|
||||
week <- newIORef IS.empty
|
||||
month <- newIORef IS.empty
|
||||
pure PeriodStats {day, week, month}
|
||||
|
||||
data PeriodStatsData a = PeriodStatsData
|
||||
{ _day :: Set a,
|
||||
_week :: Set a,
|
||||
_month :: Set a
|
||||
data PeriodStatsData = PeriodStatsData
|
||||
{ _day :: IntSet,
|
||||
_week :: IntSet,
|
||||
_month :: IntSet
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
newPeriodStatsData :: PeriodStatsData a
|
||||
newPeriodStatsData = PeriodStatsData {_day = S.empty, _week = S.empty, _month = S.empty}
|
||||
newPeriodStatsData :: PeriodStatsData
|
||||
newPeriodStatsData = PeriodStatsData {_day = IS.empty, _week = IS.empty, _month = IS.empty}
|
||||
|
||||
getPeriodStatsData :: PeriodStats a -> STM (PeriodStatsData a)
|
||||
getPeriodStatsData :: PeriodStats -> IO PeriodStatsData
|
||||
getPeriodStatsData s = do
|
||||
_day <- readTVar $ day s
|
||||
_week <- readTVar $ week s
|
||||
_month <- readTVar $ month s
|
||||
_day <- readIORef $ day s
|
||||
_week <- readIORef $ week s
|
||||
_month <- readIORef $ month s
|
||||
pure PeriodStatsData {_day, _week, _month}
|
||||
|
||||
setPeriodStats :: PeriodStats a -> PeriodStatsData a -> STM ()
|
||||
-- this function is not thread safe, it is used on server start only
|
||||
setPeriodStats :: PeriodStats -> PeriodStatsData -> IO ()
|
||||
setPeriodStats s d = do
|
||||
writeTVar (day s) $! _day d
|
||||
writeTVar (week s) $! _week d
|
||||
writeTVar (month s) $! _month d
|
||||
writeIORef (day s) $! _day d
|
||||
writeIORef (week s) $! _week d
|
||||
writeIORef (month s) $! _month d
|
||||
|
||||
instance (Ord a, StrEncoding a) => StrEncoding (PeriodStatsData a) where
|
||||
instance StrEncoding PeriodStatsData where
|
||||
strEncode PeriodStatsData {_day, _week, _month} =
|
||||
"day=" <> strEncode _day <> "\nweek=" <> strEncode _week <> "\nmonth=" <> strEncode _month
|
||||
"dayHashes=" <> strEncode _day <> "\nweekHashes=" <> strEncode _week <> "\nmonthHashes=" <> strEncode _month
|
||||
strP = do
|
||||
_day <- "day=" *> strP <* A.endOfLine
|
||||
_week <- "week=" *> strP <* A.endOfLine
|
||||
_month <- "month=" *> strP
|
||||
_day <- ("day=" *> bsSetP <|> "dayHashes=" *> strP) <* A.endOfLine
|
||||
_week <- ("week=" *> bsSetP <|> "weekHashes=" *> strP) <* A.endOfLine
|
||||
_month <- "month=" *> bsSetP <|> "monthHashes=" *> 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,
|
||||
@@ -421,7 +610,7 @@ data PeriodStatCounts = PeriodStatCounts
|
||||
monthCount :: String
|
||||
}
|
||||
|
||||
periodStatCounts :: forall a. PeriodStats a -> UTCTime -> STM PeriodStatCounts
|
||||
periodStatCounts :: PeriodStats -> UTCTime -> IO PeriodStatCounts
|
||||
periodStatCounts ps ts = do
|
||||
let d = utctDay ts
|
||||
(_, wDay) = mondayStartWeek d
|
||||
@@ -431,33 +620,34 @@ periodStatCounts ps ts = do
|
||||
monthCount <- periodCount mDay $ month ps
|
||||
pure PeriodStatCounts {dayCount, weekCount, monthCount}
|
||||
where
|
||||
periodCount :: Int -> TVar (Set a) -> STM String
|
||||
periodCount 1 pVar = show . S.size <$> swapTVar pVar S.empty
|
||||
periodCount :: Int -> IORef IntSet -> IO String
|
||||
periodCount 1 ref = show . IS.size <$> atomicSwapIORef ref IS.empty
|
||||
periodCount _ _ = pure ""
|
||||
|
||||
updatePeriodStats :: Ord a => PeriodStats a -> a -> STM ()
|
||||
updatePeriodStats stats pId = do
|
||||
updatePeriod day
|
||||
updatePeriod week
|
||||
updatePeriod month
|
||||
updatePeriodStats :: PeriodStats -> EntityId -> IO ()
|
||||
updatePeriodStats ps (EntityId pId) = do
|
||||
updatePeriod $ day ps
|
||||
updatePeriod $ week ps
|
||||
updatePeriod $ month ps
|
||||
where
|
||||
updatePeriod pSel = modifyTVar' (pSel stats) (S.insert pId)
|
||||
ph = hash pId
|
||||
updatePeriod ref = unlessM (IS.member ph <$> readIORef ref) $ atomicModifyIORef'_ ref $ IS.insert ph
|
||||
|
||||
data ProxyStats = ProxyStats
|
||||
{ pRequests :: TVar Int,
|
||||
pSuccesses :: TVar Int, -- includes destination server error responses that will be forwarded to the client
|
||||
pErrorsConnect :: TVar Int,
|
||||
pErrorsCompat :: TVar Int,
|
||||
pErrorsOther :: TVar Int
|
||||
{ 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
|
||||
}
|
||||
|
||||
newProxyStats :: STM ProxyStats
|
||||
newProxyStats :: IO ProxyStats
|
||||
newProxyStats = do
|
||||
pRequests <- newTVar 0
|
||||
pSuccesses <- newTVar 0
|
||||
pErrorsConnect <- newTVar 0
|
||||
pErrorsCompat <- newTVar 0
|
||||
pErrorsOther <- newTVar 0
|
||||
pRequests <- newIORef 0
|
||||
pSuccesses <- newIORef 0
|
||||
pErrorsConnect <- newIORef 0
|
||||
pErrorsCompat <- newIORef 0
|
||||
pErrorsOther <- newIORef 0
|
||||
pure ProxyStats {pRequests, pSuccesses, pErrorsConnect, pErrorsCompat, pErrorsOther}
|
||||
|
||||
data ProxyStatsData = ProxyStatsData
|
||||
@@ -472,31 +662,32 @@ data ProxyStatsData = ProxyStatsData
|
||||
newProxyStatsData :: ProxyStatsData
|
||||
newProxyStatsData = ProxyStatsData {_pRequests = 0, _pSuccesses = 0, _pErrorsConnect = 0, _pErrorsCompat = 0, _pErrorsOther = 0}
|
||||
|
||||
getProxyStatsData :: ProxyStats -> STM ProxyStatsData
|
||||
getProxyStatsData :: ProxyStats -> IO ProxyStatsData
|
||||
getProxyStatsData s = do
|
||||
_pRequests <- readTVar $ pRequests s
|
||||
_pSuccesses <- readTVar $ pSuccesses s
|
||||
_pErrorsConnect <- readTVar $ pErrorsConnect s
|
||||
_pErrorsCompat <- readTVar $ pErrorsCompat s
|
||||
_pErrorsOther <- readTVar $ pErrorsOther s
|
||||
_pRequests <- readIORef $ pRequests s
|
||||
_pSuccesses <- readIORef $ pSuccesses s
|
||||
_pErrorsConnect <- readIORef $ pErrorsConnect s
|
||||
_pErrorsCompat <- readIORef $ pErrorsCompat s
|
||||
_pErrorsOther <- readIORef $ pErrorsOther s
|
||||
pure ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther}
|
||||
|
||||
getResetProxyStatsData :: ProxyStats -> STM ProxyStatsData
|
||||
getResetProxyStatsData :: ProxyStats -> IO ProxyStatsData
|
||||
getResetProxyStatsData s = do
|
||||
_pRequests <- swapTVar (pRequests s) 0
|
||||
_pSuccesses <- swapTVar (pSuccesses s) 0
|
||||
_pErrorsConnect <- swapTVar (pErrorsConnect s) 0
|
||||
_pErrorsCompat <- swapTVar (pErrorsCompat s) 0
|
||||
_pErrorsOther <- swapTVar (pErrorsOther s) 0
|
||||
_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
|
||||
pure ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther}
|
||||
|
||||
setProxyStats :: ProxyStats -> ProxyStatsData -> STM ()
|
||||
-- this function is not thread safe, it is used on server start only
|
||||
setProxyStats :: ProxyStats -> ProxyStatsData -> IO ()
|
||||
setProxyStats s d = do
|
||||
writeTVar (pRequests s) $! _pRequests d
|
||||
writeTVar (pSuccesses s) $! _pSuccesses d
|
||||
writeTVar (pErrorsConnect s) $! _pErrorsConnect d
|
||||
writeTVar (pErrorsCompat s) $! _pErrorsCompat d
|
||||
writeTVar (pErrorsOther s) $! _pErrorsOther d
|
||||
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
|
||||
|
||||
instance StrEncoding ProxyStatsData where
|
||||
strEncode ProxyStatsData {_pRequests, _pSuccesses, _pErrorsConnect, _pErrorsCompat, _pErrorsOther} =
|
||||
|
||||
@@ -20,12 +20,14 @@ 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 (($>))
|
||||
@@ -33,10 +35,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 (NtfCreds (..), QueueRec (..), ServerQueueStatus (..))
|
||||
import Simplex.Messaging.Server.QueueStore
|
||||
import Simplex.Messaging.Transport.Buffer (trimCR)
|
||||
import Simplex.Messaging.Util (ifM)
|
||||
import System.Directory (doesFileExist)
|
||||
import System.Directory (doesFileExist, renameFile)
|
||||
import System.IO
|
||||
|
||||
-- | opaque container for file handle with a type-safe IOMode
|
||||
@@ -52,9 +54,19 @@ data StoreLogRecord
|
||||
| SuspendQueue QueueId
|
||||
| DeleteQueue QueueId
|
||||
| DeleteNotifier QueueId
|
||||
| UpdateTime QueueId RoundedSystemTime
|
||||
|
||||
data SLRTag
|
||||
= CreateQueue_
|
||||
| SecureQueue_
|
||||
| AddNotifier_
|
||||
| SuspendQueue_
|
||||
| DeleteQueue_
|
||||
| DeleteNotifier_
|
||||
| UpdateTime_
|
||||
|
||||
instance StrEncoding QueueRec where
|
||||
strEncode QueueRec {recipientId, recipientKey, rcvDhSecret, senderId, senderKey, sndSecure, notifier} =
|
||||
strEncode QueueRec {recipientId, recipientKey, rcvDhSecret, senderId, senderKey, sndSecure, notifier, updatedAt} =
|
||||
B.unwords
|
||||
[ "rid=" <> strEncode recipientId,
|
||||
"rk=" <> strEncode recipientKey,
|
||||
@@ -64,8 +76,10 @@ instance StrEncoding QueueRec where
|
||||
]
|
||||
<> if sndSecure then " sndSecure=" <> strEncode sndSecure else ""
|
||||
<> maybe "" notifierStr notifier
|
||||
<> maybe "" updatedAtStr updatedAt
|
||||
where
|
||||
notifierStr ntfCreds = " notifier=" <> strEncode ntfCreds
|
||||
updatedAtStr t = " updated_at=" <> strEncode t
|
||||
|
||||
strP = do
|
||||
recipientId <- "rid=" *> strP_
|
||||
@@ -75,24 +89,49 @@ instance StrEncoding QueueRec where
|
||||
senderKey <- "sk=" *> strP
|
||||
sndSecure <- (" sndSecure=" *> strP) <|> pure False
|
||||
notifier <- optional $ " notifier=" *> strP
|
||||
pure QueueRec {recipientId, recipientKey, rcvDhSecret, senderId, senderKey, sndSecure, notifier, status = QueueActive}
|
||||
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
|
||||
|
||||
instance StrEncoding StoreLogRecord where
|
||||
strEncode = \case
|
||||
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)
|
||||
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)
|
||||
|
||||
strP =
|
||||
"CREATE " *> (CreateQueue <$> strP)
|
||||
<|> "SECURE " *> (SecureQueue <$> strP_ <*> strP)
|
||||
<|> "NOTIFIER " *> (AddNotifier <$> strP_ <*> strP)
|
||||
<|> "SUSPEND " *> (SuspendQueue <$> strP)
|
||||
<|> "DELETE " *> (DeleteQueue <$> strP)
|
||||
<|> "NDELETE " *> (DeleteNotifier <$> 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
|
||||
|
||||
openWriteStoreLog :: FilePath -> IO (StoreLog 'WriteMode)
|
||||
openWriteStoreLog f = do
|
||||
@@ -138,12 +177,17 @@ 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) (readQueues f) (pure M.empty)
|
||||
qs <- ifM (doesFileExist f) readQS (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
|
||||
@@ -167,5 +211,6 @@ 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
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
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
|
||||
@@ -31,14 +28,10 @@ getSessVar sessSeq sessKey vs sessionVarTs = maybe (Left <$> newSessionVar) (pur
|
||||
pure v
|
||||
|
||||
removeSessVar :: Ord k => SessionVar a -> k -> TMap k (SessionVar a) -> STM ()
|
||||
removeSessVar = void .:. removeSessVar'
|
||||
{-# INLINE removeSessVar #-}
|
||||
|
||||
removeSessVar' :: Ord k => SessionVar a -> k -> TMap k (SessionVar a) -> STM Bool
|
||||
removeSessVar' v sessKey vs =
|
||||
removeSessVar v sessKey vs =
|
||||
TM.lookup sessKey vs >>= \case
|
||||
Just v' | sessionVarId v == sessionVarId v' -> TM.delete sessKey vs $> True
|
||||
_ -> pure False
|
||||
Just v' | sessionVarId v == sessionVarId v' -> TM.delete sessKey vs
|
||||
_ -> pure ()
|
||||
|
||||
tryReadSessVar :: Ord k => k -> TMap k (SessionVar a) -> STM (Maybe a)
|
||||
tryReadSessVar sessKey vs = TM.lookup sessKey vs $>>= (tryReadTMVar . sessionVar)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
module Simplex.Messaging.TMap
|
||||
( TMap,
|
||||
empty,
|
||||
emptyIO,
|
||||
singleton,
|
||||
clear,
|
||||
Simplex.Messaging.TMap.null,
|
||||
Simplex.Messaging.TMap.lookup,
|
||||
lookupIO,
|
||||
member,
|
||||
memberIO,
|
||||
insert,
|
||||
delete,
|
||||
lookupInsert,
|
||||
@@ -24,9 +26,9 @@ import qualified Data.Map.Strict as M
|
||||
|
||||
type TMap k a = TVar (Map k a)
|
||||
|
||||
empty :: STM (TMap k a)
|
||||
empty = newTVar M.empty
|
||||
{-# INLINE empty #-}
|
||||
emptyIO :: IO (TMap k a)
|
||||
emptyIO = newTVarIO M.empty
|
||||
{-# INLINE emptyIO #-}
|
||||
|
||||
singleton :: k -> a -> STM (TMap k a)
|
||||
singleton k v = newTVar $ M.singleton k v
|
||||
@@ -44,10 +46,18 @@ 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 #-}
|
||||
|
||||
@@ -47,6 +47,7 @@ module Simplex.Messaging.Transport
|
||||
authCmdsSMPVersion,
|
||||
sendingProxySMPVersion,
|
||||
sndAuthKeySMPVersion,
|
||||
deletedEventSMPVersion,
|
||||
simplexMQVersion,
|
||||
smpBlockSize,
|
||||
TransportConfig (..),
|
||||
@@ -113,9 +114,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
|
||||
|
||||
@@ -130,6 +131,9 @@ smpBlockSize = 16384
|
||||
-- 5 - basic auth for SMP servers (11/12/2022)
|
||||
-- 6 - allow creating queues without subscribing (9/10/2023)
|
||||
-- 7 - support authenticated encryption to verify senders' commands, imply but do NOT send session ID in signed part (4/30/2024)
|
||||
-- 8 - SMP proxy for sender commands
|
||||
-- 9 - faster handshake: SKEY command for sender to secure queue
|
||||
-- 10 - DELD event to subscriber when queue is deleted via another connnection
|
||||
|
||||
data SMPVersion
|
||||
|
||||
@@ -160,14 +164,17 @@ sendingProxySMPVersion = VersionSMP 8
|
||||
sndAuthKeySMPVersion :: VersionSMP
|
||||
sndAuthKeySMPVersion = VersionSMP 9
|
||||
|
||||
deletedEventSMPVersion :: VersionSMP
|
||||
deletedEventSMPVersion = VersionSMP 10
|
||||
|
||||
currentClientSMPRelayVersion :: VersionSMP
|
||||
currentClientSMPRelayVersion = VersionSMP 9
|
||||
currentClientSMPRelayVersion = VersionSMP 10
|
||||
|
||||
legacyServerSMPRelayVersion :: VersionSMP
|
||||
legacyServerSMPRelayVersion = VersionSMP 6
|
||||
|
||||
currentServerSMPRelayVersion :: VersionSMP
|
||||
currentServerSMPRelayVersion = VersionSMP 9
|
||||
currentServerSMPRelayVersion = VersionSMP 10
|
||||
|
||||
-- Max SMP protocol version to be used in e2e encrypted
|
||||
-- connection between client and server, as defined by SMP proxy.
|
||||
@@ -285,7 +292,7 @@ getTLS :: TransportPeer -> TransportConfig -> X.CertificateChain -> T.Context ->
|
||||
getTLS tlsPeer cfg tlsServerCerts cxt = withTlsUnique tlsPeer cxt newTLS
|
||||
where
|
||||
newTLS tlsUniq = do
|
||||
tlsBuffer <- atomically newTBuffer
|
||||
tlsBuffer <- newTBuffer
|
||||
tlsALPN <- T.getNegotiatedProtocol cxt
|
||||
pure TLS {tlsContext = cxt, tlsALPN, tlsTransportConfig = cfg, tlsServerCerts, tlsPeer, tlsUniq, tlsBuffer}
|
||||
|
||||
@@ -339,11 +346,12 @@ instance Transport TLS where
|
||||
|
||||
getLn :: TLS -> IO ByteString
|
||||
getLn TLS {tlsContext, tlsBuffer} = do
|
||||
getLnBuffered tlsBuffer (T.recvData tlsContext) `E.catch` handleEOF
|
||||
getLnBuffered tlsBuffer (T.recvData tlsContext) `E.catches` [E.Handler handleTlsEOF, E.Handler handleEOF]
|
||||
where
|
||||
handleEOF = \case
|
||||
T.Error_EOF -> E.throwIO TEBadBlock
|
||||
handleTlsEOF = \case
|
||||
T.PostHandshake 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
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@ data TBuffer = TBuffer
|
||||
getLock :: TMVar ()
|
||||
}
|
||||
|
||||
newTBuffer :: STM TBuffer
|
||||
newTBuffer :: IO TBuffer
|
||||
newTBuffer = do
|
||||
buffer <- newTVar ""
|
||||
getLock <- newTMVar ()
|
||||
buffer <- newTVarIO ""
|
||||
getLock <- newTMVarIO ()
|
||||
pure TBuffer {buffer, getLock}
|
||||
|
||||
withBufferLock :: TBuffer -> IO a -> IO a
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
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
|
||||
@@ -75,7 +74,7 @@ instance HTTP2BodyChunk HS.Request where
|
||||
|
||||
getHTTP2Body :: HTTP2BodyChunk a => a -> Int -> IO HTTP2Body
|
||||
getHTTP2Body r n = do
|
||||
bodyBuffer <- atomically newTBuffer
|
||||
bodyBuffer <- newTBuffer
|
||||
let getPart n' = getBuffered bodyBuffer n' Nothing $ getBodyChunk r
|
||||
bodyHead <- getPart n
|
||||
let bodySize = fromMaybe 0 $ getBodySize r
|
||||
|
||||
@@ -104,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 =
|
||||
(atomically mkHTTPS2Client >>= runClient)
|
||||
(mkHTTPS2Client >>= runClient)
|
||||
`E.catch` \(e :: IOException) -> pure . Left $ HCIOError e
|
||||
where
|
||||
mkHTTPS2Client :: STM HClient
|
||||
mkHTTPS2Client :: IO HClient
|
||||
mkHTTPS2Client = do
|
||||
connected <- newTVar False
|
||||
reqQ <- newTBQueue $ qSize config
|
||||
connected <- newTVarIO False
|
||||
reqQ <- newTBQueueIO $ qSize config
|
||||
pure HClient {connected, disconnected, host, port, config, reqQ}
|
||||
|
||||
runClient :: HClient -> IO (Either HTTP2ClientError HTTP2Client)
|
||||
|
||||
@@ -12,7 +12,7 @@ module Simplex.Messaging.Transport.Server
|
||||
newSocketState,
|
||||
runTransportServer,
|
||||
runTransportServerSocket,
|
||||
runTCPServer,
|
||||
runLocalTCPServer,
|
||||
runTCPServerSocket,
|
||||
startTCPServer,
|
||||
loadSupportedTLSServerParams,
|
||||
@@ -76,16 +76,16 @@ serverTransportConfig TransportServerConfig {logTLSErrors} =
|
||||
-- All accepted connections are passed to the passed function.
|
||||
runTransportServer :: forall c. Transport c => TMVar Bool -> ServiceName -> T.ServerParams -> TransportServerConfig -> (c -> IO ()) -> IO ()
|
||||
runTransportServer started port params cfg server = do
|
||||
ss <- atomically newSocketState
|
||||
ss <- newSocketState
|
||||
runTransportServerState ss started port params cfg server
|
||||
|
||||
runTransportServerState :: forall c . Transport c => SocketState -> TMVar Bool -> ServiceName -> T.ServerParams -> TransportServerConfig -> (c -> IO ()) -> IO ()
|
||||
runTransportServerState ss started port = runTransportServerSocketState ss started (startTCPServer started port) (transportName (TProxy :: TProxy c))
|
||||
runTransportServerState ss started port = runTransportServerSocketState ss started (startTCPServer started Nothing port) (transportName (TProxy :: TProxy c))
|
||||
|
||||
-- | Run a transport server with provided connection setup and handler.
|
||||
runTransportServerSocket :: Transport a => TMVar Bool -> IO Socket -> String -> T.ServerParams -> TransportServerConfig -> (a -> IO ()) -> IO ()
|
||||
runTransportServerSocket started getSocket threadLabel serverParams cfg server = do
|
||||
ss <- atomically newSocketState
|
||||
ss <- newSocketState
|
||||
runTransportServerSocketState ss started getSocket threadLabel serverParams cfg server
|
||||
|
||||
-- | Run a transport server with provided connection setup and handler.
|
||||
@@ -107,10 +107,10 @@ tlsServerCredentials serverParams = case T.sharedCredentials $ T.serverShared se
|
||||
_ -> error "server has more than one key"
|
||||
|
||||
-- | Run TCP server without TLS
|
||||
runTCPServer :: TMVar Bool -> ServiceName -> (Socket -> IO ()) -> IO ()
|
||||
runTCPServer started port server = do
|
||||
ss <- atomically newSocketState
|
||||
runTCPServerSocket ss started (startTCPServer started port) server
|
||||
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
|
||||
|
||||
-- | Wrap socket provider in a TCP server bracket.
|
||||
runTCPServerSocket :: SocketState -> TMVar Bool -> IO Socket -> (Socket -> IO ()) -> IO ()
|
||||
@@ -148,8 +148,8 @@ safeAccept sock =
|
||||
|
||||
type SocketState = (TVar Int, TVar Int, TVar (IntMap (Weak ThreadId)))
|
||||
|
||||
newSocketState :: STM SocketState
|
||||
newSocketState = (,,) <$> newTVar 0 <*> newTVar 0 <*> newTVar mempty
|
||||
newSocketState :: IO SocketState
|
||||
newSocketState = (,,) <$> newTVarIO 0 <*> newTVarIO 0 <*> newTVarIO mempty
|
||||
|
||||
closeServer :: TMVar Bool -> TVar (IntMap (Weak ThreadId)) -> Socket -> IO ()
|
||||
closeServer started clients sock = do
|
||||
@@ -157,12 +157,12 @@ closeServer started clients sock = do
|
||||
close sock
|
||||
void . atomically $ tryPutTMVar started False
|
||||
|
||||
startTCPServer :: TMVar Bool -> ServiceName -> IO Socket
|
||||
startTCPServer started port = withSocketsDo $ resolve >>= open >>= setStarted
|
||||
startTCPServer :: TMVar Bool -> Maybe HostName -> ServiceName -> IO Socket
|
||||
startTCPServer started host port = withSocketsDo $ resolve >>= open >>= setStarted
|
||||
where
|
||||
resolve =
|
||||
let hints = defaultHints {addrFlags = [AI_PASSIVE], addrSocketType = Stream}
|
||||
in select <$> getAddrInfo (Just hints) Nothing (Just port)
|
||||
in select <$> getAddrInfo (Just hints) host (Just port)
|
||||
select as = fromJust $ family AF_INET6 <|> family AF_INET
|
||||
where
|
||||
family f = find ((== f) . addrFamily) as
|
||||
|
||||
@@ -25,6 +25,7 @@ import Simplex.Messaging.Transport
|
||||
withTlsUnique,
|
||||
)
|
||||
import Simplex.Messaging.Transport.Buffer (trimCR)
|
||||
import System.IO.Error (isEOFError)
|
||||
|
||||
data WS = WS
|
||||
{ wsPeer :: TransportPeer,
|
||||
@@ -108,9 +109,11 @@ makeTLSContextStream cxt =
|
||||
S.makeStream readStream writeStream
|
||||
where
|
||||
readStream :: IO (Maybe ByteString)
|
||||
readStream =
|
||||
(Just <$> T.recvData cxt) `E.catch` \case
|
||||
T.Error_EOF -> pure Nothing
|
||||
e -> E.throwIO e
|
||||
readStream = (Just <$> T.recvData cxt) `E.catches` [E.Handler handleTlsEOF, E.Handler handleEOF]
|
||||
where
|
||||
handleTlsEOF = \case
|
||||
T.PostHandshake T.Error_EOF -> pure Nothing
|
||||
e -> E.throwIO e
|
||||
handleEOF e = if isEOFError e then pure Nothing else E.throwIO e
|
||||
writeStream :: Maybe LB.ByteString -> IO ()
|
||||
writeStream = maybe (closeTLS cxt) (T.sendData cxt)
|
||||
|
||||
@@ -15,6 +15,7 @@ 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.IORef
|
||||
import Data.List (groupBy, sortOn)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
@@ -23,7 +24,7 @@ import qualified Data.Text as T
|
||||
import Data.Text.Encoding (decodeUtf8With, encodeUtf8)
|
||||
import Data.Time (NominalDiffTime)
|
||||
import GHC.Conc (labelThread, myThreadId, threadDelay)
|
||||
import UnliftIO
|
||||
import UnliftIO hiding (atomicModifyIORef')
|
||||
import qualified UnliftIO.Exception as UE
|
||||
|
||||
raceAny_ :: MonadUnliftIO m => [m a] -> m ()
|
||||
@@ -166,14 +167,19 @@ threadDelay' = loop
|
||||
loop $ time - maxWait
|
||||
|
||||
diffToMicroseconds :: NominalDiffTime -> Int64
|
||||
diffToMicroseconds diff = fromIntegral ((truncate $ diff * 1000000) :: Integer)
|
||||
diffToMicroseconds diff = truncate $ diff * 1000000
|
||||
{-# INLINE diffToMicroseconds #-}
|
||||
|
||||
diffToMilliseconds :: NominalDiffTime -> Int64
|
||||
diffToMilliseconds diff = fromIntegral ((truncate $ diff * 1000) :: Integer)
|
||||
diffToMilliseconds diff = truncate $ diff * 1000
|
||||
{-# INLINE diffToMilliseconds #-}
|
||||
|
||||
labelMyThread :: MonadIO m => String -> m ()
|
||||
labelMyThread label = liftIO $ myThreadId >>= (`labelThread` label)
|
||||
|
||||
atomicModifyIORef'_ :: IORef a -> (a -> a) -> IO ()
|
||||
atomicModifyIORef'_ r f = atomicModifyIORef' r (\v -> (f v, ()))
|
||||
|
||||
encodeJSON :: ToJSON a => a -> Text
|
||||
encodeJSON = safeDecodeUtf8 . LB.toStrict . J.encode
|
||||
|
||||
|
||||
@@ -305,7 +305,7 @@ connectRCCtrl_ drg pairing'@RCCtrlPairing {caKey, caCert} inv@RCInvitation {ca,
|
||||
|
||||
catchRCError :: ExceptT RCErrorType IO a -> (RCErrorType -> ExceptT RCErrorType IO a) -> ExceptT RCErrorType IO a
|
||||
catchRCError = catchAllErrors $ \e -> case fromException e of
|
||||
Just (TLS.Terminated _ _ (TLS.Error_Protocol (_, _, TLS.UnknownCa))) -> RCEIdentity
|
||||
Just (TLS.Terminated _ _ (TLS.Error_Protocol _ TLS.UnknownCa)) -> RCEIdentity
|
||||
_ -> RCEException $ show e
|
||||
{-# INLINE catchRCError #-}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ preferAddress RCCtrlAddress {address, interface} addrs =
|
||||
startTLSServer :: Maybe Word16 -> TMVar (Maybe N.PortNumber) -> TLS.Credentials -> TLS.ServerHooks -> (Transport.TLS -> IO ()) -> IO (Async ())
|
||||
startTLSServer port_ startedOnPort credentials hooks server = async . liftIO $ do
|
||||
started <- newEmptyTMVarIO
|
||||
bracketOnError (startTCPServer started $ maybe "0" show port_) (\_e -> setPort Nothing) $ \socket ->
|
||||
bracketOnError (startTCPServer started Nothing $ maybe "0" show port_) (\_e -> setPort Nothing) $ \socket ->
|
||||
ifM
|
||||
(atomically $ readTMVar started)
|
||||
(runServer started socket)
|
||||
|
||||
@@ -20,7 +20,7 @@ import Simplex.Messaging.Agent.Protocol
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.Ratchet
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (ProtocolServer (..), currentSMPClientVersion, supportedSMPClientVRange, pattern VersionSMPC)
|
||||
import Simplex.Messaging.Protocol (EntityId (..), ProtocolServer (..), currentSMPClientVersion, supportedSMPClientVRange, pattern VersionSMPC)
|
||||
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
|
||||
import Simplex.Messaging.Version
|
||||
import Test.Hspec
|
||||
@@ -35,7 +35,7 @@ queueAddr :: SMPQueueAddress
|
||||
queueAddr =
|
||||
SMPQueueAddress
|
||||
{ smpServer = srv,
|
||||
senderId = "\223\142z\251",
|
||||
senderId = EntityId "\223\142z\251",
|
||||
dhPublicKey = testDhKey,
|
||||
sndSecure = False
|
||||
}
|
||||
@@ -225,23 +225,23 @@ connectionRequestTests =
|
||||
queueV1NoPort #== ("smp://1234-w==@smp.simplex.im/3456-w==#/?v=1-1&dh=" <> url testDhKeyStr <> "&srv=jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion")
|
||||
queueV1NoPort #== ("smp://1234-w==@smp.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion/3456-w==#" <> testDhKeyStr)
|
||||
it "should serialize and parse connection invitations and contact addresses" $ do
|
||||
connectionRequest #==# ("simplex:/invitation#/?v=2-6&smp=" <> url queueStr <> "&e2e=" <> testE2ERatchetParamsStrUri)
|
||||
connectionRequest #== ("https://simplex.chat/invitation#/?v=2-6&smp=" <> url queueStr <> "&e2e=" <> testE2ERatchetParamsStrUri)
|
||||
connectionRequestSK #==# ("simplex:/invitation#/?v=2-6&smp=" <> url queueStrSK <> "&e2e=" <> testE2ERatchetParamsStrUri)
|
||||
connectionRequest1 #==# ("simplex:/invitation#/?v=2-6&smp=" <> url queue1Str <> "&e2e=" <> testE2ERatchetParamsStrUri)
|
||||
connectionRequest2queues #==# ("simplex:/invitation#/?v=2-6&smp=" <> url (queueStr <> ";" <> queueStr) <> "&e2e=" <> testE2ERatchetParamsStrUri)
|
||||
connectionRequestNew #==# ("simplex:/invitation#/?v=2-6&smp=" <> url queueNewStr <> "&e2e=" <> testE2ERatchetParamsStrUri)
|
||||
connectionRequestNew1 #==# ("simplex:/invitation#/?v=2-6&smp=" <> url queueNew1Str <> "&e2e=" <> testE2ERatchetParamsStrUri)
|
||||
connectionRequest2queuesNew #==# ("simplex:/invitation#/?v=2-6&smp=" <> url (queueNewStr <> ";" <> queueNewStr) <> "&e2e=" <> testE2ERatchetParamsStrUri)
|
||||
connectionRequest #==# ("simplex:/invitation#/?v=2-7&smp=" <> url queueStr <> "&e2e=" <> testE2ERatchetParamsStrUri)
|
||||
connectionRequest #== ("https://simplex.chat/invitation#/?v=2-7&smp=" <> url queueStr <> "&e2e=" <> testE2ERatchetParamsStrUri)
|
||||
connectionRequestSK #==# ("simplex:/invitation#/?v=2-7&smp=" <> url queueStrSK <> "&e2e=" <> testE2ERatchetParamsStrUri)
|
||||
connectionRequest1 #==# ("simplex:/invitation#/?v=2-7&smp=" <> url queue1Str <> "&e2e=" <> testE2ERatchetParamsStrUri)
|
||||
connectionRequest2queues #==# ("simplex:/invitation#/?v=2-7&smp=" <> url (queueStr <> ";" <> queueStr) <> "&e2e=" <> testE2ERatchetParamsStrUri)
|
||||
connectionRequestNew #==# ("simplex:/invitation#/?v=2-7&smp=" <> url queueNewStr <> "&e2e=" <> testE2ERatchetParamsStrUri)
|
||||
connectionRequestNew1 #==# ("simplex:/invitation#/?v=2-7&smp=" <> url queueNew1Str <> "&e2e=" <> testE2ERatchetParamsStrUri)
|
||||
connectionRequest2queuesNew #==# ("simplex:/invitation#/?v=2-7&smp=" <> url (queueNewStr <> ";" <> queueNewStr) <> "&e2e=" <> testE2ERatchetParamsStrUri)
|
||||
connectionRequestV1 #== ("https://simplex.chat/invitation#/?v=1&smp=" <> url queueStr <> "&e2e=" <> testE2ERatchetParamsStrUri)
|
||||
connectionRequestClientDataEmpty #==# ("simplex:/invitation#/?v=2-6&smp=" <> url queueStr <> "&e2e=" <> testE2ERatchetParamsStrUri <> "&data=" <> url "{}")
|
||||
contactAddress #==# ("simplex:/contact#/?v=2-6&smp=" <> url queueStr)
|
||||
contactAddress #== ("https://simplex.chat/contact#/?v=2-6&smp=" <> url queueStr)
|
||||
contactAddress2queues #==# ("simplex:/contact#/?v=2-6&smp=" <> url (queueStr <> ";" <> queueStr))
|
||||
contactAddressNew #==# ("simplex:/contact#/?v=2-6&smp=" <> url queueNewStr)
|
||||
contactAddress2queuesNew #==# ("simplex:/contact#/?v=2-6&smp=" <> url (queueNewStr <> ";" <> queueNewStr))
|
||||
connectionRequestClientDataEmpty #==# ("simplex:/invitation#/?v=2-7&smp=" <> url queueStr <> "&e2e=" <> testE2ERatchetParamsStrUri <> "&data=" <> url "{}")
|
||||
contactAddress #==# ("simplex:/contact#/?v=2-7&smp=" <> url queueStr)
|
||||
contactAddress #== ("https://simplex.chat/contact#/?v=2-7&smp=" <> url queueStr)
|
||||
contactAddress2queues #==# ("simplex:/contact#/?v=2-7&smp=" <> url (queueStr <> ";" <> queueStr))
|
||||
contactAddressNew #==# ("simplex:/contact#/?v=2-7&smp=" <> url queueNewStr)
|
||||
contactAddress2queuesNew #==# ("simplex:/contact#/?v=2-7&smp=" <> url (queueNewStr <> ";" <> queueNewStr))
|
||||
contactAddressV2 #==# ("simplex:/contact#/?v=2&smp=" <> url queueStr)
|
||||
contactAddressV2 #== ("https://simplex.chat/contact#/?v=1&smp=" <> url queueStr) -- adjusted to v2
|
||||
contactAddressV2 #== ("https://simplex.chat/contact#/?v=1-2&smp=" <> url queueStr) -- adjusted to v2
|
||||
contactAddressV2 #== ("https://simplex.chat/contact#/?v=2-2&smp=" <> url queueStr)
|
||||
contactAddressClientData #==# ("simplex:/contact#/?v=2-6&smp=" <> url queueStr <> "&data=" <> url "{\"type\":\"group_link\", \"group_link_id\":\"abc\"}")
|
||||
contactAddressClientData #==# ("simplex:/contact#/?v=2-7&smp=" <> url queueStr <> "&data=" <> url "{\"type\":\"group_link\", \"group_link_id\":\"abc\"}")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
|
||||
@@ -48,7 +49,9 @@ import Data.Bifunctor (bimap, first)
|
||||
import qualified Data.ByteString.Base64.URL as U
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
import NtfClient
|
||||
import SMPAgentClient (agentCfg, initAgentServers, initAgentServers2, testDB, testDB2, testNtfServer, testNtfServer2)
|
||||
import SMPClient (cfg, cfgVPrev, testPort, testPort2, testStoreLogFile2, withSmpServer, withSmpServerConfigOn, withSmpServerStoreLogOn)
|
||||
@@ -56,13 +59,15 @@ import Simplex.Messaging.Agent hiding (createConnection, joinConnection, sendMes
|
||||
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), withStore')
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, Env (..), InitialAgentServers)
|
||||
import Simplex.Messaging.Agent.Protocol hiding (CON, CONF, INFO, SENT)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (closeSQLiteStore, getSavedNtfToken, reopenSQLiteStore)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (closeSQLiteStore, getSavedNtfToken, reopenSQLiteStore, withTransaction)
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..))
|
||||
import Simplex.Messaging.Notifications.Server.Push.APNS
|
||||
import Simplex.Messaging.Notifications.Types (NtfToken (..))
|
||||
import Simplex.Messaging.Notifications.Types (NtfTknAction (..), NtfToken (..))
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
import Simplex.Messaging.Protocol (ErrorType (AUTH), MsgFlags (MsgFlags), NtfServer, ProtocolServer (..), SMPMsgMeta (..), SubscriptionMode (..))
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Server.Env.STM (ServerConfig (..))
|
||||
@@ -88,9 +93,21 @@ notificationTests t = do
|
||||
it "should allow the second registration with different credentials and delete the first after verification" $
|
||||
withAPNSMockServer $ \apns ->
|
||||
withNtfServer t $ testNtfTokenSecondRegistration apns
|
||||
it "should re-register token when notification server is restarted" $
|
||||
it "should verify token after notification server is restarted" $
|
||||
withAPNSMockServer $ \apns ->
|
||||
testNtfTokenServerRestart t apns
|
||||
it "should re-verify token after notification server is restarted" $
|
||||
withAPNSMockServer $ \apns ->
|
||||
testNtfTokenServerRestartReverify t apns
|
||||
it "should re-verify token after notification server is restarted when first request timed-out" $
|
||||
withAPNSMockServer $ \apns ->
|
||||
testNtfTokenServerRestartReverifyTimeout t apns
|
||||
it "should re-register token when notification server is restarted" $
|
||||
withAPNSMockServer $ \apns ->
|
||||
testNtfTokenServerRestartReregister t apns
|
||||
it "should re-register token when notification server is restarted when first request timed-out" $
|
||||
withAPNSMockServer $ \apns ->
|
||||
testNtfTokenServerRestartReregisterTimeout t apns
|
||||
it "should work with multiple configured servers" $
|
||||
withAPNSMockServer $ \apns ->
|
||||
testNtfTokenMultipleServers t apns
|
||||
@@ -105,7 +122,7 @@ notificationTests t = do
|
||||
describe "Managing notification subscriptions" $ do
|
||||
describe "should create notification subscription for existing connection" $
|
||||
testNtfMatrix t testNotificationSubscriptionExistingConnection
|
||||
xdescribe "should create notification subscription for new connection" $
|
||||
describe "should create notification subscription for new connection" $
|
||||
testNtfMatrix t testNotificationSubscriptionNewConnection
|
||||
it "should change notifications mode" $
|
||||
withSmpServer t $
|
||||
@@ -116,19 +133,19 @@ notificationTests t = do
|
||||
withAPNSMockServer $ \apns ->
|
||||
withNtfServer t $ testChangeToken apns
|
||||
describe "Notifications server store log" $
|
||||
xit "should save and restore tokens and subscriptions" $
|
||||
it "should save and restore tokens and subscriptions" $
|
||||
withSmpServer t $
|
||||
withAPNSMockServer $ \apns ->
|
||||
testNotificationsStoreLog t apns
|
||||
describe "Notifications after SMP server restart" $
|
||||
xit "should resume subscriptions after SMP server is restarted" $
|
||||
it "should resume subscriptions after SMP server is restarted" $
|
||||
withAPNSMockServer $ \apns ->
|
||||
withNtfServer t $ testNotificationsSMPRestart t apns
|
||||
describe "Notifications after SMP server restart" $
|
||||
it "should resume batched subscriptions after SMP server is restarted" $
|
||||
withAPNSMockServer $ \apns ->
|
||||
withNtfServer t $ testNotificationsSMPRestartBatch 100 t apns
|
||||
xdescribe "should switch notifications to the new queue" $
|
||||
describe "should switch notifications to the new queue" $
|
||||
testServerMatrix2 t $ \servers ->
|
||||
withAPNSMockServer $ \apns ->
|
||||
withNtfServer t $ testSwitchNotifications servers apns
|
||||
@@ -146,10 +163,10 @@ notificationTests t = do
|
||||
testNtfMatrix :: HasCallStack => ATransport -> (APNSMockServer -> AgentMsgId -> AgentClient -> AgentClient -> IO ()) -> Spec
|
||||
testNtfMatrix t runTest = do
|
||||
describe "next and current" $ do
|
||||
xit "curr servers; curr clients" $ runNtfTestCfg t 1 cfg ntfServerCfg agentCfg agentCfg runTest
|
||||
it "curr servers; curr clients" $ runNtfTestCfg t 1 cfg ntfServerCfg agentCfg agentCfg runTest
|
||||
it "curr servers; prev clients" $ runNtfTestCfg t 3 cfg ntfServerCfg agentCfgVPrevPQ agentCfgVPrevPQ runTest
|
||||
it "prev servers; prev clients" $ runNtfTestCfg t 3 cfgVPrev ntfServerCfgVPrev agentCfgVPrevPQ agentCfgVPrevPQ runTest
|
||||
it "prev servers; curr clients" $ runNtfTestCfg t 3 cfgVPrev ntfServerCfgVPrev agentCfg agentCfg runTest
|
||||
it "prev servers; curr clients" $ runNtfTestCfg t 1 cfgVPrev ntfServerCfgVPrev agentCfg agentCfg runTest
|
||||
-- servers can be upgraded in any order
|
||||
it "servers: curr SMP, prev NTF; prev clients" $ runNtfTestCfg t 3 cfg ntfServerCfgVPrev agentCfgVPrevPQ agentCfgVPrevPQ runTest
|
||||
it "servers: prev SMP, curr NTF; prev clients" $ runNtfTestCfg t 3 cfgVPrev ntfServerCfg agentCfgVPrevPQ agentCfgVPrevPQ runTest
|
||||
@@ -251,7 +268,7 @@ testNtfTokenServerRestart :: ATransport -> APNSMockServer -> IO ()
|
||||
testNtfTokenServerRestart t APNSMockServer {apnsQ} = do
|
||||
let tkn = DeviceToken PPApnsTest "abcd"
|
||||
ntfData <- withAgent 1 agentCfg initAgentServers testDB $ \a ->
|
||||
withNtfServer t . runRight $ do
|
||||
withNtfServerStoreLog t $ \_ -> runRight $ do
|
||||
NTRegistered <- registerNtfToken a tkn NMPeriodic
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse} <-
|
||||
atomically $ readTBQueue apnsQ
|
||||
@@ -262,16 +279,131 @@ testNtfTokenServerRestart t APNSMockServer {apnsQ} = do
|
||||
withAgent 2 agentCfg initAgentServers testDB $ \a' ->
|
||||
-- server stopped before token is verified, so now the attempt to verify it will return AUTH error but re-register token,
|
||||
-- so that repeat verification happens without restarting the clients, when notification arrives
|
||||
withNtfServer t . runRight_ $ do
|
||||
withNtfServerStoreLog t $ \_ -> runRight_ $ do
|
||||
verification <- ntfData .-> "verification"
|
||||
nonce <- C.cbNonce <$> ntfData .-> "nonce"
|
||||
Left (NTF _ AUTH) <- tryE $ verifyNtfToken a' tkn nonce verification
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData'}, sendApnsResponse = sendApnsResponse'} <-
|
||||
verifyNtfToken a' tkn nonce verification
|
||||
NTActive <- checkNtfToken a' tkn
|
||||
pure ()
|
||||
|
||||
testNtfTokenServerRestartReverify :: ATransport -> APNSMockServer -> IO ()
|
||||
testNtfTokenServerRestartReverify t APNSMockServer {apnsQ} = do
|
||||
let tkn = DeviceToken PPApnsTest "abcd"
|
||||
withAgent 1 agentCfg initAgentServers testDB $ \a -> do
|
||||
ntfData <- withNtfServerStoreLog t $ \_ -> runRight $ do
|
||||
NTRegistered <- registerNtfToken a tkn NMPeriodic
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse} <-
|
||||
atomically $ readTBQueue apnsQ
|
||||
verification' <- ntfData' .-> "verification"
|
||||
nonce' <- C.cbNonce <$> ntfData' .-> "nonce"
|
||||
liftIO $ sendApnsResponse' APNSRespOk
|
||||
verifyNtfToken a' tkn nonce' verification'
|
||||
liftIO $ sendApnsResponse APNSRespOk
|
||||
pure ntfData
|
||||
runRight_ $ do
|
||||
verification <- ntfData .-> "verification"
|
||||
nonce <- C.cbNonce <$> ntfData .-> "nonce"
|
||||
Left (BROKER _ NETWORK) <- tryE $ verifyNtfToken a tkn nonce verification
|
||||
pure ()
|
||||
threadDelay 1000000
|
||||
withAgent 2 agentCfg initAgentServers testDB $ \a' ->
|
||||
-- server stopped before token is verified, so now the attempt to verify it will return AUTH error but re-register token,
|
||||
-- so that repeat verification happens without restarting the clients, when notification arrives
|
||||
withNtfServerStoreLog t $ \_ -> runRight_ $ do
|
||||
NTActive <- registerNtfToken a' tkn NMPeriodic
|
||||
NTActive <- checkNtfToken a' tkn
|
||||
pure ()
|
||||
|
||||
testNtfTokenServerRestartReverifyTimeout :: ATransport -> APNSMockServer -> IO ()
|
||||
testNtfTokenServerRestartReverifyTimeout t APNSMockServer {apnsQ} = do
|
||||
let tkn = DeviceToken PPApnsTest "abcd"
|
||||
withAgent 1 agentCfg initAgentServers testDB $ \a@AgentClient {agentEnv = Env {store}} -> do
|
||||
(nonce, verification) <- withNtfServerStoreLog t $ \_ -> runRight $ do
|
||||
NTRegistered <- registerNtfToken a tkn NMPeriodic
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse} <-
|
||||
atomically $ readTBQueue apnsQ
|
||||
liftIO $ sendApnsResponse APNSRespOk
|
||||
verification <- ntfData .-> "verification"
|
||||
nonce <- C.cbNonce <$> ntfData .-> "nonce"
|
||||
verifyNtfToken a tkn nonce verification
|
||||
pure (nonce, verification)
|
||||
-- this emulates the situation when server verified token but the client did not receive the response
|
||||
Just NtfToken {ntfTknStatus = NTActive, ntfTknAction = Just NTACheck, ntfDhSecret = Just dhSecret} <- withTransaction store getSavedNtfToken
|
||||
Right code <- pure $ NtfRegCode <$> C.cbDecrypt dhSecret nonce verification
|
||||
withTransaction store $ \db ->
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE ntf_tokens
|
||||
SET tkn_status = ?, tkn_action = ?
|
||||
WHERE provider = ? AND device_token = ?
|
||||
|]
|
||||
(NTConfirmed, Just (NTAVerify code), PPApnsTest, "abcd" :: ByteString)
|
||||
Just NtfToken {ntfTknStatus = NTConfirmed, ntfTknAction = Just (NTAVerify _)} <- withTransaction store getSavedNtfToken
|
||||
pure ()
|
||||
threadDelay 1000000
|
||||
withAgent 2 agentCfg initAgentServers testDB $ \a' ->
|
||||
-- server stopped before token is verified, so now the attempt to verify it will return AUTH error but re-register token,
|
||||
-- so that repeat verification happens without restarting the clients, when notification arrives
|
||||
withNtfServerStoreLog t $ \_ -> runRight_ $ do
|
||||
NTActive <- registerNtfToken a' tkn NMPeriodic
|
||||
NTActive <- checkNtfToken a' tkn
|
||||
pure ()
|
||||
|
||||
testNtfTokenServerRestartReregister :: ATransport -> APNSMockServer -> IO ()
|
||||
testNtfTokenServerRestartReregister t APNSMockServer {apnsQ} = do
|
||||
let tkn = DeviceToken PPApnsTest "abcd"
|
||||
withAgent 1 agentCfg initAgentServers testDB $ \a ->
|
||||
withNtfServerStoreLog t $ \_ -> runRight $ do
|
||||
NTRegistered <- registerNtfToken a tkn NMPeriodic
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just _}, sendApnsResponse} <-
|
||||
atomically $ readTBQueue apnsQ
|
||||
liftIO $ sendApnsResponse APNSRespOk
|
||||
-- the new agent is created as otherwise when running the tests in CI the old agent was keeping the connection to the server
|
||||
threadDelay 1000000
|
||||
withAgent 2 agentCfg initAgentServers testDB $ \a' ->
|
||||
-- server stopped before token is verified, and client might have lost verification notification.
|
||||
-- so that repeat registration happens when client is restarted.
|
||||
withNtfServerStoreLog t $ \_ -> runRight_ $ do
|
||||
NTRegistered <- registerNtfToken a' tkn NMPeriodic
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse} <-
|
||||
atomically $ readTBQueue apnsQ
|
||||
liftIO $ sendApnsResponse APNSRespOk
|
||||
verification <- ntfData .-> "verification"
|
||||
nonce <- C.cbNonce <$> ntfData .-> "nonce"
|
||||
verifyNtfToken a' tkn nonce verification
|
||||
NTActive <- checkNtfToken a' tkn
|
||||
pure ()
|
||||
|
||||
testNtfTokenServerRestartReregisterTimeout :: ATransport -> APNSMockServer -> IO ()
|
||||
testNtfTokenServerRestartReregisterTimeout t APNSMockServer {apnsQ} = do
|
||||
let tkn = DeviceToken PPApnsTest "abcd"
|
||||
withAgent 1 agentCfg initAgentServers testDB $ \a@AgentClient {agentEnv = Env {store}} -> do
|
||||
withNtfServerStoreLog t $ \_ -> runRight $ do
|
||||
NTRegistered <- registerNtfToken a tkn NMPeriodic
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just _}, sendApnsResponse} <-
|
||||
atomically $ readTBQueue apnsQ
|
||||
liftIO $ sendApnsResponse APNSRespOk
|
||||
-- this emulates the situation when server registered token but the client did not receive the response
|
||||
withTransaction store $ \db ->
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE ntf_tokens
|
||||
SET tkn_id = NULL, tkn_dh_secret = NULL, tkn_status = ?, tkn_action = ?
|
||||
WHERE provider = ? AND device_token = ?
|
||||
|]
|
||||
(NTNew, Just NTARegister, PPApnsTest, "abcd" :: ByteString)
|
||||
Just NtfToken {ntfTokenId = Nothing, ntfTknStatus = NTNew, ntfTknAction = Just NTARegister} <- withTransaction store getSavedNtfToken
|
||||
pure ()
|
||||
threadDelay 1000000
|
||||
withAgent 2 agentCfg initAgentServers testDB $ \a' ->
|
||||
-- server stopped before token is verified, and client might have lost verification notification.
|
||||
-- so that repeat registration happens when client is restarted.
|
||||
withNtfServerStoreLog t $ \_ -> runRight_ $ do
|
||||
NTRegistered <- registerNtfToken a' tkn NMPeriodic
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse} <-
|
||||
atomically $ readTBQueue apnsQ
|
||||
liftIO $ sendApnsResponse APNSRespOk
|
||||
verification <- ntfData .-> "verification"
|
||||
nonce <- C.cbNonce <$> ntfData .-> "nonce"
|
||||
verifyNtfToken a' tkn nonce verification
|
||||
NTActive <- checkNtfToken a' tkn
|
||||
pure ()
|
||||
|
||||
@@ -347,7 +479,7 @@ testNotificationSubscriptionExistingConnection APNSMockServer {apnsQ} baseId ali
|
||||
(bobId, aliceId, nonce, message) <- runRight $ do
|
||||
-- establish connection
|
||||
(bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe
|
||||
aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe
|
||||
(aliceId, _sqSecured) <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe
|
||||
("", _, CONF confId _ "bob's connInfo") <- get alice
|
||||
allowConnection alice bobId confId "alice's connInfo"
|
||||
get bob ##> ("", aliceId, INFO "alice's connInfo")
|
||||
@@ -374,27 +506,29 @@ testNotificationSubscriptionExistingConnection APNSMockServer {apnsQ} baseId ali
|
||||
-- alice client already has subscription for the connection
|
||||
Left (CMD PROHIBITED _) <- runExceptT $ getNotificationMessage alice nonce message
|
||||
|
||||
threadDelay 200000
|
||||
threadDelay 500000
|
||||
suspendAgent alice 0
|
||||
closeSQLiteStore store
|
||||
threadDelay 200000
|
||||
threadDelay 1000000
|
||||
putStrLn "before opening the database from another agent"
|
||||
|
||||
-- aliceNtf client doesn't have subscription and is allowed to get notification message
|
||||
withAgent 3 aliceCfg initAgentServers testDB $ \aliceNtf -> runRight_ $ do
|
||||
(_, [SMPMsgMeta {msgFlags = MsgFlags True}]) <- getNotificationMessage aliceNtf nonce message
|
||||
(_, Just SMPMsgMeta {msgFlags = MsgFlags True}) <- getNotificationMessage aliceNtf nonce message
|
||||
pure ()
|
||||
|
||||
threadDelay 200000
|
||||
threadDelay 1000000
|
||||
putStrLn "after closing the database in another agent"
|
||||
reopenSQLiteStore store
|
||||
foregroundAgent alice
|
||||
threadDelay 200000
|
||||
threadDelay 500000
|
||||
|
||||
runRight_ $ do
|
||||
get alice =##> \case ("", c, Msg "hello") -> c == bobId; _ -> False
|
||||
ackMessage alice bobId (baseId + 1) Nothing
|
||||
-- delete notification subscription
|
||||
toggleConnectionNtfs alice bobId False
|
||||
liftIO $ threadDelay 250000
|
||||
liftIO $ threadDelay 500000
|
||||
-- send message
|
||||
2 <- msgId <$> sendMessage bob aliceId (SMP.MsgFlags True) "hello again"
|
||||
get bob ##> ("", aliceId, SENT $ baseId + 2)
|
||||
@@ -414,7 +548,7 @@ testNotificationSubscriptionNewConnection APNSMockServer {apnsQ} baseId alice bo
|
||||
liftIO $ threadDelay 50000
|
||||
(bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe
|
||||
liftIO $ threadDelay 1000000
|
||||
aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe
|
||||
(aliceId, _sqSecured) <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe
|
||||
liftIO $ threadDelay 750000
|
||||
void $ messageNotificationData alice apnsQ
|
||||
("", _, CONF confId _ "bob's connInfo") <- get alice
|
||||
@@ -461,7 +595,8 @@ testChangeNotificationsMode APNSMockServer {apnsQ} =
|
||||
withAgentClients2 $ \alice bob -> runRight_ $ do
|
||||
-- establish connection
|
||||
(bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe
|
||||
aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe
|
||||
(aliceId, sqSecured) <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe
|
||||
liftIO $ sqSecured `shouldBe` True
|
||||
("", _, CONF confId _ "bob's connInfo") <- get alice
|
||||
allowConnection alice bobId confId "alice's connInfo"
|
||||
get bob ##> ("", aliceId, INFO "alice's connInfo")
|
||||
@@ -515,7 +650,7 @@ testChangeNotificationsMode APNSMockServer {apnsQ} =
|
||||
-- no notifications should follow
|
||||
noNotification apnsQ
|
||||
where
|
||||
baseId = 3
|
||||
baseId = 1
|
||||
msgId = subtract baseId
|
||||
|
||||
testChangeToken :: APNSMockServer -> IO ()
|
||||
@@ -523,7 +658,8 @@ testChangeToken APNSMockServer {apnsQ} = withAgent 1 agentCfg initAgentServers t
|
||||
(aliceId, bobId) <- withAgent 2 agentCfg initAgentServers testDB $ \alice -> runRight $ do
|
||||
-- establish connection
|
||||
(bobId, qInfo) <- createConnection alice 1 True SCMInvitation Nothing SMSubscribe
|
||||
aliceId <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe
|
||||
(aliceId, sqSecured) <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe
|
||||
liftIO $ sqSecured `shouldBe` True
|
||||
("", _, CONF confId _ "bob's connInfo") <- get alice
|
||||
allowConnection alice bobId confId "alice's connInfo"
|
||||
get bob ##> ("", aliceId, INFO "alice's connInfo")
|
||||
@@ -554,7 +690,7 @@ testChangeToken APNSMockServer {apnsQ} = withAgent 1 agentCfg initAgentServers t
|
||||
-- no notifications should follow
|
||||
noNotification apnsQ
|
||||
where
|
||||
baseId = 3
|
||||
baseId = 1
|
||||
msgId = subtract baseId
|
||||
|
||||
testNotificationsStoreLog :: ATransport -> APNSMockServer -> IO ()
|
||||
@@ -738,8 +874,8 @@ messageNotificationData :: HasCallStack => AgentClient -> TBQueue APNSMockReques
|
||||
messageNotificationData c apnsQ = do
|
||||
(nonce, message) <- messageNotification apnsQ
|
||||
NtfToken {ntfDhSecret = Just dhSecret} <- getNtfTokenData c
|
||||
Right pnMsgData <- liftEither . first INTERNAL $ Right . strDecode =<< first show (C.cbDecrypt dhSecret nonce message)
|
||||
pure pnMsgData
|
||||
Right pnMsgs <- liftEither . first INTERNAL $ Right . parseAll pnMessagesP =<< first show (C.cbDecrypt dhSecret nonce message)
|
||||
pure $ L.last pnMsgs
|
||||
|
||||
noNotification :: TBQueue APNSMockRequest -> ExceptT AgentErrorType IO ()
|
||||
noNotification apnsQ = do
|
||||
|
||||
@@ -50,7 +50,7 @@ import Simplex.Messaging.Crypto.File (CryptoFile (..))
|
||||
import Simplex.Messaging.Crypto.Ratchet (InitialKeys (..), pattern PQSupportOn)
|
||||
import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
||||
import Simplex.Messaging.Encoding.String (StrEncoding (..))
|
||||
import Simplex.Messaging.Protocol (SubscriptionMode (..), pattern VersionSMPC)
|
||||
import Simplex.Messaging.Protocol (EntityId (..), SubscriptionMode (..), pattern VersionSMPC)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import System.Random
|
||||
import Test.Hspec
|
||||
@@ -114,6 +114,8 @@ storeTests = do
|
||||
testDeleteRcvConn
|
||||
testDeleteSndConn
|
||||
testDeleteDuplexConn
|
||||
describe "setConnUserId" $ do
|
||||
testSetConnUserIdNewConn
|
||||
describe "upgradeRcvConnToDuplex" $ do
|
||||
testUpgradeRcvConnToDuplex
|
||||
describe "upgradeSndConnToDuplex" $ do
|
||||
@@ -215,12 +217,12 @@ rcvQueue1 =
|
||||
{ userId = 1,
|
||||
connId = "conn1",
|
||||
server = smpServer1,
|
||||
rcvId = "1234",
|
||||
rcvId = EntityId "1234",
|
||||
rcvPrivateKey = testPrivateAuthKey,
|
||||
rcvDhSecret = testDhSecret,
|
||||
e2ePrivKey = testPrivDhKey,
|
||||
e2eDhSecret = Nothing,
|
||||
sndId = "2345",
|
||||
sndId = EntityId "2345",
|
||||
sndSecure = True,
|
||||
status = New,
|
||||
dbQueueId = DBNewQueue,
|
||||
@@ -238,7 +240,7 @@ sndQueue1 =
|
||||
{ userId = 1,
|
||||
connId = "conn1",
|
||||
server = smpServer1,
|
||||
sndId = "3456",
|
||||
sndId = EntityId "3456",
|
||||
sndSecure = True,
|
||||
sndPublicKey = testPublicAuthKey,
|
||||
sndPrivateKey = testPrivateAuthKey,
|
||||
@@ -330,12 +332,27 @@ testGetRcvConn :: SpecWith SQLiteStore
|
||||
testGetRcvConn =
|
||||
it "should get connection using rcv queue id and server" . withStoreTransaction $ \db -> do
|
||||
let smpServer = SMPServer "smp.simplex.im" "5223" testKeyHash
|
||||
let recipientId = "1234"
|
||||
let recipientId = EntityId "1234"
|
||||
g <- C.newRandom
|
||||
Right (_, rq) <- createRcvConn db g cData1 rcvQueue1 SCMInvitation
|
||||
getRcvConn db smpServer recipientId
|
||||
`shouldReturn` Right (rq, SomeConn SCRcv (RcvConnection cData1 rq))
|
||||
|
||||
testSetConnUserIdNewConn :: SpecWith SQLiteStore
|
||||
testSetConnUserIdNewConn =
|
||||
it "should set user id for new connection" . withStoreTransaction $ \db -> do
|
||||
g <- C.newRandom
|
||||
Right connId <- createNewConn db g cData1 {connId = ""} SCMInvitation
|
||||
newUserId <- createUserRecord db
|
||||
_ <- setConnUserId db 1 connId newUserId
|
||||
connResult <- getConn db connId
|
||||
case connResult of
|
||||
Right (SomeConn SCNew (NewConnection connData)) -> do
|
||||
let ConnData {userId} = connData
|
||||
userId `shouldBe` newUserId
|
||||
_ -> do
|
||||
expectationFailure "Failed to get connection"
|
||||
|
||||
testDeleteRcvConn :: SpecWith SQLiteStore
|
||||
testDeleteRcvConn =
|
||||
it "should create RcvConnection and delete it" . withStoreTransaction $ \db -> do
|
||||
@@ -383,7 +400,7 @@ testUpgradeRcvConnToDuplex =
|
||||
{ userId = 1,
|
||||
connId = "conn1",
|
||||
server = SMPServer "smp.simplex.im" "5223" testKeyHash,
|
||||
sndId = "2345",
|
||||
sndId = EntityId "2345",
|
||||
sndSecure = True,
|
||||
sndPublicKey = testPublicAuthKey,
|
||||
sndPrivateKey = testPrivateAuthKey,
|
||||
@@ -412,12 +429,12 @@ testUpgradeSndConnToDuplex =
|
||||
{ userId = 1,
|
||||
connId = "conn1",
|
||||
server = SMPServer "smp.simplex.im" "5223" testKeyHash,
|
||||
rcvId = "3456",
|
||||
rcvId = EntityId "3456",
|
||||
rcvPrivateKey = testPrivateAuthKey,
|
||||
rcvDhSecret = testDhSecret,
|
||||
e2ePrivKey = testPrivDhKey,
|
||||
e2eDhSecret = Nothing,
|
||||
sndId = "4567",
|
||||
sndId = EntityId "4567",
|
||||
sndSecure = True,
|
||||
status = New,
|
||||
dbQueueId = DBNewQueue,
|
||||
@@ -539,7 +556,7 @@ mkSndMsgData internalId internalSndId internalHash =
|
||||
testCreateSndMsg_ :: DB.Connection -> PrevSndMsgHash -> ConnId -> SndQueue -> SndMsgData -> Expectation
|
||||
testCreateSndMsg_ db expectedPrevHash connId sq sndMsgData@SndMsgData {..} = do
|
||||
updateSndIds db connId
|
||||
`shouldReturn` (internalId, internalSndId, expectedPrevHash)
|
||||
`shouldReturn` Right (internalId, internalSndId, expectedPrevHash)
|
||||
createSndMsg db connId sndMsgData
|
||||
`shouldReturn` ()
|
||||
createSndMsgDelivery db connId sq internalId
|
||||
@@ -644,30 +661,30 @@ testGetPendingServerCommand :: SQLiteStore -> Expectation
|
||||
testGetPendingServerCommand st = do
|
||||
g <- C.newRandom
|
||||
withTransaction st $ \db -> do
|
||||
Right Nothing <- getPendingServerCommand db Nothing
|
||||
Right Nothing <- getPendingServerCommand db "" Nothing
|
||||
Right connId <- createNewConn db g cData1 {connId = ""} SCMInvitation
|
||||
Right () <- createCommand db "1" connId Nothing command
|
||||
corruptCmd db "1" connId
|
||||
Right () <- createCommand db "2" connId Nothing command
|
||||
|
||||
Left e <- getPendingServerCommand db Nothing
|
||||
Left e <- getPendingServerCommand db connId Nothing
|
||||
show e `shouldContain` "bad AgentCmdType"
|
||||
DB.query_ db "SELECT conn_id, corr_id FROM commands WHERE failed = 1" `shouldReturn` [(connId, "1" :: ByteString)]
|
||||
|
||||
Right (Just PendingCommand {corrId}) <- getPendingServerCommand db Nothing
|
||||
Right (Just PendingCommand {corrId}) <- getPendingServerCommand db connId Nothing
|
||||
corrId `shouldBe` "2"
|
||||
|
||||
Right _ <- updateNewConnRcv db connId rcvQueue1
|
||||
Right Nothing <- getPendingServerCommand db $ Just smpServer1
|
||||
Right Nothing <- getPendingServerCommand db connId $ Just smpServer1
|
||||
Right () <- createCommand db "3" connId (Just smpServer1) command
|
||||
corruptCmd db "3" connId
|
||||
Right () <- createCommand db "4" connId (Just smpServer1) command
|
||||
|
||||
Left e' <- getPendingServerCommand db (Just smpServer1)
|
||||
Left e' <- getPendingServerCommand db connId (Just smpServer1)
|
||||
show e' `shouldContain` "bad AgentCmdType"
|
||||
DB.query_ db "SELECT conn_id, corr_id FROM commands WHERE failed = 1" `shouldReturn` [(connId, "1" :: ByteString), (connId, "3" :: ByteString)]
|
||||
|
||||
Right (Just PendingCommand {corrId = corrId'}) <- getPendingServerCommand db (Just smpServer1)
|
||||
Right (Just PendingCommand {corrId = corrId'}) <- getPendingServerCommand db connId (Just smpServer1)
|
||||
corrId' `shouldBe` "4"
|
||||
where
|
||||
command = AClientCommand $ NEW True (ACM SCMInvitation) (IKNoPQ PQSupportOn) SMSubscribe
|
||||
@@ -698,7 +715,7 @@ rcvFileDescr1 =
|
||||
}
|
||||
where
|
||||
defaultChunkSize = FileSize $ mb 8
|
||||
replicaId = ChunkReplicaId "abc"
|
||||
replicaId = ChunkReplicaId $ EntityId "abc"
|
||||
chunkDigest = FileDigest "ghi"
|
||||
|
||||
testFileSbKey :: C.SbKey
|
||||
@@ -724,7 +741,7 @@ testGetNextRcvChunkToDownload st = do
|
||||
show e `shouldContain` "ConversionFailed"
|
||||
DB.query_ db "SELECT rcv_file_id FROM rcv_files WHERE failed = 1" `shouldReturn` [Only (1 :: Int)]
|
||||
|
||||
Right (Just (RcvFileChunk {rcvFileEntityId}, _)) <- getNextRcvChunkToDownload db xftpServer1 86400
|
||||
Right (Just (RcvFileChunk {rcvFileEntityId}, _, Nothing)) <- getNextRcvChunkToDownload db xftpServer1 86400
|
||||
rcvFileEntityId `shouldBe` fId2
|
||||
|
||||
testGetNextRcvFileToDecrypt :: SQLiteStore -> Expectation
|
||||
@@ -768,9 +785,9 @@ newSndChunkReplica1 :: NewSndChunkReplica
|
||||
newSndChunkReplica1 =
|
||||
NewSndChunkReplica
|
||||
{ server = xftpServer1,
|
||||
replicaId = ChunkReplicaId "abc",
|
||||
replicaId = ChunkReplicaId $ EntityId "abc",
|
||||
replicaKey = testFileReplicaKey,
|
||||
rcvIdsKeys = [(ChunkReplicaId "abc", testFileReplicaKey)]
|
||||
rcvIdsKeys = [(ChunkReplicaId $ EntityId "abc", testFileReplicaKey)]
|
||||
}
|
||||
|
||||
testGetNextSndChunkToUpload :: SQLiteStore -> Expectation
|
||||
@@ -801,9 +818,9 @@ testGetNextDeletedSndChunkReplica st = do
|
||||
withTransaction st $ \db -> do
|
||||
Right Nothing <- getNextDeletedSndChunkReplica db xftpServer1 86400
|
||||
|
||||
createDeletedSndChunkReplica db 1 (FileChunkReplica xftpServer1 (ChunkReplicaId "abc") testFileReplicaKey) (FileDigest "ghi")
|
||||
createDeletedSndChunkReplica db 1 (FileChunkReplica xftpServer1 (ChunkReplicaId $ EntityId "abc") testFileReplicaKey) (FileDigest "ghi")
|
||||
DB.execute_ db "UPDATE deleted_snd_chunk_replicas SET delay = 'bad' WHERE deleted_snd_chunk_replica_id = 1"
|
||||
createDeletedSndChunkReplica db 1 (FileChunkReplica xftpServer1 (ChunkReplicaId "abc") testFileReplicaKey) (FileDigest "ghi")
|
||||
createDeletedSndChunkReplica db 1 (FileChunkReplica xftpServer1 (ChunkReplicaId $ EntityId "abc") testFileReplicaKey) (FileDigest "ghi")
|
||||
|
||||
Left e <- getNextDeletedSndChunkReplica db xftpServer1 86400
|
||||
show e `shouldContain` "ConversionFailed"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
@@ -40,6 +41,7 @@ batchingTests = do
|
||||
it "should break on large message" testClientBatchWithLargeMessage
|
||||
describe "v7 (next)" $ do
|
||||
it "should batch with 136 subscriptions per batch" testClientBatchSubscriptionsV7
|
||||
it "should batch with N ENDs per batch" testClientBatchENDs
|
||||
it "should break on message that does not fit" testClientBatchWithMessageV7
|
||||
it "should break on large message" testClientBatchWithLargeMessageV7
|
||||
|
||||
@@ -165,6 +167,20 @@ testClientBatchSubscriptionsV7 = do
|
||||
(length rs1, length rs2, length rs3) `shouldBe` (28, 136, 136)
|
||||
all lenOk [s1, s2, s3] `shouldBe` True
|
||||
|
||||
testClientBatchENDs :: IO ()
|
||||
testClientBatchENDs = do
|
||||
client <- clientStubV7
|
||||
ends <- replicateM 300 randomENDCmd
|
||||
let ends' = map (\t -> Right (Nothing, encodeTransmission (thParams client) t)) ends
|
||||
batches1 = batchTransmissions False smpBlockSize $ L.fromList ends'
|
||||
all lenOk1 batches1 `shouldBe` True
|
||||
let batches = batchTransmissions True smpBlockSize $ L.fromList ends'
|
||||
length batches `shouldBe` 2
|
||||
[TBTransmissions s1 n1 rs1, TBTransmissions s2 n2 rs2] <- pure batches
|
||||
(n1, n2) `shouldBe` (45, 255)
|
||||
(length rs1, length rs2) `shouldBe` (45, 255)
|
||||
all lenOk [s1, s2] `shouldBe` True
|
||||
|
||||
testClientBatchWithMessage :: IO ()
|
||||
testClientBatchWithMessage = do
|
||||
client <- testClientStub
|
||||
@@ -261,7 +277,7 @@ testClientStub :: IO (ProtocolClient SMPVersion ErrorType BrokerMsg)
|
||||
testClientStub = do
|
||||
g <- C.newRandom
|
||||
sessId <- atomically $ C.randomBytes 32 g
|
||||
atomically $ smpClientStub g sessId subModeSMPVersion Nothing
|
||||
smpClientStub g sessId subModeSMPVersion Nothing
|
||||
|
||||
clientStubV7 :: IO (ProtocolClient SMPVersion ErrorType BrokerMsg)
|
||||
clientStubV7 = do
|
||||
@@ -269,7 +285,7 @@ clientStubV7 = do
|
||||
sessId <- atomically $ C.randomBytes 32 g
|
||||
(rKey, _) <- atomically $ C.generateAuthKeyPair C.SX25519 g
|
||||
thAuth_ <- testTHandleAuth authCmdsSMPVersion g rKey
|
||||
atomically $ smpClientStub g sessId authCmdsSMPVersion thAuth_
|
||||
smpClientStub g sessId authCmdsSMPVersion thAuth_
|
||||
|
||||
randomSUB :: ByteString -> IO (Either TransportError (Maybe TransmissionAuth, ByteString))
|
||||
randomSUB = randomSUB_ C.SEd25519 subModeSMPVersion
|
||||
@@ -285,7 +301,7 @@ randomSUB_ a v sessId = do
|
||||
(rKey, rpKey) <- atomically $ C.generateAuthKeyPair a g
|
||||
thAuth_ <- testTHandleAuth v g rKey
|
||||
let thParams = testTHandleParams v sessId
|
||||
TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, rId, Cmd SRecipient SUB)
|
||||
TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, EntityId rId, Cmd SRecipient SUB)
|
||||
pure $ (,tToSend) <$> authTransmission thAuth_ (Just rpKey) nonce tForAuth
|
||||
|
||||
randomSUBCmd :: ProtocolClient SMPVersion ErrorType BrokerMsg -> IO (PCTransmission ErrorType BrokerMsg)
|
||||
@@ -299,7 +315,13 @@ randomSUBCmd_ a c = do
|
||||
g <- C.newRandom
|
||||
rId <- atomically $ C.randomBytes 24 g
|
||||
(_, rpKey) <- atomically $ C.generateAuthKeyPair a g
|
||||
mkTransmission c (Just rpKey, rId, Cmd SRecipient SUB)
|
||||
mkTransmission c (Just rpKey, EntityId rId, Cmd SRecipient SUB)
|
||||
|
||||
randomENDCmd :: IO (Transmission BrokerMsg)
|
||||
randomENDCmd = do
|
||||
g <- C.newRandom
|
||||
rId <- atomically $ C.randomBytes 24 g
|
||||
pure (CorrId "", EntityId rId, END)
|
||||
|
||||
randomSEND :: ByteString -> Int -> IO (Either TransportError (Maybe TransmissionAuth, ByteString))
|
||||
randomSEND = randomSEND_ C.SEd25519 subModeSMPVersion
|
||||
@@ -316,7 +338,7 @@ randomSEND_ a v sessId len = do
|
||||
thAuth_ <- testTHandleAuth v g sKey
|
||||
msg <- atomically $ C.randomBytes len g
|
||||
let thParams = testTHandleParams v sessId
|
||||
TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, sId, Cmd SSender $ SEND noMsgFlags msg)
|
||||
TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, EntityId sId, Cmd SSender $ SEND noMsgFlags msg)
|
||||
pure $ (,tToSend) <$> authTransmission thAuth_ (Just spKey) nonce tForAuth
|
||||
|
||||
testTHandleParams :: VersionSMP -> ByteString -> THandleParams SMPVersion 'TClient
|
||||
@@ -355,7 +377,7 @@ randomSENDCmd_ a c len = do
|
||||
sId <- atomically $ C.randomBytes 24 g
|
||||
(_, rpKey) <- atomically $ C.generateAuthKeyPair a g
|
||||
msg <- atomically $ C.randomBytes len g
|
||||
mkTransmission c (Just rpKey, sId, Cmd SSender $ SEND noMsgFlags msg)
|
||||
mkTransmission c (Just rpKey, EntityId sId, Cmd SSender $ SEND noMsgFlags msg)
|
||||
|
||||
lenOk :: ByteString -> Bool
|
||||
lenOk s = 0 < B.length s && B.length s <= smpBlockSize - 2
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
module CoreTests.RetryIntervalTests where
|
||||
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Concurrent.Async (concurrently_)
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad (when)
|
||||
import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds)
|
||||
@@ -13,6 +15,10 @@ retryIntervalTests = do
|
||||
describe "Retry interval with 2 modes and lock" $ do
|
||||
testRetryIntervalSameMode
|
||||
testRetryIntervalSwitchMode
|
||||
describe "Foreground retry interval" $ do
|
||||
testRetryForeground
|
||||
testRetryToBackground
|
||||
testRetrySkipWhenForeground
|
||||
|
||||
testRI :: RetryInterval2
|
||||
testRI =
|
||||
@@ -23,12 +29,15 @@ testRI =
|
||||
increaseAfter = 40000,
|
||||
maxInterval = 40000
|
||||
},
|
||||
riFast =
|
||||
RetryInterval
|
||||
{ initialInterval = 10000,
|
||||
increaseAfter = 20000,
|
||||
maxInterval = 40000
|
||||
}
|
||||
riFast = testFastRI
|
||||
}
|
||||
|
||||
testFastRI :: RetryInterval
|
||||
testFastRI =
|
||||
RetryInterval
|
||||
{ initialInterval = 10000,
|
||||
increaseAfter = 20000,
|
||||
maxInterval = 40000
|
||||
}
|
||||
|
||||
testRetryIntervalSameMode :: Spec
|
||||
@@ -81,6 +90,67 @@ testRetryIntervalSwitchMode =
|
||||
(40000, 40000)
|
||||
]
|
||||
|
||||
testRetryForeground :: Spec
|
||||
testRetryForeground =
|
||||
it "should increase elapased time and interval" $ do
|
||||
intervals <- newTVarIO []
|
||||
reportedIntervals <- newTVarIO []
|
||||
ts <- newTVarIO =<< getCurrentTime
|
||||
let isForeground = pure True
|
||||
withRetryForeground testFastRI isForeground (pure True) $ \delay loop -> do
|
||||
ints <- addInterval intervals ts
|
||||
atomically $ modifyTVar' reportedIntervals (delay :)
|
||||
when (length ints < 8) $ loop
|
||||
(reverse <$> readTVarIO intervals) `shouldReturn` [0, 1, 1, 1, 2, 3, 4, 4]
|
||||
(reverse <$> readTVarIO reportedIntervals)
|
||||
`shouldReturn` [ 10000, 10000, 15000, 22500, 33750, 40000, 40000, 40000]
|
||||
|
||||
testRetryToBackground :: Spec
|
||||
testRetryToBackground =
|
||||
it "should not change interval when moving to background" $ do
|
||||
intervals <- newTVarIO []
|
||||
reportedIntervals <- newTVarIO []
|
||||
ts <- newTVarIO =<< getCurrentTime
|
||||
foreground <- newTVarIO True
|
||||
concurrently_
|
||||
( do
|
||||
threadDelay 50000
|
||||
atomically $ writeTVar foreground False
|
||||
)
|
||||
( withRetryForeground testFastRI (readTVar foreground) (pure True) $ \delay loop -> do
|
||||
ints <- addInterval intervals ts
|
||||
atomically $ modifyTVar' reportedIntervals (delay :)
|
||||
when (length ints < 8) $ loop
|
||||
)
|
||||
(reverse <$> readTVarIO intervals) `shouldReturn` [0, 1, 1, 1, 2, 3, 4, 4]
|
||||
(reverse <$> readTVarIO reportedIntervals)
|
||||
`shouldReturn` [ 10000, 10000, 15000, 22500, 33750, 40000, 40000, 40000]
|
||||
|
||||
testRetrySkipWhenForeground :: Spec
|
||||
testRetrySkipWhenForeground =
|
||||
it "should repeat loop as soon as moving to foreground" $ do
|
||||
intervals <- newTVarIO []
|
||||
reportedIntervals <- newTVarIO []
|
||||
ts <- newTVarIO =<< getCurrentTime
|
||||
foreground <- newTVarIO False
|
||||
concurrently_
|
||||
( do
|
||||
threadDelay 65000
|
||||
atomically $ writeTVar foreground True
|
||||
threadDelay 10000
|
||||
atomically $ writeTVar foreground False
|
||||
threadDelay 100000
|
||||
atomically $ writeTVar foreground True
|
||||
)
|
||||
( withRetryForeground testFastRI (readTVar foreground) (pure True) $ \delay loop -> do
|
||||
ints <- addInterval intervals ts
|
||||
atomically $ modifyTVar' reportedIntervals (delay :)
|
||||
when (length ints < 12) $ loop
|
||||
)
|
||||
(reverse <$> readTVarIO intervals) `shouldReturn` [0, 1, 1, 1, 2, 0, 1, 1, 1, 2, 3, 1]
|
||||
(reverse <$> readTVarIO reportedIntervals)
|
||||
`shouldReturn` [ 10000, 10000, 15000, 22500, 33750, 10000, 10000, 15000, 22500, 33750, 40000, 10000]
|
||||
|
||||
addInterval :: TVar [Int] -> TVar UTCTime -> IO [Int]
|
||||
addInterval intervals ts = do
|
||||
ts' <- getCurrentTime
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -Wno-orphans #-}
|
||||
|
||||
module CoreTests.TRcvQueuesTests where
|
||||
|
||||
import AgentTests.EqInstances ()
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import qualified Data.Map as M
|
||||
import qualified Data.Set as S
|
||||
import Data.String (IsString (..))
|
||||
import Simplex.Messaging.Agent.Protocol (ConnId, QueueStatus (..), UserId)
|
||||
import Simplex.Messaging.Agent.Store (DBQueueId (..), RcvQueue, StoredRcvQueue (..))
|
||||
import qualified Simplex.Messaging.Agent.TRcvQueues as RQ
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Protocol (SMPServer, pattern VersionSMPC)
|
||||
import Simplex.Messaging.Protocol (EntityId (..), RecipientId, SMPServer, pattern NoEntity, pattern VersionSMPC)
|
||||
import Test.Hspec
|
||||
import UnliftIO
|
||||
|
||||
@@ -30,24 +34,26 @@ tRcvQueuesTests = do
|
||||
describe "queue transfer" $ do
|
||||
it "getDelSessQueues-batchAddQueues preserves total length" removeSubsTest
|
||||
|
||||
checkDataInvariant :: RQ.TRcvQueues -> IO Bool
|
||||
instance IsString EntityId where fromString = EntityId . B.pack
|
||||
|
||||
checkDataInvariant :: RQ.Queue q => RQ.TRcvQueues q -> IO Bool
|
||||
checkDataInvariant trq = atomically $ do
|
||||
conns <- readTVar $ RQ.getConnections trq
|
||||
qs <- readTVar $ RQ.getRcvQueues trq
|
||||
-- three invariant checks
|
||||
let inv1 = all (\cId -> (S.fromList . L.toList <$> M.lookup cId conns) == Just (M.keysSet (M.filter (\q -> connId q == cId) qs))) (M.keys conns)
|
||||
inv2 = all (\(k, q) -> maybe False ((k `elem`) . L.toList) (M.lookup (connId q) conns)) (M.assocs qs)
|
||||
let inv1 = all (\cId -> (S.fromList . L.toList <$> M.lookup cId conns) == Just (M.keysSet (M.filter (\q -> RQ.connId' q == cId) qs))) (M.keys conns)
|
||||
inv2 = all (\(k, q) -> maybe False ((k `elem`) . L.toList) (M.lookup (RQ.connId' q) conns)) (M.assocs qs)
|
||||
inv3 = all (\(k, q) -> RQ.qKey q == k) (M.assocs qs)
|
||||
pure $ inv1 && inv2 && inv3
|
||||
|
||||
hasConnTest :: IO ()
|
||||
hasConnTest = do
|
||||
trq <- atomically RQ.empty
|
||||
atomically $ RQ.addQueue (dummyRQ 0 "smp://1234-w==@alpha" "c1") trq
|
||||
trq <- RQ.empty
|
||||
atomically $ RQ.addQueue (dummyRQ 0 "smp://1234-w==@alpha" "c1" "r1") trq
|
||||
checkDataInvariant trq `shouldReturn` True
|
||||
atomically $ RQ.addQueue (dummyRQ 0 "smp://1234-w==@alpha" "c2") trq
|
||||
atomically $ RQ.addQueue (dummyRQ 0 "smp://1234-w==@alpha" "c2" "r2") trq
|
||||
checkDataInvariant trq `shouldReturn` True
|
||||
atomically $ RQ.addQueue (dummyRQ 0 "smp://1234-w==@beta" "c3") trq
|
||||
atomically $ RQ.addQueue (dummyRQ 0 "smp://1234-w==@beta" "c3" "r3") trq
|
||||
checkDataInvariant trq `shouldReturn` True
|
||||
atomically (RQ.hasConn "c1" trq) `shouldReturn` True
|
||||
atomically (RQ.hasConn "c2" trq) `shouldReturn` True
|
||||
@@ -56,8 +62,8 @@ hasConnTest = do
|
||||
|
||||
hasConnTestBatch :: IO ()
|
||||
hasConnTestBatch = do
|
||||
trq <- atomically RQ.empty
|
||||
let qs = [dummyRQ 0 "smp://1234-w==@alpha" "c1", dummyRQ 0 "smp://1234-w==@alpha" "c2", dummyRQ 0 "smp://1234-w==@beta" "c3"]
|
||||
trq <- RQ.empty
|
||||
let qs = [dummyRQ 0 "smp://1234-w==@alpha" "c1" "r1", dummyRQ 0 "smp://1234-w==@alpha" "c2" "r2", dummyRQ 0 "smp://1234-w==@beta" "c3" "r3"]
|
||||
atomically $ RQ.batchAddQueues trq qs
|
||||
checkDataInvariant trq `shouldReturn` True
|
||||
atomically (RQ.hasConn "c1" trq) `shouldReturn` True
|
||||
@@ -67,8 +73,8 @@ hasConnTestBatch = do
|
||||
|
||||
batchIdempotentTest :: IO ()
|
||||
batchIdempotentTest = do
|
||||
trq <- atomically RQ.empty
|
||||
let qs = [dummyRQ 0 "smp://1234-w==@alpha" "c1", dummyRQ 0 "smp://1234-w==@alpha" "c2", dummyRQ 0 "smp://1234-w==@beta" "c3"]
|
||||
trq <- RQ.empty
|
||||
let qs = [dummyRQ 0 "smp://1234-w==@alpha" "c1" "r1", dummyRQ 0 "smp://1234-w==@alpha" "c2" "r2", dummyRQ 0 "smp://1234-w==@beta" "c3" "r3"]
|
||||
atomically $ RQ.batchAddQueues trq qs
|
||||
checkDataInvariant trq `shouldReturn` True
|
||||
qs' <- readTVarIO $ RQ.getRcvQueues trq
|
||||
@@ -76,15 +82,15 @@ batchIdempotentTest = do
|
||||
atomically $ RQ.batchAddQueues trq qs
|
||||
checkDataInvariant trq `shouldReturn` True
|
||||
readTVarIO (RQ.getRcvQueues trq) `shouldReturn` qs'
|
||||
fmap L.nub <$> readTVarIO (RQ.getConnections trq) `shouldReturn`cs' -- connections get duplicated, but that doesn't appear to affect anybody
|
||||
fmap L.nub <$> readTVarIO (RQ.getConnections trq) `shouldReturn` cs' -- connections get duplicated, but that doesn't appear to affect anybody
|
||||
|
||||
deleteConnTest :: IO ()
|
||||
deleteConnTest = do
|
||||
trq <- atomically RQ.empty
|
||||
trq <- RQ.empty
|
||||
atomically $ do
|
||||
RQ.addQueue (dummyRQ 0 "smp://1234-w==@alpha" "c1") trq
|
||||
RQ.addQueue (dummyRQ 0 "smp://1234-w==@alpha" "c2") trq
|
||||
RQ.addQueue (dummyRQ 0 "smp://1234-w==@beta" "c3") trq
|
||||
RQ.addQueue (dummyRQ 0 "smp://1234-w==@alpha" "c1" "r1") trq
|
||||
RQ.addQueue (dummyRQ 0 "smp://1234-w==@alpha" "c2" "r2") trq
|
||||
RQ.addQueue (dummyRQ 0 "smp://1234-w==@beta" "c3" "r3") trq
|
||||
checkDataInvariant trq `shouldReturn` True
|
||||
atomically $ RQ.deleteConn "c1" trq
|
||||
checkDataInvariant trq `shouldReturn` True
|
||||
@@ -94,41 +100,49 @@ deleteConnTest = do
|
||||
|
||||
getSessQueuesTest :: IO ()
|
||||
getSessQueuesTest = do
|
||||
trq <- atomically RQ.empty
|
||||
atomically $ RQ.addQueue (dummyRQ 0 "smp://1234-w==@alpha" "c1") trq
|
||||
trq <- RQ.empty
|
||||
atomically $ RQ.addQueue (dummyRQ 0 "smp://1234-w==@alpha" "c1" "r1") trq
|
||||
checkDataInvariant trq `shouldReturn` True
|
||||
atomically $ RQ.addQueue (dummyRQ 0 "smp://1234-w==@alpha" "c2") trq
|
||||
atomically $ RQ.addQueue (dummyRQ 0 "smp://1234-w==@alpha" "c2" "r2") trq
|
||||
checkDataInvariant trq `shouldReturn` True
|
||||
atomically $ RQ.addQueue (dummyRQ 0 "smp://1234-w==@beta" "c3") trq
|
||||
atomically $ RQ.addQueue (dummyRQ 0 "smp://1234-w==@beta" "c3" "r3") trq
|
||||
checkDataInvariant trq `shouldReturn` True
|
||||
atomically $ RQ.addQueue (dummyRQ 1 "smp://1234-w==@beta" "c4") trq
|
||||
atomically $ RQ.addQueue (dummyRQ 1 "smp://1234-w==@beta" "c4" "r4") trq
|
||||
checkDataInvariant trq `shouldReturn` True
|
||||
atomically (RQ.getSessQueues (0, "smp://1234-w==@alpha", Just "c1") trq) `shouldReturn` [dummyRQ 0 "smp://1234-w==@alpha" "c1"]
|
||||
atomically (RQ.getSessQueues (1, "smp://1234-w==@alpha", Just "c1") trq) `shouldReturn` []
|
||||
atomically (RQ.getSessQueues (0, "smp://1234-w==@alpha", Just "nope") trq) `shouldReturn` []
|
||||
atomically (RQ.getSessQueues (0, "smp://1234-w==@alpha", Nothing) trq) `shouldReturn` [dummyRQ 0 "smp://1234-w==@alpha" "c2", dummyRQ 0 "smp://1234-w==@alpha" "c1"]
|
||||
let tSess1 = (0, "smp://1234-w==@alpha", Just "c1")
|
||||
RQ.getSessQueues tSess1 trq `shouldReturn` [dummyRQ 0 "smp://1234-w==@alpha" "c1" "r1"]
|
||||
atomically (RQ.hasSessQueues tSess1 trq) `shouldReturn` True
|
||||
let tSess2 = (1, "smp://1234-w==@alpha", Just "c1")
|
||||
RQ.getSessQueues tSess2 trq `shouldReturn` []
|
||||
atomically (RQ.hasSessQueues tSess2 trq) `shouldReturn` False
|
||||
let tSess3 = (0, "smp://1234-w==@alpha", Just "nope")
|
||||
RQ.getSessQueues tSess3 trq `shouldReturn` []
|
||||
atomically (RQ.hasSessQueues tSess3 trq) `shouldReturn` False
|
||||
let tSess4 = (0, "smp://1234-w==@alpha", Nothing)
|
||||
RQ.getSessQueues tSess4 trq `shouldReturn` [dummyRQ 0 "smp://1234-w==@alpha" "c2" "r2", dummyRQ 0 "smp://1234-w==@alpha" "c1" "r1"]
|
||||
atomically (RQ.hasSessQueues tSess4 trq) `shouldReturn`True
|
||||
|
||||
getDelSessQueuesTest :: IO ()
|
||||
getDelSessQueuesTest = do
|
||||
trq <- atomically RQ.empty
|
||||
trq <- RQ.empty
|
||||
let qs =
|
||||
[ dummyRQ 0 "smp://1234-w==@alpha" "c1",
|
||||
dummyRQ 0 "smp://1234-w==@alpha" "c2",
|
||||
dummyRQ 0 "smp://1234-w==@beta" "c3",
|
||||
dummyRQ 1 "smp://1234-w==@beta" "c4"
|
||||
[ ("1", dummyRQ 0 "smp://1234-w==@alpha" "c1" "r1"),
|
||||
("1", dummyRQ 0 "smp://1234-w==@alpha" "c2" "r2"),
|
||||
("1", dummyRQ 0 "smp://1234-w==@beta" "c3" "r3"),
|
||||
("1", dummyRQ 1 "smp://1234-w==@beta" "c4" "r4")
|
||||
]
|
||||
atomically $ RQ.batchAddQueues trq qs
|
||||
checkDataInvariant trq `shouldReturn` True
|
||||
-- no user
|
||||
atomically (RQ.getDelSessQueues (2, "smp://1234-w==@alpha", Nothing) trq) `shouldReturn` ([], [])
|
||||
atomically (RQ.getDelSessQueues (2, "smp://1234-w==@alpha", Nothing) "1" trq) `shouldReturn` ([], [])
|
||||
checkDataInvariant trq `shouldReturn` True
|
||||
-- wrong user
|
||||
atomically (RQ.getDelSessQueues (1, "smp://1234-w==@alpha", Nothing) trq) `shouldReturn` ([], [])
|
||||
atomically (RQ.getDelSessQueues (1, "smp://1234-w==@alpha", Nothing) "1" trq) `shouldReturn` ([], [])
|
||||
checkDataInvariant trq `shouldReturn` True
|
||||
-- connections intact
|
||||
atomically (RQ.hasConn "c1" trq) `shouldReturn` True
|
||||
atomically (RQ.hasConn "c2" trq) `shouldReturn` True
|
||||
atomically (RQ.getDelSessQueues (0, "smp://1234-w==@alpha", Nothing) trq) `shouldReturn` ([dummyRQ 0 "smp://1234-w==@alpha" "c2", dummyRQ 0 "smp://1234-w==@alpha" "c1"], ["c1", "c2"])
|
||||
atomically (RQ.getDelSessQueues (0, "smp://1234-w==@alpha", Nothing) "1" trq) `shouldReturn` ([dummyRQ 0 "smp://1234-w==@alpha" "c2" "r2", dummyRQ 0 "smp://1234-w==@alpha" "c1" "r1"], ["c1", "c2"])
|
||||
checkDataInvariant trq `shouldReturn` True
|
||||
-- connections gone
|
||||
atomically (RQ.hasConn "c1" trq) `shouldReturn` False
|
||||
@@ -139,31 +153,31 @@ getDelSessQueuesTest = do
|
||||
|
||||
removeSubsTest :: IO ()
|
||||
removeSubsTest = do
|
||||
aq <- atomically RQ.empty
|
||||
aq <- RQ.empty
|
||||
let qs =
|
||||
[ dummyRQ 0 "smp://1234-w==@alpha" "c1",
|
||||
dummyRQ 0 "smp://1234-w==@alpha" "c2",
|
||||
dummyRQ 0 "smp://1234-w==@beta" "c3",
|
||||
dummyRQ 1 "smp://1234-w==@beta" "c4"
|
||||
[ ("1", dummyRQ 0 "smp://1234-w==@alpha" "c1" "r1"),
|
||||
("1", dummyRQ 0 "smp://1234-w==@alpha" "c2" "r2"),
|
||||
("1", dummyRQ 0 "smp://1234-w==@beta" "c3" "r3"),
|
||||
("1", dummyRQ 1 "smp://1234-w==@beta" "c4" "r4")
|
||||
]
|
||||
atomically $ RQ.batchAddQueues aq qs
|
||||
|
||||
pq <- atomically RQ.empty
|
||||
pq <- RQ.empty
|
||||
atomically (totalSize aq pq) `shouldReturn` (4, 4)
|
||||
|
||||
atomically $ RQ.getDelSessQueues (0, "smp://1234-w==@alpha", Nothing) aq >>= RQ.batchAddQueues pq . fst
|
||||
atomically $ RQ.getDelSessQueues (0, "smp://1234-w==@alpha", Nothing) "1" aq >>= RQ.batchAddQueues pq . map ("1",) . fst
|
||||
atomically (totalSize aq pq) `shouldReturn` (4, 4)
|
||||
|
||||
atomically $ RQ.getDelSessQueues (0, "smp://1234-w==@beta", Just "non-existent") aq >>= RQ.batchAddQueues pq . fst
|
||||
atomically $ RQ.getDelSessQueues (0, "smp://1234-w==@beta", Just "non-existent") "1" aq >>= RQ.batchAddQueues pq . map ("1",) . fst
|
||||
atomically (totalSize aq pq) `shouldReturn` (4, 4)
|
||||
|
||||
atomically $ RQ.getDelSessQueues (0, "smp://1234-w==@localhost", Nothing) aq >>= RQ.batchAddQueues pq . fst
|
||||
atomically $ RQ.getDelSessQueues (0, "smp://1234-w==@localhost", Nothing) "1" aq >>= RQ.batchAddQueues pq . map ("1",) . fst
|
||||
atomically (totalSize aq pq) `shouldReturn` (4, 4)
|
||||
|
||||
atomically $ RQ.getDelSessQueues (0, "smp://1234-w==@beta", Just "c3") aq >>= RQ.batchAddQueues pq . fst
|
||||
atomically $ RQ.getDelSessQueues (0, "smp://1234-w==@beta", Just "c3") "1" aq >>= RQ.batchAddQueues pq . map ("1",) . fst
|
||||
atomically (totalSize aq pq) `shouldReturn` (4, 4)
|
||||
|
||||
totalSize :: RQ.TRcvQueues -> RQ.TRcvQueues -> STM (Int, Int)
|
||||
totalSize :: RQ.TRcvQueues q -> RQ.TRcvQueues q -> STM (Int, Int)
|
||||
totalSize a b = do
|
||||
qsizeA <- M.size <$> readTVar (RQ.getRcvQueues a)
|
||||
qsizeB <- M.size <$> readTVar (RQ.getRcvQueues b)
|
||||
@@ -171,18 +185,18 @@ totalSize a b = do
|
||||
csizeB <- M.size <$> readTVar (RQ.getConnections b)
|
||||
pure (qsizeA + qsizeB, csizeA + csizeB)
|
||||
|
||||
dummyRQ :: UserId -> SMPServer -> ConnId -> RcvQueue
|
||||
dummyRQ userId server connId =
|
||||
dummyRQ :: UserId -> SMPServer -> ConnId -> RecipientId -> RcvQueue
|
||||
dummyRQ userId server connId rcvId =
|
||||
RcvQueue
|
||||
{ userId,
|
||||
connId,
|
||||
server,
|
||||
rcvId = "",
|
||||
rcvId,
|
||||
rcvPrivateKey = C.APrivateAuthKey C.SEd25519 "MC4CAQAwBQYDK2VwBCIEIDfEfevydXXfKajz3sRkcQ7RPvfWUPoq6pu1TYHV1DEe",
|
||||
rcvDhSecret = "01234567890123456789012345678901",
|
||||
e2ePrivKey = "MC4CAQAwBQYDK2VuBCIEINCzbVFaCiYHoYncxNY8tSIfn0pXcIAhLBfFc0m+gOpk",
|
||||
e2eDhSecret = Nothing,
|
||||
sndId = "",
|
||||
sndId = NoEntity,
|
||||
sndSecure = True,
|
||||
status = New,
|
||||
dbQueueId = DBQueueId 0,
|
||||
|
||||
@@ -13,6 +13,7 @@ import Simplex.FileTransfer.Description
|
||||
import Simplex.FileTransfer.Protocol
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String (StrEncoding (..))
|
||||
import Simplex.Messaging.Protocol (EntityId (..))
|
||||
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
|
||||
import System.Directory (removeFile)
|
||||
import Test.Hspec
|
||||
@@ -91,7 +92,7 @@ fileDesc =
|
||||
}
|
||||
where
|
||||
defaultChunkSize = FileSize $ mb 8
|
||||
replicaId = ChunkReplicaId "abc"
|
||||
replicaId = ChunkReplicaId $ EntityId "abc"
|
||||
replicaKey = C.APrivateAuthKey C.SEd25519 "MC4CAQAwBQYDK2VwBCIEIDfEfevydXXfKajz3sRkcQ7RPvfWUPoq6pu1TYHV1DEe"
|
||||
chunkDigest = FileDigest "ghi"
|
||||
|
||||
|
||||
+1
-1
@@ -158,7 +158,7 @@ ntfServerTest _ t = runNtfTest $ \h -> tPut' h t >> tGet' h
|
||||
[Right ()] <- tPut h [Right (sig, t')]
|
||||
pure ()
|
||||
tGet' h = do
|
||||
[(Nothing, _, (CorrId corrId, qId, Right cmd))] <- tGet h
|
||||
[(Nothing, _, (CorrId corrId, EntityId qId, Right cmd))] <- tGet h
|
||||
pure (Nothing, corrId, qId, cmd)
|
||||
|
||||
ntfTest :: Transport c => TProxy c -> (THandleNTF c 'TClient -> IO ()) -> Expectation
|
||||
|
||||
@@ -17,6 +17,7 @@ import qualified Data.Aeson.Types as JT
|
||||
import Data.Bifunctor (first)
|
||||
import qualified Data.ByteString.Base64.URL as U
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import NtfClient
|
||||
import SMPClient as SMP
|
||||
@@ -35,7 +36,6 @@ import ServerTests
|
||||
import qualified Simplex.Messaging.Agent.Protocol as AP
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Notifications.Server.Push.APNS
|
||||
import qualified Simplex.Messaging.Notifications.Server.Push.APNS as APNS
|
||||
@@ -72,13 +72,13 @@ pattern RespNtf corrId queueId command <- (_, _, (corrId, queueId, Right command
|
||||
|
||||
deriving instance Eq NtfResponse
|
||||
|
||||
sendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandleNTF c 'TClient -> (Maybe TransmissionAuth, ByteString, ByteString, NtfCommand e) -> IO (SignedTransmission ErrorType NtfResponse)
|
||||
sendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandleNTF c 'TClient -> (Maybe TransmissionAuth, ByteString, NtfEntityId, NtfCommand e) -> IO (SignedTransmission ErrorType NtfResponse)
|
||||
sendRecvNtf h@THandle {params} (sgn, corrId, qId, cmd) = do
|
||||
let TransmissionForAuth {tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd)
|
||||
Right () <- tPut1 h (sgn, tToSend)
|
||||
tGet1 h
|
||||
|
||||
signSendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandleNTF c 'TClient -> C.APrivateAuthKey -> (ByteString, ByteString, NtfCommand e) -> IO (SignedTransmission ErrorType NtfResponse)
|
||||
signSendRecvNtf :: forall c e. (Transport c, NtfEntityI e) => THandleNTF c 'TClient -> C.APrivateAuthKey -> (ByteString, NtfEntityId, NtfCommand e) -> IO (SignedTransmission ErrorType NtfResponse)
|
||||
signSendRecvNtf h@THandle {params} (C.APrivateAuthKey a pk) (corrId, qId, cmd) = do
|
||||
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd)
|
||||
Right () <- tPut1 h (authorize tForAuth, tToSend)
|
||||
@@ -110,7 +110,7 @@ testNotificationSubscription (ATransport t) =
|
||||
-- create queue
|
||||
(sId, rId, rKey, rcvDhSecret) <- createAndSecureQueue rh sPub
|
||||
-- register and verify token
|
||||
RespNtf "1" "" (NRTknId tId ntfDh) <- signSendRecvNtf nh tknKey ("1", "", TNEW $ NewNtfTkn tkn tknPub dhPub)
|
||||
RespNtf "1" NoEntity (NRTknId tId ntfDh) <- signSendRecvNtf nh tknKey ("1", NoEntity, TNEW $ NewNtfTkn tkn tknPub dhPub)
|
||||
APNSMockRequest {notification = APNSNotification {aps = APNSBackground _, notificationData = Just ntfData}, sendApnsResponse = send} <-
|
||||
atomically $ readTBQueue apnsQ
|
||||
send APNSRespOk
|
||||
@@ -126,7 +126,7 @@ testNotificationSubscription (ATransport t) =
|
||||
let srv = SMPServer SMP.testHost SMP.testPort SMP.testKeyHash
|
||||
q = SMPQueueNtf srv nId
|
||||
rcvNtfDhSecret = C.dh' rcvNtfSrvPubDhKey rcvNtfPrivDhKey
|
||||
RespNtf "4" _ (NRSubId _subId) <- signSendRecvNtf nh tknKey ("4", "", SNEW $ NewNtfSub tId q nKey)
|
||||
RespNtf "4" _ (NRSubId _subId) <- signSendRecvNtf nh tknKey ("4", NoEntity, SNEW $ NewNtfSub tId q nKey)
|
||||
-- send message
|
||||
threadDelay 50000
|
||||
Resp "5" _ OK <- signSendRecv sh sKey ("5", sId, _SEND' "hello")
|
||||
@@ -136,8 +136,8 @@ testNotificationSubscription (ATransport t) =
|
||||
Right nonce' = C.cbNonce <$> ntfData' .-> "nonce"
|
||||
Right message = ntfData' .-> "message"
|
||||
Right ntfDataDecrypted = C.cbDecrypt dhSecret nonce' message
|
||||
Right APNS.PNMessageData {smpQueue = SMPQueueNtf {smpServer, notifierId}, nmsgNonce, encNMsgMeta} =
|
||||
parse strP (AP.INTERNAL "error parsing PNMessageData") ntfDataDecrypted
|
||||
Right pnMsgs1 = parse pnMessagesP (AP.INTERNAL "error parsing PNMessageData") ntfDataDecrypted
|
||||
APNS.PNMessageData {smpQueue = SMPQueueNtf {smpServer, notifierId}, nmsgNonce, encNMsgMeta} = L.last pnMsgs1
|
||||
Right nMsgMeta = C.cbDecrypt rcvNtfDhSecret nmsgNonce encNMsgMeta
|
||||
Right NMsgMeta {msgId, msgTs} = parse smpP (AP.INTERNAL "error parsing NMsgMeta") nMsgMeta
|
||||
smpServer `shouldBe` srv
|
||||
@@ -169,8 +169,8 @@ testNotificationSubscription (ATransport t) =
|
||||
Right nonce3 = C.cbNonce <$> ntfData3 .-> "nonce"
|
||||
Right message3 = ntfData3 .-> "message"
|
||||
Right ntfDataDecrypted3 = C.cbDecrypt dhSecret nonce3 message3
|
||||
Right APNS.PNMessageData {smpQueue = SMPQueueNtf {smpServer = smpServer3, notifierId = notifierId3}} =
|
||||
parse strP (AP.INTERNAL "error parsing PNMessageData") ntfDataDecrypted3
|
||||
Right pnMsgs2 = parse pnMessagesP (AP.INTERNAL "error parsing PNMessageData") ntfDataDecrypted3
|
||||
APNS.PNMessageData {smpQueue = SMPQueueNtf {smpServer = smpServer3, notifierId = notifierId3}} = L.last pnMsgs2
|
||||
smpServer3 `shouldBe` srv
|
||||
notifierId3 `shouldBe` nId
|
||||
send3 APNSRespOk
|
||||
|
||||
+13
-8
@@ -11,6 +11,7 @@
|
||||
module SMPAgentClient where
|
||||
|
||||
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 NtfClient (ntfTestPort)
|
||||
@@ -20,7 +21,7 @@ import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Client (ProtocolClientConfig (..), SMPProxyFallback, SMPProxyMode, defaultNetworkConfig, defaultSMPClientConfig)
|
||||
import Simplex.Messaging.Notifications.Client (defaultNTFClientConfig)
|
||||
import Simplex.Messaging.Protocol (NtfServer, ProtoServerWithAuth)
|
||||
import Simplex.Messaging.Protocol (NtfServer, ProtoServerWithAuth (..), ProtocolServer)
|
||||
import Simplex.Messaging.Transport
|
||||
import XFTPClient (testXFTPServer)
|
||||
|
||||
@@ -48,14 +49,14 @@ testNtfServer2 = "ntf://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:6
|
||||
initAgentServers :: InitialAgentServers
|
||||
initAgentServers =
|
||||
InitialAgentServers
|
||||
{ smp = userServers [noAuthSrv testSMPServer],
|
||||
{ smp = userServers [testSMPServer],
|
||||
ntf = [testNtfServer],
|
||||
xftp = userServers [noAuthSrv testXFTPServer],
|
||||
xftp = userServers [testXFTPServer],
|
||||
netCfg = defaultNetworkConfig {tcpTimeout = 500_000, tcpConnectTimeout = 500_000}
|
||||
}
|
||||
|
||||
initAgentServers2 :: InitialAgentServers
|
||||
initAgentServers2 = initAgentServers {smp = userServers [noAuthSrv testSMPServer, noAuthSrv testSMPServer2]}
|
||||
initAgentServers2 = initAgentServers {smp = userServers [testSMPServer, testSMPServer2]}
|
||||
|
||||
initAgentServersProxy :: SMPProxyMode -> SMPProxyFallback -> InitialAgentServers
|
||||
initAgentServersProxy smpProxyMode smpProxyFallback =
|
||||
@@ -71,8 +72,6 @@ agentCfg =
|
||||
ntfCfg = defaultNTFClientConfig {qSize = 1, defaultTransport = (ntfTestPort, transport @TLS), networkConfig},
|
||||
reconnectInterval = fastRetryInterval,
|
||||
persistErrorInterval = 1,
|
||||
ntfWorkerDelay = 100,
|
||||
ntfSMPWorkerDelay = 100,
|
||||
caCertificateFile = "tests/fixtures/ca.crt",
|
||||
privateKeyFile = "tests/fixtures/server.key",
|
||||
certificateFile = "tests/fixtures/server.crt"
|
||||
@@ -89,5 +88,11 @@ fastRetryInterval = defaultReconnectInterval {initialInterval = 50_000}
|
||||
fastMessageRetryInterval :: RetryInterval2
|
||||
fastMessageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval}
|
||||
|
||||
userServers :: NonEmpty (ProtoServerWithAuth p) -> Map UserId (NonEmpty (ProtoServerWithAuth p))
|
||||
userServers srvs = M.fromList [(1, srvs)]
|
||||
userServers :: NonEmpty (ProtocolServer p) -> Map UserId (NonEmpty (ServerCfg p))
|
||||
userServers = userServers' . L.map noAuthSrv
|
||||
|
||||
userServers' :: NonEmpty (ProtoServerWithAuth p) -> Map UserId (NonEmpty (ServerCfg p))
|
||||
userServers' srvs = M.fromList [(1, L.map (presetServerCfg True) srvs)]
|
||||
|
||||
noAuthSrvCfg :: ProtocolServer p -> ServerCfg p
|
||||
noAuthSrvCfg = presetServerCfg True . noAuthSrv
|
||||
|
||||
+2
-2
@@ -100,7 +100,6 @@ cfg =
|
||||
{ transports = [],
|
||||
smpHandshakeTimeout = 60000000,
|
||||
tbqSize = 1,
|
||||
-- serverTbqSize = 1,
|
||||
msgQueueQuota = 4,
|
||||
queueIdBytes = 24,
|
||||
msgIdBytes = 24,
|
||||
@@ -116,6 +115,7 @@ cfg =
|
||||
logStatsStartTime = 0,
|
||||
serverStatsLogFile = "tests/smp-server-stats.daily.log",
|
||||
serverStatsBackupFile = Nothing,
|
||||
pendingENDInterval = 500000,
|
||||
caCertificateFile = "tests/fixtures/ca.crt",
|
||||
privateKeyFile = "tests/fixtures/server.key",
|
||||
certificateFile = "tests/fixtures/server.crt",
|
||||
@@ -219,7 +219,7 @@ smpServerTest _ t = runSmpTest $ \h -> tPut' h t >> tGet' h
|
||||
[Right ()] <- tPut h [Right (sig, t')]
|
||||
pure ()
|
||||
tGet' h = do
|
||||
[(Nothing, _, (CorrId corrId, qId, Right cmd))] <- tGet h
|
||||
[(Nothing, _, (CorrId corrId, EntityId qId, Right cmd))] <- tGet h
|
||||
pure (Nothing, corrId, qId, cmd)
|
||||
|
||||
smpTest :: (HasCallStack, Transport c) => TProxy c -> (HasCallStack => THandleSMP c 'TClient -> IO ()) -> Expectation
|
||||
|
||||
+55
-25
@@ -34,7 +34,8 @@ import Simplex.Messaging.Client
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.Ratchet (pattern PQSupportOn)
|
||||
import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
||||
import Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Protocol (EncRcvMsgBody (..), MsgBody, RcvMessage (..), SubscriptionMode (..), pattern NoEntity, maxMessageLength, noMsgFlags)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Server.Env.STM (ServerConfig (..))
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Util (bshow, tshow)
|
||||
@@ -101,27 +102,29 @@ smpProxyTests = do
|
||||
it "500x20" . twoServersFirstProxy $ 500 `inParrallel` deliver 20
|
||||
describe "agent API" $ do
|
||||
describe "one server" $ do
|
||||
xit "always via proxy" . oneServer $
|
||||
it "always via proxy" . oneServer $
|
||||
agentDeliverMessageViaProxy ([srv1], SPMAlways, True) ([srv1], SPMAlways, True) C.SEd448 "hello 1" "hello 2" 1
|
||||
xit "without proxy" . oneServer $
|
||||
it "without proxy" . oneServer $
|
||||
agentDeliverMessageViaProxy ([srv1], SPMNever, False) ([srv1], SPMNever, False) C.SEd448 "hello 1" "hello 2" 1
|
||||
describe "two servers" $ do
|
||||
xit "always via proxy" . twoServers $
|
||||
it "always via proxy" . twoServers $
|
||||
agentDeliverMessageViaProxy ([srv1], SPMAlways, True) ([srv2], SPMAlways, True) C.SEd448 "hello 1" "hello 2" 1
|
||||
xit "both via proxy" . twoServers $
|
||||
it "both via proxy" . twoServers $
|
||||
agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv2], SPMUnknown, True) C.SEd448 "hello 1" "hello 2" 1
|
||||
xit "first via proxy" . twoServers $
|
||||
it "first via proxy" . twoServers $
|
||||
agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv2], SPMNever, False) C.SEd448 "hello 1" "hello 2" 1
|
||||
xit "without proxy" . twoServers $
|
||||
it "without proxy" . twoServers $
|
||||
agentDeliverMessageViaProxy ([srv1], SPMNever, False) ([srv2], SPMNever, False) C.SEd448 "hello 1" "hello 2" 1
|
||||
xit "first via proxy for unknown" . twoServers $
|
||||
it "first via proxy for unknown" . twoServers $
|
||||
agentDeliverMessageViaProxy ([srv1], SPMUnknown, True) ([srv1, srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2" 1
|
||||
it "without proxy with fallback" . twoServers_ proxyCfg cfgV7 $
|
||||
agentDeliverMessageViaProxy ([srv1], SPMUnknown, False) ([srv2], SPMUnknown, False) C.SEd448 "hello 1" "hello 2" 3
|
||||
it "fails when fallback is prohibited" . twoServers_ proxyCfg cfgV7 $
|
||||
agentViaProxyVersionError
|
||||
xit "retries sending when destination or proxy relay is offline" $
|
||||
it "retries sending when destination or proxy relay is offline" $
|
||||
agentViaProxyRetryOffline
|
||||
it "retries sending when destination relay session disconnects in proxy" $
|
||||
agentViaProxyRetryNoSession
|
||||
describe "stress test 1k" $ do
|
||||
let deliver nAgents nMsgs = agentDeliverMessagesViaProxyConc (replicate nAgents [srv1]) (map bshow [1 :: Int .. nMsgs])
|
||||
it "2 agents, 250 messages" . oneServer $ deliver 2 250
|
||||
@@ -157,7 +160,7 @@ deliverMessagesViaProxy proxyServ relayServ alg unsecuredMsgs securedMsgs = do
|
||||
-- prepare receiving queue
|
||||
(rPub, rPriv) <- atomically $ C.generateAuthKeyPair alg g
|
||||
(rdhPub, rdhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
||||
QIK {rcvId, sndId, rcvPublicDhKey = srvDh} <- runExceptT' $ createSMPQueue rc (rPub, rPriv) rdhPub (Just "correct") SMSubscribe False
|
||||
SMP.QIK {rcvId, sndId, rcvPublicDhKey = srvDh} <- runExceptT' $ createSMPQueue rc (rPub, rPriv) rdhPub (Just "correct") SMSubscribe False
|
||||
let dec = decryptMsgV3 $ C.dh' srvDh rdhPriv
|
||||
-- get proxy session
|
||||
sess0 <- runExceptT' $ connectSMPProxiedRelay pc relayServ (Just "correct")
|
||||
@@ -204,7 +207,8 @@ agentDeliverMessageViaProxy aTestCfg@(aSrvs, _, aViaProxy) bTestCfg@(bSrvs, _, b
|
||||
withAgent 1 aCfg (servers aTestCfg) testDB $ \alice ->
|
||||
withAgent 2 aCfg (servers bTestCfg) testDB2 $ \bob -> runRight_ $ do
|
||||
(bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (CR.IKNoPQ PQSupportOn) SMSubscribe
|
||||
aliceId <- A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe
|
||||
(aliceId, sqSecured) <- A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe
|
||||
liftIO $ sqSecured `shouldBe` True
|
||||
("", _, A.CONF confId pqSup' _ "bob's connInfo") <- get alice
|
||||
liftIO $ pqSup' `shouldBe` PQSupportOn
|
||||
allowConnection alice bobId confId "alice's connInfo"
|
||||
@@ -234,7 +238,7 @@ agentDeliverMessageViaProxy aTestCfg@(aSrvs, _, aViaProxy) bTestCfg@(bSrvs, _, b
|
||||
where
|
||||
msgId = subtract baseId . fst
|
||||
aCfg = agentCfg {sndAuthAlg = C.AuthAlg alg, rcvAuthAlg = C.AuthAlg alg}
|
||||
servers (srvs, smpProxyMode, _) = (initAgentServersProxy smpProxyMode SPFAllow) {smp = userServers $ L.map noAuthSrv srvs}
|
||||
servers (srvs, smpProxyMode, _) = (initAgentServersProxy smpProxyMode SPFAllow) {smp = userServers srvs}
|
||||
|
||||
agentDeliverMessagesViaProxyConc :: [NonEmpty SMPServer] -> [MsgBody] -> IO ()
|
||||
agentDeliverMessagesViaProxyConc agentServers msgs =
|
||||
@@ -258,7 +262,8 @@ agentDeliverMessagesViaProxyConc agentServers msgs =
|
||||
-- otherwise the CONF messages would get mixed with MSG
|
||||
prePair alice bob = do
|
||||
(bobId, qInfo) <- runExceptT' $ A.createConnection alice 1 True SCMInvitation Nothing (CR.IKNoPQ PQSupportOn) SMSubscribe
|
||||
aliceId <- runExceptT' $ A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe
|
||||
(aliceId, sqSecured) <- runExceptT' $ A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe
|
||||
liftIO $ sqSecured `shouldBe` True
|
||||
confId <-
|
||||
get alice >>= \case
|
||||
("", _, A.CONF confId pqSup' _ "bob's connInfo") -> do
|
||||
@@ -299,7 +304,7 @@ agentDeliverMessagesViaProxyConc agentServers msgs =
|
||||
logDebug "run finished"
|
||||
pqEnc = CR.PQEncOn
|
||||
aCfg = agentCfg {sndAuthAlg = C.AuthAlg C.SEd448, rcvAuthAlg = C.AuthAlg C.SEd448}
|
||||
servers srvs = (initAgentServersProxy SPMAlways SPFAllow) {smp = userServers $ L.map noAuthSrv srvs}
|
||||
servers srvs = (initAgentServersProxy SPMAlways SPFAllow) {smp = userServers srvs}
|
||||
|
||||
agentViaProxyVersionError :: IO ()
|
||||
agentViaProxyVersionError =
|
||||
@@ -310,7 +315,7 @@ agentViaProxyVersionError =
|
||||
A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe
|
||||
pure ()
|
||||
where
|
||||
servers srvs = (initAgentServersProxy SPMUnknown SPFProhibit) {smp = userServers $ L.map noAuthSrv srvs}
|
||||
servers srvs = (initAgentServersProxy SPMUnknown SPFProhibit) {smp = userServers srvs}
|
||||
|
||||
agentViaProxyRetryOffline :: IO ()
|
||||
agentViaProxyRetryOffline = do
|
||||
@@ -326,7 +331,8 @@ agentViaProxyRetryOffline = do
|
||||
withServer $ \_ -> do
|
||||
(aliceId, bobId) <- withServer2 $ \_ -> runRight $ do
|
||||
(bobId, qInfo) <- A.createConnection alice 1 True SCMInvitation Nothing (CR.IKNoPQ PQSupportOn) SMSubscribe
|
||||
aliceId <- A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe
|
||||
(aliceId, sqSecured) <- A.joinConnection bob 1 Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe
|
||||
liftIO $ sqSecured `shouldBe` True
|
||||
("", _, A.CONF confId pqSup' _ "bob's connInfo") <- get alice
|
||||
liftIO $ pqSup' `shouldBe` PQSupportOn
|
||||
allowConnection alice bobId confId "alice's connInfo"
|
||||
@@ -355,11 +361,15 @@ agentViaProxyRetryOffline = do
|
||||
-- proxy relay down
|
||||
4 <- msgId <$> A.sendMessage bob aliceId pqEnc noMsgFlags msg2
|
||||
bob `down` aliceId
|
||||
withServer2 $ \_ -> runRight_ $ do
|
||||
bob `up` aliceId
|
||||
get bob ##> ("", aliceId, A.SENT (baseId + 4) bProxySrv)
|
||||
get alice =##> \case ("", c, Msg' _ pq msg2') -> c == bobId && pq == pqEnc && msg2 == msg2'; _ -> False
|
||||
ackMessage alice bobId (baseId + 4) Nothing
|
||||
withServer2 $ \_ -> do
|
||||
getInAnyOrder
|
||||
bob
|
||||
[ \case ("", "", AEvt SAENone (UP _ [c])) -> c == aliceId; _ -> False,
|
||||
\case ("", c, AEvt SAEConn (A.SENT mId srv)) -> c == aliceId && mId == baseId + 4 && srv == bProxySrv; _ -> False
|
||||
]
|
||||
runRight_ $ do
|
||||
get alice =##> \case ("", c, Msg' _ pq msg2') -> c == bobId && pq == pqEnc && msg2 == msg2'; _ -> False
|
||||
ackMessage alice bobId (baseId + 4) Nothing
|
||||
where
|
||||
withServer :: (ThreadId -> IO a) -> IO a
|
||||
withServer = withServer_ testStoreLogFile testStoreMsgsFile testPort
|
||||
@@ -370,22 +380,42 @@ agentViaProxyRetryOffline = do
|
||||
a `up` cId = nGet a =##> \case ("", "", UP _ [c]) -> c == cId; _ -> False
|
||||
a `down` cId = nGet a =##> \case ("", "", DOWN _ [c]) -> c == cId; _ -> False
|
||||
aCfg = agentCfg {messageRetryInterval = fastMessageRetryInterval}
|
||||
baseId = 3
|
||||
baseId = 1
|
||||
msgId = subtract baseId . fst
|
||||
servers srv = (initAgentServersProxy SPMAlways SPFProhibit) {smp = userServers $ L.map noAuthSrv [srv]}
|
||||
servers srv = (initAgentServersProxy SPMAlways SPFProhibit) {smp = userServers [srv]}
|
||||
|
||||
agentViaProxyRetryNoSession :: IO ()
|
||||
agentViaProxyRetryNoSession = do
|
||||
let srv1 = SMPServer testHost testPort testKeyHash
|
||||
srv2 = SMPServer testHost testPort2 testKeyHash
|
||||
withAgent 1 agentCfg (servers srv1) testDB $ \a ->
|
||||
withAgent 2 agentCfg (servers srv2) testDB2 $ \b -> do
|
||||
withSmpServerConfigOn (transport @TLS) proxyCfg testPort $ \_ -> do
|
||||
(aId, _) <- withServer2 $ \_ -> runRight $ makeConnection a b
|
||||
nGet b =##> \case ("", "", DOWN _ [c]) -> c == aId; _ -> False
|
||||
withServer2 $ \_ -> do
|
||||
nGet b =##> \case ("", "", UP _ [c]) -> c == aId; _ -> False
|
||||
-- to test retry in case of NO_SESSION error,
|
||||
-- the client using server 1 as proxy and server 2 as destination
|
||||
-- should be joining the connection, so the order is swapped here.
|
||||
_ <- runRight $ makeConnection b a
|
||||
pure ()
|
||||
where
|
||||
withServer2 = withSmpServerConfigOn (transport @TLS) proxyCfg {storeLogFile = Just testStoreLogFile2, storeMsgsFile = Just testStoreMsgsFile2} testPort2
|
||||
servers srv = (initAgentServersProxy SPMAlways SPFProhibit) {smp = userServers [srv]}
|
||||
|
||||
testNoProxy :: IO ()
|
||||
testNoProxy = do
|
||||
withSmpServerConfigOn (transport @TLS) cfg testPort2 $ \_ -> do
|
||||
testSMPClient_ "127.0.0.1" testPort2 proxyVRangeV8 $ \(th :: THandleSMP TLS 'TClient) -> do
|
||||
(_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer Nothing)
|
||||
(_, _, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", NoEntity, SMP.PRXY testSMPServer Nothing)
|
||||
reply `shouldBe` Right (SMP.ERR $ SMP.PROXY SMP.BASIC_AUTH)
|
||||
|
||||
testProxyAuth :: IO ()
|
||||
testProxyAuth = do
|
||||
withSmpServerConfigOn (transport @TLS) proxyCfgAuth testPort $ \_ -> do
|
||||
testSMPClient_ "127.0.0.1" testPort proxyVRangeV8 $ \(th :: THandleSMP TLS 'TClient) -> do
|
||||
(_, _s, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", "", PRXY testSMPServer2 $ Just "wrong")
|
||||
(_, _s, (_corrId, _entityId, reply)) <- sendRecv th (Nothing, "0", NoEntity, SMP.PRXY testSMPServer2 $ Just "wrong")
|
||||
reply `shouldBe` Right (SMP.ERR $ SMP.PROXY SMP.BASIC_AUTH)
|
||||
where
|
||||
proxyCfgAuth = proxyCfg {newQueueBasicAuth = Just "correct"}
|
||||
|
||||
+54
-44
@@ -25,7 +25,8 @@ import Data.Bifunctor (first)
|
||||
import Data.ByteString.Base64
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.Set as S
|
||||
import Data.Hashable (hash)
|
||||
import qualified Data.IntSet as IS
|
||||
import Data.Type.Equality
|
||||
import GHC.Stack (withFrozenCallStack)
|
||||
import SMPClient
|
||||
@@ -78,13 +79,13 @@ pattern Ids rId sId srvDh <- IDS (QIK rId sId srvDh _sndSecure)
|
||||
pattern Msg :: MsgId -> MsgBody -> BrokerMsg
|
||||
pattern Msg msgId body <- MSG RcvMessage {msgId, msgBody = EncRcvMsgBody body}
|
||||
|
||||
sendRecv :: forall c p. (Transport c, PartyI p) => THandleSMP c 'TClient -> (Maybe TransmissionAuth, ByteString, ByteString, Command p) -> IO (SignedTransmission ErrorType BrokerMsg)
|
||||
sendRecv :: forall c p. (Transport c, PartyI p) => THandleSMP c 'TClient -> (Maybe TransmissionAuth, ByteString, EntityId, Command p) -> IO (SignedTransmission ErrorType BrokerMsg)
|
||||
sendRecv h@THandle {params} (sgn, corrId, qId, cmd) = do
|
||||
let TransmissionForAuth {tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd)
|
||||
Right () <- tPut1 h (sgn, tToSend)
|
||||
tGet1 h
|
||||
|
||||
signSendRecv :: forall c p. (Transport c, PartyI p) => THandleSMP c 'TClient -> C.APrivateAuthKey -> (ByteString, ByteString, Command p) -> IO (SignedTransmission ErrorType BrokerMsg)
|
||||
signSendRecv :: forall c p. (Transport c, PartyI p) => THandleSMP c 'TClient -> C.APrivateAuthKey -> (ByteString, EntityId, Command p) -> IO (SignedTransmission ErrorType BrokerMsg)
|
||||
signSendRecv h@THandle {params} (C.APrivateAuthKey a pk) (corrId, qId, cmd) = do
|
||||
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth params (CorrId corrId, qId, cmd)
|
||||
Right () <- tPut1 h (authorize tForAuth, tToSend)
|
||||
@@ -134,9 +135,9 @@ testCreateSecure (ATransport t) =
|
||||
g <- C.newRandom
|
||||
(rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g
|
||||
(dhPub, dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
||||
Resp "abcd" rId1 (Ids rId sId srvDh) <- signSendRecv r rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe False)
|
||||
Resp "abcd" rId1 (Ids rId sId srvDh) <- signSendRecv r rKey ("abcd", NoEntity, NEW rPub dhPub Nothing SMSubscribe False)
|
||||
let dec = decryptMsgV3 $ C.dh' srvDh dhPriv
|
||||
(rId1, "") #== "creates queue"
|
||||
(rId1, NoEntity) #== "creates queue"
|
||||
|
||||
Resp "bcda" sId1 ok1 <- sendRecv s ("", "bcda", sId, _SEND "hello")
|
||||
(ok1, OK) #== "accepts unsigned SEND"
|
||||
@@ -199,9 +200,9 @@ testCreateDelete (ATransport t) =
|
||||
g <- C.newRandom
|
||||
(rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
||||
(dhPub, dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
||||
Resp "abcd" rId1 (Ids rId sId srvDh) <- signSendRecv rh rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe False)
|
||||
Resp "abcd" rId1 (Ids rId sId srvDh) <- signSendRecv rh rKey ("abcd", NoEntity, NEW rPub dhPub Nothing SMSubscribe False)
|
||||
let dec = decryptMsgV3 $ C.dh' srvDh dhPriv
|
||||
(rId1, "") #== "creates queue"
|
||||
(rId1, NoEntity) #== "creates queue"
|
||||
|
||||
(sPub, sKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
||||
Resp "bcda" _ ok1 <- signSendRecv rh rKey ("bcda", rId, KEY sPub)
|
||||
@@ -271,7 +272,7 @@ stressTest (ATransport t) =
|
||||
(rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
||||
(dhPub, _ :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
||||
rIds <- forM ([1 .. 50] :: [Int]) . const $ do
|
||||
Resp "" "" (Ids rId _ _) <- signSendRecv h1 rKey ("", "", NEW rPub dhPub Nothing SMSubscribe False)
|
||||
Resp "" NoEntity (Ids rId _ _) <- signSendRecv h1 rKey ("", NoEntity, NEW rPub dhPub Nothing SMSubscribe False)
|
||||
pure rId
|
||||
let subscribeQueues h = forM_ rIds $ \rId -> do
|
||||
Resp "" rId' OK <- signSendRecv h rKey ("", rId, SUB)
|
||||
@@ -289,7 +290,7 @@ testAllowNewQueues t =
|
||||
g <- C.newRandom
|
||||
(rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g
|
||||
(dhPub, _ :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
||||
Resp "abcd" "" (ERR AUTH) <- signSendRecv h rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe False)
|
||||
Resp "abcd" NoEntity (ERR AUTH) <- signSendRecv h rKey ("abcd", NoEntity, NEW rPub dhPub Nothing SMSubscribe False)
|
||||
pure ()
|
||||
|
||||
testDuplex :: ATransport -> Spec
|
||||
@@ -299,7 +300,7 @@ testDuplex (ATransport t) =
|
||||
g <- C.newRandom
|
||||
(arPub, arKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g
|
||||
(aDhPub, aDhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
||||
Resp "abcd" _ (Ids aRcv aSnd aSrvDh) <- signSendRecv alice arKey ("abcd", "", NEW arPub aDhPub Nothing SMSubscribe False)
|
||||
Resp "abcd" _ (Ids aRcv aSnd aSrvDh) <- signSendRecv alice arKey ("abcd", NoEntity, NEW arPub aDhPub Nothing SMSubscribe False)
|
||||
let aDec = decryptMsgV3 $ C.dh' aSrvDh aDhPriv
|
||||
-- aSnd ID is passed to Bob out-of-band
|
||||
|
||||
@@ -315,15 +316,15 @@ testDuplex (ATransport t) =
|
||||
|
||||
(brPub, brKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g
|
||||
(bDhPub, bDhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
||||
Resp "abcd" _ (Ids bRcv bSnd bSrvDh) <- signSendRecv bob brKey ("abcd", "", NEW brPub bDhPub Nothing SMSubscribe False)
|
||||
Resp "abcd" _ (Ids bRcv bSnd bSrvDh) <- signSendRecv bob brKey ("abcd", NoEntity, NEW brPub bDhPub Nothing SMSubscribe False)
|
||||
let bDec = decryptMsgV3 $ C.dh' bSrvDh bDhPriv
|
||||
Resp "bcda" _ OK <- signSendRecv bob bsKey ("bcda", aSnd, _SEND $ "reply_id " <> encode bSnd)
|
||||
Resp "bcda" _ OK <- signSendRecv bob bsKey ("bcda", aSnd, _SEND $ "reply_id " <> encode (unEntityId bSnd))
|
||||
-- "reply_id ..." is ad-hoc, not a part of SMP protocol
|
||||
|
||||
Resp "" _ (Msg mId2 msg2) <- tGet1 alice
|
||||
Resp "cdab" _ OK <- signSendRecv alice arKey ("cdab", aRcv, ACK mId2)
|
||||
Right ["reply_id", bId] <- pure $ B.words <$> aDec mId2 msg2
|
||||
(bId, encode bSnd) #== "reply queue ID received from Bob"
|
||||
(bId, encode (unEntityId bSnd)) #== "reply queue ID received from Bob"
|
||||
|
||||
(asPub, asKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g
|
||||
Resp "dabc" _ OK <- sendRecv alice ("", "dabc", bSnd, _SEND $ "key " <> strEncode asPub)
|
||||
@@ -354,7 +355,7 @@ testSwitchSub (ATransport t) =
|
||||
g <- C.newRandom
|
||||
(rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g
|
||||
(dhPub, dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
||||
Resp "abcd" _ (Ids rId sId srvDh) <- signSendRecv rh1 rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe False)
|
||||
Resp "abcd" _ (Ids rId sId srvDh) <- signSendRecv rh1 rKey ("abcd", NoEntity, NEW rPub dhPub Nothing SMSubscribe False)
|
||||
let dec = decryptMsgV3 $ C.dh' srvDh dhPriv
|
||||
Resp "bcda" _ ok1 <- sendRecv sh ("", "bcda", sId, _SEND "test1")
|
||||
(ok1, OK) #== "sent test message 1"
|
||||
@@ -384,6 +385,10 @@ testSwitchSub (ATransport t) =
|
||||
Resp "bcda" _ ok3 <- signSendRecv rh2 rKey ("bcda", rId, ACK mId3)
|
||||
(ok3, OK) #== "accepts ACK from the 2nd TCP connection"
|
||||
|
||||
Resp "cdab" _ OK <- signSendRecv rh1 rKey ("cdab", rId, DEL)
|
||||
Resp "" rId' DELD <- tGet1 rh2
|
||||
(rId', rId) #== "connection deleted event delivered to subscribed client"
|
||||
|
||||
1000 `timeout` tGet @SMPVersion @ErrorType @BrokerMsg rh1 >>= \case
|
||||
Nothing -> return ()
|
||||
Just _ -> error "nothing else is delivered to the 1st TCP connection"
|
||||
@@ -491,12 +496,12 @@ testWithStoreLog at@(ATransport t) =
|
||||
(sPub1, sKey1) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
||||
(sPub2, sKey2) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
||||
(nPub, nKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
||||
recipientId1 <- newTVarIO ""
|
||||
recipientId1 <- newTVarIO NoEntity
|
||||
recipientKey1 <- newTVarIO Nothing
|
||||
dhShared1 <- newTVarIO Nothing
|
||||
senderId1 <- newTVarIO ""
|
||||
senderId2 <- newTVarIO ""
|
||||
notifierId <- newTVarIO ""
|
||||
senderId1 <- newTVarIO NoEntity
|
||||
senderId2 <- newTVarIO NoEntity
|
||||
notifierId <- newTVarIO NoEntity
|
||||
|
||||
withSmpServerStoreLogOn at testPort . runTest t $ \h -> runClient t $ \h1 -> do
|
||||
(sId1, rId1, rKey1, dhShared) <- createAndSecureQueue h sPub1
|
||||
@@ -509,19 +514,21 @@ testWithStoreLog at@(ATransport t) =
|
||||
writeTVar senderId1 sId1
|
||||
writeTVar notifierId nId
|
||||
Resp "dabc" _ OK <- signSendRecv h1 nKey ("dabc", nId, NSUB)
|
||||
signSendRecv h sKey1 ("bcda", sId1, _SEND' "hello") >>= \case
|
||||
Resp "bcda" _ OK -> pure ()
|
||||
r -> unexpected r
|
||||
Resp "" _ (Msg mId1 msg1) <- tGet1 h
|
||||
(mId1, msg1) <-
|
||||
signSendRecv h sKey1 ("bcda", sId1, _SEND' "hello") >>= \case
|
||||
Resp "" _ (Msg mId1 msg1) -> pure (mId1, msg1)
|
||||
r -> error $ "unexpected response " <> take 100 (show r)
|
||||
Resp "bcda" _ OK <- tGet1 h
|
||||
(decryptMsgV3 dhShared mId1 msg1, Right "hello") #== "delivered from queue 1"
|
||||
Resp "" _ (NMSG _ _) <- tGet1 h1
|
||||
|
||||
(sId2, rId2, rKey2, dhShared2) <- createAndSecureQueue h sPub2
|
||||
atomically $ writeTVar senderId2 sId2
|
||||
signSendRecv h sKey2 ("cdab", sId2, _SEND "hello too") >>= \case
|
||||
Resp "cdab" _ OK -> pure ()
|
||||
r -> unexpected r
|
||||
Resp "" _ (Msg mId2 msg2) <- tGet1 h
|
||||
(mId2, msg2) <-
|
||||
signSendRecv h sKey2 ("cdab", sId2, _SEND "hello too") >>= \case
|
||||
Resp "" _ (Msg mId2 msg2) -> pure (mId2, msg2)
|
||||
r -> error $ "unexpected response " <> take 100 (show r)
|
||||
Resp "cdab" _ OK <- tGet1 h
|
||||
(decryptMsgV3 dhShared2 mId2 msg2, Right "hello too") #== "delivered from queue 2"
|
||||
|
||||
Resp "dabc" _ OK <- signSendRecv h rKey2 ("dabc", rId2, DEL)
|
||||
@@ -578,10 +585,10 @@ testRestoreMessages at@(ATransport t) =
|
||||
|
||||
g <- C.newRandom
|
||||
(sPub, sKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
||||
recipientId <- newTVarIO ""
|
||||
recipientId <- newTVarIO NoEntity
|
||||
recipientKey <- newTVarIO Nothing
|
||||
dhShared <- newTVarIO Nothing
|
||||
senderId <- newTVarIO ""
|
||||
senderId <- newTVarIO NoEntity
|
||||
|
||||
withSmpServerStoreMsgLogOn at testPort . runTest t $ \h -> do
|
||||
runClient t $ \h1 -> do
|
||||
@@ -608,7 +615,7 @@ testRestoreMessages at@(ATransport t) =
|
||||
|
||||
logSize testStoreLogFile `shouldReturn` 2
|
||||
logSize testStoreMsgsFile `shouldReturn` 5
|
||||
logSize testServerStatsBackupFile `shouldReturn` 55
|
||||
logSize testServerStatsBackupFile `shouldReturn` 72
|
||||
Right stats1 <- strDecode <$> B.readFile testServerStatsBackupFile
|
||||
checkStats stats1 [rId] 5 1
|
||||
|
||||
@@ -626,7 +633,7 @@ testRestoreMessages at@(ATransport t) =
|
||||
logSize testStoreLogFile `shouldReturn` 1
|
||||
-- the last message is not removed because it was not ACK'd
|
||||
logSize testStoreMsgsFile `shouldReturn` 3
|
||||
logSize testServerStatsBackupFile `shouldReturn` 55
|
||||
logSize testServerStatsBackupFile `shouldReturn` 72
|
||||
Right stats2 <- strDecode <$> B.readFile testServerStatsBackupFile
|
||||
checkStats stats2 [rId] 5 3
|
||||
|
||||
@@ -645,7 +652,7 @@ testRestoreMessages at@(ATransport t) =
|
||||
|
||||
logSize testStoreLogFile `shouldReturn` 1
|
||||
logSize testStoreMsgsFile `shouldReturn` 0
|
||||
logSize testServerStatsBackupFile `shouldReturn` 55
|
||||
logSize testServerStatsBackupFile `shouldReturn` 72
|
||||
Right stats3 <- strDecode <$> B.readFile testServerStatsBackupFile
|
||||
checkStats stats3 [rId] 5 5
|
||||
|
||||
@@ -673,19 +680,19 @@ checkStats s qs sent received = do
|
||||
_msgSentNtf s `shouldBe` 0
|
||||
_msgRecvNtf s `shouldBe` 0
|
||||
let PeriodStatsData {_day, _week, _month} = _activeQueues s
|
||||
S.toList _day `shouldBe` qs
|
||||
S.toList _week `shouldBe` qs
|
||||
S.toList _month `shouldBe` qs
|
||||
IS.toList _day `shouldBe` map (hash . unEntityId) qs
|
||||
IS.toList _week `shouldBe` map (hash . unEntityId) qs
|
||||
IS.toList _month `shouldBe` map (hash . unEntityId) qs
|
||||
|
||||
testRestoreExpireMessages :: ATransport -> Spec
|
||||
testRestoreExpireMessages at@(ATransport t) =
|
||||
it "should store messages on exit and restore on start" $ do
|
||||
g <- C.newRandom
|
||||
(sPub, sKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
||||
recipientId <- newTVarIO ""
|
||||
recipientId <- newTVarIO NoEntity
|
||||
recipientKey <- newTVarIO Nothing
|
||||
dhShared <- newTVarIO Nothing
|
||||
senderId <- newTVarIO ""
|
||||
senderId <- newTVarIO NoEntity
|
||||
|
||||
withSmpServerStoreMsgLogOn at testPort . runTest t $ \h -> do
|
||||
runClient t $ \h1 -> do
|
||||
@@ -740,7 +747,7 @@ createAndSecureQueue h sPub = do
|
||||
g <- C.newRandom
|
||||
(rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g
|
||||
(dhPub, dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
||||
Resp "abcd" "" (Ids rId sId srvDh) <- signSendRecv h rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe False)
|
||||
Resp "abcd" NoEntity (Ids rId sId srvDh) <- signSendRecv h rKey ("abcd", NoEntity, NEW rPub dhPub Nothing SMSubscribe False)
|
||||
let dhShared = C.dh' srvDh dhPriv
|
||||
Resp "dabc" rId' OK <- signSendRecv h rKey ("dabc", rId, KEY sPub)
|
||||
(rId', rId) #== "same queue ID"
|
||||
@@ -769,13 +776,13 @@ testTiming (ATransport t) =
|
||||
(C.AuthAlg C.SX25519, C.AuthAlg C.SX25519, 200) -- correct key type
|
||||
]
|
||||
timeRepeat n = fmap fst . timeItT . forM_ (replicate n ()) . const
|
||||
similarTime t1 t2 = abs (t2 / t1 - 1) < 0.2 -- normally the difference between "no queue" and "wrong key" is less than 5%
|
||||
similarTime t1 t2 = abs (t2 / t1 - 1) < 0.25 -- normally the difference between "no queue" and "wrong key" is less than 5%
|
||||
testSameTiming :: forall c. Transport c => THandleSMP c 'TClient -> THandleSMP c 'TClient -> (C.AuthAlg, C.AuthAlg, Int) -> Expectation
|
||||
testSameTiming rh sh (C.AuthAlg goodKeyAlg, C.AuthAlg badKeyAlg, n) = do
|
||||
g <- C.newRandom
|
||||
(rPub, rKey) <- atomically $ C.generateAuthKeyPair goodKeyAlg g
|
||||
(dhPub, dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
|
||||
Resp "abcd" "" (Ids rId sId srvDh) <- signSendRecv rh rKey ("abcd", "", NEW rPub dhPub Nothing SMSubscribe False)
|
||||
Resp "abcd" NoEntity (Ids rId sId srvDh) <- signSendRecv rh rKey ("abcd", NoEntity, NEW rPub dhPub Nothing SMSubscribe False)
|
||||
let dec = decryptMsgV3 $ C.dh' srvDh dhPriv
|
||||
Resp "cdab" _ OK <- signSendRecv rh rKey ("cdab", rId, SUB)
|
||||
|
||||
@@ -791,12 +798,12 @@ testTiming (ATransport t) =
|
||||
|
||||
runTimingTest sh badKey sId $ _SEND "hello"
|
||||
where
|
||||
runTimingTest :: PartyI p => THandleSMP c 'TClient -> C.APrivateAuthKey -> ByteString -> Command p -> IO ()
|
||||
runTimingTest :: PartyI p => THandleSMP c 'TClient -> C.APrivateAuthKey -> EntityId -> Command p -> IO ()
|
||||
runTimingTest h badKey qId cmd = do
|
||||
threadDelay 100000
|
||||
_ <- timeRepeat n $ do
|
||||
-- "warm up" the server
|
||||
Resp "dabc" _ (ERR AUTH) <- signSendRecv h badKey ("dabc", "1234", cmd)
|
||||
Resp "dabc" _ (ERR AUTH) <- signSendRecv h badKey ("dabc", EntityId "1234", cmd)
|
||||
return ()
|
||||
threadDelay 100000
|
||||
timeWrongKey <- timeRepeat n $ do
|
||||
@@ -804,7 +811,7 @@ testTiming (ATransport t) =
|
||||
return ()
|
||||
threadDelay 100000
|
||||
timeNoQueue <- timeRepeat n $ do
|
||||
Resp "dabc" _ (ERR AUTH) <- signSendRecv h badKey ("dabc", "1234", cmd)
|
||||
Resp "dabc" _ (ERR AUTH) <- signSendRecv h badKey ("dabc", EntityId "1234", cmd)
|
||||
return ()
|
||||
let ok = similarTime timeNoQueue timeWrongKey
|
||||
unless ok . putStrLn . unwords $
|
||||
@@ -836,7 +843,8 @@ testMessageNotifications (ATransport t) =
|
||||
Resp "3a" _ OK <- signSendRecv rh rKey ("3a", rId, ACK mId1)
|
||||
Resp "" _ (NMSG _ _) <- tGet1 nh1
|
||||
Resp "4" _ OK <- signSendRecv nh2 nKey ("4", nId, NSUB)
|
||||
Resp "" _ END <- tGet1 nh1
|
||||
Resp "" nId2 END <- tGet1 nh1
|
||||
nId2 `shouldBe` nId
|
||||
Resp "5" _ OK <- signSendRecv sh sKey ("5", sId, _SEND' "hello again")
|
||||
Resp "" _ (Msg mId2 msg2) <- tGet1 rh
|
||||
Resp "5a" _ OK <- signSendRecv rh rKey ("5a", rId, ACK mId2)
|
||||
@@ -846,6 +854,8 @@ testMessageNotifications (ATransport t) =
|
||||
Nothing -> pure ()
|
||||
Just _ -> error "nothing else should be delivered to the 1st notifier's TCP connection"
|
||||
Resp "6" _ OK <- signSendRecv rh rKey ("6", rId, NDEL)
|
||||
Resp "" nId3 DELD <- tGet1 nh2
|
||||
nId3 `shouldBe` nId
|
||||
Resp "7" _ OK <- signSendRecv sh sKey ("7", sId, _SEND' "hello there")
|
||||
Resp "" _ (Msg mId3 msg3) <- tGet1 rh
|
||||
(dec mId3 msg3, Right "hello there") #== "delivered from queue again"
|
||||
@@ -884,7 +894,7 @@ testMsgExpireOnInterval t =
|
||||
testSMPClient @c $ \sh -> do
|
||||
(sId, rId, rKey, _) <- testSMPClient @c $ \rh -> createAndSecureQueue rh sPub
|
||||
Resp "1" _ OK <- signSendRecv sh sKey ("1", sId, _SEND "hello (should expire)")
|
||||
threadDelay 2500000
|
||||
threadDelay 3000000
|
||||
testSMPClient @c $ \rh -> do
|
||||
signSendRecv rh rKey ("2", rId, SUB) >>= \case
|
||||
Resp "2" _ OK -> pure ()
|
||||
|
||||
+2
-2
@@ -429,7 +429,7 @@ testXFTPAgentSendRestore = withGlobalLogging logCfgNoLogs $ do
|
||||
("", sfId', SFPROG _ _) <- sfGet sndr'
|
||||
liftIO $ sfId' `shouldBe` sfId
|
||||
|
||||
threadDelay 100000
|
||||
threadDelay 200000
|
||||
|
||||
withXFTPServerStoreLogOn $ \_ -> do
|
||||
-- send file - should continue uploading with server up
|
||||
@@ -443,7 +443,7 @@ testXFTPAgentSendRestore = withGlobalLogging logCfgNoLogs $ do
|
||||
pure rfd1
|
||||
|
||||
-- prefix path should be removed after sending file
|
||||
threadDelay 100000
|
||||
threadDelay 200000
|
||||
doesDirectoryExist prefixPath `shouldReturn` False
|
||||
doesFileExist encPath `shouldReturn` False
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module XFTPServerTests where
|
||||
@@ -20,13 +21,13 @@ import Data.List (isInfixOf)
|
||||
import ServerTests (logSize)
|
||||
import Simplex.FileTransfer.Client
|
||||
import Simplex.FileTransfer.Description (kb)
|
||||
import Simplex.FileTransfer.Protocol (FileInfo (..))
|
||||
import Simplex.FileTransfer.Protocol (FileInfo (..), XFTPFileId)
|
||||
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..))
|
||||
import Simplex.FileTransfer.Transport (XFTPErrorType (..), XFTPRcvChunkSpec (..))
|
||||
import Simplex.Messaging.Client (ProtocolClientError (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||
import Simplex.Messaging.Protocol (BasicAuth, SenderId)
|
||||
import Simplex.Messaging.Protocol (BasicAuth, EntityId (..), pattern NoEntity)
|
||||
import Simplex.Messaging.Server.Expiration (ExpirationConfig (..))
|
||||
import System.Directory (createDirectoryIfMissing, removeDirectoryRecursive, removeFile)
|
||||
import System.FilePath ((</>))
|
||||
@@ -74,8 +75,8 @@ createTestChunk fp = do
|
||||
B.writeFile fp bytes
|
||||
pure bytes
|
||||
|
||||
readChunk :: SenderId -> IO ByteString
|
||||
readChunk sId = B.readFile (xftpServerFiles </> B.unpack (B64.encode sId))
|
||||
readChunk :: XFTPFileId -> IO ByteString
|
||||
readChunk sId = B.readFile (xftpServerFiles </> B.unpack (B64.encode $ unEntityId sId))
|
||||
|
||||
testFileChunkDelivery :: Expectation
|
||||
testFileChunkDelivery = xftpTest $ \c -> runRight_ $ runTestFileChunkDelivery c c
|
||||
@@ -267,9 +268,9 @@ testFileLog = do
|
||||
(rcvKey1, rpKey1) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
||||
(rcvKey2, rpKey2) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
||||
digest <- liftIO $ LC.sha256Hash <$> LB.readFile testChunkPath
|
||||
sIdVar <- newTVarIO ""
|
||||
rIdVar1 <- newTVarIO ""
|
||||
rIdVar2 <- newTVarIO ""
|
||||
sIdVar <- newTVarIO NoEntity
|
||||
rIdVar1 <- newTVarIO NoEntity
|
||||
rIdVar2 <- newTVarIO NoEntity
|
||||
|
||||
threadDelay 100000
|
||||
|
||||
|
||||
Reference in New Issue
Block a user