mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-08-30 14:08:22 +00:00
Compare commits
165
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09348c46f7 | ||
|
|
5a7e4e0da3 | ||
|
|
dba10c9b37 | ||
|
|
a83e65891d | ||
|
|
ae4f890e4b | ||
|
|
ac7ad150af | ||
|
|
758eefc547 | ||
|
|
bace6a99c5 | ||
|
|
a010e1e836 | ||
|
|
9c157a943c | ||
|
|
32da2f7482 | ||
|
|
beb204cc2b | ||
|
|
18337be331 | ||
|
|
9861df3e3d | ||
|
|
0c962f2df6 | ||
|
|
01374d5a66 | ||
|
|
67f5f2ea8f | ||
|
|
43ef908309 | ||
|
|
d352d518c2 | ||
|
|
f0dc600016 | ||
|
|
f44ea0a6d8 | ||
|
|
f7d31d4c02 | ||
|
|
b90e25a3a5 | ||
|
|
cf4b9f669d | ||
|
|
e417d35cce | ||
|
|
deaec3cce2 | ||
|
|
7bbd99644a | ||
|
|
cb59a449dd | ||
|
|
a632eea75b | ||
|
|
3d10c9bf9e | ||
|
|
2f0cdc40af | ||
|
|
0a3d014f5d | ||
|
|
7d0115daec | ||
|
|
f024ab1c3f | ||
|
|
f4bc1f0926 | ||
|
|
42dbb887f7 | ||
|
|
850d2fa423 | ||
|
|
08b84deba4 | ||
|
|
ec5a60430d | ||
|
|
e4d4b51c59 | ||
|
|
6b60f8bab6 | ||
|
|
aa9b93eee5 | ||
|
|
afb338a41a | ||
|
|
1e29f7c811 | ||
|
|
d11be15295 | ||
|
|
305f79d2a6 | ||
|
|
af988f774e | ||
|
|
82e389298a | ||
|
|
c784d5ce0c | ||
|
|
c2c4730953 | ||
|
|
ccdd8e1775 | ||
|
|
307a784174 | ||
|
|
9abc0fa88d | ||
|
|
76aad61f00 | ||
|
|
37ce109009 | ||
|
|
a66163dc46 | ||
|
|
bdf8bf093c | ||
|
|
b7a9542213 | ||
|
|
3a3f9fd51e | ||
|
|
6dc9d76ed3 | ||
|
|
a2a4b80af4 | ||
|
|
7ec0ae3bb5 | ||
|
|
2c5530c9f0 | ||
|
|
94ee3ceced | ||
|
|
dd67de4d71 | ||
|
|
285fd93c32 | ||
|
|
56bec06856 | ||
|
|
04cbed90fb | ||
|
|
c1a6647f19 | ||
|
|
1dd677eec2 | ||
|
|
7636bc7491 | ||
|
|
79adb83782 | ||
|
|
b83d897650 | ||
|
|
0c3b25706a | ||
|
|
c3f57beafd | ||
|
|
aace3fd2fb | ||
|
|
2e67ed9c4c | ||
|
|
614fa2b163 | ||
|
|
903e96bdfa | ||
|
|
5c0adcbbff | ||
|
|
b2f16eeff4 | ||
|
|
6db79808aa | ||
|
|
f4b55bfc0c | ||
|
|
fe64d42db1 | ||
|
|
1b5a9f3b0c | ||
|
|
fdf8bd7ee2 | ||
|
|
019a32a623 | ||
|
|
d44f09d111 | ||
|
|
6b5de2c51b | ||
|
|
9410fb6f16 | ||
|
|
7b42aaa132 | ||
|
|
e4b9aa9746 | ||
|
|
aa26a55df4 | ||
|
|
6e505f5c0b | ||
|
|
a491a1d878 | ||
|
|
36f5539b9a | ||
|
|
1a2afe8bfd | ||
|
|
9fece9ce3d | ||
|
|
2e2ede5968 | ||
|
|
205d4ead1c | ||
|
|
80a070a8ea | ||
|
|
172540984c | ||
|
|
4dc40bd795 | ||
|
|
f9d7b1eebc | ||
|
|
ffbc733d58 | ||
|
|
2286726d72 | ||
|
|
1b8110a332 | ||
|
|
dad7e1b60c | ||
|
|
72c2ddcf57 | ||
|
|
a75e138965 | ||
|
|
fa319d798a | ||
|
|
c192339af9 | ||
|
|
fa67d128d1 | ||
|
|
55ff581655 | ||
|
|
560b257af7 | ||
|
|
7ac80bffcb | ||
|
|
0d8a1a2879 | ||
|
|
bd97cb0449 | ||
|
|
a58d3540ad | ||
|
|
5dbe6337ea | ||
|
|
b633f89c1a | ||
|
|
944a22a2fb | ||
|
|
ce24f83b64 | ||
|
|
45373e7f1f | ||
|
|
efe71cd598 | ||
|
|
ae41717b9b | ||
|
|
e78ab60c97 | ||
|
|
b3c8358a43 | ||
|
|
be81fe1f74 | ||
|
|
817f5e1737 | ||
|
|
eda9e36c82 | ||
|
|
2318975375 | ||
|
|
298666380f | ||
|
|
6a9075141f | ||
|
|
488c7082f3 | ||
|
|
fdde9863cd | ||
|
|
9404a3ab63 | ||
|
|
dadf6ec5b6 | ||
|
|
c528ea4f31 | ||
|
|
3d4e0b06c0 | ||
|
|
9d9ec8cd0b | ||
|
|
992b42e922 | ||
|
|
3cf9dacbc0 | ||
|
|
cfde5932fd | ||
|
|
69fb9a9e0e | ||
|
|
0a118f5631 | ||
|
|
0b986c1158 | ||
|
|
184a95cd2a | ||
|
|
b7a8df40c4 | ||
|
|
bf28902327 | ||
|
|
426bf68763 | ||
|
|
e557dc86e7 | ||
|
|
f5cef2556b | ||
|
|
cf66aadc20 | ||
|
|
77a5ed2ec6 | ||
|
|
4d640c16aa | ||
|
|
0a827307d6 | ||
|
|
64149599de | ||
|
|
07be469f49 | ||
|
|
79e9447b73 | ||
|
|
9893935e7c | ||
|
|
966b9990e0 | ||
|
|
bef1e38295 | ||
|
|
4b43cb8054 | ||
|
|
38ad3c046e |
@@ -0,0 +1,44 @@
|
||||
name: 'Set Swap Space'
|
||||
description: 'Add moar swap'
|
||||
branding:
|
||||
icon: 'crop'
|
||||
color: 'orange'
|
||||
inputs:
|
||||
swap-size-gb:
|
||||
description: 'Swap space to create, in Gigabytes.'
|
||||
required: false
|
||||
default: '10'
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Swap space report before modification
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Memory and swap:"
|
||||
free -h
|
||||
echo
|
||||
swapon --show
|
||||
echo
|
||||
- name: Set Swap
|
||||
shell: bash
|
||||
run: |
|
||||
export SWAP_FILE=$(swapon --show=NAME | tail -n 1)
|
||||
echo "Swap file: $SWAP_FILE"
|
||||
if [ -z "$SWAP_FILE" ]; then
|
||||
SWAP_FILE=/opt/swapfile
|
||||
else
|
||||
sudo swapoff $SWAP_FILE
|
||||
sudo rm $SWAP_FILE
|
||||
fi
|
||||
sudo fallocate -l ${{ inputs.swap-size-gb }}G $SWAP_FILE
|
||||
sudo chmod 600 $SWAP_FILE
|
||||
sudo mkswap $SWAP_FILE
|
||||
sudo swapon $SWAP_FILE
|
||||
- name: Swap space report after modification
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Memory and swap:"
|
||||
free -h
|
||||
echo
|
||||
swapon --show
|
||||
echo
|
||||
+231
-56
@@ -10,62 +10,25 @@ on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build-${{ matrix.os }}-${{ matrix.ghc }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-20.04
|
||||
platform_name: 20_04-x86-64
|
||||
ghc: "8.10.7"
|
||||
- os: ubuntu-20.04
|
||||
platform_name: 20_04-x86-64
|
||||
ghc: "9.6.3"
|
||||
- os: ubuntu-22.04
|
||||
platform_name: 22_04-x86-64
|
||||
ghc: "9.6.3"
|
||||
|
||||
# =============================
|
||||
# Create release
|
||||
# =============================
|
||||
|
||||
# Create release, but only if it's triggered by tag push.
|
||||
# On pull requests/commits push, this job will always complete.
|
||||
|
||||
maybe-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone project
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Haskell
|
||||
uses: haskell-actions/setup@v2
|
||||
with:
|
||||
ghc-version: ${{ matrix.ghc }}
|
||||
cabal-version: "3.10.1.0"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cabal/store
|
||||
dist-newstyle
|
||||
key: ${{ matrix.os }}-${{ hashFiles('cabal.project', 'simplexmq.cabal') }}
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: cabal build --enable-tests
|
||||
|
||||
- name: Test
|
||||
timeout-minutes: 40
|
||||
shell: bash
|
||||
run: cabal test --test-show-details=direct
|
||||
|
||||
- name: Prepare binaries
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
shell: bash
|
||||
run: |
|
||||
mv $(cabal list-bin smp-server) smp-server-ubuntu-${{ matrix.platform_name}}
|
||||
mv $(cabal list-bin ntf-server) ntf-server-ubuntu-${{ matrix.platform_name}}
|
||||
mv $(cabal list-bin xftp-server) xftp-server-ubuntu-${{ matrix.platform_name}}
|
||||
mv $(cabal list-bin xftp) xftp-ubuntu-${{ matrix.platform_name}}
|
||||
|
||||
- name: Build changelog
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
id: build_changelog
|
||||
uses: mikepenz/release-changelog-builder-action@v1
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: simplex-chat/release-changelog-builder-action@v5
|
||||
with:
|
||||
configuration: .github/changelog_conf.json
|
||||
failOnError: true
|
||||
@@ -75,8 +38,8 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create release
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.ghc != '8.10.7'
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: simplex-chat/action-gh-release@v2
|
||||
with:
|
||||
body: |
|
||||
See full changelog [here](https://github.com/simplex-chat/simplexmq/blob/master/CHANGELOG.md).
|
||||
@@ -86,10 +49,222 @@ jobs:
|
||||
prerelease: true
|
||||
files: |
|
||||
LICENSE
|
||||
smp-server-ubuntu-${{ matrix.platform_name}}
|
||||
ntf-server-ubuntu-${{ matrix.platform_name}}
|
||||
xftp-server-ubuntu-${{ matrix.platform_name}}
|
||||
xftp-ubuntu-${{ matrix.platform_name}}
|
||||
fail_on_unmatched_files: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# =============================
|
||||
# Main build job
|
||||
# =============================
|
||||
|
||||
build:
|
||||
name: "ubuntu-${{ matrix.os }}, GHC: ${{ matrix.ghc }}"
|
||||
needs: maybe-release
|
||||
env:
|
||||
apps: "smp-server xftp-server ntf-server xftp"
|
||||
runs-on: ubuntu-${{ matrix.os }}
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
env:
|
||||
POSTGRES_HOST_AUTH_METHOD: trust # Allows passwordless access
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
# Maps tcp port 5432 on service container to the host
|
||||
- 5432:5432
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 22.04
|
||||
ghc: "8.10.7"
|
||||
platform_name: 22_04-8.10.7
|
||||
should_run: ${{ !(github.ref == 'refs/heads/stable' || startsWith(github.ref, 'refs/tags/v')) }}
|
||||
- os: 22.04
|
||||
ghc: "9.6.3"
|
||||
platform_name: 22_04-x86-64
|
||||
should_run: true
|
||||
- os: 24.04
|
||||
ghc: "9.6.3"
|
||||
platform_name: 24_04-x86-64
|
||||
should_run: true
|
||||
steps:
|
||||
- name: Clone project
|
||||
if: matrix.should_run == true
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: matrix.should_run == true
|
||||
uses: simplex-chat/docker-setup-buildx-action@v3
|
||||
|
||||
- name: Setup swap
|
||||
if: matrix.ghc == '8.10.7' && matrix.should_run == true
|
||||
uses: ./.github/actions/swap
|
||||
with:
|
||||
swap-size-gb: 20
|
||||
|
||||
- name: Install PostgreSQL 15 client tools
|
||||
if: matrix.os == '22.04' && matrix.should_run == true
|
||||
shell: bash
|
||||
run: |
|
||||
# Import the repository signing key
|
||||
sudo install -d /usr/share/postgresql-common/pgdg
|
||||
sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc
|
||||
# Add the PostgreSQL APT repository
|
||||
sudo sh -c 'echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
||||
# Update repository and install postgresql tools
|
||||
sudo apt update
|
||||
sudo apt -y install postgresql-client-15
|
||||
|
||||
- name: Build and cache Docker image
|
||||
if: matrix.should_run == true
|
||||
uses: simplex-chat/docker-build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
load: true
|
||||
file: Dockerfile.build
|
||||
tags: build/${{ matrix.platform_name }}:latest
|
||||
cache-from: |
|
||||
type=gha
|
||||
type=gha,scope=master
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
TAG=${{ matrix.os }}
|
||||
GHC=${{ matrix.ghc }}
|
||||
|
||||
- name: Cache dependencies
|
||||
if: matrix.should_run == true
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cabal/store
|
||||
dist-newstyle
|
||||
key: ${{ matrix.os }}-${{ hashFiles('cabal.project', 'simplexmq.cabal') }}
|
||||
|
||||
- name: Start container
|
||||
if: matrix.should_run == true
|
||||
shell: bash
|
||||
run: |
|
||||
docker run -t -d \
|
||||
--name builder \
|
||||
-v ~/.cabal:/root/.cabal \
|
||||
-v /home/runner/work/_temp:/home/runner/work/_temp \
|
||||
-v ${{ github.workspace }}:/project \
|
||||
build/${{ matrix.platform_name }}:latest
|
||||
|
||||
- name: Build smp-server (postgresql) and tests
|
||||
if: matrix.should_run == true
|
||||
shell: docker exec -t builder sh -eu {0}
|
||||
run: |
|
||||
cabal update
|
||||
cabal build --jobs=$(nproc) --enable-tests -fserver_postgres
|
||||
mkdir -p /out
|
||||
for i in smp-server simplexmq-test; do
|
||||
bin=$(find /project/dist-newstyle -name "$i" -type f -executable)
|
||||
chmod +x "$bin"
|
||||
mv "$bin" /out/
|
||||
done
|
||||
strip /out/smp-server
|
||||
|
||||
- name: Copy simplexmq-test from container
|
||||
if: matrix.should_run == true
|
||||
shell: bash
|
||||
run: |
|
||||
docker cp builder:/out/simplexmq-test .
|
||||
|
||||
- name: Copy smp-server (postgresql) from container and prepare it
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true
|
||||
id: prepare-postgres
|
||||
shell: bash
|
||||
run: |
|
||||
name="smp-server-postgres-ubuntu-${{ matrix.platform_name }}"
|
||||
docker cp builder:/out/smp-server $name
|
||||
|
||||
path="${{ github.workspace }}/$name"
|
||||
echo "bin=$path" >> $GITHUB_OUTPUT
|
||||
|
||||
hash="SHA2-256($name)= $(openssl sha256 $path | cut -d' ' -f 2)"
|
||||
printf 'hash=%s' "$hash" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build everything else (standard)
|
||||
if: matrix.should_run == true
|
||||
shell: docker exec -t builder sh -eu {0}
|
||||
run: |
|
||||
cabal build --jobs=$(nproc)
|
||||
mkdir -p /out
|
||||
for i in ${{ env.apps }}; do
|
||||
bin=$(find /project/dist-newstyle -name "$i" -type f -executable)
|
||||
strip "$bin"
|
||||
chmod +x "$bin"
|
||||
mv "$bin" /out/
|
||||
done
|
||||
|
||||
- name: Copy binaries from container and prepare them
|
||||
id: prepare-regular
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true
|
||||
shell: bash
|
||||
run: |
|
||||
docker cp builder:/out .
|
||||
|
||||
printf 'bins<<EOF\n' > bins.output
|
||||
printf 'hashes<<EOF\n' > hashes.output
|
||||
for i in ${{ env.apps }}; do
|
||||
mv ./out/$i ./$i-ubuntu-${{ matrix.platform_name }}
|
||||
|
||||
name="$i-ubuntu-${{ matrix.platform_name }}"
|
||||
|
||||
path="${{ github.workspace }}/$name"
|
||||
hash="SHA2-256($name)= $(openssl sha256 $path | cut -d' ' -f 2)"
|
||||
|
||||
printf '%s\n' "$path" >> bins.output
|
||||
printf '%s\n\n' "$hash" >> hashes.output
|
||||
done
|
||||
printf 'EOF\n' >> bins.output
|
||||
printf 'EOF\n' >> hashes.output
|
||||
|
||||
cat bins.output >> "$GITHUB_OUTPUT"
|
||||
cat hashes.output >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload binaries
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true
|
||||
uses: simplex-chat/action-gh-release@v2
|
||||
with:
|
||||
append_body: true
|
||||
prerelease: true
|
||||
fail_on_unmatched_files: true
|
||||
body: |
|
||||
${{ steps.prepare-regular.outputs.hashes }}
|
||||
${{ steps.prepare-postgres.outputs.hash }}
|
||||
files: |
|
||||
${{ steps.prepare-regular.outputs.bins }}
|
||||
${{ steps.prepare-postgres.outputs.bin }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Test
|
||||
if: matrix.should_run == true
|
||||
timeout-minutes: 120
|
||||
shell: bash
|
||||
env:
|
||||
PGHOST: localhost
|
||||
run: |
|
||||
i=1
|
||||
attempts=1
|
||||
${{ (github.ref == 'refs/heads/stable' || startsWith(github.ref, 'refs/tags/v')) }} && attempts=3
|
||||
while [ "$i" -le "$attempts" ]; do
|
||||
if ./simplexmq-test; then
|
||||
break
|
||||
else
|
||||
echo "Attempt $i failed, retrying..."
|
||||
i=$((i + 1))
|
||||
sleep 1
|
||||
fi
|
||||
done
|
||||
if [ "$i" -gt "$attempts" ]; then
|
||||
echo "All "$attempts" attempts failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -14,22 +14,22 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- app: smp-server
|
||||
app_port: 5223
|
||||
app_port: "443 5223"
|
||||
- app: xftp-server
|
||||
app_port: 443
|
||||
app_port: 443
|
||||
steps:
|
||||
- name: Clone project
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: simplex-chat/docker-login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Extract metadata for Docker image
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: simplex-chat/docker-metadata-action@v5
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.app }}
|
||||
flavor: |
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
type=semver,pattern=v{{major}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
uses: simplex-chat/docker-build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
build-args: |
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
name: Reproduce latest release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # every day at 02:00 night
|
||||
|
||||
jobs:
|
||||
reproduce:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get latest release
|
||||
shell: bash
|
||||
run: |
|
||||
curl --proto '=https' \
|
||||
--tlsv1.2 \
|
||||
-sSf -L \
|
||||
'https://api.github.com/repos/simplex-chat/simplexmq/releases/latest' \
|
||||
2>/dev/null | \
|
||||
grep -i "tag_name" | \
|
||||
awk -F \" '{print "TAG="$4}' >> $GITHUB_ENV
|
||||
|
||||
- name: Execute reproduce script
|
||||
run: |
|
||||
${GITHUB_WORKSPACE}/scripts/reproduce-builds.sh "$TAG"
|
||||
|
||||
- name: Check if build has been reproduced
|
||||
env:
|
||||
url: ${{ secrets.STATUS_SIMPLEX_WEBHOOK_URL }}
|
||||
user: ${{ secrets.STATUS_SIMPLEX_WEBHOOK_USER }}
|
||||
pass: ${{ secrets.STATUS_SIMPLEX_WEBHOOK_PASS }}
|
||||
run: |
|
||||
if [ -f "${GITHUB_WORKSPACE}/$TAG/_sha256sums" ]; then
|
||||
exit 0
|
||||
else
|
||||
curl --proto '=https' --tlsv1.2 -sSf \
|
||||
-u "${user}:${pass}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"title": "👾 GitHub: Runner", "description": "⛔️ '"$TAG"' did not reproduce."}' \
|
||||
"$url"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,3 +1,69 @@
|
||||
# 6.3.2
|
||||
|
||||
Servers:
|
||||
- enable store log by default (#1501).
|
||||
|
||||
SMP server:
|
||||
- reduce memory usage (#1498)
|
||||
|
||||
SMP agent:
|
||||
- handle client/agent version downgrades after connection was established (#1508).
|
||||
|
||||
# 6.3.1
|
||||
|
||||
Servers:
|
||||
- handle ECONNABORTED error on client connections.
|
||||
- reproducible builds.
|
||||
- blocking records for content moderation.
|
||||
- update script (simplex-servers-update) downloads scripts from the specified or the latest stable tag.
|
||||
|
||||
SMP server:
|
||||
- support for PostrgreSQL database for queue records for higher traffic servers.
|
||||
- fix old clients sending messages to new servers (#1443)
|
||||
- remove empty journals when opening message queues and expiring idle queues (#1456, #1458).
|
||||
- additional start options (#1465):
|
||||
- `maintenance` to run all start/stop operations without starting server.
|
||||
- `skip-warnings` to ignore the last corrupted line in store log (can happen on abnormal termination).
|
||||
|
||||
Ntf server:
|
||||
- record date of last token activity, to allow expiring inactive tokens.
|
||||
- additional token invalidation reasons in logs.
|
||||
|
||||
SMP agent:
|
||||
- store message sent to multiple connections only once, to reduce storage when sending to groups (#1453).
|
||||
- encrypt messages on delivery, to reduce database writes (#1446).
|
||||
- don't block method calls on congested sockets for better concurrency (#1454).
|
||||
- check notification token status on client connection.
|
||||
- option to skip SQLite vacuum on migrations.
|
||||
|
||||
# 6.3.0
|
||||
|
||||
SMP agent: fix joining connection after failure by using the same ratchet.
|
||||
|
||||
# 6.2.2
|
||||
|
||||
SMP server:
|
||||
- add optional Prometheus metrics (#1411).
|
||||
|
||||
Build:
|
||||
- remove three modules from client library.
|
||||
|
||||
# 6.2.0
|
||||
|
||||
Version 6.2.0.7
|
||||
|
||||
Build:
|
||||
- client_library flag to build only used modules in the clients, remove package yaml
|
||||
|
||||
SMP server:
|
||||
- journal storage for messages (BETA).
|
||||
- prevent race condition when deleting queue and to avoid "orphan" messages (#1395).
|
||||
|
||||
SMP agent:
|
||||
- support SMP and XFTP server roles (storage/proxy) and operators (#1343).
|
||||
- treat blocked STM and other critical errors that offer restart as temporary for message delivery (#1405).
|
||||
- fix inconsistent state after app restart while accepting contact request (#1412).
|
||||
|
||||
# 6.1.3
|
||||
|
||||
SMP server: fix restoring notification credentials.
|
||||
|
||||
+31
-8
@@ -1,15 +1,20 @@
|
||||
ARG TAG=22.04
|
||||
# syntax=docker/dockerfile:1.7.0-labs
|
||||
ARG TAG=24.04
|
||||
|
||||
FROM ubuntu:${TAG} AS build
|
||||
|
||||
### Build stage
|
||||
|
||||
# Install curl and git and simplexmq dependencies
|
||||
RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev llvm-12 llvm-12-dev libnuma-dev libssl-dev
|
||||
RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev llvm-18 llvm-18-dev libnuma-dev libssl-dev
|
||||
|
||||
# Specify bootstrap Haskell versions
|
||||
ENV BOOTSTRAP_HASKELL_GHC_VERSION=9.6.3
|
||||
ENV BOOTSTRAP_HASKELL_CABAL_VERSION=3.10.1.0
|
||||
ENV BOOTSTRAP_HASKELL_CABAL_VERSION=3.12.1.0
|
||||
|
||||
# Do not install Stack
|
||||
ENV BOOTSTRAP_HASKELL_INSTALL_NO_STACK=true
|
||||
ENV BOOTSTRAP_HASKELL_INSTALL_NO_STACK_HOOK=true
|
||||
|
||||
# Install ghcup
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 sh
|
||||
@@ -21,26 +26,42 @@ ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH"
|
||||
RUN ghcup set ghc "${BOOTSTRAP_HASKELL_GHC_VERSION}" && \
|
||||
ghcup set cabal "${BOOTSTRAP_HASKELL_CABAL_VERSION}"
|
||||
|
||||
COPY . /project
|
||||
# Copy only the source code
|
||||
COPY apps /project/apps/
|
||||
COPY cbits /project/cbits/
|
||||
COPY src /project/src/
|
||||
|
||||
COPY cabal.project Setup.hs simplexmq.cabal LICENSE /project
|
||||
|
||||
WORKDIR /project
|
||||
|
||||
# Debug
|
||||
#ARG CACHEBUST=1
|
||||
|
||||
#ADD --chmod=755 https://github.com/MShekow/directory-checksum/releases/download/v1.4.6/directory-checksum_1.4.6_linux_amd64 /usr/local/bin/directory-checksum
|
||||
#RUN directory-checksum --max-depth 2 .
|
||||
|
||||
# Set build arguments and check if they exist
|
||||
ARG APP
|
||||
ARG APP_PORT
|
||||
RUN if [ -z "$APP" ] || [ -z "$APP_PORT" ]; then printf "Please spcify \$APP and \$APP_PORT build-arg.\n"; exit 1; fi
|
||||
RUN if [ -z "$APP" ]; then printf "Please spcify \$APP build-arg.\n"; exit 1; fi
|
||||
|
||||
# Compile app
|
||||
RUN cabal update
|
||||
RUN cabal build exe:$APP
|
||||
|
||||
# Copy scripts
|
||||
COPY scripts /project/scripts/
|
||||
|
||||
# Create new path containing all files needed
|
||||
RUN mkdir /final
|
||||
WORKDIR /final
|
||||
|
||||
# Strip the binary from debug symbols to reduce size
|
||||
RUN bin=$(find /project/dist-newstyle -name "$APP" -type f -executable) && \
|
||||
RUN bin="$(find /project/dist-newstyle -name "$APP" -type f -executable)" && \
|
||||
mv "$bin" ./ && \
|
||||
strip ./"$APP" &&\
|
||||
mv /project/scripts/docker/entrypoint-"$APP" ./entrypoint
|
||||
mv /project/scripts/docker/entrypoint-"$APP" ./entrypoint &&\
|
||||
mv /project/scripts/main/simplex-servers-stopscript ./simplex-servers-stopscript
|
||||
|
||||
### Final stage
|
||||
FROM ubuntu:${TAG}
|
||||
@@ -53,6 +74,8 @@ COPY --from=build /final /usr/local/bin/
|
||||
|
||||
# Open app listening port
|
||||
ARG APP_PORT
|
||||
RUN if [ -z "$APP_PORT" ]; then printf "Please spcify \$APP_PORT build-arg.\n"; exit 1; fi
|
||||
|
||||
EXPOSE $APP_PORT
|
||||
|
||||
# simplexmq requires using SIGINT to correctly preserve undelivered messages and restore them on restart
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# syntax=docker/dockerfile:1.7.0-labs
|
||||
ARG TAG=24.04
|
||||
FROM ubuntu:${TAG} AS build
|
||||
|
||||
### Build stage
|
||||
|
||||
ARG GHC=9.6.3
|
||||
ARG CABAL=3.14.1.1
|
||||
|
||||
# Install curl, git and and simplexmq dependencies
|
||||
RUN apt-get update && apt-get install -y curl libpq-dev git sqlite3 libsqlite3-dev build-essential libgmp3-dev zlib1g-dev llvm llvm-dev libnuma-dev libssl-dev
|
||||
|
||||
# Specify bootstrap Haskell versions
|
||||
ENV BOOTSTRAP_HASKELL_GHC_VERSION=${GHC}
|
||||
ENV BOOTSTRAP_HASKELL_CABAL_VERSION=${CABAL}
|
||||
|
||||
# Do not install Stack
|
||||
ENV BOOTSTRAP_HASKELL_INSTALL_NO_STACK=true
|
||||
ENV BOOTSTRAP_HASKELL_INSTALL_NO_STACK_HOOK=true
|
||||
|
||||
# Install ghcup
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 sh
|
||||
|
||||
# Adjust PATH
|
||||
ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH"
|
||||
|
||||
# Set both as default
|
||||
RUN ghcup set ghc "${GHC}" && \
|
||||
ghcup set cabal "${CABAL}"
|
||||
|
||||
WORKDIR /project
|
||||
@@ -15,7 +15,6 @@ logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
setLogLevel LogInfo
|
||||
cfgPath <- getEnvPath "NTF_SERVER_CFG_PATH" defaultCfgPath
|
||||
logPath <- getEnvPath "NTF_SERVER_LOG_PATH" defaultLogPath
|
||||
withGlobalLogging logCfg $ ntfServerCLI cfgPath logPath
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"applinks": {
|
||||
"details": [
|
||||
{
|
||||
"appIDs": [
|
||||
"5NN7GUYB6T.chat.simplex.app"
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"/": "/contact/*"
|
||||
},
|
||||
{
|
||||
"/": "/contact"
|
||||
},
|
||||
{
|
||||
"/": "/invitation/*"
|
||||
},
|
||||
{
|
||||
"/": "/invitation"
|
||||
},
|
||||
{
|
||||
"/": "/a/*"
|
||||
},
|
||||
{
|
||||
"/": "/a"
|
||||
},
|
||||
{
|
||||
"/": "/c/*"
|
||||
},
|
||||
{
|
||||
"/": "/c"
|
||||
},
|
||||
{
|
||||
"/": "/g/*"
|
||||
},
|
||||
{
|
||||
"/": "/g"
|
||||
},
|
||||
{
|
||||
"/": "/i/*"
|
||||
},
|
||||
{
|
||||
"/": "/i"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"relation": [
|
||||
"delegate_permission/common.handle_all_urls"
|
||||
],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "chat.simplex.app",
|
||||
"sha256_cert_fingerprints": [
|
||||
"5E:3E:DC:C2:00:FB:A8:D5:F4:88:F3:CA:4C:32:5B:05:78:C5:6A:9C:03:A1:CC:B5:92:9C:D7:5C:7E:57:E2:4D",
|
||||
"3C:52:C4:FD:3C:AD:1C:07:C9:B0:0A:70:80:E3:58:FA:B9:FE:FC:B8:AF:5A:EC:14:77:65:F1:6D:0F:21:AD:85",
|
||||
"AE:C1:95:DC:FD:46:14:BD:3A:91:EC:26:D1:D5:14:C8:75:71:C5:CC:8D:CF:48:08:3F:92:83:14:3C:A2:B9:A6"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
+1
@@ -0,0 +1 @@
|
||||
../link.html
|
||||
+1
@@ -0,0 +1 @@
|
||||
../link.html
|
||||
+1
@@ -0,0 +1 @@
|
||||
../link.html
|
||||
@@ -142,8 +142,7 @@
|
||||
class="hidden xl:block h-screen pt-[66px] bg-white dark:bg-gradient-radial-mobile dark:lg:bg-gradient-radial">
|
||||
<div class="container m-auto h-full flex items-center justify-between px-5">
|
||||
<div class="flex flex-col items-start justify-center w-full">
|
||||
<h1 class="text-[38px] leading-[43px] font-bold max-w-[500px] mb-[30px] primary-header-contact">You received a
|
||||
1-time link to connect on SimpleX Chat</h1>
|
||||
<h1 class="text-[38px] leading-[43px] font-bold max-w-[500px] mb-[30px] primary-header-contact">This is a one-time link of the SimpleX network user</h1>
|
||||
<h2
|
||||
class="text-[20px] leading-[28px] text-[#606C71] dark:text-white font-bold max-w-[475px] mb-[80px] secondary-header-contact">
|
||||
Scan the QR code with the SimpleX Chat app on your phone or tablet.</h2>
|
||||
@@ -184,10 +183,8 @@
|
||||
class="block xl:hidden pt-[106px] py-[90px] bg-white dark:bg-gradient-radial-mobile dark:lg:bg-gradient-radial">
|
||||
<div class="container m-auto px-5">
|
||||
<div class="flex flex-col items-center">
|
||||
<h1 class="text-[28px] font-bold text-center max-w-[602px] mb-[40px] primary-header-contact">You received a
|
||||
1-time link to connect on SimpleX Chat</h1>
|
||||
<p class="text-[20px] leading-[28px] text-grey-black dark:text-white font-medium mb-[30px]">To make a
|
||||
connection:</p>
|
||||
<h1 class="text-[28px] font-bold text-center max-w-[602px] mb-[40px] primary-header-contact">This is a one-time link of the SimpleX network user</h1>
|
||||
<p class="text-[20px] leading-[28px] text-grey-black dark:text-white font-medium mb-[30px]">To make a connection:</p>
|
||||
<div
|
||||
class="flex flex-col justify-center items-center p-4 w-full max-w-[468px] min-h-[131px] rounded-[30px] border-[1px] border-[#A8B0B4] dark:border-white border-opacity-60 mb-6 relative">
|
||||
<p class="text-xl font-medium text-grey-black dark:text-white mb-4">Install SimpleX app</p>
|
||||
@@ -506,13 +503,15 @@
|
||||
const url = window.location.href
|
||||
const messageElements = document.getElementsByClassName('primary-header-contact')
|
||||
|
||||
if (url.includes('/invitation')) {
|
||||
for (let element of messageElements) {
|
||||
element.textContent = 'You received a 1-time link to connect on SimpleX Chat'
|
||||
}
|
||||
} else {
|
||||
for (let element of messageElements) {
|
||||
element.textContent = 'You received an address to connect on SimpleX Chat'
|
||||
for (let element of messageElements) {
|
||||
if (url.includes('/g') || url.includes('&data=%7B%22groupLinkId%22%3A')) {
|
||||
element.innerHTML = 'This is a public group address on SimpleX network'
|
||||
} else if (url.includes('/a') || url.includes('/contact')) {
|
||||
element.innerHTML = 'This is a public address of the SimpleX network user'
|
||||
} else if (url.includes('/i') || url.includes('/invitation')) {
|
||||
element.innerHTML = 'This is a one-time link of the SimpleX network user'
|
||||
} else if (url.includes('/c')) {
|
||||
element.innerHTML = 'This is a public channel address on SimpleX network'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -20,7 +20,18 @@
|
||||
parsedURI.pathname = "/" + action
|
||||
connURI = parsedURI.toString()
|
||||
console.log("connection URI: ", connURI)
|
||||
mobileConnURIanchor.href = "simplex:" + parsedURI.pathname + parsedURI.hash
|
||||
const hash = parsedURI.hash
|
||||
const hostname = parsedURI.hostname
|
||||
let appURI = "simplex:" + parsedURI.pathname
|
||||
appURI += action.length > 1 // not short link
|
||||
? hash
|
||||
: !hash.includes("?") // otherwise add server hostname
|
||||
? hash + "?h=" + hostname // no parameters
|
||||
: !hash.includes("?h=") && !hash.includes("&h=")
|
||||
? hash + "&h=" + hostname // no "h" parameter
|
||||
: hash.replace(/([?&])h=([^&]+)/, `$1h=${hostname},$2`) // add as the first hostname to "h" parameter
|
||||
mobileConnURIanchor.href = appURI
|
||||
console.log("app URI: ", appURI)
|
||||
connURIel.innerText = "/c " + connURI
|
||||
for (const connQRCode of connQRCodes) {
|
||||
try {
|
||||
|
||||
@@ -14,7 +14,8 @@ import Data.Maybe (fromMaybe)
|
||||
import Data.String (fromString)
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Network.Socket (getPeerName)
|
||||
import Network.Wai (Application)
|
||||
import Network.Wai (Application, Request (..))
|
||||
import Network.Wai.Application.Static (StaticSettings (..))
|
||||
import qualified Network.Wai.Application.Static as S
|
||||
import qualified Network.Wai.Handler.Warp as W
|
||||
import qualified Network.Wai.Handler.Warp.Internal as WI
|
||||
@@ -31,12 +32,13 @@ import System.Directory (createDirectoryIfMissing)
|
||||
import System.FilePath
|
||||
import UnliftIO.Concurrent (forkFinally)
|
||||
import UnliftIO.Exception (bracket, finally)
|
||||
import qualified WaiAppStatic.Types as WAT
|
||||
|
||||
serveStaticFiles :: EmbeddedWebParams -> IO ()
|
||||
serveStaticFiles EmbeddedWebParams {webStaticPath, webHttpPort, webHttpsParams} = do
|
||||
forM_ webHttpPort $ \port -> flip forkFinally (\e -> logError $ "HTTP server crashed: " <> tshow e) $ do
|
||||
logInfo $ "Serving static site on port " <> tshow port
|
||||
W.runSettings (mkSettings port) (S.staticApp $ S.defaultFileServerSettings webStaticPath)
|
||||
W.runSettings (mkSettings port) app
|
||||
forM_ webHttpsParams $ \WebHttpsParams {port, cert, key} -> flip forkFinally (\e -> logError $ "HTTPS server crashed: " <> tshow e) $ do
|
||||
logInfo $ "Serving static site on port " <> tshow port <> " (TLS)"
|
||||
WT.runTLS (WT.tlsSettings cert key) (mkSettings port) app
|
||||
@@ -72,23 +74,44 @@ warpSettings :: W.Settings
|
||||
warpSettings = W.setGracefulShutdownTimeout (Just 1) W.defaultSettings
|
||||
|
||||
staticFiles :: FilePath -> Application
|
||||
staticFiles root = S.staticApp settings
|
||||
staticFiles root = S.staticApp settings . changeWellKnownPath
|
||||
where
|
||||
settings = (S.defaultFileServerSettings root)
|
||||
{ S.ssListing = Nothing
|
||||
}
|
||||
settings = defSettings {ssListing = Nothing, ssGetMimeType = getMimeType}
|
||||
defSettings = S.defaultFileServerSettings root
|
||||
getMimeType f
|
||||
| WAT.fromPiece (WAT.fileName f) == "apple-app-site-association" = pure "application/json"
|
||||
| otherwise = (ssGetMimeType defSettings) f
|
||||
changeWellKnownPath req = case pathInfo req of
|
||||
".well-known" : rest ->
|
||||
req
|
||||
{ pathInfo = "well-known" : rest,
|
||||
rawPathInfo = "/well-known/" <> B.drop pfxLen (rawPathInfo req)
|
||||
}
|
||||
_ -> req
|
||||
pfxLen = B.length "/.well-known/"
|
||||
|
||||
generateSite :: ServerInformation -> Maybe TransportHost -> FilePath -> IO ()
|
||||
generateSite si onionHost sitePath = do
|
||||
createDirectoryIfMissing True sitePath
|
||||
B.writeFile (sitePath </> "index.html") $ serverInformation si onionHost
|
||||
createDirectoryIfMissing True $ sitePath </> "media"
|
||||
forM_ E.mediaContent $ \(path, bs) -> B.writeFile (sitePath </> "media" </> path) bs
|
||||
createDirectoryIfMissing True $ sitePath </> "contact"
|
||||
B.writeFile (sitePath </> "contact" </> "index.html") E.linkHtml
|
||||
createDirectoryIfMissing True $ sitePath </> "invitation"
|
||||
B.writeFile (sitePath </> "invitation" </> "index.html") E.linkHtml
|
||||
copyDir "media" E.mediaContent
|
||||
-- `.well-known` path is re-written in changeWellKnownPath,
|
||||
-- staticApp does not allow hidden folders.
|
||||
copyDir "well-known" E.wellKnown
|
||||
createLinkPage "contact"
|
||||
createLinkPage "invitation"
|
||||
createLinkPage "a"
|
||||
createLinkPage "c"
|
||||
createLinkPage "g"
|
||||
createLinkPage "i"
|
||||
logInfo $ "Generated static site contents at " <> tshow sitePath
|
||||
where
|
||||
copyDir dir content = do
|
||||
createDirectoryIfMissing True $ sitePath </> dir
|
||||
forM_ content $ \(path, s) -> B.writeFile (sitePath </> dir </> path) s
|
||||
createLinkPage path = do
|
||||
createDirectoryIfMissing True $ sitePath </> path
|
||||
B.writeFile (sitePath </> path </> "index.html") E.linkHtml
|
||||
|
||||
serverInformation :: ServerInformation -> Maybe TransportHost -> ByteString
|
||||
serverInformation ServerInformation {config, information} onionHost = render E.indexHtml substs
|
||||
|
||||
@@ -13,3 +13,6 @@ linkHtml = $(embedFile "apps/smp-server/static/link.html")
|
||||
|
||||
mediaContent :: [(FilePath, ByteString)]
|
||||
mediaContent = $(embedDir "apps/smp-server/static/media/")
|
||||
|
||||
wellKnown :: [(FilePath, ByteString)]
|
||||
wellKnown = $(embedDir "apps/smp-server/static/.well-known/")
|
||||
|
||||
@@ -4,6 +4,7 @@ packages: .
|
||||
-- packages: . ../http2
|
||||
-- packages: . ../network-transport
|
||||
|
||||
-- uncomment two sections below to run tests with coverage
|
||||
-- package *
|
||||
-- coverage: True
|
||||
-- library-coverage: True
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
# Protocol changes for creating and connecting to SMP queues
|
||||
|
||||
## Problems
|
||||
|
||||
This change is related to these problems:
|
||||
- differentiating queue retention time,
|
||||
- supporting MITM-resistant short connection links,
|
||||
- improving notifications.
|
||||
|
||||
This RFC is based on the previous discussions about short links, blob storage and notifications ([1](./2024-06-21-short-links.md), [2](./2024-09-09-smp-blobs.md), [3](./2024-11-25-queue-blobs-2.md), [4](./2024-09-25-ios-notifications-2.md)).
|
||||
|
||||
SMP protocol supports two types of queues - queues to communicate over and queues to send invitations. While SMP protocol was originally "unaware" of these queue types, it could differentiate it by message flow, and with the recent addition of SKEY command to allow securing the queue by the sender this difference became persistent.
|
||||
|
||||
Simply designating queue types would allow to use this information to decide for how long to retain queues, and potentially extending it:
|
||||
- unsecured 1-time invitation queues with sndSecure (support of securing by sender) - e.g., 3 months.
|
||||
- contact address queues without sndSecure - e.g., 3 years without activity.
|
||||
- Possibly, "queues" that prohibit messages and used only as blob storage - they would be used to store group profiles and superpeer addresses for the group.
|
||||
|
||||
This proposal also combines NEW and NKEY command to streamline notifications in preparation to reworking of the notifications protocol.
|
||||
|
||||
## Design objectives
|
||||
|
||||
We want to achieve these objectives:
|
||||
1. no possibility to provide incorrect SenderId inside link data (e.g. from another queue).
|
||||
2. link data cannot be accessed by the server unless it has the link.
|
||||
3. prevent MITM attack by the server, including the server that obtained the link.
|
||||
4. prevent changing of connection request by the user (to prevent MITM via break-in attack in the originating client).
|
||||
5. for one-time links, prevent accessing link data by link observers who did not compromise the server.
|
||||
6. allow changing the user-defined part of link data.
|
||||
7. avoid changing the link when user-defined part of link data changes, while preventing MITM attack by the server on user-defined part, even if it has the link.
|
||||
8. retain the quality that it is impossible to check the existense of secured queue from having any of its temporary visible IDs (sender ID and link ID in 1-time invitations) - it requires that these IDs remain server-generated (contrary to the previous RFCs).
|
||||
|
||||
To achieve these objectives the queue data must have immutable part and mutable part.
|
||||
|
||||
Immutable part would include:
|
||||
- full conection request (the current long link with all keys, including PQ keys). This includes SenderId that must match server response.
|
||||
- public signature key to verify mutable part of link data.
|
||||
|
||||
Signed mutable part would inlcude:
|
||||
- any links to chat relays that should be contacted instead of this queue (not in this RFC), but would allow delegating group connections and contact request connections to prevent spam, hiding online presense, etc.
|
||||
- and user-defined data - user profile or group profile.
|
||||
|
||||
The link itself should include both the key and auth tag from the encryption of immutable part. Accessing one-time link data should require providing sender key and signing the command (`LKEY`).
|
||||
|
||||
## Solution
|
||||
|
||||
Current NEW and NKEY commands:
|
||||
|
||||
```haskell
|
||||
NEW :: RcvPublicAuthKey -> RcvPublicDhKey -> Maybe BasicAuth -> SubscriptionMode -> SenderCanSecure -> Command Recipient
|
||||
|
||||
NKEY :: NtfPublicAuthKey -> RcvNtfPublicDhKey -> Command Recipient
|
||||
|
||||
-- | Queue IDs and keys, returned in IDS response
|
||||
data QueueIdsKeys = QIK
|
||||
{ rcvId :: RecipientId,
|
||||
sndId :: SenderId,
|
||||
rcvPublicDhKey :: RcvPublicDhKey,
|
||||
sndSecure :: SenderCanSecure
|
||||
}
|
||||
```
|
||||
|
||||
Proposed NEW command replaces SenderCanSecure with QueueMode, adds link data, and combines NKEY command:
|
||||
|
||||
```haskell
|
||||
NEW :: NewQueueRequest -> Command Recipient
|
||||
|
||||
data NewQueueReq = NewQueueReq
|
||||
{ rcvAuthKey :: RcvPublicAuthKey,
|
||||
rcvDhKey :: RcvPublicDhKey,
|
||||
auth_ :: Maybe BasicAuth,
|
||||
subMode :: SubscriptionMode,
|
||||
queueData :: Maybe QueueReqData,
|
||||
ntfCreds :: Maybe NewNtfCreds
|
||||
}
|
||||
|
||||
-- Replaces NKEY command
|
||||
-- This avoids additional command required from the client to enable notifications.
|
||||
-- Further changes would move NotifierId generation to the client, and including a signed and encrypted command to be forwarded by SMP server to notification server.
|
||||
data NtfRequest = NtfRequest NtfPublicAuthKey RcvNtfPublicDhKey
|
||||
|
||||
-- QRMessaging implies that sender can secure the queue.
|
||||
-- LinkId is not used with QRMessaging, to prevent the possibility of checking when connection is established by re-using the same link ID when creating another queue – the creating would have to fail if it is used.
|
||||
-- LinkId is required with QRContact, to have shorter link - it will be derived from the link_uri. And in this case we do not need to prevent checks that this queue exists.
|
||||
data QueueReqData = QRMessaging (Maybe QueueLinkData) | QRContact (Maybe (LinkId, QueueLinkData))
|
||||
|
||||
-- SenderId should be computed client-side as the first 24 bytes of sha3-384(correlation_id),
|
||||
-- The server must verify it and reject if it is not.
|
||||
type QueueLinkData = (SenderId, EncImmutableDataBytes, EncUserDataBytes)
|
||||
|
||||
type EncImmutableDataBytes = ByteString
|
||||
|
||||
type EncUserDataBytes = ByteString
|
||||
|
||||
-- We need to use binary encoding for AConnectionRequestUri to reduce its size
|
||||
-- connReq including the full link allows connection redundancy.
|
||||
-- The clients would reject changed immutable data (based on auth tag in the link) and
|
||||
-- AConnectionRequestUri where SenderId of the queue does not match.
|
||||
data ImmutableLinkData = ImmutableLinkData
|
||||
{ signature :: SignatureEd25519, -- signature of the remaining part of immutable data
|
||||
connReq :: AConnectionRequestUri,
|
||||
sigKey :: PublicKeyEd25519
|
||||
}
|
||||
|
||||
-- This part of link data can also include any relays, but possibly we need a separate blob for it
|
||||
data UserLinkData = UserLinkData
|
||||
{ signature :: SignatureEd25519, -- signs the remaining part of the data
|
||||
userData :: ByteString -- the max size needs to be estimated, but it is likely to be ~ 14kb
|
||||
}
|
||||
|
||||
-- | Updated queue IDs and keys, returned in IDS response
|
||||
data QueueIdsKeys = QIK
|
||||
{ rcvId :: RecipientId, -- server-generated
|
||||
sndId :: SenderId, -- server-generated
|
||||
rcvPublicDhKey :: RcvPublicDhKey,
|
||||
sndSecure :: SenderCanSecure, -- possibly, can be removed? or implied?
|
||||
linkId :: Maybe LinkId, -- server-generated
|
||||
serverNtfCreds :: Maybe ServerNtfCreds -- currently returned in NID response
|
||||
}
|
||||
|
||||
data ServerNtfCreds = ServerNtfCreds NotifierId RcvNtfPublicDhKey -- NotifierId is server-generated.
|
||||
```
|
||||
|
||||
In addition to that we add the command allowing to update and also to retrieve and, optionally, secure the queue and get link data in one request, to have only one request:
|
||||
|
||||
```haskell
|
||||
-- This command allows to set all data or to update mutlable part of contact address queue.
|
||||
-- This command should fail on queues that support sndSecure and also on new queues created with QRMessaging.
|
||||
-- This should fail if LinkId or immutable part of data is changed with the update, but will succeed if only mutable part is updated, so it can be retried.
|
||||
-- Entity ID is RecipientId.
|
||||
-- The response to this command is `OK`.
|
||||
LSET :: LinkId -> QueueLinkData -> Command Recipient
|
||||
|
||||
-- Delete should link and associated data
|
||||
-- Entity ID is RecipientId
|
||||
LDEL :: Command Recipient
|
||||
|
||||
-- To be used with 1-time links.
|
||||
-- Sender's key provided on the first request prevents observers from undetectably accessing 1-time link data.
|
||||
-- If queue mode is QRContact (and queue does NOT allow sndSecure) the command will fail, same as SKEY.
|
||||
-- Once queue is secured, the key must be the same in subsequent requests - to allow retries in case of network failures, and to prevent passive attacks.
|
||||
-- The difference with securing queues is that queues allow sending unsecured messages to queues that allow sndSecure (for backwards compatibility), and 1-time links will NOT allow retrieving link data without securing the queue at the same time, preventing undetected access by observers.
|
||||
-- Entity ID is LinkId
|
||||
LKEY :: SndPublicAuthKey -> Command Sender
|
||||
|
||||
-- If queue mode is QRMessaging the command will fail.
|
||||
-- Entity ID is LinkId
|
||||
LGET :: Command Sender
|
||||
|
||||
-- Response to LGET, LSKEY and LSGET
|
||||
-- Entity ID is the same as in the command
|
||||
LNK :: SenderId -> QueueLinkData -> BrokerMsg
|
||||
```
|
||||
|
||||
To both include sender_id into the full link before the server response, and to prevent "oracle attack" when a failure to create the queue with the supplied `sender_id` can be used as a proof of queue existense, it is proposed that `sender_id` is computed client-side as the first 24 bytes of 48 in `sha3-384(correlation_id)` and validated server-side, where `corelation_id` is the transmission correlation ID.
|
||||
|
||||
To allow retries and to avoid regenerating all queue data, NEW command must be idempotent, and `correlation_id` must be preserved in command for queue creation, so that the same `correlation_id` and all other data is used in retries. `correlation_id` should be removed after queue creation success.
|
||||
|
||||
To allow retries, every time the command is sent a new random `correlation_id` and new `sender_id` / `link_id` should be used on each attempt, because other IDs would be generated randomly on the server, and in case the previous command succeeded on the server but failed to be communicated to the client, the retry will fail if the same ID is used.
|
||||
|
||||
Alternative solutions considered and rejected:
|
||||
- additional request to save queue data, after `sender_id` is returned by the server. The scenarios that require short links are interactive - creating user addresses and 1-time invitations - so making two requests instead of one would make the UX worse.
|
||||
- include empty sender_id in the immutable data and have it replaced by the accepting party with `sender_id` received in `LINK` response - both a weird design, and might create possibility for some attacks via server, especially for contact addresses.
|
||||
- making NEW commands idempotent. Doing it would require generating all IDs client-side, not only `sender_id`. It increases complexity, and it is not really necessary as the only scenarios when retries are needed are async NEW commands, that do not require short links. For future short links of chat relays the retries are much less likely, as chat relays will have good network connections.
|
||||
|
||||
## Algorithm to prepare and to interpret queue link data.
|
||||
|
||||
For contact addresses this approach follows the design proposed in [Short links](./2024-06-21-short-links.md) RFC - when link id is derived from the same random binary as key. For 1-time invitations link ID is independent and server-generated, to prevent existense checks.
|
||||
|
||||
**Prepare queue link data**
|
||||
|
||||
- the queue owner generates a random 256 bit `link_key` that will be used in the link URI.
|
||||
- for 1-time links: crypto_box key and 2 nonces to encrypt link data are derived from link_uri using HKDF: `cb_key <> nonce1 <> nonce2 = HKDF(link_key, 80 bytes)` (nonce1 is used for immutable and nonce2 for user-defined parts).
|
||||
- for contact address links: key and 2 nonces and linkId will be derived: `link_id <> cb_key <> nonce1 <> nonce2 = HKDF(link_key, 104 bytes)`
|
||||
- both parts of link data are encrypted with crypto_box, and included into `NEW` or `LNEW` commands.
|
||||
|
||||
**Retrieving queue link data**
|
||||
|
||||
- the sender uses `LinkId` from URI (or derived from URI) as entity ID to retrieve link data.
|
||||
- for one time links the sender must authorize the request to retrieve the data, the key is provided with the first request, preventing undetected access by link observers.
|
||||
- having received the link data, the client can now decrypt it using secret_box.
|
||||
|
||||
## Improved algorithm to prepare and to interpret queue link data.
|
||||
|
||||
This scheme reduces the size of the binary in the link from 48 bytes (72 in case of 1-time links) to 32 bytes (56 bytes for 1-time links).
|
||||
|
||||
For immutable data.
|
||||
|
||||
1. `link_key = SHA3-256(immutable_data)` - used as part of link, and to encrypt content.
|
||||
2. HKDF:
|
||||
1) contact address: `(link_id, key) = HKDF(link_key, 56 bytes)`.
|
||||
2) 1-time invitation: `key = HKDF(link_key, 32 bytes)`, `link-id` - server-generated.
|
||||
3.
|
||||
3. Random `nonce1` (for immutable data), to be stored with the link data.
|
||||
4. Encrypt: `(ct1, tag1) = secret_box(immutable_data, key, nonce1)`.
|
||||
5. Store: `(nonce1, ct1, tag1)` stored as immutable link data.
|
||||
|
||||
For mutable user data:
|
||||
|
||||
1. Random `nonce2` and the same key are used.
|
||||
2. Sign `user_data` with key included in `immutable_data`.
|
||||
3. Encrypt: `(ct2, tag2) = secret_box(signed_used_data, key, nonce2)`.
|
||||
4. Store: `(nonce2, ct2, tag2)`
|
||||
|
||||
Link recipient:
|
||||
|
||||
1. Receives `link_key` in the link, for 1-time invitations also `link_id`.
|
||||
2. HKDF:
|
||||
1) contact address: `(link_id, key) = HKDF(link_key, 56 bytes)`.
|
||||
2) 1-time invitation: `key = HKDF(link_key, 32 bytes)`.
|
||||
3. Retrieves via `link_id`: `(nonce1, ct1, tag1)` and `(nonce2, ct2, tag2)`.
|
||||
4. Decrypt: `immutable_data = decrypt (nonce1, ct1, tag1)`.
|
||||
5. Verify: `SHA3-256(immutable_data) == link_key`, abort if not.
|
||||
6. Decrypt: `signed_used_data = decrypt(nonce2, ct2, tag2)`
|
||||
7. Verify signature with key in immutable data.
|
||||
|
||||
While using content hash as encryption key is unconventional, it is not completely unheard of - e.g., it is used in convergent encryption (although in our case using random nonce makes it not convergent, but other use cases suggest that this approach preserves encryption security). It is particularly acceptable for our use case, as `immutable_data` contains mostly random keys.
|
||||
|
||||
## Threat model
|
||||
|
||||
**Compromised SMP server**
|
||||
|
||||
can:
|
||||
- delete link data.
|
||||
- hide link selectively from some requests.
|
||||
|
||||
cannot:
|
||||
- undetectably replace link data, even if they have the link (objective 3).
|
||||
- access unencrypted link data, whether it was or was not accessed by the accepting party, provided it has no link (objective 2).
|
||||
- observe IP addresses of the users accessing link data, if private routing is used.
|
||||
|
||||
**Passive observer who observed short link**:
|
||||
|
||||
can:
|
||||
- access original unencrypted link data for contact address links.
|
||||
|
||||
cannot:
|
||||
- undetectably access observed 1-time link data, accessing the link would make the link inaccessible to the sender (objective 5).
|
||||
- undetectbly check the existense of messaging queue or 1-time link (objective 8).
|
||||
- replace or delete the link data.
|
||||
|
||||
**Queue owner who did not comprmise the server**:
|
||||
|
||||
cannot:
|
||||
- redirect connecting user to another queue, on the same or on another server (objective 1).
|
||||
- replace connection request in the link (objective 4).
|
||||
|
||||
## Correlation of design objectives with design elements
|
||||
|
||||
1. The presense of `SenderId` in `LINK` response from the server.
|
||||
2. Encryption of link data with crypto_box.
|
||||
3. Auth tag in the link prevents server modification of immutable part of link data. Signature verification key in immutable part, and signing of mutable part prevents server modification of mutable part of link data.
|
||||
4. No server command to change immutable part of link data once it's set.
|
||||
5. 1-time link data can only be accessed with `LKEY` command, that while allows retries to mitigate network failures, will require the same key for retries.
|
||||
6. `LSET` command.
|
||||
7. The link only includes auth tag for immutable part, mutable part includes signature.
|
||||
8. Temporarily public IDs (SenderId and LinkId for 1-time invitations) are generated server-side, and cannot be provided by the clients when creating the queues to check if these IDs are free.
|
||||
|
||||
## Syntax for short links
|
||||
|
||||
The proposed syntax:
|
||||
|
||||
```abnf
|
||||
shortConnectionLink = %s"https://" smpServerHost "/" linkUri [ "?" param *( "&" param ) ]
|
||||
smpServerHost = <hostname> ; RFC1123, RFC5891
|
||||
linkUri = %s"i#" serverInfo oneTimeLinkBytes / %s"c#" serverInfo contactLinkBytes
|
||||
oneTimeLinkBytes = <base64url(linkId | linkKey)> ; 56 bytes / 75 base64 encoded characters
|
||||
contactLinkBytes = <base64url(linkKey)> ; 32 bytes / 43 base64 encoded characters
|
||||
; linkId - 96 bits/24 bytes
|
||||
; linkKey - 256 bits/32 bytes
|
||||
|
||||
serverInfo = [fingerprint "@" [hostnames "/"]] ; not needed for preset servers, required otherwise - the clients must refuse to connect if they don't have fingerprint in the code.
|
||||
|
||||
fingerprint = <base64url(server offline certificate fingerprint)>
|
||||
hostnames = "h=" <hostname> *( "," <hostname> ) ; additional hostnames, e.g. onion
|
||||
```
|
||||
|
||||
To have shorter links fingerpring and additional server hostnames do not need to be specified for preconfigured servers, even if they are disabled - they can be used from the client code. Any user defined servers will require including additional hosts and server fingerprint.
|
||||
|
||||
Example one-time link for preset server (103 characters):
|
||||
|
||||
```
|
||||
https://smp12.simplex.im/i#abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij01234
|
||||
```
|
||||
|
||||
Example contact link for preset server (71 characters):
|
||||
|
||||
```
|
||||
https://smp12.simplex.im/c#abcdefghij0123456789abcdefghij0123456789abc
|
||||
```
|
||||
|
||||
Example contact link for user-defined server (with fingerprint, but without onion hostname - 115 characters):
|
||||
|
||||
```
|
||||
https://smp1.example.com/c#0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU@abcdefghij0123456789abcdefghij0123456789abc
|
||||
```
|
||||
|
||||
Example contact link for user-defined server (with fingerprint ant onion hostname - 178 characters):
|
||||
|
||||
```
|
||||
https://smp1.example.com/c#0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU@beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion/abcdefghij0123456789abcdefghij0123456789abc
|
||||
```
|
||||
|
||||
For the links to work in the browser the servers must provide server pages.
|
||||
@@ -0,0 +1,93 @@
|
||||
# New notifications protocol
|
||||
|
||||
## Problem
|
||||
|
||||
iOS notifications have these problems:
|
||||
- iOS notification service crashes exceeding memory limit. This is being addressed by changes in GHC RTS.
|
||||
- there is a large number of connections, because each member in a group requires individual connection. This will improve with chat relays when each group would require 2-3 connections.
|
||||
- some notification may be not shown if notification with reply/mention is skipped, and instead some other message is delivered, which may be muted. This would not improve without some changes, as notifications may be skipped anyway.
|
||||
- client devices delay communication with ntf server because it is done in background, and by that time the app may be suspended.
|
||||
- notification server represents a bottleneck, as it has to be owned by the app vendor, and the current design when ntf server subscribes to notifications scales very badly.
|
||||
|
||||
This RFC is based on the previous [RFC related to notifications](./2024-09-25-ios-notifications-2.md).
|
||||
|
||||
## Solution
|
||||
|
||||
As notification server has to know client token and currently it associates subscriptions with this token anyway, we are not gaining any privacy and security by using per-subscription keys - both authorization and encryption keys of notification subscription can be dropped.
|
||||
|
||||
We still need to store the list of queue IDs associated with the token on the notification server, but we do not need any per-queue keys on the notification server, and we don't need subscriptions - it's effectively a simple set of IDs, with no other information.
|
||||
|
||||
In this case, when queue is created the client would supply notifier ID - it has to be derived from correlation ID, to prevent existense check (see previous RFC). As we also supply sender ID, instead of deriving it as sha3-192 of correlation ID, they both can be derived as sha3-384 and split to two IDs - 24 bytes each.
|
||||
|
||||
The notification server will maintain a rotating list of server keys with the latest key communicated to the client every time the token is registered and checked. The keys would expire after, say, 1 week or 1 month, and removed from notification server on expiration.
|
||||
|
||||
The packet containing association between notifier queue ID and token will be crypto_box encrypted using key agreement between identified notification server master key and an ephemeral per packet (effectively, per-queue) client-key.
|
||||
|
||||
Deleting the queue may also include encrypted packet that would verify that the client deleted the queue.
|
||||
|
||||
Instead of notification server subscribing to the notifications creating a lot of traffic for the queues without messages, the SMP server would push notifications via NTF server connection (whether via NTF or via SMP protocol). This could be used as a mechanism to migrate existing queues when with the next subscription the notification server would communicate it's address to SMP server and this association would be stored together with the queue.
|
||||
|
||||
## Protocol design
|
||||
|
||||
Additional/changed SMP commands:
|
||||
|
||||
```haskell
|
||||
-- register notification server
|
||||
-- should be signed with server key
|
||||
NSRV :: NtfServerCreds -> Command NtfServer
|
||||
|
||||
-- response
|
||||
NSID :: NtfServerId -> BrokerMsg
|
||||
|
||||
-- to communicate which server is responsible for the queue
|
||||
-- should be signed with queue key
|
||||
NSUB :: Maybe NtfServerId -> Command Notifier
|
||||
|
||||
-- subscribe to notificaions from all queues associated with the server
|
||||
-- should be signed with server key
|
||||
-- entity ID - NtfServerId
|
||||
NSSUB :: Command NtfServer
|
||||
|
||||
data NtfServerCreds = NtfServerCreds
|
||||
{ server :: NtfServer,
|
||||
-- NTF server certificate chain that should match fingerpring in address
|
||||
cert :: X.CertificateChain,
|
||||
-- server autorizatio key to sign server subscription requests
|
||||
authKey :: X.SignedExact X.PubKey
|
||||
}
|
||||
|
||||
-- entity ID is recipient ID
|
||||
NSKEY :: NtfSubscription -> Command Recipient
|
||||
|
||||
data NtfSubscription = NtfSubscription
|
||||
-- key to encrypt notifications e2e with the client
|
||||
{ ntfPubDbKey :: RcvNtfPublicDhKey,
|
||||
ntfServer :: NtfServer,
|
||||
-- should be linked to correlation ID to prevent existense check
|
||||
-- the ID sent to notification server could be its hash?
|
||||
ntfId :: NotifierId,
|
||||
encNtfTokenAssoc :: EncDataBytes
|
||||
}
|
||||
|
||||
-- before the encryption - equivalent to NSUB command, but without key to authorize requests to specific queue
|
||||
data NtfTokenAssoc = NtfTokenAssoc
|
||||
{ signature :: SignatureEd25519,
|
||||
tknId :: NtfTokenId,
|
||||
ntfQueue :: SMPQueueNtf
|
||||
}
|
||||
```
|
||||
|
||||
SMP server will need to maintain the list of Ntf servers and their credentials, and when NSSUB arrives to make only one subscription. When message arrives it would deliver notification to the correct connection via queue / ntf server association.
|
||||
|
||||
Ntf server needs to maintain three indices to the same data:
|
||||
- `(smpServer, queueId) -> tokenId` - to deliver notification to the correct token
|
||||
- `tokenId -> [smpServer -> [queueId]]` - to remove all queues when token is removed, and to store/update these associations effficiently - store log may have one compact line per token (after compacting), or per token/server combination.
|
||||
- `[smpServer]` - array of SMP servers to subscribe to.
|
||||
|
||||
## Mention notifications
|
||||
|
||||
Currently we are marking messages with T (true) for messages that require notifications and F (false) for messages that don't require. Sender does not know whether the recipient has notifications disabled, enabled or in mentions-only mode.
|
||||
|
||||
The proposal is to:
|
||||
- add additional values to this metadata, e.g. 2 (priority) and 3 (high priority) (and T/F could be sent as 0/1 respectively) - that is, to deliver notifications even if notifications are generally disabled (they can still be further filtered by the client).
|
||||
- instead of deleting notification credentials when notifications are disabled - which is costly - communicate to SMP server the change of notificaion priority level, e.g. the client could set minimal notification priority to deliver notifications, where 0 would mean disabling it completely, 1 enable for all, 2 for priority 2+, 3 for priority 3. The downside here is that it could be used for timing correlation of queues in the group, but it already can be used on bulk deletions of ntf credentials for these queues and when sending messages.
|
||||
@@ -0,0 +1,159 @@
|
||||
# Using short links as group links
|
||||
|
||||
## Problem
|
||||
|
||||
To use the short links for groups these problems has to be / can be solved:
|
||||
1. recognizing link as a group link.
|
||||
2. permanent link with the ability to change chat relays.
|
||||
3. binding owners signatures to the link.
|
||||
4. allowing to add/remove owners, both to share ownership and for reliability in case of one owner losing keys/access.
|
||||
|
||||
While current short links solve problems 1-3 (via contact type, and via extension of user data in the link), the problem 4 is solved only partially.
|
||||
|
||||
We could include the current list of root owners in the user data, and we could send any history of ownership changes from this baseline as a short blockchain on joining the group, we still requrie one master owner to retain access to the queue associated with the group.
|
||||
|
||||
## Possible solution approaches
|
||||
|
||||
1. "Kick this can down the road" - ignore this problem until there is a namespace, and a group name can be associated with multiple queues.
|
||||
|
||||
Pros: simple and reasonable, and it suggests postponing multisig for owners too. The users can still see the list of owners and their keys in user data of the link, and receive admin roster signed by owners on joining.
|
||||
|
||||
Cons: if this "master owner" loses the access to the device, no further changes to group profile will be possible.
|
||||
|
||||
2. The queue access can be shared by sharing the key and recipient IDs with all owners.
|
||||
|
||||
The problems:
|
||||
- preventing MITM attack between owners (this protect exists for other solutions too).
|
||||
- protecting these credentials from chat relays. So somehow there should be direct key agreement between members allowing to send e2e encrypted message inaccessible to chat relays.
|
||||
|
||||
Pros: simpler than alternatives, and still provides protection against losing the key.
|
||||
Cons:
|
||||
- quite clunky, and requires the new primitive anyway (e2e encryption).
|
||||
- no multisig
|
||||
|
||||
This could possibly be evolved into the requirement to have a direct connection with other owners, and verifying the security code before they have access to group.
|
||||
|
||||
3. Allow "joint management" of SMP queues.
|
||||
|
||||
SMP servers can support multiple recipients for contact queues:\
|
||||
- subscription would be possible to the "subscriber recipient".
|
||||
- all other changes (update data, change subscriber recipient, add or remove recipients) would require multiple recipient signatures on SMP command in line with n-of-m multisig rules, that the command sender would have to collect out-of-band (from SMP protocol point of view).
|
||||
|
||||
Pros: allows joint ownership, and protects from losing access to master owner device.
|
||||
Cons:
|
||||
- complicates queue abstraction with approach that is not needed for most queues.
|
||||
- still retains the server as a single point of failure.
|
||||
|
||||
4. Introduce "group" as a new type of entity managed by SMP servers.
|
||||
|
||||
SMP servers would provide a separate set of commands for managing group records that would include in an encrypted container:
|
||||
- the group profile
|
||||
- the list of chat relay links
|
||||
- the list of owner member IDs with their public keys
|
||||
- multisig rules
|
||||
- alternative group entity locations
|
||||
- possibly, a globally unique group identity (as the hash of the initial/seed group data).
|
||||
|
||||
While the server domain would be used as the hostname in group link, it may contain alternative hosts (not just hostnames of the same server), both in the link and in the group record data.
|
||||
|
||||
Pros: separates additional complexity to where it is needed, allowing reliability and redundancy for group ownership.
|
||||
Cons: complexity, coupling between SMP and chat protocol.
|
||||
|
||||
## Design for channel/group as a separate queue mode
|
||||
|
||||
Option 1.
|
||||
|
||||
A queue mode "channel" when owners are represented by their individual queues (either a separate mode, or a submode of "channel", or just normal contact address queues). In this case sending message to channel queue would broadcast message to queue owners, without exposing even the number of owners.
|
||||
|
||||
Pros:
|
||||
- allows chat relays to send messages to all owners (e.g., channel can be secured with the list of snd keys, one per relay).
|
||||
- quite easy to evolve from the current design.
|
||||
- extensible.
|
||||
Cons:
|
||||
- close to "solution in search of a problem".
|
||||
- does not require data model changes - channel queue would simply have a list of owner "recipient IDs", and each owner queue would also point to channel.
|
||||
|
||||
Option 2.
|
||||
|
||||
Also a separate queue mode "channel", but instead of having a linked owner queues, it would simply maintain a list of owner keys to maintain the data. In this case, messages cannot be sent to this "queue" at all.
|
||||
|
||||
Pros:
|
||||
- simpler design.
|
||||
- we could allow sending messages to it too, with the "main" owner receiving them. This could be negotiated in the protocol.
|
||||
- it may be easier to migrate the current groups, as the admin link would be this queue (although for public groups in directory it would have to be recreated anyway).
|
||||
- Possibly, when queue is created there should be a flag whether it should accept unsigned messages - then contact addresses would be created with unsigned messages ON, messages queues, once SKEY is universally supported, with unsigned messages OFF, and channel queues with unsigned messages OFF too for new public queues.
|
||||
Cons:
|
||||
- if no messages are accepted, this is not even a queue.
|
||||
- no way to directly contact owners (maybe it is not a downside, as for relays there would be a communication channel anyway as part of the group).
|
||||
|
||||
Option 2 looks more simple and attractive, implementing server broadcast for SMP seems unnecessary, as while it could have been used for simple groups, it does not solve such problems as spam and pre-moderation anyway - it requires a higher level protocol.
|
||||
|
||||
The command to update owner keys would be `RKEY` with the list of keys, and we can make `NEW` accept multiple keys too, although the use case here is less clear.
|
||||
|
||||
## Multiple owners managing queue data.
|
||||
|
||||
Option 1: Use the same keys in SMP as when signing queue data.
|
||||
|
||||
Option 2: Use different keys.
|
||||
|
||||
The value here could be that the server could validate these signatures too, and also maintain the chain of key changes. While tempting, it is probably unnecessary, and this chain of ownership is better to be maintained on chat relay level, as there are no size constraints on the size of this chain. Also, it is better for metadata privacy to not couple transport and chat protocol keys.
|
||||
|
||||
We still need to bind the mutable data updates to the "genesis" signature key (the one included in the immutable data).
|
||||
|
||||
The proposed design:
|
||||
|
||||
- when mutable data is signed by genesis key, then it is bound, and no changes is needed.
|
||||
- mutable data may be signed by the key of the new owner, in which case mutable part itself must contain the binding. We could also use ring signature to sign the mutable data, concealing which owner signed the data - that would increase the signature size from 64 bytes to `32 * (n + 1)` bytes.
|
||||
|
||||
Current mutable data:
|
||||
|
||||
```haskell
|
||||
data UserLinkData = UserLinkData
|
||||
{ agentVRange :: VersionRangeSMPA,
|
||||
userData :: ConnInfo
|
||||
}
|
||||
```
|
||||
|
||||
Proposed mutable data:
|
||||
|
||||
```haskell
|
||||
data UserLinkData = UserLinkData
|
||||
{ agentVRange :: VersionRangeSMPA,
|
||||
owners :: [OwnerInfo]
|
||||
userData :: ConnInfo
|
||||
}
|
||||
|
||||
type OwnerId = ByteString
|
||||
|
||||
data OwnerInfo = OwnerInfo
|
||||
{ ownerId :: OwnerId, -- unique in the list, application specific - e.g., MemberId
|
||||
ownerKey :: PublicKeyEd25519,
|
||||
-- owner signature of sender ID,
|
||||
-- confirms that the owner agreed with being the owner,
|
||||
-- prevents a member being added as an owner without consent.
|
||||
ownerSig :: SignatureEd25519,
|
||||
-- owner authorization, sig(ownerId || ownerKey, prevKey), where prevKey is either a "genesis key" or some other key previously signed by the genesis key.
|
||||
authOwnerId :: OwnerId, -- null for "genesis"
|
||||
authOwnerSig :: SignatureEd25519
|
||||
}
|
||||
```
|
||||
|
||||
The size of the OwnerInfo record encoding is:
|
||||
- ownerId: 1 + 12
|
||||
- ownerKey: 1 + 32
|
||||
- ownerSig: 1 + 64
|
||||
- ownerAuthId: 1 + 12
|
||||
- ownerAuthSig: 1 + 64
|
||||
|
||||
~189 bytes, so we should practically limit the number of owners to say 8 - 1 original + 7 addiitonal. Original creator could use a different key as a "genesis" key, to conceal creator identity from other members, and it needs to include the record with memberId anyway.
|
||||
|
||||
The structure is simplified, and it does not allow arbitrary ownership changes. Its purpose is not to comprehensively manage ownership changes - while it is possible with a generic blockchain, it seems not appropriate at this stage, - but rather to ensure access continuity and that the server cannot modify the data (although nothing prevents the server from removing the data completely or from serving the previous version of the data).
|
||||
|
||||
For example it would only allow any given owner to remove subsequenty added owners, preserving the group link and identity, but it won't allow removing owners that signed this owner authorization. So owners are not equal, with the creator having the highest rank and being able to remove all additional owners, and owners authorise by creator can remove all other owners but themselves and creator, and so on - they have to maintain the chain that authorized themselves, at least. We could explicitely include owner rank into OwnerInfo, or we could require that they are sorted by rank, or the rank can be simply derived from signatures.
|
||||
|
||||
When additional owners want to be added to the group, they would have to provide any of the current owners:
|
||||
- the key for SMP commands authorization - this will be passed to SMP server together with other keys. There could be either RKEY to pass all keys (some risk to miss some, or of race conditions), or RADD/RGET/RDEL to add and remove recipient keys, which has no risk of race conditions.
|
||||
- the signature of the immutable data by their member key included in their profile.
|
||||
- the current owner would then include their member key into the queue data, and update it with LSET command. In any case there should be some simple consensus protocol between owners for owner changes, and it has to be maintained as a blockchain by owners and by chat relays, as otherwise it may lead to race conditions with LSET command.
|
||||
|
||||
Potentially, there could be one command to update keys and link data, so that they are consistent.
|
||||
@@ -0,0 +1,67 @@
|
||||
name: SimpleX Chat - smp-server
|
||||
|
||||
services:
|
||||
oneshot:
|
||||
image: ubuntu:latest
|
||||
environment:
|
||||
CADDYCONF: |
|
||||
${CADDY_OPTS:-}
|
||||
|
||||
http://{$$ADDR} {
|
||||
redir https://{$$ADDR}{uri} permanent
|
||||
}
|
||||
|
||||
{$$ADDR}:8443 {
|
||||
tls {
|
||||
key_type rsa4096
|
||||
}
|
||||
}
|
||||
command: sh -c 'if [ ! -f /etc/caddy/Caddyfile ]; then printf "$${CADDYCONF}" > /etc/caddy/Caddyfile; fi'
|
||||
volumes:
|
||||
- ./caddy_conf:/etc/caddy
|
||||
|
||||
caddy:
|
||||
image: caddy:latest
|
||||
depends_on:
|
||||
oneshot:
|
||||
condition: service_completed_successfully
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
environment:
|
||||
ADDR: ${ADDR?"Please specify the domain."}
|
||||
volumes:
|
||||
- ./caddy_conf:/etc/caddy
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
ports:
|
||||
- 80:80
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: "test -d /data/caddy/certificates/${CERT_PATH:-acme-v02.api.letsencrypt.org-directory}/${ADDR} || exit 1"
|
||||
interval: 1s
|
||||
retries: 60
|
||||
|
||||
smp-server:
|
||||
image: ${SIMPLEX_IMAGE:-simplexchat/smp-server:latest}
|
||||
depends_on:
|
||||
caddy:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
ADDR: ${ADDR?"Please specify the domain."}
|
||||
PASS: ${PASS:-}
|
||||
volumes:
|
||||
- ./smp_configs:/etc/opt/simplex
|
||||
- ./smp_state:/var/opt/simplex
|
||||
- type: volume
|
||||
source: caddy_data
|
||||
target: /certificates
|
||||
volume:
|
||||
subpath: "caddy/certificates/${CERT_PATH:-acme-v02.api.letsencrypt.org-directory}/${ADDR}"
|
||||
ports:
|
||||
- 443:443
|
||||
- 5223:5223
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
@@ -0,0 +1,15 @@
|
||||
name: SimpleX Chat - smp-server
|
||||
|
||||
services:
|
||||
smp-server:
|
||||
image: ${SIMPLEX_IMAGE:-simplexchat/smp-server:latest}
|
||||
environment:
|
||||
WEB_MANUAL: ${WEB_MANUAL:-1}
|
||||
ADDR: ${ADDR?"Please specify the domain."}
|
||||
PASS: ${PASS:-}
|
||||
volumes:
|
||||
- ./smp_configs:/etc/opt/simplex
|
||||
- ./smp_state:/var/opt/simplex
|
||||
ports:
|
||||
- 5223:5223
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,11 @@
|
||||
# Mandatory
|
||||
ADDR=your_ip_or_addr
|
||||
|
||||
# Optional
|
||||
#PASS='123123'
|
||||
#WEB_MANUAL=1
|
||||
|
||||
# Debug
|
||||
#SIMPLEX_SMP_IMAGE=smp-server-dev
|
||||
#CERT_PATH=acme-staging-v02.api.letsencrypt.org-directory
|
||||
#CADDY_OPTS='{\n acme_ca https://acme-staging-v02.api.letsencrypt.org/directory\n}'
|
||||
@@ -0,0 +1,9 @@
|
||||
# Mandatory
|
||||
ADDR=your_ip_or_addr
|
||||
QUOTA=120gb
|
||||
|
||||
# Optional
|
||||
#PASS='123123'
|
||||
|
||||
# Debug
|
||||
#SIMPLEX_XFTP_IMAGE=xftp-server-dev
|
||||
@@ -0,0 +1,16 @@
|
||||
name: SimpleX Chat - xftp-server
|
||||
|
||||
services:
|
||||
xftp-server:
|
||||
image: ${SIMPLEX_XFTP_IMAGE:-simplexchat/xftp-server:latest}
|
||||
environment:
|
||||
ADDR: ${ADDR?"Please specify the domain."}
|
||||
QUOTA: ${QUOTA?"Please specify disk quota."}
|
||||
PASS: ${PASS:-}
|
||||
volumes:
|
||||
- ./xftp_configs:/etc/opt/simplex-xftp
|
||||
- ./xftp_state:/var/opt/simplex-xftp
|
||||
- ./xftp_files:/srv/xftp
|
||||
ports:
|
||||
- 443:443
|
||||
restart: unless-stopped
|
||||
@@ -1,48 +1,87 @@
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
|
||||
confd='/etc/opt/simplex'
|
||||
logd='/var/opt/simplex/'
|
||||
cert_path='/certificates'
|
||||
|
||||
# Check if server has been initialized
|
||||
if [ ! -f "${confd}/smp-server.ini" ]; then
|
||||
# If not, determine ip or domain
|
||||
case "${ADDR}" in
|
||||
'') printf 'Please specify $ADDR environment variable.\n'; exit 1 ;;
|
||||
'')
|
||||
printf 'Please specify $ADDR environment variable.\n'
|
||||
exit 1
|
||||
;;
|
||||
|
||||
# Determine domain or IPv6
|
||||
*[a-zA-Z]*)
|
||||
case "${ADDR}" in
|
||||
*:*) set -- --ip "${ADDR}" ;;
|
||||
*) set -- -n "${ADDR}" ;;
|
||||
# IPv6
|
||||
*:*)
|
||||
set -- --ip "${ADDR}"
|
||||
;;
|
||||
|
||||
# Domain
|
||||
*)
|
||||
case "${ADDR}" in
|
||||
# It's in domain format
|
||||
*.*)
|
||||
# Determine the base domain
|
||||
ADDR_BASE="$(printf '%s' "$ADDR" | awk -F. '{print $(NF-1)"."$NF}')"
|
||||
set -- --fqdn "${ADDR}" --own-domains="${ADDR_BASE}"
|
||||
;;
|
||||
|
||||
# Incorrect domain
|
||||
*)
|
||||
printf 'Incorrect $ADDR environment variable. Please specify the correct one in format: smp1.example.org / example.org \n'
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
esac
|
||||
;;
|
||||
*) set -- --ip "${ADDR}" ;;
|
||||
|
||||
# Assume everything else is IPv4
|
||||
*)
|
||||
set -- --ip "${ADDR}" ;;
|
||||
esac
|
||||
|
||||
# Optionally, set password
|
||||
case "${PASS}" in
|
||||
'') set -- "$@" --no-password ;;
|
||||
*) set -- "$@" --password "${PASS}" ;;
|
||||
# Empty value = no password
|
||||
'')
|
||||
set -- "$@" --no-password
|
||||
;;
|
||||
|
||||
# Assume that everything else is a password
|
||||
*)
|
||||
set -- "$@" --password "${PASS}"
|
||||
;;
|
||||
esac
|
||||
|
||||
# And init certificates and configs
|
||||
smp-server init -y -l "$@"
|
||||
smp-server init --yes \
|
||||
--store-log \
|
||||
--daily-stats \
|
||||
--source-code \
|
||||
"$@" > /dev/null 2>&1
|
||||
|
||||
# Fix path to certificates
|
||||
if [ -n "${WEB_MANUAL}" ]; then
|
||||
sed -i -e 's|^[^#]*https: |#&|' \
|
||||
-e 's|^[^#]*cert: |#&|' \
|
||||
-e 's|^[^#]*key: |#&|' \
|
||||
-e 's|^port:.*|port: 5223|' \
|
||||
"${confd}/smp-server.ini"
|
||||
else
|
||||
sed -i -e "s|cert: /etc/opt/simplex/web.crt|cert: $cert_path/$ADDR.crt|" \
|
||||
-e "s|key: /etc/opt/simplex/web.key|key: $cert_path/$ADDR.key|" \
|
||||
"${confd}/smp-server.ini"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Backup store log just in case
|
||||
#
|
||||
# Uses the UTC (universal) time zone and this
|
||||
# format: YYYY-mm-dd'T'HH:MM:SS
|
||||
# year, month, day, letter T, hour, minute, second
|
||||
#
|
||||
# This is the ISO 8601 format without the time zone at the end.
|
||||
#
|
||||
_file="${logd}/smp-server-store.log"
|
||||
if [ -f "${_file}" ]; then
|
||||
_backup_extension="$(date -u '+%Y-%m-%dT%H:%M:%S')"
|
||||
cp -v -p "${_file}" "${_file}.${_backup_extension:-date-failed}"
|
||||
unset -v _backup_extension
|
||||
fi
|
||||
unset -v _file
|
||||
DOCKER=true /usr/local/bin/simplex-servers-stopscript smp-server
|
||||
|
||||
# Finally, run smp-sever. Notice that "exec" here is important:
|
||||
# smp-server replaces our helper script, so that it can catch INT signal
|
||||
exec smp-server start +RTS -N -RTS
|
||||
|
||||
|
||||
@@ -1,50 +1,90 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
confd='/etc/opt/simplex-xftp'
|
||||
logd='/var/opt/simplex-xftp'
|
||||
|
||||
# Check if server has been initialized
|
||||
if [ ! -f "${confd}/file-server.ini" ]; then
|
||||
# If not, determine ip or domain
|
||||
case "${ADDR}" in
|
||||
'') printf 'Please specify $ADDR environment variable.\n'; exit 1 ;;
|
||||
'')
|
||||
printf 'Please specify $ADDR environment variable.\n'
|
||||
exit 1
|
||||
;;
|
||||
|
||||
# Determine domain or IPv6
|
||||
*[a-zA-Z]*)
|
||||
case "${ADDR}" in
|
||||
*:*) set -- --ip "${ADDR}" ;;
|
||||
*) set -- -n "${ADDR}" ;;
|
||||
# IPv6
|
||||
*:*)
|
||||
set -- --ip "${ADDR}"
|
||||
;;
|
||||
|
||||
# Domain
|
||||
*)
|
||||
case "${ADDR}" in
|
||||
# Check if format is correct
|
||||
*.*)
|
||||
set -- --fqdn "${ADDR}"
|
||||
;;
|
||||
|
||||
# Incorrect domain
|
||||
*)
|
||||
printf 'Incorrect $ADDR environment variable. Please specify the correct one in format: smp1.example.org / example.org \n'
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*) set -- --ip "${ADDR}" ;;
|
||||
|
||||
# Assume everything else is IPv4
|
||||
*)
|
||||
set -- --ip "${ADDR}"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Set quota
|
||||
# Set global disk quota
|
||||
case "${QUOTA}" in
|
||||
'') printf 'Please specify $QUOTA environment variable.\n'; exit 1 ;;
|
||||
*GB) QUOTA="$(printf ${QUOTA} | tr '[:upper:]' '[:lower:]')"; set -- "$@" --quota "${QUOTA}" ;;
|
||||
*gb) set -- "$@" --quota "${QUOTA}" ;;
|
||||
*) printf 'Wrong format. Format should be: 1gb, 10gb, 100gb.\n'; exit 1 ;;
|
||||
'')
|
||||
printf 'Please specify $QUOTA environment variable.\n'
|
||||
exit 1
|
||||
;;
|
||||
|
||||
# Incorrect format in uppercase, but automagically workaround this, replacing characters to lowercase
|
||||
*GB)
|
||||
QUOTA="$(printf '%s' "${QUOTA}" | tr '[:upper:]' '[:lower:]')"
|
||||
set -- "$@" --quota "${QUOTA}"
|
||||
;;
|
||||
|
||||
# Correct format
|
||||
*gb)
|
||||
set -- "$@" --quota "${QUOTA}"
|
||||
;;
|
||||
|
||||
# Incorrect format
|
||||
*)
|
||||
printf 'Wrong format. Format should be: 1gb, 10gb, 100gb.\n'
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Init the certificates and configs
|
||||
xftp-server init -l -p /srv/xftp "$@"
|
||||
xftp-server init --store-log \
|
||||
--path /srv/xftp \
|
||||
"$@" > /dev/null 2>&1
|
||||
|
||||
# Optionally, set password
|
||||
if [ -n "${PASS}" ]; then
|
||||
sed -i -e "/^# create_password:/a create_password: $PASS" \
|
||||
"${confd}/file-server.ini"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Backup store log just in case
|
||||
#
|
||||
# Uses the UTC (universal) time zone and this
|
||||
# format: YYYY-mm-dd'T'HH:MM:SS
|
||||
# year, month, day, letter T, hour, minute, second
|
||||
#
|
||||
# This is the ISO 8601 format without the time zone at the end.
|
||||
#
|
||||
_file="${logd}/file-server-store.log"
|
||||
if [ -f "${_file}" ]; then
|
||||
_backup_extension="$(date -u '+%Y-%m-%dT%H:%M:%S')"
|
||||
cp -v -p "${_file}" "${_file}.${_backup_extension:-date-failed}"
|
||||
unset -v _backup_extension
|
||||
fi
|
||||
unset -v _file
|
||||
|
||||
DOCKER=true /usr/local/bin/simplex-servers-stopscript xftp-server
|
||||
|
||||
# Finally, run xftp-sever. Notice that "exec" here is important:
|
||||
# smp-server replaces our helper script, so that it can catch INT signal
|
||||
exec xftp-server start +RTS -N -RTS
|
||||
|
||||
|
||||
@@ -148,8 +148,10 @@ xftp_cleanup() {
|
||||
|
||||
main() {
|
||||
type="${1:-}"
|
||||
|
||||
checks
|
||||
|
||||
if [ -z "${DOCKER+x}" ]; then
|
||||
checks
|
||||
fi
|
||||
|
||||
case "$type" in
|
||||
smp-server)
|
||||
|
||||
+512
-169
@@ -1,13 +1,8 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# Links to scripts/configs
|
||||
scripts="https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/scripts/main"
|
||||
scripts_systemd_smp="$scripts/smp-server.service"
|
||||
scripts_systemd_xftp="$scripts/xftp-server.service"
|
||||
scripts_update="$scripts/simplex-servers-update"
|
||||
scripts_uninstall="$scripts/simplex-servers-uninstall"
|
||||
scripts_stopscript="$scripts/simplex-servers-stopscript"
|
||||
# Make sure that PATH variable contains /usr/local/bin
|
||||
PATH="/usr/local/bin:$PATH"
|
||||
|
||||
# Default installation paths
|
||||
path_bin="/usr/local/bin"
|
||||
@@ -22,7 +17,9 @@ path_systemd_smp="$path_systemd/smp-server.service"
|
||||
path_systemd_xftp="$path_systemd/xftp-server.service"
|
||||
|
||||
# Temporary paths
|
||||
path_tmp_bin="$(mktemp -d)"
|
||||
path_tmp_bin="/tmp/simplex-servers"
|
||||
path_tmp_bin_smp="$path_tmp_bin/smp-server"
|
||||
path_tmp_bin_xftp="$path_tmp_bin/xftp-server"
|
||||
path_tmp_bin_update="$path_tmp_bin/simplex-servers-update"
|
||||
path_tmp_bin_uninstall="$path_tmp_bin/simplex-servers-uninstall"
|
||||
path_tmp_bin_stopscript="$path_tmp_bin/simplex-servers-stopscript"
|
||||
@@ -37,203 +34,549 @@ BLU='\033[1;36m'
|
||||
YLW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
BLD='\033[1m'
|
||||
UNDRL='\033[4m'
|
||||
|
||||
NL='
|
||||
'
|
||||
|
||||
# Set VER globally and only once
|
||||
VER="${VER:-latest}"
|
||||
|
||||
# Currently, XFTP default to v0.1.0, so it doesn't make sense to check its version
|
||||
|
||||
os_test() {
|
||||
. /etc/os-release
|
||||
######################
|
||||
### Misc functions ###
|
||||
######################
|
||||
|
||||
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
|
||||
# Checks "sanity" of downloaded thing, e.g. if it's really a script or binary
|
||||
check_sanity() {
|
||||
path="$1"
|
||||
criteria="$2"
|
||||
|
||||
version="$(printf '%s' "$VERSION_ID" | tr '.' '_')"
|
||||
arch="$(uname -p)"
|
||||
case "$criteria" in
|
||||
string:*)
|
||||
pattern="$(printf '%s' "$criteria" | awk '{print $2}')"
|
||||
|
||||
if grep -q "$pattern" "$path"; then
|
||||
sane=0
|
||||
else
|
||||
sane=1
|
||||
fi
|
||||
;;
|
||||
file:*)
|
||||
pattern="$(printf '%s' "$criteria" | awk '{print $2}')"
|
||||
|
||||
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
|
||||
if file "$path" | grep -q "$pattern"; then
|
||||
sane=0
|
||||
else
|
||||
sane=1
|
||||
fi
|
||||
;;
|
||||
*) printf 'Unknown criteria.\n'; sane=1 ;;
|
||||
esac
|
||||
|
||||
bin_smp="$bin/smp-server-ubuntu-${version}-${arch}"
|
||||
bin_xftp="$bin/xftp-server-ubuntu-${version}-${arch}"
|
||||
unset path string
|
||||
|
||||
return "$sane"
|
||||
}
|
||||
|
||||
installed_test() {
|
||||
# Checks if old thing and new thing is different
|
||||
change_check() {
|
||||
old="$1"
|
||||
new="$2"
|
||||
|
||||
if [ -x "$new" ] || [ -f "$new" ]; then
|
||||
type="$(file $new)"
|
||||
else
|
||||
type='string'
|
||||
fi
|
||||
|
||||
case "$type" in
|
||||
*script*|*text*)
|
||||
if diff -q "$old" "$new" > /dev/null 2>&1; then
|
||||
changed=1
|
||||
else
|
||||
changed=0
|
||||
fi
|
||||
;;
|
||||
string)
|
||||
if [ "$old" = "$new" ]; then
|
||||
changed=1
|
||||
else
|
||||
changed=0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
return "$changed"
|
||||
}
|
||||
|
||||
##########################
|
||||
### Misc functions END ###
|
||||
##########################
|
||||
|
||||
#########################
|
||||
### Support functions ###
|
||||
#########################
|
||||
|
||||
# Sets local/remote versions and "apps" variables
|
||||
check_versions() {
|
||||
# Sets:
|
||||
# - ver
|
||||
# - bin_url
|
||||
# - remote_version
|
||||
# - local_version
|
||||
# - apps
|
||||
|
||||
case "$VER" in
|
||||
latest)
|
||||
remote_version="$(curl --proto '=https' --tlsv1.2 -sSf -L https://api.github.com/repos/simplex-chat/simplexmq/releases/latest 2>/dev/null | grep -i "tag_name" | awk -F \" '{print $4}')"
|
||||
|
||||
if [ -z "$remote_version" ]; then
|
||||
printf "${RED}Something went wrong when ${YLW}resolving the lastest version${NC}: either you don't have connection to Github or you're rate-limited.\n"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Check if this version really exist
|
||||
ver_check="https://github.com/simplex-chat/simplexmq/releases/tag/${VER}"
|
||||
|
||||
if curl -o /dev/null --proto '=https' --tlsv1.2 -sf -L "${ver_check}"; then
|
||||
remote_version="${VER}"
|
||||
else
|
||||
printf "Provided version ${BLU}%s${NC} ${RED}doesn't exist${NC}! Switching to ${BLU}latest${NC}.\n" "${VER}"
|
||||
VER='latest'
|
||||
|
||||
# Re-execute check
|
||||
check_versions
|
||||
|
||||
# Everything has been done, so return from the function
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# Links to scripts/configs
|
||||
bin_url="https://github.com/simplex-chat/simplexmq/releases/download/${remote_version}"
|
||||
scripts_url="https://raw.githubusercontent.com/simplex-chat/simplexmq/refs/tags/${remote_version}/scripts/main"
|
||||
scripts_url_systemd_smp="$scripts_url/smp-server.service"
|
||||
scripts_url_systemd_xftp="$scripts_url/xftp-server.service"
|
||||
scripts_url_update="$scripts_url/simplex-servers-update"
|
||||
scripts_url_uninstall="$scripts_url/simplex-servers-uninstall"
|
||||
scripts_url_stopscript="$scripts_url/simplex-servers-stopscript"
|
||||
|
||||
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
|
||||
for i in smp xftp; do
|
||||
# Only check local directory where binaries are installed by the script
|
||||
if command -v "/usr/local/bin/$i-server" >/dev/null; then
|
||||
apps="$i $apps"
|
||||
fi
|
||||
done
|
||||
set -u
|
||||
}
|
||||
|
||||
set_version() {
|
||||
ver="${VER:-latest}"
|
||||
|
||||
case "$ver" in
|
||||
latest)
|
||||
bin="https://github.com/simplex-chat/simplexmq/releases/latest/download"
|
||||
remote_version="$(curl --proto '=https' --tlsv1.2 -sSf -L https://api.github.com/repos/simplex-chat/simplexmq/releases/latest | grep -i "tag_name" | awk -F \" '{print $4}')"
|
||||
;;
|
||||
*)
|
||||
bin="https://github.com/simplex-chat/simplexmq/releases/download/${ver}"
|
||||
remote_version="${ver}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
update_scripts() {
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_update" -o "$path_tmp_bin_update" && chmod +x "$path_tmp_bin_update"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_uninstall" -o "$path_tmp_bin_uninstall" && chmod +x "$path_tmp_bin_uninstall"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_stopscript" -o "$path_tmp_bin_stopscript" && chmod +x "$path_tmp_bin_stopscript"
|
||||
|
||||
if diff -q "$path_bin_uninstall" "$path_tmp_bin_uninstall" > /dev/null 2>&1; then
|
||||
printf -- "- ${YLW}Uninstall script is up-to-date${NC}.\n"
|
||||
rm "$path_tmp_bin_uninstall"
|
||||
else
|
||||
printf -- "- Updating uninstall script..."
|
||||
mv "$path_tmp_bin_uninstall" "$path_bin_uninstall"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
if [ -z "$apps" ]; then
|
||||
printf "${RED}No simplex servers installed! Aborting.${NC}\n"
|
||||
exit 1
|
||||
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"
|
||||
else
|
||||
printf -- "- Updating stopscript script..."
|
||||
mv "$path_tmp_bin_stopscript" "$path_bin_stopscript"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
fi
|
||||
for server in $apps; do
|
||||
# Check if info file is present
|
||||
if [ -f "$path_conf_info/release" ]; then
|
||||
# If present, source it
|
||||
. "$path_conf_info/release" 2>/dev/null
|
||||
|
||||
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"
|
||||
else
|
||||
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..."
|
||||
exec sh "$path_bin_update" "continue"
|
||||
fi
|
||||
}
|
||||
|
||||
update_systemd() {
|
||||
service="${1}-server"
|
||||
eval "scripts_systemd=\$scripts_systemd_${1}"
|
||||
eval "path_systemd=\$path_systemd_${1}"
|
||||
eval "path_tmp_systemd=\$path_tmp_systemd_${1}"
|
||||
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd" -o "$path_tmp_systemd"
|
||||
|
||||
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 %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() {
|
||||
service="${1}-server"
|
||||
eval "bin=\$bin_${1}"
|
||||
eval "path_bin=\$path_bin_${1}"
|
||||
|
||||
set_ver() {
|
||||
local_version='unset'
|
||||
sed -i -- "s/local_version_${1}=.*/local_version_${1}='${remote_version}'/" "$path_conf_info/release"
|
||||
}
|
||||
|
||||
if [ -f "$path_conf_info/release" ]; then
|
||||
. "$path_conf_info/release" 2>/dev/null
|
||||
|
||||
set +u
|
||||
eval "local_version=\$local_version_${1}"
|
||||
set -u
|
||||
|
||||
if [ -z "${local_version}" ]; then
|
||||
set_ver "$1"
|
||||
fi
|
||||
else
|
||||
printf 'local_version_xftp=\nlocal_version_smp=\n' > "$path_conf_info/release"
|
||||
set_ver "$1"
|
||||
fi
|
||||
|
||||
if [ "$local_version" != "$remote_version" ]; then
|
||||
if systemctl is-active --quiet "$service"; then
|
||||
printf -- "- Stopping %s service..." "$service"
|
||||
systemctl stop "$service"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
|
||||
printf -- "- Updating %s to %s..." "$service" "$remote_version"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$bin" -o "$path_bin" && chmod +x "$path_bin"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
|
||||
printf -- "- Starting %s service..." "$service"
|
||||
systemctl start "$service"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
# Check if line containing local version exists in file
|
||||
if grep -q "local_version_${server}" "$path_conf_info/release"; then
|
||||
# if exists, set the var
|
||||
eval "local_version=\$local_version_${server}"
|
||||
else
|
||||
# If it doesn't, append it to file
|
||||
printf "local_version_${server}=unset\n" >> "$path_conf_info/release"
|
||||
# And set it in script (so we don't have to re-source the file)
|
||||
eval "local_version_${server}=unset"
|
||||
fi
|
||||
else
|
||||
printf -- "- Updating %s to %s..." "$service" "$remote_version"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$bin" -o "$path_bin" && chmod +x "$path_bin"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
# If there isn't info file, populate it
|
||||
printf "local_version_${server}=unset\n" >> "$path_conf_info/release"
|
||||
fi
|
||||
else
|
||||
printf -- "- ${YLW}%s is up-to-date${NC}.\n" "$service"
|
||||
fi
|
||||
done
|
||||
|
||||
set_ver "$1"
|
||||
|
||||
unset service bin path_bin local_version
|
||||
# Return
|
||||
return 0
|
||||
}
|
||||
|
||||
# Checks the distro and sets the urls variables
|
||||
check_distro() {
|
||||
. /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_url_smp="$bin_url/smp-server-ubuntu-${version}-${arch}"
|
||||
bin_url_xftp="$bin_url/xftp-server-ubuntu-${version}-${arch}"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# General checks that must be performed on the initial execution of script
|
||||
checks() {
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
printf "This script is intended to be run with root privileges. Please re-run script using sudo.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set_version
|
||||
os_test
|
||||
installed_test
|
||||
check_versions
|
||||
check_distro
|
||||
|
||||
mkdir -p $path_conf_info
|
||||
mkdir -p $path_conf_info $path_tmp_bin
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
main() {
|
||||
checks
|
||||
#############################
|
||||
### Support functions END ###
|
||||
#############################
|
||||
|
||||
set +u
|
||||
if [ "$1" != "continue" ]; then
|
||||
set -u
|
||||
printf "Updating scripts...\n"
|
||||
update_scripts
|
||||
else
|
||||
set -u
|
||||
######################
|
||||
### Main functions ###
|
||||
######################
|
||||
|
||||
# Downloads thing to directory and checks its sanity
|
||||
download_thing() {
|
||||
thing="$1"
|
||||
path="$2"
|
||||
check_pattern="$3"
|
||||
err_msg="$4"
|
||||
|
||||
if ! curl --proto '=https' --tlsv1.2 -sSf -L "$thing" -o "$path"; then
|
||||
printf "${RED}Something went wrong when downloading ${YLW}%s${NC}: either you don't have connection to Github or you're rate-limited.\n" "$err_msg"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
type="$(file "$path")"
|
||||
|
||||
case "$type" in
|
||||
*script*|*executable*) chmod +x "$path" ;;
|
||||
esac
|
||||
|
||||
if ! check_sanity "$path" "$check_pattern"; then
|
||||
printf "${RED}Something went wrong with downloaded ${YLW}%s${NC}: file is corrupted.\n" "$err_msg"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Downloads all necessary files to temp dir and set update messages for the menu
|
||||
download_all() {
|
||||
download_thing "$scripts_url_update" "$path_tmp_bin_update" 'string: /usr/bin/env' 'Update script'
|
||||
if change_check "$path_tmp_bin_update" "$path_bin_update"; then
|
||||
msg_scripts="${msg_scripts+$msg_scripts, }${YLW}simplex-servers-update${NC}"
|
||||
msg_scripts_raw="${msg_scripts_raw+$msg_scripts_raw/}update"
|
||||
fi
|
||||
|
||||
download_thing "$scripts_url_stopscript" "$path_tmp_bin_stopscript" 'string: /usr/bin/env' 'Stop script'
|
||||
if change_check "$path_tmp_bin_stopscript" "$path_bin_stopscript"; then
|
||||
msg_scripts="${msg_scripts+$msg_scripts, }${YLW}simplex-servers-stopscript${NC}"
|
||||
msg_scripts_raw="${msg_scripts_raw+$msg_scripts_raw/}stop"
|
||||
fi
|
||||
|
||||
download_thing "$scripts_url_uninstall" "$path_tmp_bin_uninstall" 'string: /usr/bin/env' 'Uninstall script'
|
||||
if change_check "$path_tmp_bin_uninstall" "$path_bin_uninstall"; then
|
||||
msg_scripts="${msg_scripts+$msg_scripts, }${YLW}simplex-servers-uninstall${NC}"
|
||||
msg_scripts_raw="${msg_scripts_raw+$msg_scripts_raw/}uninstall"
|
||||
fi
|
||||
|
||||
for i in $apps; do
|
||||
service="${i}-server"
|
||||
eval "scripts_url_systemd_final=\$scripts_url_systemd_${i}"
|
||||
eval "path_tmp_systemd_final=\$path_tmp_systemd_${i}"
|
||||
eval "path_systemd_final=\$path_systemd_${i}"
|
||||
|
||||
download_thing "$scripts_url_systemd_final" "$path_tmp_systemd_final" 'string: [Unit]' "$service systemd service"
|
||||
if change_check "$path_tmp_systemd_final" "$path_systemd_final"; then
|
||||
msg_services="${msg_services+$msg_services, }${YLW}$service.service${NC}"
|
||||
msg_services_raw="${msg_services_raw+$msg_services_raw/}$service"
|
||||
fi
|
||||
done
|
||||
|
||||
for i in $apps; do
|
||||
service="${i}-server"
|
||||
eval "local_version=\$local_version_${i}"
|
||||
|
||||
if change_check "$local_version" "$remote_version"; then
|
||||
msg_bins="${msg_bins+$msg_bins$NL} - ${YLW}$service${NC}: from ${BLU}$local_version${NC} to ${BLU}$remote_version${NC}"
|
||||
msg_bins_alt="${msg_bins_alt+$msg_bins_alt, }${YLW}$service${NC}"
|
||||
msg_bins_raw="${msg_bins_raw+$msg_bins_raw/}$service"
|
||||
fi
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Updates systemd and scripts. This function depends om variables from "download_all"
|
||||
update_misc() {
|
||||
OLD_IFS="$IFS"
|
||||
|
||||
IFS='/'
|
||||
for script in ${msg_scripts_raw:-}; do
|
||||
case "$script" in
|
||||
update)
|
||||
printf -- "- Updating update script..."
|
||||
mv "$path_tmp_bin_update" "$path_bin_update"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
printf -- "- Re-executing Update script..."
|
||||
exec env UPDATE_SCRIPT_DONE=1 VER="$remote_version" "$path_bin_update" "${selection}"
|
||||
;;
|
||||
stop)
|
||||
printf -- "- Updating stopscript script..."
|
||||
mv "$path_tmp_bin_stopscript" "$path_bin_stopscript"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
;;
|
||||
uninstall)
|
||||
printf -- "- Updating uninstall script..."
|
||||
mv "$path_tmp_bin_uninstall" "$path_bin_uninstall"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
for service in ${msg_services_raw:-}; do
|
||||
app="${service%%-*}"
|
||||
eval "path_systemd=\$path_systemd_${app}"
|
||||
eval "path_tmp_systemd=\$path_tmp_systemd_${app}"
|
||||
|
||||
printf -- "- Updating %s service..." "$service"
|
||||
mv "$path_tmp_systemd" "$path_systemd"
|
||||
systemctl daemon-reload
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
done
|
||||
|
||||
IFS="$OLD_IFS"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Updates binaries. This function depends on variables from "download_all"
|
||||
update_bins() {
|
||||
OLD_IFS="$IFS"
|
||||
|
||||
IFS='/'
|
||||
for service in ${msg_bins_raw:-}; do
|
||||
app="${service%%-*}"
|
||||
eval "local_version=\$local_version_${app}"
|
||||
eval "bin_url_final=\$bin_url_${app}"
|
||||
eval "path_tmp_bin_final=\$path_tmp_bin_${app}"
|
||||
eval "path_bin_final=\$path_bin_${app}"
|
||||
|
||||
# If systemd service is active
|
||||
if systemctl is-active --quiet "$service"; then
|
||||
printf -- "- Stopping %s service..." "$service"
|
||||
systemctl stop "$service"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
|
||||
printf -- "- Updating ${YLW}%s${NC} from ${BLU}%s${NC} to ${BLU}%s${NC}..." "$service" "$local_version" "$remote_version"
|
||||
download_thing "$bin_url_final" "$path_tmp_bin_final" 'file: ELF' "$service"
|
||||
mv "$path_tmp_bin_final" "$path_bin_final"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
|
||||
printf -- "- Starting %s service..." "$service"
|
||||
systemctl start "$service"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
else
|
||||
# If systemd service is NOT active
|
||||
printf -- "- Updating ${YLW}%s${NC} from ${BLU}%s${NC} to ${BLU}%s${NC}..." "$service" "$local_version" "$remote_version"
|
||||
download_thing "$bin_url_final" "$path_tmp_bin_final" 'file: ELF' "$service"
|
||||
mv "$path_tmp_bin_final" "$path_bin_final"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
fi
|
||||
|
||||
# Don't forget to set version
|
||||
sed -i -- "s|local_version_${app}=.*|local_version_${app}='${remote_version}'|" "$path_conf_info/release"
|
||||
done
|
||||
|
||||
IFS="$OLD_IFS"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Just download binaries
|
||||
download_bins() {
|
||||
OLD_IFS="$IFS"
|
||||
|
||||
IFS='/'
|
||||
for service in ${msg_bins_raw:-}; do
|
||||
app="${service%%-*}"
|
||||
eval "local_version=\$local_version_${app}"
|
||||
eval "bin_url_final=\$bin_url_${app}"
|
||||
eval "path_tmp_bin_final=\$path_tmp_bin_${app}"
|
||||
eval "path_bin_final=\$path_bin_${app}"
|
||||
|
||||
printf -- "- Downloading ${YLW}%s${NC} binary..." "$service"
|
||||
download_thing "$bin_url_final" "$path_tmp_bin_final" 'file: ELF' "$service"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
done
|
||||
|
||||
IFS="$OLD_IFS"
|
||||
return 0
|
||||
}
|
||||
|
||||
menu_init_help() {
|
||||
menu_help="Update script for SimpleX servers and scripts.${NL}${NL}"
|
||||
menu_help="${menu_help}${BLD}${UNDRL}Usage:${NC} [<VARIABLE>] ${BLD}simplex-servers-update${NC}${NL} [<VARIABLE>] ${BLD}simplex-servers-update${NC} [<SUBCOMMAND>]${NL}${NL}"
|
||||
menu_help="${menu_help}${BLD}${UNDRL}Subcommands:${NC}${NL}"
|
||||
menu_help_sub=" ${BLD}[a]ll${NC} Update everything without confirmation${NL}"
|
||||
menu_help_sub="${menu_help_sub} ${BLD}[b]inaries${NC} Update binaries only without confirmation${NL}"
|
||||
menu_help_sub="${menu_help_sub} ${BLD}[d]ownload${NC} Download everything without updating${NL}"
|
||||
menu_help_sub="${menu_help_sub} ${BLD}[h]elp${NC} Print this message${NL}${NL}"
|
||||
menu_help="${menu_help}${menu_help_sub}"
|
||||
menu_help="${menu_help}${BLD}${UNDRL}Variables:${NC}${NL}"
|
||||
menu_help="${menu_help} ${BLD}VER=v3.2.1-beta.0${NC} Update binaries to specified version${NL}"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
menu_init() {
|
||||
menu_end="${RED}x${NC}) Exit${NL}${NL}Selection: "
|
||||
menu_option_download="${GRN}d${NC}) Download files only${NL}"
|
||||
|
||||
if [ -n "${msg_scripts:-}" ]; then
|
||||
menu_option_misc_raw="${menu_option_misc_raw+${menu_option_misc_raw}${NL}} - script(s): ${msg_scripts}"
|
||||
fi
|
||||
|
||||
if [ -n "${msg_services:-}" ]; then
|
||||
menu_option_misc_raw="${menu_option_misc_raw+${menu_option_misc_raw}${NL}} - systemd service file(s): ${msg_services}"
|
||||
fi
|
||||
|
||||
menu_option_all="${GRN}a${NC}) Update all: ${BLU}(recommended)${NC}${NL}${menu_option_misc_raw+${menu_option_misc_raw}${NL}}${msg_bins+${msg_bins}${NL}}"
|
||||
|
||||
if [ -n "${msg_bins:-}" ]; then
|
||||
menu_option_bins="${GRN}b${NC}) Update server binaries: ${msg_bins_alt}${NL}"
|
||||
fi
|
||||
|
||||
# Abort early if there's neither update binaries, nor update scripts options
|
||||
if [ -z "${menu_option_bins:-}" ] && [ -z "${menu_option_misc_raw:-}" ]; then
|
||||
printf "${YLW}Everything is up-to-date${NC}.\n"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
menu="${menu_option_all}${menu_option_bins:-}${menu_option_download}${menu_end}"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
options_parse() {
|
||||
selection="$1"
|
||||
|
||||
case "$selection" in
|
||||
a|all)
|
||||
check=0
|
||||
if [ -z "${menu_option_misc_raw:-}" ] && [ -z "${menu_option_bins:-}" ]; then
|
||||
printf "${YLW}Everything is up-to-date${NC}.\n"
|
||||
else
|
||||
if [ -n "${menu_option_misc_raw:-}" ]; then
|
||||
update_misc
|
||||
fi
|
||||
if [ -n "${menu_option_bins:-}" ]; then
|
||||
update_bins
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
b|binaries)
|
||||
check=0
|
||||
if [ -n "${menu_option_bins:-}" ]; then
|
||||
update_bins
|
||||
else
|
||||
printf "${YLW}Binaries is up-to-date${NC}.\n"
|
||||
fi
|
||||
;;
|
||||
d|download)
|
||||
check=0
|
||||
if [ -n "${menu_option_bins:-}" ]; then
|
||||
download_bins
|
||||
fi
|
||||
|
||||
printf "\n${YLW}Scripts${NC}/${YLW}services${NC}/${YLW}binaries${NC} has been downloaded to ${BLU}%s${NC}\n" "$path_tmp_bin"
|
||||
;;
|
||||
x)
|
||||
check=0
|
||||
;;
|
||||
*)
|
||||
check=1
|
||||
;;
|
||||
esac
|
||||
|
||||
return "$check"
|
||||
}
|
||||
|
||||
##########################
|
||||
### Main functions END ###
|
||||
##########################
|
||||
|
||||
############
|
||||
### Init ###
|
||||
############
|
||||
|
||||
main() {
|
||||
# Early hook to print Done after script re-execution
|
||||
if [ -n "${UPDATE_SCRIPT_DONE:-}" ]; then
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
fi
|
||||
|
||||
printf "Updating systemd services...\n"
|
||||
for i in $apps; do
|
||||
update_systemd "$i"
|
||||
done
|
||||
|
||||
printf "Updating simplex servers...\n"
|
||||
for i in $apps; do
|
||||
update_bins "$i"
|
||||
done
|
||||
# Early help menu
|
||||
menu_init_help
|
||||
|
||||
rm -rf "$path_tmp_bin"
|
||||
case "${1:-}" in
|
||||
h|help)
|
||||
printf '%b' "$menu_help"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
checks
|
||||
download_all
|
||||
menu_init
|
||||
|
||||
onetime=0
|
||||
while true; do
|
||||
if [ "$onetime" = 0 ]; then
|
||||
onetime=1
|
||||
|
||||
if [ -n "${1:-}" ]; then
|
||||
selection="$1"
|
||||
else
|
||||
printf '%b' "$menu"
|
||||
read selection
|
||||
fi
|
||||
else
|
||||
read selection
|
||||
fi
|
||||
|
||||
if options_parse "$selection"; then
|
||||
break
|
||||
else
|
||||
# Rerender whole menu if the first non-interactive option was bogus
|
||||
if [ -n "${1:-}" ]; then
|
||||
onetime=0
|
||||
shift 1
|
||||
else
|
||||
# Erase last line
|
||||
printf '\e[A\e[K'
|
||||
# Only rerended selection
|
||||
printf 'Selection: '
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -5,12 +5,21 @@ Description=SMP server
|
||||
User=smp
|
||||
Group=smp
|
||||
Type=simple
|
||||
|
||||
ExecStart=/usr/local/bin/smp-server start +RTS -N -RTS
|
||||
ExecStopPost=/usr/local/bin/simplex-servers-stopscript smp-server
|
||||
|
||||
LimitNOFILE=65535
|
||||
KillSignal=SIGINT
|
||||
|
||||
TimeoutStartSec=infinity
|
||||
TimeoutStopSec=infinity
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=10s
|
||||
StartLimitBurst=3
|
||||
StartLimitInterval=60s
|
||||
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -5,12 +5,21 @@ Description=XFTP server
|
||||
User=xftp
|
||||
Group=xftp
|
||||
Type=simple
|
||||
|
||||
ExecStart=/usr/local/bin/xftp-server start +RTS -N -RTS
|
||||
ExecStopPost=/usr/local/bin/simplex-servers-stopscript xftp-server
|
||||
|
||||
LimitNOFILE=65535
|
||||
KillSignal=SIGINT
|
||||
|
||||
TimeoutStartSec=infinity
|
||||
TimeoutStopSec=infinity
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=10s
|
||||
StartLimitBurst=3
|
||||
StartLimitInterval=60s
|
||||
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
|
||||
[Install]
|
||||
|
||||
Executable
+139
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
TAG="$1"
|
||||
|
||||
tempdir="$(mktemp -d)"
|
||||
init_dir="$PWD"
|
||||
|
||||
repo_name="simplexmq"
|
||||
repo="https://github.com/simplex-chat/${repo_name}"
|
||||
export DOCKER_BUILDKIT=1
|
||||
|
||||
cleanup() {
|
||||
docker exec -t builder sh -c 'rm -rf ./dist-newstyle' 2>/dev/null || :
|
||||
rm -rf -- "$tempdir"
|
||||
docker rm --force builder 2>/dev/null || :
|
||||
docker image rm local 2>/dev/null || :
|
||||
cd "$init_dir"
|
||||
}
|
||||
trap 'cleanup' EXIT INT
|
||||
|
||||
mkdir -p "$init_dir/$TAG-$repo_name/from-source" "$init_dir/$TAG-$repo_name/prebuilt"
|
||||
|
||||
git -C "$tempdir" clone "$repo.git" &&\
|
||||
cd "$tempdir/${repo_name}" &&\
|
||||
git checkout "$TAG"
|
||||
|
||||
for os in 22.04 24.04; do
|
||||
os_url="$(printf '%s' "$os" | tr '.' '_')"
|
||||
|
||||
# Build image
|
||||
docker build \
|
||||
--no-cache \
|
||||
--build-arg TAG=${os} \
|
||||
--build-arg GHC=9.6.3 \
|
||||
-f "$tempdir/simplexmq/Dockerfile.build" \
|
||||
-t local \
|
||||
.
|
||||
|
||||
# Run container in background
|
||||
docker run -t -d \
|
||||
--name builder \
|
||||
-v "$tempdir/${repo_name}:/project" \
|
||||
local
|
||||
|
||||
# PostgreSQL build (only smp-server)
|
||||
docker exec \
|
||||
-t \
|
||||
builder \
|
||||
sh -c 'cabal update && cabal build --jobs=$(nproc) --enable-tests -fserver_postgres && mkdir -p /out && for i in smp-server simplexmq-test; do bin=$(find /project/dist-newstyle -name "$i" -type f -executable) && chmod +x "$bin" && mv "$bin" /out/; done && strip /out/smp-server'
|
||||
|
||||
# Copy smp-server postgresql binary and prepare it
|
||||
docker cp \
|
||||
builder:/out/smp-server \
|
||||
"$init_dir/$TAG-$repo_name/from-source/smp-server-postgres-ubuntu-${os_url}-x86-64"
|
||||
|
||||
# Download prebuilt postgresql binary
|
||||
curl -L \
|
||||
--output-dir "$init_dir/$TAG-$repo_name/prebuilt/" \
|
||||
-O \
|
||||
"$repo/releases/download/${TAG}/smp-server-postgres-ubuntu-${os_url}-x86-64"
|
||||
|
||||
# Regular build (all)
|
||||
apps='smp-server xftp-server ntf-server xftp'
|
||||
|
||||
docker exec \
|
||||
-t \
|
||||
-e apps="$apps" \
|
||||
builder \
|
||||
sh -c 'cabal build --jobs=$(nproc) && mkdir -p /out && for i in $apps; do bin=$(find /project/dist-newstyle -name "$i" -type f -executable) && strip "$bin" && chmod +x "$bin" && mv "$bin" /out/; done'
|
||||
|
||||
# Copy regular binaries
|
||||
docker cp \
|
||||
builder:/out \
|
||||
out-${os}
|
||||
|
||||
# Prepare regular binaries and download the prebuilt ones
|
||||
for app in $apps; do
|
||||
curl -L \
|
||||
--output-dir "$init_dir/$TAG-$repo_name/prebuilt/" \
|
||||
-O \
|
||||
"$repo/releases/download/${TAG}/${app}-ubuntu-${os_url}-x86-64"
|
||||
|
||||
mv "./out-${os}/$app" "$init_dir/$TAG-$repo_name/from-source/${app}-ubuntu-${os_url}-x86-64"
|
||||
done
|
||||
|
||||
# Important! Remove dist-newstyle for the next interation
|
||||
docker exec \
|
||||
-t \
|
||||
builder \
|
||||
sh -c 'rm -rf ./dist-newstyle'
|
||||
|
||||
# Also restore git to previous state
|
||||
git reset --hard && git clean -dfx
|
||||
|
||||
# Stop containers, delete images
|
||||
docker stop builder
|
||||
docker rm --force builder
|
||||
docker image rm local
|
||||
done
|
||||
|
||||
# Cleanup
|
||||
rm -rf -- "$tempdir"
|
||||
cd "$init_dir"
|
||||
|
||||
# Final stage: compare hashes
|
||||
|
||||
# Path to binaries
|
||||
path_bin="$init_dir/$TAG-$repo_name"
|
||||
|
||||
# Assume everything is okay for now
|
||||
bad=0
|
||||
|
||||
# Check hashes for all binaries
|
||||
for file in "$path_bin"/from-source/*; do
|
||||
# Extract binary name
|
||||
app="$(basename $file)"
|
||||
|
||||
# Compute hash for compiled binary
|
||||
compiled=$(sha256sum "$path_bin/from-source/$app" | awk '{print $1}')
|
||||
# Compute hash for prebuilt binary
|
||||
prebuilt=$(sha256sum "$path_bin/prebuilt/$app" | awk '{print $1}')
|
||||
|
||||
# Compare
|
||||
if [ "$compiled" != "$prebuilt" ]; then
|
||||
# If hashes doesn't match, set bad...
|
||||
bad=1
|
||||
|
||||
# ... and print affected binary
|
||||
printf "%s - sha256sum hash doesn't match\n" "$app"
|
||||
fi
|
||||
done
|
||||
|
||||
# If everything is still okay, compute checksums file
|
||||
if [ "$bad" = 0 ]; then
|
||||
sha256sum "$path_bin"/from-source/* | sed -e "s|$PWD/||g" -e 's|from-source/||g' -e "s|-$repo_name||g" > "$path_bin/_sha256sums"
|
||||
|
||||
printf 'Checksums computed - %s\n' "$path_bin/_sha256sums"
|
||||
fi
|
||||
+163
-69
@@ -1,7 +1,7 @@
|
||||
cabal-version: 1.12
|
||||
|
||||
name: simplexmq
|
||||
version: 6.2.0.4
|
||||
version: 6.4.0.4
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
<./docs/Simplex-Messaging-Client.html client> and
|
||||
@@ -67,13 +67,22 @@ flag client_library
|
||||
manual: True
|
||||
default: False
|
||||
|
||||
flag client_postgres
|
||||
description: Build with PostgreSQL instead of SQLite.
|
||||
manual: True
|
||||
default: False
|
||||
|
||||
flag server_postgres
|
||||
description: Build server with support of PostgreSQL.
|
||||
manual: True
|
||||
default: False
|
||||
|
||||
library
|
||||
exposed-modules:
|
||||
Simplex.FileTransfer.Agent
|
||||
Simplex.FileTransfer.Chunks
|
||||
Simplex.FileTransfer.Client
|
||||
Simplex.FileTransfer.Client.Agent
|
||||
Simplex.FileTransfer.Client.Main
|
||||
Simplex.FileTransfer.Client.Presets
|
||||
Simplex.FileTransfer.Crypto
|
||||
Simplex.FileTransfer.Description
|
||||
@@ -91,46 +100,14 @@ library
|
||||
Simplex.Messaging.Agent.RetryInterval
|
||||
Simplex.Messaging.Agent.Stats
|
||||
Simplex.Messaging.Agent.Store
|
||||
Simplex.Messaging.Agent.Store.SQLite
|
||||
Simplex.Messaging.Agent.Store.SQLite.Common
|
||||
Simplex.Messaging.Agent.Store.SQLite.DB
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220101_initial
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220301_snd_queue_keys
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220322_notifications
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220608_v2
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220625_v2_ntf_mode
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220811_onion_hosts
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220817_connection_ntfs
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220905_commands
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220915_connection_queues
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230110_users
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230117_fkey_indexes
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230120_delete_errors
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230217_server_key_hash
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230223_files
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230320_retry_state
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230401_snd_files
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230510_files_pending_replicas_indexes
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230516_encrypted_rcv_message_hashes
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230531_switch_status
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230615_ratchet_sync
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230701_delivery_receipts
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230720_delete_expired_messages
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230722_indexes
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230814_indexes
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230829_crypto_files
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20231222_command_created_at
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20231225_failed_work_items
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240121_message_delivery_indexes
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240124_file_redirect
|
||||
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.M20240624_snd_secure
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240702_servers_stats
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240930_ntf_tokens_to_delete
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20241007_rcv_queues_last_broker_ts
|
||||
Simplex.Messaging.Agent.Store.AgentStore
|
||||
Simplex.Messaging.Agent.Store.Common
|
||||
Simplex.Messaging.Agent.Store.DB
|
||||
Simplex.Messaging.Agent.Store.Interface
|
||||
Simplex.Messaging.Agent.Store.Migrations
|
||||
Simplex.Messaging.Agent.Store.Migrations.App
|
||||
Simplex.Messaging.Agent.Store.Postgres.Options
|
||||
Simplex.Messaging.Agent.Store.Shared
|
||||
Simplex.Messaging.Agent.TRcvQueues
|
||||
Simplex.Messaging.Client
|
||||
Simplex.Messaging.Client.Agent
|
||||
@@ -144,6 +121,7 @@ library
|
||||
Simplex.Messaging.Crypto.SNTRUP761.Bindings.Defines
|
||||
Simplex.Messaging.Crypto.SNTRUP761.Bindings.FFI
|
||||
Simplex.Messaging.Crypto.SNTRUP761.Bindings.RNG
|
||||
Simplex.Messaging.Crypto.ShortLink
|
||||
Simplex.Messaging.Encoding
|
||||
Simplex.Messaging.Encoding.String
|
||||
Simplex.Messaging.Notifications.Client
|
||||
@@ -152,8 +130,8 @@ library
|
||||
Simplex.Messaging.Notifications.Types
|
||||
Simplex.Messaging.Parsers
|
||||
Simplex.Messaging.Protocol
|
||||
Simplex.Messaging.Server.CLI
|
||||
Simplex.Messaging.Server.Expiration
|
||||
Simplex.Messaging.Server.QueueStore.Postgres.Config
|
||||
Simplex.Messaging.Server.QueueStore.QueueInfo
|
||||
Simplex.Messaging.ServiceScheme
|
||||
Simplex.Messaging.Session
|
||||
@@ -168,7 +146,6 @@ library
|
||||
Simplex.Messaging.Transport.HTTP2.Server
|
||||
Simplex.Messaging.Transport.KeepAlive
|
||||
Simplex.Messaging.Transport.Server
|
||||
Simplex.Messaging.Transport.WebSockets
|
||||
Simplex.Messaging.Util
|
||||
Simplex.Messaging.Version
|
||||
Simplex.Messaging.Version.Internal
|
||||
@@ -177,8 +154,61 @@ library
|
||||
Simplex.RemoteControl.Discovery.Multicast
|
||||
Simplex.RemoteControl.Invitation
|
||||
Simplex.RemoteControl.Types
|
||||
if flag(client_postgres)
|
||||
exposed-modules:
|
||||
Simplex.Messaging.Agent.Store.Postgres.Migrations.App
|
||||
Simplex.Messaging.Agent.Store.Postgres.Migrations.M20241210_initial
|
||||
Simplex.Messaging.Agent.Store.Postgres.Migrations.M20250203_msg_bodies
|
||||
Simplex.Messaging.Agent.Store.Postgres.Migrations.M20250322_short_links
|
||||
else
|
||||
exposed-modules:
|
||||
Simplex.Messaging.Agent.Store.SQLite
|
||||
Simplex.Messaging.Agent.Store.SQLite.Common
|
||||
Simplex.Messaging.Agent.Store.SQLite.DB
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.App
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220101_initial
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220301_snd_queue_keys
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220322_notifications
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220608_v2
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220625_v2_ntf_mode
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220811_onion_hosts
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220817_connection_ntfs
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220905_commands
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220915_connection_queues
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230110_users
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230117_fkey_indexes
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230120_delete_errors
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230217_server_key_hash
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230223_files
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230320_retry_state
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230401_snd_files
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230510_files_pending_replicas_indexes
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230516_encrypted_rcv_message_hashes
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230531_switch_status
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230615_ratchet_sync
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230701_delivery_receipts
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230720_delete_expired_messages
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230722_indexes
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230814_indexes
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230829_crypto_files
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20231222_command_created_at
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20231225_failed_work_items
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240121_message_delivery_indexes
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240124_file_redirect
|
||||
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.M20240624_snd_secure
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240702_servers_stats
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240930_ntf_tokens_to_delete
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20241007_rcv_queues_last_broker_ts
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20241224_ratchet_e2e_snd_params
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20250203_msg_bodies
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20250322_short_links
|
||||
if !flag(client_library)
|
||||
exposed-modules:
|
||||
Simplex.FileTransfer.Client.Main
|
||||
Simplex.FileTransfer.Server
|
||||
Simplex.FileTransfer.Server.Control
|
||||
Simplex.FileTransfer.Server.Env
|
||||
@@ -187,29 +217,52 @@ library
|
||||
Simplex.FileTransfer.Server.Store
|
||||
Simplex.FileTransfer.Server.StoreLog
|
||||
Simplex.Messaging.Server
|
||||
Simplex.Messaging.Server.CLI
|
||||
Simplex.Messaging.Server.Control
|
||||
Simplex.Messaging.Server.Env.STM
|
||||
Simplex.Messaging.Server.Information
|
||||
Simplex.Messaging.Server.Main
|
||||
Simplex.Messaging.Server.Main.Init
|
||||
Simplex.Messaging.Server.MsgStore
|
||||
Simplex.Messaging.Server.MsgStore.Journal
|
||||
Simplex.Messaging.Server.MsgStore.Journal.SharedLock
|
||||
Simplex.Messaging.Server.MsgStore.STM
|
||||
Simplex.Messaging.Server.MsgStore.Types
|
||||
Simplex.Messaging.Server.NtfStore
|
||||
Simplex.Messaging.Server.Prometheus
|
||||
Simplex.Messaging.Server.QueueStore
|
||||
Simplex.Messaging.Server.QueueStore.STM
|
||||
Simplex.Messaging.Server.QueueStore.Types
|
||||
Simplex.Messaging.Server.Stats
|
||||
Simplex.Messaging.Server.StoreLog
|
||||
Simplex.Messaging.Server.StoreLog.ReadWrite
|
||||
Simplex.Messaging.Server.StoreLog.Types
|
||||
Simplex.Messaging.Notifications.Server
|
||||
Simplex.Messaging.Notifications.Server.Control
|
||||
Simplex.Messaging.Notifications.Server.Env
|
||||
Simplex.Messaging.Notifications.Server.Main
|
||||
Simplex.Messaging.Notifications.Server.Push.APNS
|
||||
Simplex.Messaging.Notifications.Server.Push.APNS.Internal
|
||||
Simplex.Messaging.Notifications.Server.Stats
|
||||
Simplex.Messaging.Notifications.Server.Store
|
||||
Simplex.Messaging.Notifications.Server.StoreLog
|
||||
Simplex.Messaging.Transport.WebSockets
|
||||
if flag(client_postgres) || flag(server_postgres)
|
||||
exposed-modules:
|
||||
Simplex.Messaging.Agent.Store.Postgres
|
||||
Simplex.Messaging.Agent.Store.Postgres.Common
|
||||
Simplex.Messaging.Agent.Store.Postgres.DB
|
||||
Simplex.Messaging.Agent.Store.Postgres.Migrations
|
||||
Simplex.Messaging.Agent.Store.Postgres.Util
|
||||
|
||||
if flag(server_postgres)
|
||||
exposed-modules:
|
||||
Simplex.Messaging.Notifications.Server
|
||||
Simplex.Messaging.Notifications.Server.Control
|
||||
Simplex.Messaging.Notifications.Server.Env
|
||||
Simplex.Messaging.Notifications.Server.Main
|
||||
Simplex.Messaging.Notifications.Server.Prometheus
|
||||
Simplex.Messaging.Notifications.Server.Push.APNS
|
||||
Simplex.Messaging.Notifications.Server.Push.APNS.Internal
|
||||
Simplex.Messaging.Notifications.Server.Stats
|
||||
Simplex.Messaging.Notifications.Server.Store
|
||||
Simplex.Messaging.Notifications.Server.Store.Migrations
|
||||
Simplex.Messaging.Notifications.Server.Store.Postgres
|
||||
Simplex.Messaging.Notifications.Server.Store.Types
|
||||
Simplex.Messaging.Notifications.Server.StoreLog
|
||||
Simplex.Messaging.Server.QueueStore.Postgres
|
||||
Simplex.Messaging.Server.QueueStore.Postgres.Migrations
|
||||
other-modules:
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
@@ -241,13 +294,11 @@ library
|
||||
, crypton-x509-validation ==1.6.*
|
||||
, cryptostore ==0.3.*
|
||||
, data-default ==0.7.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, filepath ==1.4.*
|
||||
, hourglass ==0.2.*
|
||||
, http-types ==0.12.*
|
||||
, http2 >=4.2.2 && <4.3
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.18.*
|
||||
@@ -256,21 +307,16 @@ library
|
||||
, network-info ==0.2.*
|
||||
, network-transport ==0.5.6
|
||||
, network-udp ==0.0.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
, simple-logger ==0.1.*
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, temporary ==1.3.*
|
||||
, time ==1.12.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.9.0 && <1.10
|
||||
, transformers ==0.6.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, yaml ==0.11.*
|
||||
, zstd ==0.1.3.*
|
||||
default-language: Haskell2010
|
||||
@@ -280,6 +326,26 @@ library
|
||||
build-depends:
|
||||
case-insensitive ==1.2.*
|
||||
, hashable ==1.4.*
|
||||
, ini ==0.4.1
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, temporary ==1.3.*
|
||||
, websockets ==0.12.*
|
||||
if flag(client_postgres) || flag(server_postgres)
|
||||
build-depends:
|
||||
postgresql-libpq >=0.10.0.0
|
||||
, postgresql-simple ==0.7.*
|
||||
, raw-strings-qq ==1.1.*
|
||||
if flag(client_postgres)
|
||||
cpp-options: -DdbPostgres
|
||||
else
|
||||
build-depends:
|
||||
direct-sqlcipher ==2.3.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
if flag(server_postgres)
|
||||
cpp-options: -DdbServerPostgres
|
||||
build-depends:
|
||||
hex-text ==0.1.*
|
||||
if impl(ghc >= 9.6.2)
|
||||
build-depends:
|
||||
bytestring ==0.11.*
|
||||
@@ -292,6 +358,10 @@ library
|
||||
executable ntf-server
|
||||
if flag(client_library)
|
||||
buildable: False
|
||||
if flag(server_postgres)
|
||||
cpp-options: -DdbServerPostgres
|
||||
else
|
||||
buildable: False
|
||||
main-is: Main.hs
|
||||
other-modules:
|
||||
Paths_simplexmq
|
||||
@@ -309,6 +379,8 @@ executable ntf-server
|
||||
executable smp-server
|
||||
if flag(client_library)
|
||||
buildable: False
|
||||
if flag(server_postgres)
|
||||
cpp-options: -DdbServerPostgres
|
||||
main-is: Main.hs
|
||||
other-modules:
|
||||
Static
|
||||
@@ -382,10 +454,8 @@ test-suite simplexmq-test
|
||||
AgentTests.EqInstances
|
||||
AgentTests.FunctionalAPITests
|
||||
AgentTests.MigrationTests
|
||||
AgentTests.NotificationTests
|
||||
AgentTests.SchemaDump
|
||||
AgentTests.ServerChoice
|
||||
AgentTests.SQLiteTests
|
||||
AgentTests.ShortLinkTests
|
||||
CLITests
|
||||
CoreTests.BatchingTests
|
||||
CoreTests.CryptoFileTests
|
||||
@@ -399,8 +469,6 @@ test-suite simplexmq-test
|
||||
CoreTests.UtilTests
|
||||
CoreTests.VersionRangeTests
|
||||
FileDescriptionTests
|
||||
NtfClient
|
||||
NtfServerTests
|
||||
RemoteControl
|
||||
ServerTests
|
||||
SMPAgentClient
|
||||
@@ -414,11 +482,25 @@ test-suite simplexmq-test
|
||||
Static
|
||||
Static.Embedded
|
||||
Paths_simplexmq
|
||||
if flag(client_postgres)
|
||||
other-modules:
|
||||
Fixtures
|
||||
else
|
||||
other-modules:
|
||||
AgentTests.SchemaDump
|
||||
AgentTests.SQLiteTests
|
||||
if flag(server_postgres)
|
||||
other-modules:
|
||||
AgentTests.NotificationTests
|
||||
NtfClient
|
||||
NtfServerTests
|
||||
PostgresSchemaDump
|
||||
hs-source-dirs:
|
||||
tests
|
||||
apps/smp-server/web
|
||||
default-extensions:
|
||||
StrictData
|
||||
-- add -fhpc to ghc-options to run tests with coverage
|
||||
ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2 -threaded -rtsopts -with-rtsopts=-A64M -with-rtsopts=-N1
|
||||
build-depends:
|
||||
base
|
||||
@@ -431,13 +513,13 @@ test-suite simplexmq-test
|
||||
, crypton-x509
|
||||
, crypton-x509-store
|
||||
, crypton-x509-validation
|
||||
, deepseq ==1.4.*
|
||||
, directory
|
||||
, file-embed
|
||||
, filepath
|
||||
, generic-random ==1.5.*
|
||||
, hashable
|
||||
, hspec ==2.11.*
|
||||
, hspec-core ==2.11.*
|
||||
, http-client
|
||||
, http-types
|
||||
, http2
|
||||
@@ -445,16 +527,13 @@ test-suite simplexmq-test
|
||||
, ini
|
||||
, iso8601-time
|
||||
, main-tester ==0.2.*
|
||||
, memory
|
||||
, mtl
|
||||
, network
|
||||
, process
|
||||
, QuickCheck ==2.14.*
|
||||
, random
|
||||
, silently ==1.2.*
|
||||
, simple-logger
|
||||
, simplexmq
|
||||
, sqlcipher-simple
|
||||
, stm
|
||||
, text
|
||||
, time
|
||||
@@ -469,3 +548,18 @@ test-suite simplexmq-test
|
||||
, warp-tls
|
||||
, yaml
|
||||
default-language: Haskell2010
|
||||
if flag(server_postgres)
|
||||
cpp-options: -DdbServerPostgres
|
||||
if flag(client_postgres)
|
||||
cpp-options: -DdbPostgres
|
||||
else
|
||||
build-depends:
|
||||
memory
|
||||
, sqlcipher-simple
|
||||
if !flag(client_postgres) || flag(server_postgres)
|
||||
build-depends:
|
||||
deepseq ==1.4.*
|
||||
, process
|
||||
if flag(client_postgres) || flag(server_postgres)
|
||||
build-depends:
|
||||
postgresql-simple ==0.7.*
|
||||
|
||||
@@ -51,8 +51,7 @@ 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.Client (XFTPChunkSpec (..), getChunkDigest, prepareChunkSizes, prepareChunkSpecs, singleChunkSize)
|
||||
import Simplex.FileTransfer.Crypto
|
||||
import Simplex.FileTransfer.Description
|
||||
import Simplex.FileTransfer.Protocol (FileParty (..), SFileParty (..))
|
||||
@@ -66,8 +65,8 @@ import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Agent.Stats
|
||||
import Simplex.Messaging.Agent.Store.SQLite
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import Simplex.Messaging.Agent.Store.AgentStore
|
||||
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs)
|
||||
import qualified Simplex.Messaging.Crypto.File as CF
|
||||
@@ -546,8 +545,8 @@ runXFTPSndWorker c srv Worker {doWork} = do
|
||||
withStore' c $ \db -> updateSndFileComplete db sndFileId
|
||||
where
|
||||
addRecipients :: SndFileChunk -> SndFileChunkReplica -> AM SndFileChunkReplica
|
||||
addRecipients ch@SndFileChunk {numRecipients} cr@SndFileChunkReplica {rcvIdsKeys}
|
||||
| length rcvIdsKeys > numRecipients = throwE $ INTERNAL "too many recipients"
|
||||
addRecipients ch@SndFileChunk {numRecipients} cr@SndFileChunkReplica {sndChunkReplicaId, rcvIdsKeys}
|
||||
| length rcvIdsKeys > numRecipients = throwE $ INTERNAL ("too many recipients, sndChunkReplicaId = " <> show sndChunkReplicaId)
|
||||
| length rcvIdsKeys == numRecipients = pure cr
|
||||
| otherwise = do
|
||||
let numRecipients' = min (numRecipients - length rcvIdsKeys) maxRecipients
|
||||
|
||||
@@ -20,14 +20,18 @@ import Data.Bifunctor (first)
|
||||
import Data.ByteString.Builder (Builder, byteString)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy as LB
|
||||
import Data.Int (Int64)
|
||||
import Data.List (foldl')
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import Data.Maybe (listToMaybe)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Data.Word (Word32)
|
||||
import qualified Data.X509 as X
|
||||
import qualified Data.X509.Validation as XV
|
||||
import qualified Network.HTTP.Types as N
|
||||
import qualified Network.HTTP2.Client as H
|
||||
import Simplex.FileTransfer.Chunks
|
||||
import Simplex.FileTransfer.Protocol
|
||||
import Simplex.FileTransfer.Transport
|
||||
import Simplex.Messaging.Client
|
||||
@@ -298,3 +302,41 @@ noFile HTTP2Body {bodyPart} a = case bodyPart of
|
||||
|
||||
-- FACK :: FileCommand Recipient
|
||||
-- PING :: FileCommand Recipient
|
||||
|
||||
singleChunkSize :: Int64 -> Maybe Word32
|
||||
singleChunkSize size' =
|
||||
listToMaybe $ dropWhile (< chunkSize) serverChunkSizes
|
||||
where
|
||||
chunkSize = fromIntegral size'
|
||||
|
||||
prepareChunkSizes :: Int64 -> [Word32]
|
||||
prepareChunkSizes size' = prepareSizes size'
|
||||
where
|
||||
(smallSize, bigSize)
|
||||
| size' > size34 chunkSize3 = (chunkSize2, chunkSize3)
|
||||
| size' > size34 chunkSize2 = (chunkSize1, chunkSize2)
|
||||
| otherwise = (chunkSize0, chunkSize1)
|
||||
size34 sz = (fromIntegral sz * 3) `div` 4
|
||||
prepareSizes 0 = []
|
||||
prepareSizes size
|
||||
| size >= fromIntegral bigSize = replicate (fromIntegral n1) bigSize <> prepareSizes remSz
|
||||
| size > size34 bigSize = [bigSize]
|
||||
| otherwise = replicate (fromIntegral n2') smallSize
|
||||
where
|
||||
(n1, remSz) = size `divMod` fromIntegral bigSize
|
||||
n2' = let (n2, remSz2) = (size `divMod` fromIntegral smallSize) in if remSz2 == 0 then n2 else n2 + 1
|
||||
|
||||
prepareChunkSpecs :: FilePath -> [Word32] -> [XFTPChunkSpec]
|
||||
prepareChunkSpecs filePath chunkSizes = reverse . snd $ foldl' addSpec (0, []) chunkSizes
|
||||
where
|
||||
addSpec :: (Int64, [XFTPChunkSpec]) -> Word32 -> (Int64, [XFTPChunkSpec])
|
||||
addSpec (chunkOffset, specs) sz =
|
||||
let spec = XFTPChunkSpec {filePath, chunkOffset, chunkSize = sz}
|
||||
in (chunkOffset + fromIntegral sz, spec : specs)
|
||||
|
||||
getChunkDigest :: XFTPChunkSpec -> IO ByteString
|
||||
getChunkDigest XFTPChunkSpec {filePath = chunkPath, chunkOffset, chunkSize} =
|
||||
withFile chunkPath ReadMode $ \h -> do
|
||||
hSeek h AbsoluteSeek $ fromIntegral chunkOffset
|
||||
chunk <- LB.hGet h (fromIntegral chunkSize)
|
||||
pure $! LC.sha256Hash chunk
|
||||
|
||||
@@ -19,11 +19,7 @@ module Simplex.FileTransfer.Client.Main
|
||||
singleChunkSize,
|
||||
prepareChunkSizes,
|
||||
prepareChunkSpecs,
|
||||
maxFileSize,
|
||||
maxFileSizeHard,
|
||||
fileSizeLen,
|
||||
getChunkDigest,
|
||||
SentRecipientReplica (..),
|
||||
)
|
||||
where
|
||||
|
||||
@@ -34,7 +30,6 @@ import Control.Monad.Trans.Except
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bifunctor (first)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Char (toLower)
|
||||
@@ -45,7 +40,7 @@ import Data.List.NonEmpty (NonEmpty (..), nonEmpty)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (fromMaybe, listToMaybe)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import qualified Data.Text as T
|
||||
import Data.Word (Word32)
|
||||
import GHC.Records (HasField (getField))
|
||||
@@ -80,20 +75,6 @@ import UnliftIO.Directory
|
||||
xftpClientVersion :: String
|
||||
xftpClientVersion = "1.0.1"
|
||||
|
||||
-- | Soft limit for XFTP clients. Should be checked and reported to user.
|
||||
maxFileSize :: Int64
|
||||
maxFileSize = gb 1
|
||||
|
||||
maxFileSizeStr :: String
|
||||
maxFileSizeStr = B.unpack . strEncode $ FileSize maxFileSize
|
||||
|
||||
-- | Hard internal limit for XFTP agent after which it refuses to prepare chunks.
|
||||
maxFileSizeHard :: Int64
|
||||
maxFileSizeHard = gb 5
|
||||
|
||||
fileSizeLen :: Int64
|
||||
fileSizeLen = 8
|
||||
|
||||
newtype CLIError = CLIError String
|
||||
deriving (Eq, Show, Exception)
|
||||
|
||||
@@ -231,16 +212,6 @@ data SentFileChunkReplica = SentFileChunkReplica
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data SentRecipientReplica = SentRecipientReplica
|
||||
{ chunkNo :: Int,
|
||||
server :: XFTPServer,
|
||||
rcvNo :: Int,
|
||||
replicaId :: ChunkReplicaId,
|
||||
replicaKey :: C.APrivateAuthKey,
|
||||
digest :: FileDigest,
|
||||
chunkSize :: FileSize Word32
|
||||
}
|
||||
|
||||
logCfg :: LogConfig
|
||||
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
@@ -309,7 +280,7 @@ cliSendFileOpts SendOptions {filePath, outputDir, numRecipients, xftpServers, re
|
||||
let chunkSpecs = prepareChunkSpecs encPath chunkSizes
|
||||
fdRcv = FileDescription {party = SFRecipient, size = FileSize encSize, digest = FileDigest digest, key, nonce, chunkSize = FileSize defChunkSize, chunks = [], redirect = Nothing}
|
||||
fdSnd = FileDescription {party = SFSender, size = FileSize encSize, digest = FileDigest digest, key, nonce, chunkSize = FileSize defChunkSize, chunks = [], redirect = Nothing}
|
||||
logInfo $ "encrypted file to " <> tshow encPath
|
||||
logDebug $ "encrypted file to " <> tshow encPath
|
||||
pure (encPath, fdRcv, fdSnd, chunkSpecs, encSize)
|
||||
uploadFile :: TVar ChaChaDRG -> [XFTPChunkSpec] -> TVar [Int64] -> Int64 -> ExceptT CLIError IO [SentFileChunk]
|
||||
uploadFile g chunks uploadedChunks encSize = do
|
||||
@@ -322,14 +293,14 @@ cliSendFileOpts SendOptions {filePath, outputDir, numRecipients, xftpServers, re
|
||||
-- TODO shuffle/unshuffle chunks
|
||||
-- the reason we don't do pooled downloads here within one server is that http2 library doesn't handle cleint concurrency, even though
|
||||
-- upload doesn't allow other requests within the same client until complete (but download does allow).
|
||||
logInfo $ "uploading " <> tshow (length chunks) <> " chunks..."
|
||||
logDebug $ "uploading " <> tshow (length chunks) <> " chunks..."
|
||||
(errs, rs) <- partitionEithers . concat <$> liftIO (pooledForConcurrentlyN 16 chunks' . mapM $ runExceptT . uploadFileChunk a)
|
||||
mapM_ throwE errs
|
||||
pure $ map snd (sortOn fst rs)
|
||||
where
|
||||
uploadFileChunk :: XFTPClientAgent -> (Int, XFTPChunkSpec, XFTPServerWithAuth) -> ExceptT CLIError IO (Int, SentFileChunk)
|
||||
uploadFileChunk a (chunkNo, chunkSpec@XFTPChunkSpec {chunkSize}, ProtoServerWithAuth xftpServer auth) = do
|
||||
logInfo $ "uploading chunk " <> tshow chunkNo <> " to " <> showServer xftpServer <> "..."
|
||||
logDebug $ "uploading chunk " <> tshow chunkNo <> " to " <> showServer xftpServer <> "..."
|
||||
(sndKey, spKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
|
||||
rKeys <- atomically $ L.fromList <$> replicateM numRecipients (C.generateAuthKeyPair C.SEd25519 g)
|
||||
digest <- liftIO $ getChunkDigest chunkSpec
|
||||
@@ -337,7 +308,7 @@ cliSendFileOpts SendOptions {filePath, outputDir, numRecipients, xftpServers, re
|
||||
c <- withRetry retryCount $ getXFTPServerClient a xftpServer
|
||||
(sndId, rIds) <- withRetry retryCount $ createXFTPChunk c spKey ch (L.map fst rKeys) auth
|
||||
withReconnect a xftpServer retryCount $ \c' -> uploadXFTPChunk c' spKey sndId chunkSpec
|
||||
logInfo $ "uploaded chunk " <> tshow chunkNo
|
||||
logDebug $ "uploaded chunk " <> tshow chunkNo
|
||||
uploaded <- atomically . stateTVar uploadedChunks $ \cs ->
|
||||
let cs' = fromIntegral chunkSize : cs in (sum cs', cs')
|
||||
liftIO $ do
|
||||
@@ -414,13 +385,6 @@ cliSendFileOpts SendOptions {filePath, outputDir, numRecipients, xftpServers, re
|
||||
B.writeFile fdSndPath $ strEncode fdSnd
|
||||
pure (fdRcvPaths, fdSndPath)
|
||||
|
||||
getChunkDigest :: XFTPChunkSpec -> IO ByteString
|
||||
getChunkDigest XFTPChunkSpec {filePath = chunkPath, chunkOffset, chunkSize} =
|
||||
withFile chunkPath ReadMode $ \h -> do
|
||||
hSeek h AbsoluteSeek $ fromIntegral chunkOffset
|
||||
chunk <- LB.hGet h (fromIntegral chunkSize)
|
||||
pure $! LC.sha256Hash chunk
|
||||
|
||||
cliReceiveFile :: ReceiveOptions -> ExceptT CLIError IO ()
|
||||
cliReceiveFile ReceiveOptions {fileDescription, filePath, retryCount, tempPath, verbose, yes} =
|
||||
getFileDescription' fileDescription >>= receive
|
||||
@@ -454,11 +418,11 @@ cliReceiveFile ReceiveOptions {fileDescription, filePath, retryCount, tempPath,
|
||||
downloadFileChunk :: TVar ChaChaDRG -> XFTPClientAgent -> FilePath -> FileSize Int64 -> TVar [Int64] -> FileChunk -> ExceptT CLIError IO (Int, FilePath)
|
||||
downloadFileChunk g a encPath (FileSize encSize) downloadedChunks FileChunk {chunkNo, chunkSize, digest, replicas = replica : _} = do
|
||||
let FileChunkReplica {server, replicaId, replicaKey} = replica
|
||||
logInfo $ "downloading chunk " <> tshow chunkNo <> " from " <> showServer server <> "..."
|
||||
logDebug $ "downloading chunk " <> tshow chunkNo <> " from " <> showServer server <> "..."
|
||||
chunkPath <- uniqueCombine encPath $ show chunkNo
|
||||
let chunkSpec = XFTPRcvChunkSpec chunkPath (unFileSize chunkSize) (unFileDigest digest)
|
||||
withReconnect a server retryCount $ \c -> downloadXFTPChunk g c replicaKey (unChunkReplicaId replicaId) chunkSpec
|
||||
logInfo $ "downloaded chunk " <> tshow chunkNo <> " to " <> T.pack chunkPath
|
||||
logDebug $ "downloaded chunk " <> tshow chunkNo <> " to " <> T.pack chunkPath
|
||||
downloaded <- atomically . stateTVar downloadedChunks $ \cs ->
|
||||
let cs' = fromIntegral (unFileSize chunkSize) : cs in (sum cs', cs')
|
||||
liftIO $ do
|
||||
@@ -503,7 +467,7 @@ cliDeleteFile DeleteOptions {fileDescription, retryCount, yes} = do
|
||||
deleteFileChunk a FileChunk {chunkNo, replicas = replica : _} = do
|
||||
let FileChunkReplica {server, replicaId, replicaKey} = replica
|
||||
withReconnect a server retryCount $ \c -> deleteXFTPChunk c replicaKey (unChunkReplicaId replicaId)
|
||||
logInfo $ "deleted chunk " <> tshow chunkNo <> " from " <> showServer server
|
||||
logDebug $ "deleted chunk " <> tshow chunkNo <> " from " <> showServer server
|
||||
deleteFileChunk _ _ = throwE $ CLIError "chunk has no replicas"
|
||||
|
||||
cliFileDescrInfo :: InfoOptions -> ExceptT CLIError IO ()
|
||||
@@ -536,37 +500,6 @@ getFileDescription' path =
|
||||
getFileDescription path >>= \case
|
||||
AVFD fd -> either (throwE . CLIError) pure $ checkParty fd
|
||||
|
||||
singleChunkSize :: Int64 -> Maybe Word32
|
||||
singleChunkSize size' =
|
||||
listToMaybe $ dropWhile (< chunkSize) serverChunkSizes
|
||||
where
|
||||
chunkSize = fromIntegral size'
|
||||
|
||||
prepareChunkSizes :: Int64 -> [Word32]
|
||||
prepareChunkSizes size' = prepareSizes size'
|
||||
where
|
||||
(smallSize, bigSize)
|
||||
| size' > size34 chunkSize3 = (chunkSize2, chunkSize3)
|
||||
| size' > size34 chunkSize2 = (chunkSize1, chunkSize2)
|
||||
| otherwise = (chunkSize0, chunkSize1)
|
||||
size34 sz = (fromIntegral sz * 3) `div` 4
|
||||
prepareSizes 0 = []
|
||||
prepareSizes size
|
||||
| size >= fromIntegral bigSize = replicate (fromIntegral n1) bigSize <> prepareSizes remSz
|
||||
| size > size34 bigSize = [bigSize]
|
||||
| otherwise = replicate (fromIntegral n2') smallSize
|
||||
where
|
||||
(n1, remSz) = size `divMod` fromIntegral bigSize
|
||||
n2' = let (n2, remSz2) = (size `divMod` fromIntegral smallSize) in if remSz2 == 0 then n2 else n2 + 1
|
||||
|
||||
prepareChunkSpecs :: FilePath -> [Word32] -> [XFTPChunkSpec]
|
||||
prepareChunkSpecs filePath chunkSizes = reverse . snd $ foldl' addSpec (0, []) chunkSizes
|
||||
where
|
||||
addSpec :: (Int64, [XFTPChunkSpec]) -> Word32 -> (Int64, [XFTPChunkSpec])
|
||||
addSpec (chunkOffset, specs) sz =
|
||||
let spec = XFTPChunkSpec {filePath, chunkOffset, chunkSize = sz}
|
||||
in (chunkOffset + fromIntegral sz, spec : specs)
|
||||
|
||||
getEncPath :: MonadIO m => Maybe FilePath -> String -> m FilePath
|
||||
getEncPath path name = (`uniqueCombine` (name <> ".encrypted")) =<< maybe (liftIO getCanonicalTemporaryDirectory) pure path
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
|
||||
@@ -37,6 +38,10 @@ module Simplex.FileTransfer.Description
|
||||
FileClientData,
|
||||
fileDescriptionURI,
|
||||
qrSizeLimit,
|
||||
maxFileSize,
|
||||
maxFileSizeStr,
|
||||
maxFileSizeHard,
|
||||
fileSizeLen,
|
||||
)
|
||||
where
|
||||
|
||||
@@ -62,11 +67,10 @@ import Data.Text (Text)
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Word (Word32)
|
||||
import qualified Data.Yaml as Y
|
||||
import Database.SQLite.Simple.FromField (FromField (..))
|
||||
import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import Simplex.FileTransfer.Chunks
|
||||
import Simplex.FileTransfer.Protocol
|
||||
import Simplex.Messaging.Agent.QueryString
|
||||
import Simplex.Messaging.Agent.Store.DB (Binary (..), FromField (..), ToField (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (defaultJSON, parseAll)
|
||||
@@ -109,6 +113,9 @@ fdSeparator = "################################\n"
|
||||
|
||||
newtype FileDigest = FileDigest {unFileDigest :: ByteString}
|
||||
deriving (Eq, Show)
|
||||
deriving newtype (FromField)
|
||||
|
||||
instance ToField FileDigest where toField (FileDigest s) = toField $ Binary s
|
||||
|
||||
instance StrEncoding FileDigest where
|
||||
strEncode (FileDigest fd) = strEncode fd
|
||||
@@ -122,10 +129,6 @@ instance ToJSON FileDigest where
|
||||
toJSON = strToJSON
|
||||
toEncoding = strToJEncoding
|
||||
|
||||
instance FromField FileDigest where fromField f = FileDigest <$> fromField f
|
||||
|
||||
instance ToField FileDigest where toField (FileDigest s) = toField s
|
||||
|
||||
data FileChunk = FileChunk
|
||||
{ chunkNo :: Int,
|
||||
chunkSize :: FileSize Word32,
|
||||
@@ -266,6 +269,21 @@ instance StrEncoding FileDescriptionURI where
|
||||
qrSizeLimit :: Int
|
||||
qrSizeLimit = 1002 -- ~2 chunks in URLencoded YAML with some spare size for server hosts
|
||||
|
||||
-- | Soft limit for XFTP clients. Should be checked and reported to user.
|
||||
maxFileSize :: Int64
|
||||
maxFileSize = gb 1
|
||||
|
||||
maxFileSizeStr :: String
|
||||
maxFileSizeStr = B.unpack . strEncode $ FileSize maxFileSize
|
||||
|
||||
-- | Hard internal limit for XFTP agent after which it refuses to prepare chunks.
|
||||
maxFileSizeHard :: Int64
|
||||
maxFileSizeHard = gb 5
|
||||
|
||||
fileSizeLen :: Int64
|
||||
fileSizeLen = 8
|
||||
|
||||
|
||||
instance (Integral a, Show a) => StrEncoding (FileSize a) where
|
||||
strEncode (FileSize b)
|
||||
| b' /= 0 = bshow b
|
||||
@@ -288,9 +306,9 @@ instance (Integral a, Show a) => StrEncoding (FileSize a) where
|
||||
instance (Integral a, Show a) => IsString (FileSize a) where
|
||||
fromString = either error id . strDecode . B.pack
|
||||
|
||||
instance FromField a => FromField (FileSize a) where fromField f = FileSize <$> fromField f
|
||||
deriving newtype instance FromField a => FromField (FileSize a)
|
||||
|
||||
instance ToField a => ToField (FileSize a) where toField (FileSize s) = toField s
|
||||
deriving newtype instance ToField a => ToField (FileSize a)
|
||||
|
||||
groupReplicasByServer :: FileSize Word32 -> [FileChunk] -> [NonEmpty FileServerReplica]
|
||||
groupReplicasByServer defChunkSize =
|
||||
|
||||
@@ -25,7 +25,7 @@ import Data.List.NonEmpty (NonEmpty (..))
|
||||
import Data.Maybe (isNothing)
|
||||
import Data.Type.Equality
|
||||
import Data.Word (Word32)
|
||||
import Simplex.FileTransfer.Transport (XFTPErrorType (..), XFTPVersion, xftpClientHandshakeStub)
|
||||
import Simplex.FileTransfer.Transport (XFTPErrorType (..), XFTPVersion, blockedFilesXFTPVersion, xftpClientHandshakeStub)
|
||||
import Simplex.Messaging.Client (authTransmission)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
@@ -276,12 +276,14 @@ data FileResponse
|
||||
|
||||
instance ProtocolEncoding XFTPVersion XFTPErrorType FileResponse where
|
||||
type Tag FileResponse = FileResponseTag
|
||||
encodeProtocol _v = \case
|
||||
encodeProtocol v = \case
|
||||
FRSndIds fId rIds -> e (FRSndIds_, ' ', fId, rIds)
|
||||
FRRcvIds rIds -> e (FRRcvIds_, ' ', rIds)
|
||||
FRFile rDhKey nonce -> e (FRFile_, ' ', rDhKey, nonce)
|
||||
FROk -> e FROk_
|
||||
FRErr err -> e (FRErr_, ' ', err)
|
||||
FRErr err -> case err of
|
||||
BLOCKED _ | v < blockedFilesXFTPVersion -> e (FRErr_, ' ', AUTH)
|
||||
_ -> e (FRErr_, ' ', err)
|
||||
FRPong -> e FRPong_
|
||||
where
|
||||
e :: Encoding a => a -> ByteString
|
||||
|
||||
@@ -53,11 +53,11 @@ import qualified Simplex.Messaging.Crypto as C
|
||||
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (CorrId (..), EntityId (..), RcvPublicAuthKey, RcvPublicDhKey, RecipientId, TransmissionAuth, pattern NoEntity)
|
||||
import Simplex.Messaging.Protocol (CorrId (..), BlockingInfo, EntityId (..), RcvPublicAuthKey, RcvPublicDhKey, RecipientId, TransmissionAuth, pattern NoEntity)
|
||||
import Simplex.Messaging.Server (dummyVerifyCmd, verifyCmdAuthorization)
|
||||
import Simplex.Messaging.Server.Control (CPClientRole (..))
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Server.QueueStore (RoundedSystemTime, getRoundedSystemTime)
|
||||
import Simplex.Messaging.Server.QueueStore (RoundedSystemTime, ServerEntityStatus (..), getRoundedSystemTime)
|
||||
import Simplex.Messaging.Server.Stats
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
@@ -181,7 +181,7 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
|
||||
stopServer = do
|
||||
withFileLog closeStoreLog
|
||||
saveServerStats
|
||||
logInfo "Server stopped"
|
||||
logNote "Server stopped"
|
||||
|
||||
expireFilesThread_ :: XFTPServerConfig -> [M ()]
|
||||
expireFilesThread_ XFTPServerConfig {fileExpiration = Just fileExp} = [expireFiles fileExp]
|
||||
@@ -287,11 +287,15 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
|
||||
CPDelete fileId -> withUserRole $ unliftIO u $ do
|
||||
fs <- asks store
|
||||
r <- runExceptT $ do
|
||||
let asSender = ExceptT . atomically $ getFile fs SFSender fileId
|
||||
let asRecipient = ExceptT . atomically $ getFile fs SFRecipient fileId
|
||||
(fr, _) <- asSender `catchError` const asRecipient
|
||||
(fr, _) <- ExceptT $ atomically $ getFile fs SFRecipient fileId
|
||||
ExceptT $ deleteServerFile_ fr
|
||||
liftIO . hPutStrLn h $ either (\e -> "error: " <> show e) (\() -> "ok") r
|
||||
CPBlock fileId info -> withUserRole $ unliftIO u $ do
|
||||
fs <- asks store
|
||||
r <- runExceptT $ do
|
||||
(fr, _) <- ExceptT $ atomically $ getFile fs SFRecipient fileId
|
||||
ExceptT $ blockServerFile fr info
|
||||
liftIO . hPutStrLn h $ either (\e -> "error: " <> show e) (\() -> "ok") r
|
||||
CPHelp -> hPutStrLn h "commands: stats-rts, delete, help, quit"
|
||||
CPQuit -> pure ()
|
||||
CPSkip -> pure ()
|
||||
@@ -321,7 +325,7 @@ processRequest XFTPTransportRequest {thParams, reqBody = body@HTTP2Body {bodyHea
|
||||
let THandleParams {thAuth} = thParams
|
||||
verifyXFTPTransmission ((,C.cbNonce (bs corrId)) <$> thAuth) sig_ signed fId cmd >>= \case
|
||||
VRVerified req -> uncurry send =<< processXFTPRequest body req
|
||||
VRFailed -> send (FRErr AUTH) Nothing
|
||||
VRFailed e -> send (FRErr e) Nothing
|
||||
Left e -> send (FRErr e) Nothing
|
||||
where
|
||||
send resp = sendXFTPResponse (corrId, fId, resp)
|
||||
@@ -355,7 +359,7 @@ randomDelay = do
|
||||
threadDelay $ (d * (1000 + pc)) `div` 1000
|
||||
#endif
|
||||
|
||||
data VerificationResult = VRVerified XFTPRequest | VRFailed
|
||||
data VerificationResult = VRVerified XFTPRequest | VRFailed XFTPErrorType
|
||||
|
||||
verifyXFTPTransmission :: Maybe (THandleAuth 'TServer, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> XFTPFileId -> FileCmd -> M VerificationResult
|
||||
verifyXFTPTransmission auth_ tAuth authorized fId cmd =
|
||||
@@ -367,13 +371,19 @@ verifyXFTPTransmission auth_ tAuth authorized fId cmd =
|
||||
verifyCmd :: SFileParty p -> M VerificationResult
|
||||
verifyCmd party = do
|
||||
st <- asks store
|
||||
atomically $ verify <$> getFile st party fId
|
||||
atomically $ verify =<< getFile st party fId
|
||||
where
|
||||
verify = \case
|
||||
Right (fr, k) -> XFTPReqCmd fId fr cmd `verifyWith` k
|
||||
_ -> maybe False (dummyVerifyCmd Nothing authorized) tAuth `seq` VRFailed
|
||||
Right (fr, k) -> result <$> readTVar (fileStatus fr)
|
||||
where
|
||||
result = \case
|
||||
EntityActive -> XFTPReqCmd fId fr cmd `verifyWith` k
|
||||
EntityBlocked info -> VRFailed $ BLOCKED info
|
||||
EntityOff -> noFileAuth
|
||||
Left _ -> pure noFileAuth
|
||||
noFileAuth = maybe False (dummyVerifyCmd Nothing authorized) tAuth `seq` VRFailed AUTH
|
||||
-- TODO verify with DH authorization
|
||||
req `verifyWith` k = if verifyCmdAuthorization auth_ tAuth authorized k then VRVerified req else VRFailed
|
||||
req `verifyWith` k = if verifyCmdAuthorization auth_ tAuth authorized k then VRVerified req else VRFailed AUTH
|
||||
|
||||
processXFTPRequest :: HTTP2Body -> XFTPRequest -> M (FileResponse, Maybe ServerFile)
|
||||
processXFTPRequest HTTP2Body {bodyPart} = \case
|
||||
@@ -390,7 +400,7 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
|
||||
FACK -> noFile =<< ackFileReception fId fr
|
||||
-- it should never get to the commands below, they are passed in other constructors of XFTPRequest
|
||||
FNEW {} -> noFile $ FRErr INTERNAL
|
||||
PING -> noFile FRPong
|
||||
PING -> noFile $ FRErr INTERNAL
|
||||
XFTPReqPing -> noFile FRPong
|
||||
where
|
||||
noFile resp = pure (resp, Nothing)
|
||||
@@ -405,7 +415,7 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
|
||||
sId <- ExceptT $ addFileRetry st file 3 ts
|
||||
rcps <- mapM (ExceptT . addRecipientRetry st 3 sId) rks
|
||||
lift $ withFileLog $ \sl -> do
|
||||
logAddFile sl sId file ts
|
||||
logAddFile sl sId file ts EntityActive
|
||||
logAddRecipients sl sId rcps
|
||||
stats <- asks serverStats
|
||||
lift $ incFileStat filesCreated
|
||||
@@ -416,7 +426,7 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
|
||||
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
|
||||
ExceptT $ addFile st sId file ts EntityActive
|
||||
pure sId
|
||||
addRecipientRetry :: FileStore -> Int -> XFTPFileId -> RcvPublicAuthKey -> M (Either XFTPErrorType FileRecipient)
|
||||
addRecipientRetry st n sId rpk =
|
||||
@@ -518,15 +528,24 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
|
||||
pure FROk
|
||||
|
||||
deleteServerFile_ :: FileRec -> M (Either XFTPErrorType ())
|
||||
deleteServerFile_ FileRec {senderId, fileInfo, filePath} = do
|
||||
deleteServerFile_ fr@FileRec {senderId} = do
|
||||
withFileLog (`logDeleteFile` senderId)
|
||||
runExceptT $ do
|
||||
path <- readTVarIO filePath
|
||||
stats <- asks serverStats
|
||||
ExceptT $ first (\(_ :: SomeException) -> FILE_IO) <$> try (forM_ path $ \p -> whenM (doesFileExist p) (removeFile p >> deletedStats stats))
|
||||
st <- asks store
|
||||
void $ atomically $ deleteFile st senderId
|
||||
lift $ incFileStat filesDeleted
|
||||
deleteOrBlockServerFile_ fr filesDeleted (`deleteFile` senderId)
|
||||
|
||||
-- this also deletes the file from storage, but doesn't include it in delete statistics
|
||||
blockServerFile :: FileRec -> BlockingInfo -> M (Either XFTPErrorType ())
|
||||
blockServerFile fr@FileRec {senderId} info = do
|
||||
withFileLog $ \sl -> logBlockFile sl senderId info
|
||||
deleteOrBlockServerFile_ fr filesBlocked $ \st -> blockFile st senderId info True
|
||||
|
||||
deleteOrBlockServerFile_ :: FileRec -> (FileServerStats -> IORef Int) -> (FileStore -> STM (Either XFTPErrorType ())) -> M (Either XFTPErrorType ())
|
||||
deleteOrBlockServerFile_ FileRec {filePath, fileInfo} stat storeAction = runExceptT $ do
|
||||
path <- readTVarIO filePath
|
||||
stats <- asks serverStats
|
||||
ExceptT $ first (\(_ :: SomeException) -> FILE_IO) <$> try (forM_ path $ \p -> whenM (doesFileExist p) (removeFile p >> deletedStats stats))
|
||||
st <- asks store
|
||||
void $ atomically $ storeAction st
|
||||
lift $ incFileStat stat
|
||||
where
|
||||
deletedStats stats = do
|
||||
liftIO $ atomicModifyIORef'_ (filesCount stats) (subtract 1)
|
||||
@@ -541,13 +560,13 @@ expireServerFiles itemDelay expCfg = do
|
||||
usedStart <- readTVarIO $ usedStorage st
|
||||
old <- liftIO $ expireBeforeEpoch expCfg
|
||||
files' <- readTVarIO (files st)
|
||||
logInfo $ "Expiration check: " <> tshow (M.size files') <> " files"
|
||||
logNote $ "Expiration check: " <> tshow (M.size files') <> " files"
|
||||
forM_ (M.keys files') $ \sId -> do
|
||||
mapM_ threadDelay itemDelay
|
||||
atomically (expiredFilePath st sId old)
|
||||
>>= mapM_ (maybeRemove $ delete st sId)
|
||||
usedEnd <- readTVarIO $ usedStorage st
|
||||
logInfo $ "Used " <> mbs usedStart <> " -> " <> mbs usedEnd <> ", " <> mbs (usedStart - usedEnd) <> " reclaimed."
|
||||
logNote $ "Used " <> mbs usedStart <> " -> " <> mbs usedEnd <> ", " <> mbs (usedStart - usedEnd) <> " reclaimed."
|
||||
where
|
||||
mbs bs = tshow (bs `div` 1048576) <> "mb"
|
||||
maybeRemove del = maybe del (remove del)
|
||||
@@ -581,15 +600,15 @@ saveServerStats =
|
||||
>>= mapM_ (\f -> asks serverStats >>= liftIO . getFileServerStatsData >>= liftIO . saveStats f)
|
||||
where
|
||||
saveStats f stats = do
|
||||
logInfo $ "saving server stats to file " <> T.pack f
|
||||
logNote $ "saving server stats to file " <> T.pack f
|
||||
B.writeFile f $ strEncode stats
|
||||
logInfo "server stats saved"
|
||||
logNote "server stats saved"
|
||||
|
||||
restoreServerStats :: M ()
|
||||
restoreServerStats = asks (serverStatsBackupFile . config) >>= mapM_ restoreStats
|
||||
where
|
||||
restoreStats f = whenM (doesFileExist f) $ do
|
||||
logInfo $ "restoring server stats from file " <> T.pack f
|
||||
logNote $ "restoring server stats from file " <> T.pack f
|
||||
liftIO (strDecode <$> B.readFile f) >>= \case
|
||||
Right d@FileServerStatsData {_filesCount = statsFilesCount, _filesSize = statsFilesSize} -> do
|
||||
s <- asks serverStats
|
||||
@@ -598,10 +617,10 @@ restoreServerStats = asks (serverStatsBackupFile . config) >>= mapM_ restoreStat
|
||||
_filesSize <- readTVarIO usedStorage
|
||||
liftIO $ setFileServerStats s d {_filesCount, _filesSize}
|
||||
renameFile f $ f <> ".bak"
|
||||
logInfo "server stats restored"
|
||||
logNote "server stats restored"
|
||||
when (statsFilesCount /= _filesCount) $ logWarn $ "Files count differs: stats: " <> tshow statsFilesCount <> ", store: " <> tshow _filesCount
|
||||
when (statsFilesSize /= _filesSize) $ logWarn $ "Files size differs: stats: " <> tshow statsFilesSize <> ", store: " <> tshow _filesSize
|
||||
logInfo $ "Restored " <> tshow (_filesSize `div` 1048576) <> " MBs in " <> tshow _filesCount <> " files"
|
||||
logNote $ "Restored " <> tshow (_filesSize `div` 1048576) <> " MBs in " <> tshow _filesCount <> " files"
|
||||
Left e -> do
|
||||
logInfo $ "error restoring server stats: " <> T.pack e
|
||||
logNote $ "error restoring server stats: " <> T.pack e
|
||||
liftIO exitFailure
|
||||
|
||||
@@ -6,12 +6,13 @@ module Simplex.FileTransfer.Server.Control where
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Simplex.FileTransfer.Protocol (XFTPFileId)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (BasicAuth)
|
||||
import Simplex.Messaging.Protocol (BasicAuth, BlockingInfo)
|
||||
|
||||
data ControlProtocol
|
||||
= CPAuth BasicAuth
|
||||
| CPStatsRTS
|
||||
| CPDelete XFTPFileId
|
||||
| CPBlock XFTPFileId BlockingInfo
|
||||
| CPHelp
|
||||
| CPQuit
|
||||
| CPSkip
|
||||
@@ -21,6 +22,7 @@ instance StrEncoding ControlProtocol where
|
||||
CPAuth tok -> "auth " <> strEncode tok
|
||||
CPStatsRTS -> "stats-rts"
|
||||
CPDelete fId -> strEncode (Str "delete", fId)
|
||||
CPBlock fId info -> strEncode (Str "block", fId, info)
|
||||
CPHelp -> "help"
|
||||
CPQuit -> "quit"
|
||||
CPSkip -> ""
|
||||
@@ -29,6 +31,7 @@ instance StrEncoding ControlProtocol where
|
||||
"auth" -> CPAuth <$> _strP
|
||||
"stats-rts" -> pure CPStatsRTS
|
||||
"delete" -> CPDelete <$> _strP
|
||||
"block" -> CPBlock <$> _strP <*> _strP
|
||||
"help" -> pure CPHelp
|
||||
"quit" -> pure CPQuit
|
||||
"" -> pure CPSkip
|
||||
|
||||
@@ -103,8 +103,8 @@ newXFTPServerEnv config@XFTPServerConfig {storeLogFile, fileSizeQuota, xftpCrede
|
||||
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!"
|
||||
logNote $ "Total / available storage: " <> tshow quota <> " / " <> tshow (quota - used)
|
||||
when (quota < used) $ logWarn "WARNING: storage quota is less than used storage, no files can be uploaded!"
|
||||
tlsServerCreds <- loadServerCredential xftpCredentials
|
||||
Fingerprint fp <- loadFingerprint xftpCredentials
|
||||
serverStats <- newFileServerStats =<< getCurrentTime
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{-# LANGUAGE ApplicativeDo #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
@@ -220,14 +221,19 @@ cliCommandP cfgPath logPath iniFile =
|
||||
)
|
||||
where
|
||||
initP :: Parser InitOptions
|
||||
initP =
|
||||
InitOptions
|
||||
<$> switch
|
||||
( long "store-log"
|
||||
<> short 'l'
|
||||
<> help "Enable store log for persistence"
|
||||
initP = do
|
||||
enableStoreLog <-
|
||||
flag' False
|
||||
( long "disable-store-log"
|
||||
<> help "Disable store log for persistence (enabled by default)"
|
||||
)
|
||||
<*> option
|
||||
<|> flag True True
|
||||
( long "store-log"
|
||||
<> short 'l'
|
||||
<> help "Enable store log for persistence (DEPRECATED, enabled by default)"
|
||||
)
|
||||
signAlgorithm <-
|
||||
option
|
||||
(maybeReader readMaybe)
|
||||
( long "sign-algorithm"
|
||||
<> short 'a'
|
||||
@@ -236,7 +242,8 @@ cliCommandP cfgPath logPath iniFile =
|
||||
<> showDefault
|
||||
<> metavar "ALG"
|
||||
)
|
||||
<*> strOption
|
||||
ip <-
|
||||
strOption
|
||||
( long "ip"
|
||||
<> help
|
||||
"Server IP address, used as Common Name for TLS online certificate if FQDN is not supplied"
|
||||
@@ -244,22 +251,26 @@ cliCommandP cfgPath logPath iniFile =
|
||||
<> showDefault
|
||||
<> metavar "IP"
|
||||
)
|
||||
<*> (optional . strOption)
|
||||
fqdn <-
|
||||
(optional . strOption)
|
||||
( long "fqdn"
|
||||
<> short 'n'
|
||||
<> help "Server FQDN used as Common Name for TLS online certificate"
|
||||
<> showDefault
|
||||
<> metavar "FQDN"
|
||||
)
|
||||
<*> strOption
|
||||
filesPath <-
|
||||
strOption
|
||||
( long "path"
|
||||
<> short 'p'
|
||||
<> help "Path to the directory to store files"
|
||||
<> metavar "PATH"
|
||||
)
|
||||
<*> strOption
|
||||
fileSizeQuota <-
|
||||
strOption
|
||||
( long "quota"
|
||||
<> short 'q'
|
||||
<> help "File storage quota (e.g. 100gb)"
|
||||
<> metavar "QUOTA"
|
||||
)
|
||||
pure InitOptions {enableStoreLog, signAlgorithm, ip, fqdn, filesPath, fileSizeQuota}
|
||||
|
||||
@@ -20,6 +20,7 @@ data FileServerStats = FileServerStats
|
||||
filesUploaded :: IORef Int,
|
||||
filesExpired :: IORef Int,
|
||||
filesDeleted :: IORef Int,
|
||||
filesBlocked :: IORef Int,
|
||||
filesDownloaded :: PeriodStats,
|
||||
fileDownloads :: IORef Int,
|
||||
fileDownloadAcks :: IORef Int,
|
||||
@@ -34,6 +35,7 @@ data FileServerStatsData = FileServerStatsData
|
||||
_filesUploaded :: Int,
|
||||
_filesExpired :: Int,
|
||||
_filesDeleted :: Int,
|
||||
_filesBlocked :: Int,
|
||||
_filesDownloaded :: PeriodStatsData,
|
||||
_fileDownloads :: Int,
|
||||
_fileDownloadAcks :: Int,
|
||||
@@ -50,12 +52,13 @@ newFileServerStats ts = do
|
||||
filesUploaded <- newIORef 0
|
||||
filesExpired <- newIORef 0
|
||||
filesDeleted <- newIORef 0
|
||||
filesBlocked <- newIORef 0
|
||||
filesDownloaded <- newPeriodStats
|
||||
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}
|
||||
pure FileServerStats {fromTime, filesCreated, fileRecipients, filesUploaded, filesExpired, filesDeleted, filesBlocked, filesDownloaded, fileDownloads, fileDownloadAcks, filesCount, filesSize}
|
||||
|
||||
getFileServerStatsData :: FileServerStats -> IO FileServerStatsData
|
||||
getFileServerStatsData s = do
|
||||
@@ -65,12 +68,13 @@ getFileServerStatsData s = do
|
||||
_filesUploaded <- readIORef $ filesUploaded s
|
||||
_filesExpired <- readIORef $ filesExpired s
|
||||
_filesDeleted <- readIORef $ filesDeleted s
|
||||
_filesBlocked <- readIORef $ filesBlocked s
|
||||
_filesDownloaded <- getPeriodStatsData $ filesDownloaded 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}
|
||||
pure FileServerStatsData {_fromTime, _filesCreated, _fileRecipients, _filesUploaded, _filesExpired, _filesDeleted, _filesBlocked, _filesDownloaded, _fileDownloads, _fileDownloadAcks, _filesCount, _filesSize}
|
||||
|
||||
-- this function is not thread safe, it is used on server start only
|
||||
setFileServerStats :: FileServerStats -> FileServerStatsData -> IO ()
|
||||
@@ -81,6 +85,7 @@ setFileServerStats s d = do
|
||||
writeIORef (filesUploaded s) $! _filesUploaded d
|
||||
writeIORef (filesExpired s) $! _filesExpired d
|
||||
writeIORef (filesDeleted s) $! _filesDeleted d
|
||||
writeIORef (filesBlocked s) $! _filesBlocked d
|
||||
setPeriodStats (filesDownloaded s) $! _filesDownloaded d
|
||||
writeIORef (fileDownloads s) $! _fileDownloads d
|
||||
writeIORef (fileDownloadAcks s) $! _fileDownloadAcks d
|
||||
@@ -88,7 +93,7 @@ setFileServerStats s d = do
|
||||
writeIORef (filesSize s) $! _filesSize d
|
||||
|
||||
instance StrEncoding FileServerStatsData where
|
||||
strEncode FileServerStatsData {_fromTime, _filesCreated, _fileRecipients, _filesUploaded, _filesExpired, _filesDeleted, _filesDownloaded, _fileDownloads, _fileDownloadAcks, _filesCount, _filesSize} =
|
||||
strEncode FileServerStatsData {_fromTime, _filesCreated, _fileRecipients, _filesUploaded, _filesExpired, _filesDeleted, _filesBlocked, _filesDownloaded, _fileDownloads, _fileDownloadAcks, _filesCount, _filesSize} =
|
||||
B.unlines
|
||||
[ "fromTime=" <> strEncode _fromTime,
|
||||
"filesCreated=" <> strEncode _filesCreated,
|
||||
@@ -96,6 +101,7 @@ instance StrEncoding FileServerStatsData where
|
||||
"filesUploaded=" <> strEncode _filesUploaded,
|
||||
"filesExpired=" <> strEncode _filesExpired,
|
||||
"filesDeleted=" <> strEncode _filesDeleted,
|
||||
"filesBlocked=" <> strEncode _filesBlocked,
|
||||
"filesCount=" <> strEncode _filesCount,
|
||||
"filesSize=" <> strEncode _filesSize,
|
||||
"filesDownloaded:",
|
||||
@@ -110,9 +116,12 @@ instance StrEncoding FileServerStatsData where
|
||||
_filesUploaded <- "filesUploaded=" *> strP <* A.endOfLine
|
||||
_filesExpired <- "filesExpired=" *> strP <* A.endOfLine <|> pure 0
|
||||
_filesDeleted <- "filesDeleted=" *> strP <* A.endOfLine
|
||||
_filesBlocked <- opt "filesBlocked="
|
||||
_filesCount <- "filesCount=" *> strP <* A.endOfLine <|> pure 0
|
||||
_filesSize <- "filesSize=" *> strP <* A.endOfLine <|> pure 0
|
||||
_filesDownloaded <- "filesDownloaded:" *> A.endOfLine *> strP <* A.endOfLine
|
||||
_fileDownloads <- "fileDownloads=" *> strP <* A.endOfLine
|
||||
_fileDownloadAcks <- "fileDownloadAcks=" *> strP <* A.endOfLine
|
||||
pure FileServerStatsData {_fromTime, _filesCreated, _fileRecipients, _filesUploaded, _filesExpired, _filesDeleted, _filesDownloaded, _fileDownloads, _fileDownloadAcks, _filesCount, _filesSize}
|
||||
pure FileServerStatsData {_fromTime, _filesCreated, _fileRecipients, _filesUploaded, _filesExpired, _filesDeleted, _filesBlocked, _filesDownloaded, _fileDownloads, _fileDownloadAcks, _filesCount, _filesSize}
|
||||
where
|
||||
opt s = A.string s *> strP <* A.endOfLine <|> pure 0
|
||||
|
||||
@@ -13,6 +13,7 @@ module Simplex.FileTransfer.Server.Store
|
||||
setFilePath,
|
||||
addRecipient,
|
||||
deleteFile,
|
||||
blockFile,
|
||||
deleteRecipient,
|
||||
expiredFilePath,
|
||||
getFile,
|
||||
@@ -22,6 +23,7 @@ module Simplex.FileTransfer.Server.Store
|
||||
where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Int (Int64)
|
||||
import Data.Set (Set)
|
||||
@@ -30,8 +32,8 @@ 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.Protocol (BlockingInfo, RcvPublicAuthKey, RecipientId, SenderId)
|
||||
import Simplex.Messaging.Server.QueueStore (RoundedSystemTime (..), ServerEntityStatus (..))
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Util (ifM, ($>>=))
|
||||
@@ -47,7 +49,8 @@ data FileRec = FileRec
|
||||
fileInfo :: FileInfo,
|
||||
filePath :: TVar (Maybe FilePath),
|
||||
recipientIds :: TVar (Set RecipientId),
|
||||
createdAt :: RoundedSystemTime
|
||||
createdAt :: RoundedSystemTime,
|
||||
fileStatus :: TVar ServerEntityStatus
|
||||
}
|
||||
|
||||
fileTimePrecision :: Int64
|
||||
@@ -67,18 +70,19 @@ newFileStore = do
|
||||
usedStorage <- newTVarIO 0
|
||||
pure FileStore {files, recipients, usedStorage}
|
||||
|
||||
addFile :: FileStore -> SenderId -> FileInfo -> RoundedSystemTime -> STM (Either XFTPErrorType ())
|
||||
addFile FileStore {files} sId fileInfo createdAt =
|
||||
addFile :: FileStore -> SenderId -> FileInfo -> RoundedSystemTime -> ServerEntityStatus -> STM (Either XFTPErrorType ())
|
||||
addFile FileStore {files} sId fileInfo createdAt status =
|
||||
ifM (TM.member sId files) (pure $ Left DUPLICATE_) $ do
|
||||
f <- newFileRec sId fileInfo createdAt
|
||||
f <- newFileRec sId fileInfo createdAt status
|
||||
TM.insert sId f files
|
||||
pure $ Right ()
|
||||
|
||||
newFileRec :: SenderId -> FileInfo -> RoundedSystemTime -> STM FileRec
|
||||
newFileRec senderId fileInfo createdAt = do
|
||||
newFileRec :: SenderId -> FileInfo -> RoundedSystemTime -> ServerEntityStatus -> STM FileRec
|
||||
newFileRec senderId fileInfo createdAt status = do
|
||||
recipientIds <- newTVar S.empty
|
||||
filePath <- newTVar Nothing
|
||||
pure FileRec {senderId, fileInfo, filePath, recipientIds, createdAt}
|
||||
fileStatus <- newTVar status
|
||||
pure FileRec {senderId, fileInfo, filePath, recipientIds, createdAt, fileStatus}
|
||||
|
||||
setFilePath :: FileStore -> SenderId -> FilePath -> STM (Either XFTPErrorType ())
|
||||
setFilePath st sId fPath =
|
||||
@@ -109,6 +113,14 @@ deleteFile FileStore {files, recipients, usedStorage} senderId = do
|
||||
pure $ Right ()
|
||||
_ -> pure $ Left AUTH
|
||||
|
||||
-- this function must be called after the file is deleted from the file system
|
||||
blockFile :: FileStore -> SenderId -> BlockingInfo -> Bool -> STM (Either XFTPErrorType ())
|
||||
blockFile st@FileStore {usedStorage} senderId info deleted =
|
||||
withFile st senderId $ \FileRec {fileInfo, fileStatus} -> do
|
||||
when deleted $ modifyTVar' usedStorage $ subtract (fromIntegral $ size fileInfo)
|
||||
writeTVar fileStatus $! EntityBlocked info
|
||||
pure $ Right ()
|
||||
|
||||
deleteRecipient :: FileStore -> RecipientId -> FileRec -> STM ()
|
||||
deleteRecipient FileStore {recipients} rId FileRec {recipientIds} = do
|
||||
TM.delete rId recipients
|
||||
|
||||
@@ -14,16 +14,18 @@ module Simplex.FileTransfer.Server.StoreLog
|
||||
logPutFile,
|
||||
logAddRecipients,
|
||||
logDeleteFile,
|
||||
logBlockFile,
|
||||
logAckFile,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad.Except
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Composition ((.:), (.:.))
|
||||
import Data.Composition ((.:), (.::))
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map.Strict (Map)
|
||||
@@ -31,41 +33,44 @@ import qualified Data.Map.Strict as M
|
||||
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.Protocol (BlockingInfo, RcvPublicAuthKey, RecipientId, SenderId)
|
||||
import Simplex.Messaging.Server.QueueStore (RoundedSystemTime, ServerEntityStatus (..))
|
||||
import Simplex.Messaging.Server.StoreLog
|
||||
import Simplex.Messaging.Util (bshow)
|
||||
import System.IO
|
||||
|
||||
data FileStoreLogRecord
|
||||
= AddFile SenderId FileInfo RoundedSystemTime
|
||||
= AddFile SenderId FileInfo RoundedSystemTime ServerEntityStatus
|
||||
| PutFile SenderId FilePath
|
||||
| AddRecipients SenderId (NonEmpty FileRecipient)
|
||||
| DeleteFile SenderId
|
||||
| AckFile RecipientId
|
||||
| BlockFile SenderId BlockingInfo
|
||||
| AckFile RecipientId -- TODO add senderId as well?
|
||||
deriving (Show)
|
||||
|
||||
instance StrEncoding FileStoreLogRecord where
|
||||
strEncode = \case
|
||||
AddFile sId file createdAt -> strEncode (Str "FNEW", sId, file, createdAt)
|
||||
AddFile sId file createdAt status -> strEncode (Str "FNEW", sId, file, createdAt, status)
|
||||
PutFile sId path -> strEncode (Str "FPUT", sId, path)
|
||||
AddRecipients sId rcps -> strEncode (Str "FADD", sId, rcps)
|
||||
DeleteFile sId -> strEncode (Str "FDEL", sId)
|
||||
BlockFile sId info -> strEncode (Str "FBLK", sId, info)
|
||||
AckFile rId -> strEncode (Str "FACK", rId)
|
||||
strP =
|
||||
A.choice
|
||||
[ "FNEW " *> (AddFile <$> strP_ <*> strP_ <*> strP),
|
||||
[ "FNEW " *> (AddFile <$> strP_ <*> strP_ <*> strP <*> (_strP <|> pure EntityActive)),
|
||||
"FPUT " *> (PutFile <$> strP_ <*> strP),
|
||||
"FADD " *> (AddRecipients <$> strP_ <*> strP),
|
||||
"FDEL " *> (DeleteFile <$> strP),
|
||||
"FBLK " *> (BlockFile <$> strP_ <*> strP),
|
||||
"FACK " *> (AckFile <$> strP)
|
||||
]
|
||||
|
||||
logFileStoreRecord :: StoreLog 'WriteMode -> FileStoreLogRecord -> IO ()
|
||||
logFileStoreRecord = writeStoreLogRecord
|
||||
|
||||
logAddFile :: StoreLog 'WriteMode -> SenderId -> FileInfo -> RoundedSystemTime -> IO ()
|
||||
logAddFile s = logFileStoreRecord s .:. AddFile
|
||||
logAddFile :: StoreLog 'WriteMode -> SenderId -> FileInfo -> RoundedSystemTime -> ServerEntityStatus -> IO ()
|
||||
logAddFile s = logFileStoreRecord s .:: AddFile
|
||||
|
||||
logPutFile :: StoreLog 'WriteMode -> SenderId -> FilePath -> IO ()
|
||||
logPutFile s = logFileStoreRecord s .: PutFile
|
||||
@@ -76,6 +81,9 @@ logAddRecipients s = logFileStoreRecord s .: AddRecipients
|
||||
logDeleteFile :: StoreLog 'WriteMode -> SenderId -> IO ()
|
||||
logDeleteFile s = logFileStoreRecord s . DeleteFile
|
||||
|
||||
logBlockFile :: StoreLog 'WriteMode -> SenderId -> BlockingInfo -> IO ()
|
||||
logBlockFile s fId = logFileStoreRecord s . BlockFile fId
|
||||
|
||||
logAckFile :: StoreLog 'WriteMode -> RecipientId -> IO ()
|
||||
logAckFile s = logFileStoreRecord s . AckFile
|
||||
|
||||
@@ -92,10 +100,11 @@ readFileStore f st = mapM_ (addFileLogRecord . LB.toStrict) . LB.lines =<< LB.re
|
||||
Left e -> B.putStrLn $ "Log processing error (" <> bshow e <> "): " <> B.take 100 s
|
||||
_ -> pure ()
|
||||
addToStore = \case
|
||||
AddFile sId file createdAt -> addFile st sId file createdAt
|
||||
AddFile sId file createdAt status -> addFile st sId file createdAt status
|
||||
PutFile qId path -> setFilePath st qId path
|
||||
AddRecipients sId rcps -> runExceptT $ addRecipients sId rcps
|
||||
DeleteFile sId -> deleteFile st sId
|
||||
BlockFile sId info -> blockFile st sId info True
|
||||
AckFile rId -> ackFile st rId
|
||||
addRecipients sId rcps = mapM_ (ExceptT . addRecipient st sId) rcps
|
||||
|
||||
@@ -105,8 +114,9 @@ writeFileStore s FileStore {files, recipients} = do
|
||||
readTVarIO files >>= mapM_ (logFile allRcps)
|
||||
where
|
||||
logFile :: Map RecipientId (SenderId, RcvPublicAuthKey) -> FileRec -> IO ()
|
||||
logFile allRcps FileRec {senderId, fileInfo, filePath, recipientIds, createdAt} = do
|
||||
logAddFile s senderId fileInfo createdAt
|
||||
logFile allRcps FileRec {senderId, fileInfo, filePath, recipientIds, createdAt, fileStatus} = do
|
||||
status <- readTVarIO fileStatus
|
||||
logAddFile s senderId fileInfo createdAt status
|
||||
(rcpErrs, rcps) <- M.mapEither getRcp . M.fromSet id <$> readTVarIO recipientIds
|
||||
mapM_ (logAddRecipients s senderId) $ L.nonEmpty $ M.elems rcps
|
||||
mapM_ (B.putStrLn . ("Error storing log: " <>)) rcpErrs
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
module Simplex.FileTransfer.Transport
|
||||
( supportedFileServerVRange,
|
||||
authCmdsXFTPVersion,
|
||||
blockedFilesXFTPVersion,
|
||||
xftpClientHandshakeStub,
|
||||
supportedXFTPhandshakes,
|
||||
XFTPClientHandshake (..),
|
||||
@@ -33,7 +34,6 @@ module Simplex.FileTransfer.Transport
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import qualified Control.Exception as E
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
@@ -57,7 +57,7 @@ import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers
|
||||
import Simplex.Messaging.Protocol (CommandError)
|
||||
import Simplex.Messaging.Protocol (BlockingInfo, CommandError)
|
||||
import Simplex.Messaging.Transport (ALPN, SessionId, THandle (..), THandleParams (..), TransportError (..), TransportPeer (..))
|
||||
import Simplex.Messaging.Transport.HTTP2.File
|
||||
import Simplex.Messaging.Util (bshow, tshow)
|
||||
@@ -92,15 +92,18 @@ initialXFTPVersion = VersionXFTP 1
|
||||
authCmdsXFTPVersion :: VersionXFTP
|
||||
authCmdsXFTPVersion = VersionXFTP 2
|
||||
|
||||
blockedFilesXFTPVersion :: VersionXFTP
|
||||
blockedFilesXFTPVersion = VersionXFTP 3
|
||||
|
||||
currentXFTPVersion :: VersionXFTP
|
||||
currentXFTPVersion = VersionXFTP 2
|
||||
currentXFTPVersion = VersionXFTP 3
|
||||
|
||||
supportedFileServerVRange :: VersionRangeXFTP
|
||||
supportedFileServerVRange = mkVersionRange initialXFTPVersion currentXFTPVersion
|
||||
|
||||
-- XFTP protocol does not use this handshake method
|
||||
xftpClientHandshakeStub :: c -> Maybe C.KeyPairX25519 -> C.KeyHash -> VersionRangeXFTP -> ExceptT TransportError IO (THandle XFTPVersion c 'TClient)
|
||||
xftpClientHandshakeStub _c _ks _keyHash _xftpVRange = throwE TEVersion
|
||||
xftpClientHandshakeStub :: c -> Maybe C.KeyPairX25519 -> C.KeyHash -> VersionRangeXFTP -> Bool -> ExceptT TransportError IO (THandle XFTPVersion c 'TClient)
|
||||
xftpClientHandshakeStub _c _ks _keyHash _xftpVRange _proxyServer = throwE TEVersion
|
||||
|
||||
supportedXFTPhandshakes :: [ALPN]
|
||||
supportedXFTPhandshakes = ["xftp/1"]
|
||||
@@ -211,6 +214,8 @@ data XFTPErrorType
|
||||
CMD {cmdErr :: CommandError}
|
||||
| -- | command authorization error - bad signature or non-existing SMP queue
|
||||
AUTH
|
||||
| -- | command with the entity that was blocked
|
||||
BLOCKED {blockInfo :: BlockingInfo}
|
||||
| -- | incorrent file size
|
||||
SIZE
|
||||
| -- | storage quota exceeded
|
||||
@@ -231,15 +236,46 @@ data XFTPErrorType
|
||||
INTERNAL
|
||||
| -- | used internally, never returned by the server (to be removed)
|
||||
DUPLICATE_ -- not part of SMP protocol, used internally
|
||||
deriving (Eq, Read, Show)
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding XFTPErrorType where
|
||||
strEncode = \case
|
||||
BLOCK -> "BLOCK"
|
||||
SESSION -> "SESSION"
|
||||
HANDSHAKE -> "HANDSHAKE"
|
||||
CMD e -> "CMD " <> bshow e
|
||||
e -> bshow e
|
||||
AUTH -> "AUTH"
|
||||
BLOCKED info -> "BLOCKED " <> strEncode info
|
||||
SIZE -> "SIZE"
|
||||
QUOTA -> "QUOTA"
|
||||
DIGEST -> "DIGEST"
|
||||
CRYPTO -> "CRYPTO"
|
||||
NO_FILE -> "NO_FILE"
|
||||
HAS_FILE -> "HAS_FILE"
|
||||
FILE_IO -> "FILE_IO"
|
||||
TIMEOUT -> "TIMEOUT"
|
||||
INTERNAL -> "INTERNAL"
|
||||
DUPLICATE_ -> "DUPLICATE_"
|
||||
|
||||
strP =
|
||||
"CMD " *> (CMD <$> parseRead1)
|
||||
<|> parseRead1
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"BLOCK" -> pure BLOCK
|
||||
"SESSION" -> pure SESSION
|
||||
"HANDSHAKE" -> pure HANDSHAKE
|
||||
"CMD" -> CMD <$> parseRead1
|
||||
"AUTH" -> pure AUTH
|
||||
"BLOCKED" -> BLOCKED <$> _strP
|
||||
"SIZE" -> pure SIZE
|
||||
"QUOTA" -> pure QUOTA
|
||||
"DIGEST" -> pure DIGEST
|
||||
"CRYPTO" -> pure CRYPTO
|
||||
"NO_FILE" -> pure NO_FILE
|
||||
"HAS_FILE" -> pure HAS_FILE
|
||||
"FILE_IO" -> pure FILE_IO
|
||||
"TIMEOUT" -> pure TIMEOUT
|
||||
"INTERNAL" -> pure INTERNAL
|
||||
"DUPLICATE_" -> pure DUPLICATE_
|
||||
_ -> fail "bad error type"
|
||||
|
||||
instance Encoding XFTPErrorType where
|
||||
smpEncode = \case
|
||||
@@ -248,6 +284,7 @@ instance Encoding XFTPErrorType where
|
||||
HANDSHAKE -> "HANDSHAKE"
|
||||
CMD err -> "CMD " <> smpEncode err
|
||||
AUTH -> "AUTH"
|
||||
BLOCKED info -> "BLOCKED " <> smpEncode info
|
||||
SIZE -> "SIZE"
|
||||
QUOTA -> "QUOTA"
|
||||
DIGEST -> "DIGEST"
|
||||
@@ -266,6 +303,7 @@ instance Encoding XFTPErrorType where
|
||||
"HANDSHAKE" -> pure HANDSHAKE
|
||||
"CMD" -> CMD <$> _smpP
|
||||
"AUTH" -> pure AUTH
|
||||
"BLOCKED" -> BLOCKED <$> _smpP
|
||||
"SIZE" -> pure SIZE
|
||||
"QUOTA" -> pure QUOTA
|
||||
"DIGEST" -> pure DIGEST
|
||||
|
||||
@@ -13,8 +13,6 @@ import Data.Int (Int64)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Word (Word32)
|
||||
import Database.SQLite.Simple.FromField (FromField (..))
|
||||
import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import Simplex.FileTransfer.Client (XFTPChunkSpec (..))
|
||||
import Simplex.FileTransfer.Description
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
@@ -24,6 +22,7 @@ import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers
|
||||
import Simplex.Messaging.Protocol (XFTPServer)
|
||||
import System.FilePath ((</>))
|
||||
import Simplex.Messaging.Agent.Store.DB (FromField (..), ToField (..), fromTextField_)
|
||||
|
||||
type RcvFileId = ByteString -- Agent entity ID
|
||||
|
||||
@@ -246,6 +245,16 @@ data DeletedSndChunkReplica = DeletedSndChunkReplica
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data SentRecipientReplica = SentRecipientReplica
|
||||
{ chunkNo :: Int,
|
||||
server :: XFTPServer,
|
||||
rcvNo :: Int,
|
||||
replicaId :: ChunkReplicaId,
|
||||
replicaKey :: C.APrivateAuthKey,
|
||||
digest :: FileDigest,
|
||||
chunkSize :: FileSize Word32
|
||||
}
|
||||
|
||||
data FileErrorType
|
||||
= -- | cannot proceed with download from not approved relays without proxy
|
||||
NOT_APPROVED
|
||||
|
||||
+456
-171
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
{-# LANGUAGE AllowAmbiguousTypes #-}
|
||||
{-# LANGUAGE BangPatterns #-}
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE ConstraintKinds #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
@@ -25,6 +26,8 @@ module Simplex.Messaging.Agent.Client
|
||||
( AgentClient (..),
|
||||
ProtocolTestFailure (..),
|
||||
ProtocolTestStep (..),
|
||||
ClntQueueReqData (..),
|
||||
CQRData (..),
|
||||
newAgentClient,
|
||||
withConnLock,
|
||||
withConnLocks,
|
||||
@@ -42,6 +45,7 @@ module Simplex.Messaging.Agent.Client
|
||||
runNTFServerTest,
|
||||
getXFTPWorkPath,
|
||||
newRcvQueue,
|
||||
newRcvQueue_,
|
||||
subscribeQueues,
|
||||
getQueueMessage,
|
||||
decryptSMPMessage,
|
||||
@@ -56,6 +60,10 @@ module Simplex.Messaging.Agent.Client
|
||||
serverHostError,
|
||||
secureQueue,
|
||||
secureSndQueue,
|
||||
addQueueLink,
|
||||
deleteQueueLink,
|
||||
secureGetQueueLink,
|
||||
getQueueLink,
|
||||
enableQueueNotifications,
|
||||
EnableQueueNtfReq (..),
|
||||
enableQueuesNtfs,
|
||||
@@ -69,7 +77,7 @@ module Simplex.Messaging.Agent.Client
|
||||
agentNtfCheckToken,
|
||||
agentNtfReplaceToken,
|
||||
agentNtfDeleteToken,
|
||||
agentNtfEnableCron,
|
||||
agentNtfSetCronInterval,
|
||||
agentNtfCreateSubscription,
|
||||
agentNtfCreateSubscriptions,
|
||||
agentNtfCheckSubscription,
|
||||
@@ -145,6 +153,7 @@ module Simplex.Messaging.Agent.Client
|
||||
withStore',
|
||||
withStoreBatch,
|
||||
withStoreBatch',
|
||||
unsafeWithStore,
|
||||
storeError,
|
||||
userServers,
|
||||
pickServer,
|
||||
@@ -205,7 +214,6 @@ import Data.Text.Encoding
|
||||
import Data.Time (UTCTime, addUTCTime, defaultTimeLocale, formatTime, getCurrentTime)
|
||||
import Data.Time.Clock.System (getSystemTime)
|
||||
import Data.Word (Word16)
|
||||
import qualified Database.SQLite.Simple as SQL
|
||||
import Network.Socket (HostName)
|
||||
import Simplex.FileTransfer.Client (XFTPChunkSpec (..), XFTPClient, XFTPClientConfig (..), XFTPClientError)
|
||||
import qualified Simplex.FileTransfer.Client as X
|
||||
@@ -221,8 +229,8 @@ import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Agent.Stats
|
||||
import Simplex.Messaging.Agent.Store
|
||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), withTransaction)
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction)
|
||||
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
||||
import Simplex.Messaging.Agent.TRcvQueues (TRcvQueues (getRcvQueues))
|
||||
import qualified Simplex.Messaging.Agent.TRcvQueues as RQ
|
||||
import Simplex.Messaging.Client
|
||||
@@ -255,22 +263,24 @@ import Simplex.Messaging.Protocol
|
||||
RcvNtfPublicDhKey,
|
||||
SMPMsgMeta (..),
|
||||
SProtocolType (..),
|
||||
SenderCanSecure,
|
||||
SndPublicAuthKey,
|
||||
SubscriptionMode (..),
|
||||
QueueReqData (..),
|
||||
QueueLinkData,
|
||||
UserProtocol,
|
||||
VersionRangeSMPC,
|
||||
VersionSMPC,
|
||||
XFTPServer,
|
||||
XFTPServerWithAuth,
|
||||
pattern NoEntity,
|
||||
senderCanSecure,
|
||||
)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Server.QueueStore.QueueInfo
|
||||
import Simplex.Messaging.Session
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport (SMPVersion, SessionId, THandleParams (sessionId), TransportError (..))
|
||||
import Simplex.Messaging.Transport (SMPVersion, SessionId, THandleParams (sessionId, thVersion), TransportError (..), TransportPeer (..), sndAuthKeySMPVersion, shortLinksSMPVersion)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
import Simplex.Messaging.Util
|
||||
import Simplex.Messaging.Version
|
||||
@@ -282,6 +292,9 @@ import UnliftIO.Concurrent (forkIO, mkWeakThreadId)
|
||||
import UnliftIO.Directory (doesFileExist, getTemporaryDirectory, removeFile)
|
||||
import qualified UnliftIO.Exception as E
|
||||
import UnliftIO.STM
|
||||
#if !defined(dbPostgres)
|
||||
import qualified Database.SQLite.Simple as SQL
|
||||
#endif
|
||||
|
||||
type ClientVar msg = SessionVar (Either (AgentErrorType, Maybe UTCTime) (Client msg))
|
||||
|
||||
@@ -313,6 +326,7 @@ data AgentClient = AgentClient
|
||||
xftpServers :: TMap UserId (UserServers 'PXFTP),
|
||||
xftpClients :: TMap XFTPTransportSession XFTPClientVar,
|
||||
useNetworkConfig :: TVar (NetworkConfig, NetworkConfig), -- (slow, fast) networks
|
||||
presetSMPDomains :: [HostName],
|
||||
userNetworkInfo :: TVar UserNetworkInfo,
|
||||
userNetworkUpdated :: TVar (Maybe UTCTime),
|
||||
subscrConns :: TVar (Set ConnId),
|
||||
@@ -465,7 +479,7 @@ data UserNetworkType = UNNone | UNCellular | UNWifi | UNEthernet | UNOther
|
||||
|
||||
-- | Creates an SMP agent client instance that receives commands and sends responses via 'TBQueue's.
|
||||
newAgentClient :: Int -> InitialAgentServers -> UTCTime -> Env -> IO AgentClient
|
||||
newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} currentTs agentEnv = do
|
||||
newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg, presetDomains} currentTs agentEnv = do
|
||||
let cfg = config agentEnv
|
||||
qSize = tbqSize cfg
|
||||
proxySessTs <- newTVarIO =<< getCurrentTime
|
||||
@@ -519,6 +533,7 @@ newAgentClient clientId InitialAgentServers {smp, ntf, xftp, netCfg} currentTs a
|
||||
xftpServers,
|
||||
xftpClients,
|
||||
useNetworkConfig,
|
||||
presetSMPDomains = presetDomains,
|
||||
userNetworkInfo,
|
||||
userNetworkUpdated,
|
||||
subscrConns,
|
||||
@@ -555,7 +570,7 @@ slowNetworkConfig cfg@NetworkConfig {tcpConnectTimeout, tcpTimeout, tcpTimeoutPe
|
||||
slow :: Integral a => a -> a
|
||||
slow t = (t * 3) `div` 2
|
||||
|
||||
agentClientStore :: AgentClient -> SQLiteStore
|
||||
agentClientStore :: AgentClient -> DBStore
|
||||
agentClientStore AgentClient {agentEnv = Env {store}} = store
|
||||
{-# INLINE agentClientStore #-}
|
||||
|
||||
@@ -677,7 +692,7 @@ smpConnectClient c@AgentClient {smpClients, msgQ, proxySessTs} tSess@(_, srv, _)
|
||||
env <- ask
|
||||
liftError (protocolClientError SMP $ B.unpack $ strEncode srv) $ do
|
||||
ts <- readTVarIO proxySessTs
|
||||
smp <- ExceptT $ getProtocolClient g tSess cfg (Just msgQ) ts $ smpClientDisconnected c tSess env v' prs
|
||||
smp <- ExceptT $ getProtocolClient g tSess cfg (presetSMPDomains c) (Just msgQ) ts $ smpClientDisconnected c tSess env v' prs
|
||||
pure SMPConnectedClient {connectedClient = smp, proxiedRelays = prs}
|
||||
|
||||
smpClientDisconnected :: AgentClient -> SMPTransportSession -> Env -> SMPClientVar -> TMap SMPServer ProxiedRelayVar -> SMPClient -> IO ()
|
||||
@@ -780,7 +795,7 @@ getNtfServerClient c@AgentClient {active, ntfClients, workerSeq, proxySessTs} tS
|
||||
g <- asks random
|
||||
ts <- readTVarIO proxySessTs
|
||||
liftError' (protocolClientError NTF $ B.unpack $ strEncode srv) $
|
||||
getProtocolClient g tSess cfg Nothing ts $
|
||||
getProtocolClient g tSess cfg [] Nothing ts $
|
||||
clientDisconnected v
|
||||
|
||||
clientDisconnected :: NtfClientVar -> NtfClient -> IO ()
|
||||
@@ -1052,7 +1067,7 @@ withSMPClient c q cmdStr action = do
|
||||
|
||||
sendOrProxySMPMessage :: AgentClient -> UserId -> SMPServer -> ConnId -> ByteString -> Maybe SMP.SndPrivateAuthKey -> SMP.SenderId -> MsgFlags -> SMP.MsgBody -> AM (Maybe SMPServer)
|
||||
sendOrProxySMPMessage c userId destSrv connId cmdStr spKey_ senderId msgFlags msg =
|
||||
sendOrProxySMPCommand c userId destSrv connId cmdStr senderId sendViaProxy sendDirectly
|
||||
fst <$> sendOrProxySMPCommand c userId destSrv connId cmdStr senderId sendViaProxy sendDirectly
|
||||
where
|
||||
sendViaProxy smp proxySess = do
|
||||
atomically $ incSMPServerStat c userId destSrv sentViaProxyAttempts
|
||||
@@ -1063,18 +1078,19 @@ sendOrProxySMPMessage c userId destSrv connId cmdStr spKey_ senderId msgFlags ms
|
||||
sendSMPMessage smp spKey_ senderId msgFlags msg
|
||||
|
||||
sendOrProxySMPCommand ::
|
||||
forall a.
|
||||
AgentClient ->
|
||||
UserId ->
|
||||
SMPServer ->
|
||||
ConnId ->
|
||||
ByteString ->
|
||||
SMP.SenderId ->
|
||||
(SMPClient -> ProxiedRelay -> ExceptT SMPClientError IO (Either ProxyClientError ())) ->
|
||||
(SMPClient -> ExceptT SMPClientError IO ()) ->
|
||||
AM (Maybe SMPServer)
|
||||
sendOrProxySMPCommand c userId destSrv@ProtocolServer {host = destHosts} connId cmdStr senderId sendCmdViaProxy sendCmdDirectly = do
|
||||
ConnId -> -- session entity ID, for short links LinkId is used
|
||||
ByteString ->
|
||||
SMP.EntityId -> -- sender or link ID
|
||||
(SMPClient -> ProxiedRelay -> ExceptT SMPClientError IO (Either ProxyClientError a)) ->
|
||||
(SMPClient -> ExceptT SMPClientError IO a) ->
|
||||
AM (Maybe SMPServer, a)
|
||||
sendOrProxySMPCommand c userId destSrv@ProtocolServer {host = destHosts} connId cmdStr entId sendCmdViaProxy sendCmdDirectly = do
|
||||
tSess <- mkTransportSession c userId destSrv connId
|
||||
ifM shouldUseProxy (sendViaProxy Nothing tSess) (sendDirectly tSess $> Nothing)
|
||||
ifM shouldUseProxy (sendViaProxy Nothing tSess) ((Nothing,) <$> sendDirectly tSess)
|
||||
where
|
||||
shouldUseProxy = do
|
||||
cfg <- getNetworkConfig c
|
||||
@@ -1092,13 +1108,13 @@ sendOrProxySMPCommand c userId destSrv@ProtocolServer {host = destHosts} connId
|
||||
SPFAllowProtected -> ipAddressProtected cfg destSrv
|
||||
SPFProhibit -> False
|
||||
unknownServer = liftIO $ maybe True (\srvs -> all (`S.notMember` knownHosts srvs) destHosts) <$> TM.lookupIO userId (smpServers c)
|
||||
sendViaProxy :: Maybe SMPServerWithAuth -> SMPTransportSession -> AM (Maybe SMPServer)
|
||||
sendViaProxy :: Maybe SMPServerWithAuth -> SMPTransportSession -> AM (Maybe SMPServer, a)
|
||||
sendViaProxy proxySrv_ destSess@(_, _, connId_) = do
|
||||
r <- tryAgentError . withProxySession c proxySrv_ destSess senderId ("PFWD " <> cmdStr) $ \(SMPConnectedClient smp _, proxySess@ProxiedRelay {prBasicAuth}) -> do
|
||||
r <- tryAgentError . withProxySession c proxySrv_ destSess entId ("PFWD " <> cmdStr) $ \(SMPConnectedClient smp _, proxySess@ProxiedRelay {prBasicAuth}) -> do
|
||||
r' <- liftClient SMP (clientServer smp) $ sendCmdViaProxy smp proxySess
|
||||
let proxySrv = protocolClientServer' smp
|
||||
case r' of
|
||||
Right () -> pure $ Just proxySrv
|
||||
Right r -> pure (Just proxySrv, r)
|
||||
Left proxyErr -> do
|
||||
case proxyErr of
|
||||
ProxyProtocolError (SMP.PROXY SMP.NO_SESSION) -> do
|
||||
@@ -1132,18 +1148,17 @@ sendOrProxySMPCommand c userId destSrv@ProtocolServer {host = destHosts} connId
|
||||
sameClient smp' = sessionId (thParams smp) == sessionId (thParams smp')
|
||||
sameProxiedRelay proxySess' = prSessionId proxySess == prSessionId proxySess'
|
||||
case r of
|
||||
Right r' -> do
|
||||
Right r'@(srv_, _) -> do
|
||||
atomically $ incSMPServerStat c userId destSrv sentViaProxy
|
||||
forM_ r' $ \proxySrv -> atomically $ incSMPServerStat c userId proxySrv sentProxied
|
||||
forM_ srv_ $ \proxySrv -> atomically $ incSMPServerStat c userId proxySrv sentProxied
|
||||
pure r'
|
||||
Left e
|
||||
| serverHostError e -> ifM directAllowed (sendDirectly destSess $> Nothing) (throwE e)
|
||||
| serverHostError e -> ifM directAllowed ((Nothing,) <$> sendDirectly destSess) (throwE e)
|
||||
| otherwise -> throwE e
|
||||
sendDirectly tSess =
|
||||
withLogClient_ c tSess (unEntityId senderId) ("SEND " <> cmdStr) $ \(SMPConnectedClient smp _) -> do
|
||||
r <- tryAgentError $ liftClient SMP (clientServer smp) $ sendCmdDirectly smp
|
||||
case r of
|
||||
Right () -> atomically $ incSMPServerStat c userId destSrv sentDirect
|
||||
withLogClient_ c tSess (unEntityId entId) ("SEND " <> cmdStr) $ \(SMPConnectedClient smp _) -> do
|
||||
tryAgentError (liftClient SMP (clientServer smp) $ sendCmdDirectly smp) >>= \case
|
||||
Right r -> r <$ atomically (incSMPServerStat c userId destSrv sentDirect)
|
||||
Left e -> throwE e
|
||||
|
||||
ipAddressProtected :: NetworkConfig -> ProtocolServer p -> Bool
|
||||
@@ -1212,17 +1227,18 @@ runSMPServerTest c userId (ProtoServerWithAuth srv auth) = do
|
||||
liftIO $ do
|
||||
let tSess = (userId, srv, Nothing)
|
||||
ts <- readTVarIO $ proxySessTs c
|
||||
getProtocolClient g tSess cfg Nothing ts (\_ -> pure ()) >>= \case
|
||||
getProtocolClient g tSess cfg (presetSMPDomains c) Nothing ts (\_ -> pure ()) >>= \case
|
||||
Right smp -> do
|
||||
rKeys@(_, rpKey) <- atomically $ C.generateAuthKeyPair ra g
|
||||
(sKey, spKey) <- atomically $ C.generateAuthKeyPair sa g
|
||||
(dhKey, _) <- atomically $ C.generateKeyPair g
|
||||
r <- runExceptT $ do
|
||||
SMP.QIK {rcvId, sndId, sndSecure} <- liftError (testErr TSCreateQueue) $ createSMPQueue smp rKeys dhKey auth SMSubscribe True
|
||||
-- TODO [notifications]
|
||||
SMP.QIK {rcvId, sndId, queueMode} <- liftError (testErr TSCreateQueue) $ createSMPQueue smp Nothing rKeys dhKey auth SMSubscribe (QRMessaging Nothing) -- Nothing
|
||||
liftError (testErr TSSecureQueue) $
|
||||
if sndSecure
|
||||
then secureSndSMPQueue smp spKey sndId sKey
|
||||
else secureSMPQueue smp rpKey rcvId sKey
|
||||
case queueMode of
|
||||
Just QMMessaging -> secureSndSMPQueue smp spKey sndId sKey
|
||||
_ -> secureSMPQueue smp rpKey rcvId sKey
|
||||
liftError (testErr TSDeleteQueue) $ deleteSMPQueue smp rpKey rcvId
|
||||
ok <- tcpTimeout (networkConfig cfg) `timeout` closeProtocolClient smp
|
||||
pure $ either Just (const Nothing) r <|> maybe (Just (ProtocolTestFailure TSDisconnect $ BROKER addr TIMEOUT)) (const Nothing) ok
|
||||
@@ -1288,7 +1304,7 @@ runNTFServerTest c userId (ProtoServerWithAuth srv _) = do
|
||||
liftIO $ do
|
||||
let tSess = (userId, srv, Nothing)
|
||||
ts <- readTVarIO $ proxySessTs c
|
||||
getProtocolClient g tSess cfg Nothing ts (\_ -> pure ()) >>= \case
|
||||
getProtocolClient g tSess cfg [] Nothing ts (\_ -> pure ()) >>= \case
|
||||
Right ntf -> do
|
||||
(nKey, npKey) <- atomically $ C.generateAuthKeyPair a g
|
||||
(dhKey, _) <- atomically $ C.generateKeyPair g
|
||||
@@ -1329,19 +1345,42 @@ getSessionMode :: MonadIO m => AgentClient -> m TransportSessionMode
|
||||
getSessionMode = fmap sessionMode . getNetworkConfig
|
||||
{-# INLINE getSessionMode #-}
|
||||
|
||||
newRcvQueue :: AgentClient -> UserId -> ConnId -> SMPServerWithAuth -> VersionRangeSMPC -> SubscriptionMode -> SenderCanSecure -> AM (NewRcvQueue, SMPQueueUri, SMPTransportSession, SessionId)
|
||||
newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange subMode senderCanSecure = do
|
||||
-- TODO [notifications]
|
||||
newRcvQueue :: AgentClient -> UserId -> ConnId -> SMPServerWithAuth -> VersionRangeSMPC -> SConnectionMode c -> SubscriptionMode -> AM (NewRcvQueue, SMPQueueUri, SMPTransportSession, SessionId)
|
||||
newRcvQueue c userId connId srv vRange cMode subMode = do
|
||||
let qrd = case cMode of SCMInvitation -> CQRMessaging Nothing; SCMContact -> CQRContact Nothing
|
||||
e2eKeys <- atomically . C.generateKeyPair =<< asks random
|
||||
newRcvQueue_ c userId connId srv vRange qrd subMode Nothing e2eKeys
|
||||
|
||||
data ClntQueueReqData
|
||||
= CQRMessaging (Maybe (CQRData (SMP.SenderId, QueueLinkData)))
|
||||
| CQRContact (Maybe (CQRData (SMP.LinkId, (SMP.SenderId, QueueLinkData))))
|
||||
|
||||
data CQRData r = CQRData
|
||||
{ linkKey :: LinkKey,
|
||||
privSigKey :: C.PrivateKeyEd25519,
|
||||
srvReq :: r
|
||||
}
|
||||
|
||||
queueReqData :: ClntQueueReqData -> QueueReqData
|
||||
queueReqData = \case
|
||||
CQRMessaging d -> QRMessaging $ srvReq <$> d
|
||||
CQRContact d -> QRContact $ srvReq <$> d
|
||||
|
||||
newRcvQueue_ :: AgentClient -> UserId -> ConnId -> SMPServerWithAuth -> VersionRangeSMPC -> ClntQueueReqData -> SubscriptionMode -> Maybe C.CbNonce -> C.KeyPairX25519 -> AM (NewRcvQueue, SMPQueueUri, SMPTransportSession, SessionId)
|
||||
newRcvQueue_ c userId connId (ProtoServerWithAuth srv auth) vRange cqrd subMode nonce_ (e2eDhKey, e2ePrivKey) = do
|
||||
C.AuthAlg a <- asks (rcvAuthAlg . config)
|
||||
g <- asks random
|
||||
rKeys@(_, rcvPrivateKey) <- atomically $ C.generateAuthKeyPair a g
|
||||
(dhKey, privDhKey) <- atomically $ C.generateKeyPair g
|
||||
(e2eDhKey, e2ePrivKey) <- atomically $ C.generateKeyPair g
|
||||
logServer "-->" c srv NoEntity "NEW"
|
||||
tSess <- mkTransportSession c userId srv connId
|
||||
(sessId, QIK {rcvId, sndId, rcvPublicDhKey, sndSecure}) <-
|
||||
-- TODO [notifications]
|
||||
r@(thParams', QIK {rcvId, sndId, rcvPublicDhKey, queueMode}) <-
|
||||
withClient c tSess $ \(SMPConnectedClient smp _) ->
|
||||
(sessionId $ thParams smp,) <$> createSMPQueue smp rKeys dhKey auth subMode senderCanSecure
|
||||
(thParams smp,) <$> createSMPQueue smp nonce_ rKeys dhKey auth subMode (queueReqData cqrd)
|
||||
liftIO . logServer "<--" c srv NoEntity $ B.unwords ["IDS", logSecret rcvId, logSecret sndId]
|
||||
shortLink <- mkShortLinkCreds r
|
||||
let rq =
|
||||
RcvQueue
|
||||
{ userId,
|
||||
@@ -1353,7 +1392,8 @@ newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange subMode sender
|
||||
e2ePrivKey,
|
||||
e2eDhSecret = Nothing,
|
||||
sndId,
|
||||
sndSecure,
|
||||
queueMode,
|
||||
shortLink,
|
||||
status = New,
|
||||
dbQueueId = DBNewQueue,
|
||||
primary = True,
|
||||
@@ -1363,8 +1403,35 @@ newRcvQueue c userId connId (ProtoServerWithAuth srv auth) vRange subMode sender
|
||||
clientNtfCreds = Nothing,
|
||||
deleteErrors = 0
|
||||
}
|
||||
qUri = SMPQueueUri vRange $ SMPQueueAddress srv sndId e2eDhKey sndSecure
|
||||
pure (rq, qUri, tSess, sessId)
|
||||
qUri = SMPQueueUri vRange $ SMPQueueAddress srv sndId e2eDhKey queueMode
|
||||
pure (rq, qUri, tSess, sessionId thParams')
|
||||
where
|
||||
mkShortLinkCreds :: (THandleParams SMPVersion 'TClient, QueueIdsKeys) -> AM (Maybe ShortLinkCreds)
|
||||
mkShortLinkCreds (thParams', QIK {sndId, queueMode, linkId}) = case (cqrd, queueMode) of
|
||||
(CQRMessaging ld, Just QMMessaging) ->
|
||||
withLinkData ld $ \lnkId CQRData {linkKey, privSigKey, srvReq = (sndId', d)} ->
|
||||
if sndId == sndId'
|
||||
then pure $ Just $ ShortLinkCreds lnkId linkKey privSigKey (fst d)
|
||||
else newErr "different sender ID"
|
||||
(CQRContact ld, Just QMContact) ->
|
||||
withLinkData ld $ \lnkId CQRData {linkKey, privSigKey, srvReq = (lnkId', (sndId', d))} ->
|
||||
if sndId == sndId' && lnkId == lnkId'
|
||||
then pure $ Just $ ShortLinkCreds lnkId linkKey privSigKey (fst d)
|
||||
else newErr "different sender or link IDs"
|
||||
(_, Nothing) -> case linkId of
|
||||
Nothing | v < sndAuthKeySMPVersion -> pure Nothing
|
||||
_ -> newErr "unexpected link ID"
|
||||
_ -> newErr "unexpected queue mode"
|
||||
where
|
||||
v = thVersion thParams'
|
||||
withLinkData :: Maybe d -> (SMP.LinkId -> d -> AM (Maybe ShortLinkCreds)) -> AM (Maybe ShortLinkCreds)
|
||||
withLinkData ld_ mkLink = case (ld_, linkId) of
|
||||
(Just ld, Just lnkId) -> mkLink lnkId ld
|
||||
(Just _, Nothing) | v < shortLinksSMPVersion -> pure Nothing
|
||||
(Nothing, Nothing) -> pure Nothing
|
||||
_ -> newErr "unexpected or absent link ID"
|
||||
newErr :: String -> AM (Maybe ShortLinkCreds)
|
||||
newErr = throwE . BROKER (B.unpack $ strEncode srv) . UNEXPECTED . ("Create queue: " <>)
|
||||
|
||||
processSubResult :: AgentClient -> SessionId -> RcvQueue -> Either SMPClientError () -> STM ()
|
||||
processSubResult c sessId rq@RcvQueue {userId, server, connId} = \case
|
||||
@@ -1554,8 +1621,8 @@ logSecret' = B64.encode . B.take 3
|
||||
{-# INLINE logSecret' #-}
|
||||
|
||||
sendConfirmation :: AgentClient -> SndQueue -> ByteString -> AM (Maybe SMPServer)
|
||||
sendConfirmation c sq@SndQueue {userId, server, connId, sndId, sndSecure, sndPublicKey, sndPrivateKey, e2ePubKey = e2ePubKey@Just {}} agentConfirmation = do
|
||||
let (privHdr, spKey) = if sndSecure then (SMP.PHEmpty, Just sndPrivateKey) else (SMP.PHConfirmation sndPublicKey, Nothing)
|
||||
sendConfirmation c sq@SndQueue {userId, server, connId, sndId, queueMode, sndPublicKey, sndPrivateKey, e2ePubKey = e2ePubKey@Just {}} agentConfirmation = do
|
||||
let (privHdr, spKey) = if senderCanSecure queueMode then (SMP.PHEmpty, Just sndPrivateKey) else (SMP.PHConfirmation sndPublicKey, Nothing)
|
||||
clientMsg = SMP.ClientMessage privHdr agentConfirmation
|
||||
msg <- agentCbEncrypt sq e2ePubKey $ smpEncode clientMsg
|
||||
sendOrProxySMPMessage c userId server connId "<CONF>" spKey sndId (MsgFlags {notification = True}) msg
|
||||
@@ -1587,6 +1654,7 @@ getQueueMessage c rq@RcvQueue {server, rcvId, rcvPrivateKey} = do
|
||||
l <- maybe (newTMVar ()) pure l_
|
||||
takeTMVar l
|
||||
pure $ Just l
|
||||
{-# INLINE getQueueMessage #-}
|
||||
|
||||
decryptSMPMessage :: RcvQueue -> SMP.RcvMessage -> AM SMP.ClientRcvMsgBody
|
||||
decryptSMPMessage rq SMP.RcvMessage {msgId, msgBody = SMP.EncRcvMsgBody body} =
|
||||
@@ -1607,6 +1675,28 @@ secureSndQueue c SndQueue {userId, connId, server, sndId, sndPrivateKey, sndPubl
|
||||
secureViaProxy smp proxySess = proxySecureSndSMPQueue smp proxySess sndPrivateKey sndId sndPublicKey
|
||||
secureDirectly smp = secureSndSMPQueue smp sndPrivateKey sndId sndPublicKey
|
||||
|
||||
addQueueLink :: AgentClient -> RcvQueue -> SMP.LinkId -> QueueLinkData -> AM ()
|
||||
addQueueLink c rq@RcvQueue {rcvId, rcvPrivateKey} lnkId d =
|
||||
withSMPClient c rq "LSET" $ \smp -> addSMPQueueLink smp rcvPrivateKey rcvId lnkId d
|
||||
|
||||
deleteQueueLink :: AgentClient -> RcvQueue -> AM ()
|
||||
deleteQueueLink c rq@RcvQueue {rcvId, rcvPrivateKey} =
|
||||
withSMPClient c rq "LDEL" $ \smp -> deleteSMPQueueLink smp rcvPrivateKey rcvId
|
||||
|
||||
secureGetQueueLink :: AgentClient -> UserId -> InvShortLink -> AM (SMP.SenderId, QueueLinkData)
|
||||
secureGetQueueLink c userId InvShortLink {server, linkId, sndPrivateKey, sndPublicKey} =
|
||||
snd <$> sendOrProxySMPCommand c userId server (unEntityId linkId) "LKEY <key>" linkId secureGetViaProxy secureGetDirectly
|
||||
where
|
||||
secureGetViaProxy smp proxySess = proxySecureGetSMPQueueLink smp proxySess sndPrivateKey linkId sndPublicKey
|
||||
secureGetDirectly smp = secureGetSMPQueueLink smp sndPrivateKey linkId sndPublicKey
|
||||
|
||||
getQueueLink :: AgentClient -> UserId -> SMPServer -> SMP.LinkId -> AM (SMP.SenderId, QueueLinkData)
|
||||
getQueueLink c userId server lnkId =
|
||||
snd <$> sendOrProxySMPCommand c userId server (unEntityId lnkId) "LGET" lnkId getViaProxy getDirectly
|
||||
where
|
||||
getViaProxy smp proxySess = proxyGetSMPQueueLink smp proxySess lnkId
|
||||
getDirectly smp = getSMPQueueLink smp lnkId
|
||||
|
||||
enableQueueNotifications :: AgentClient -> RcvQueue -> SMP.NtfPublicAuthKey -> SMP.RcvNtfPublicDhKey -> AM (SMP.NotifierId, SMP.RcvNtfPublicDhKey)
|
||||
enableQueueNotifications c rq@RcvQueue {rcvId, rcvPrivateKey} notifierKey rcvNtfPublicDhKey =
|
||||
withSMPClient c rq "NKEY <nkey>" $ \smp ->
|
||||
@@ -1649,15 +1739,17 @@ disableQueuesNtfs = sendTSessionBatches "NDEL" snd disableQueues_
|
||||
sendAck :: AgentClient -> RcvQueue -> MsgId -> AM ()
|
||||
sendAck c rq@RcvQueue {rcvId, rcvPrivateKey} msgId =
|
||||
withSMPClient c rq ("ACK:" <> logSecret' msgId) $ \smp ->
|
||||
ackSMPMessage smp rcvPrivateKey rcvId msgId
|
||||
ackSMPMessage smp rcvPrivateKey rcvId msgId
|
||||
|
||||
hasGetLock :: AgentClient -> RcvQueue -> IO Bool
|
||||
hasGetLock c RcvQueue {server, rcvId} =
|
||||
TM.memberIO (server, rcvId) $ getMsgLocks c
|
||||
{-# INLINE hasGetLock #-}
|
||||
|
||||
releaseGetLock :: AgentClient -> RcvQueue -> STM ()
|
||||
releaseGetLock c RcvQueue {server, rcvId} =
|
||||
TM.lookup (server, rcvId) (getMsgLocks c) >>= mapM_ (`tryPutTMVar` ())
|
||||
{-# INLINE releaseGetLock #-}
|
||||
|
||||
suspendQueue :: AgentClient -> RcvQueue -> AM ()
|
||||
suspendQueue c rq@RcvQueue {rcvId, rcvPrivateKey} =
|
||||
@@ -1725,9 +1817,10 @@ agentNtfDeleteToken :: AgentClient -> NtfServer -> C.APrivateAuthKey -> NtfToken
|
||||
agentNtfDeleteToken c ntfServer ntfPrivKey tknId =
|
||||
withNtfClient c ntfServer tknId "TDEL" $ \ntf -> ntfDeleteToken ntf ntfPrivKey tknId
|
||||
|
||||
agentNtfEnableCron :: AgentClient -> NtfTokenId -> NtfToken -> Word16 -> AM ()
|
||||
agentNtfEnableCron c tknId NtfToken {ntfServer, ntfPrivKey} interval =
|
||||
withNtfClient c ntfServer tknId "TCRN" $ \ntf -> ntfEnableCron ntf ntfPrivKey tknId interval
|
||||
-- set to 0 to disable
|
||||
agentNtfSetCronInterval :: AgentClient -> NtfTokenId -> NtfToken -> Word16 -> AM ()
|
||||
agentNtfSetCronInterval c tknId NtfToken {ntfServer, ntfPrivKey} interval =
|
||||
withNtfClient c ntfServer tknId "TCRN" $ \ntf -> ntfSetCronInterval ntf ntfPrivKey tknId interval
|
||||
|
||||
agentNtfCreateSubscription :: AgentClient -> NtfTokenId -> NtfToken -> SMPQueueNtf -> SMP.NtfPrivateAuthKey -> AM NtfSubscriptionId
|
||||
agentNtfCreateSubscription c tknId NtfToken {ntfServer, ntfPrivKey} smpQueue nKey =
|
||||
@@ -1989,6 +2082,13 @@ withStore c action = do
|
||||
withExceptT storeError . ExceptT . liftIO . agentOperationBracket c AODatabase (\_ -> pure ()) $
|
||||
withTransaction st action `E.catches` handleDBErrors
|
||||
where
|
||||
#if defined(dbPostgres)
|
||||
-- TODO [postgres] postgres specific error handling
|
||||
handleDBErrors :: [E.Handler IO (Either StoreError a)]
|
||||
handleDBErrors =
|
||||
[ E.Handler $ \(E.SomeException e) -> pure . Left $ SEInternal $ bshow e
|
||||
]
|
||||
#else
|
||||
handleDBErrors :: [E.Handler IO (Either StoreError a)]
|
||||
handleDBErrors =
|
||||
[ E.Handler $ \(e :: SQL.SQLError) ->
|
||||
@@ -1997,6 +2097,12 @@ withStore c action = do
|
||||
in pure . Left . (if busy then SEDatabaseBusy else SEInternal) $ bshow se,
|
||||
E.Handler $ \(E.SomeException e) -> pure . Left $ SEInternal $ bshow e
|
||||
]
|
||||
#endif
|
||||
|
||||
unsafeWithStore :: AgentClient -> (DB.Connection -> IO a) -> AM' a
|
||||
unsafeWithStore c action = do
|
||||
st <- asks store
|
||||
liftIO $ agentOperationBracket c AODatabase (\_ -> pure ()) $ withTransaction st action
|
||||
|
||||
withStoreBatch :: Traversable t => AgentClient -> (DB.Connection -> t (IO (Either AgentErrorType a))) -> AM' (t (Either AgentErrorType a))
|
||||
withStoreBatch c actions = do
|
||||
@@ -2020,7 +2126,7 @@ storeError = \case
|
||||
SEConnDuplicate -> CONN DUPLICATE
|
||||
SEBadConnType CRcv -> CONN SIMPLEX
|
||||
SEBadConnType CSnd -> CONN SIMPLEX
|
||||
SEInvitationNotFound -> CMD PROHIBITED "SEInvitationNotFound"
|
||||
SEInvitationNotFound cxt invId -> CMD PROHIBITED $ "SEInvitationNotFound " <> cxt <> ", invitationId = " <> show invId
|
||||
-- this error is never reported as store error,
|
||||
-- it is used to wrap agent operations when "transaction-like" store access is needed
|
||||
-- NOTE: network IO should NOT be used inside AgentStoreMonad
|
||||
@@ -2044,7 +2150,7 @@ pickServer = \case
|
||||
getNextServer ::
|
||||
(ProtocolTypeI p, UserProtocol p) =>
|
||||
AgentClient ->
|
||||
UserId ->
|
||||
UserId ->
|
||||
(UserServers p -> NonEmpty (Maybe OperatorId, ProtoServerWithAuth p)) ->
|
||||
[ProtocolServer p] ->
|
||||
AM (ProtoServerWithAuth p)
|
||||
@@ -2097,7 +2203,7 @@ withNextSrv ::
|
||||
UserId ->
|
||||
(UserServers p -> NonEmpty (Maybe OperatorId, ProtoServerWithAuth p)) ->
|
||||
TVar (Set TransportHost) ->
|
||||
[ProtocolServer p] ->
|
||||
[ProtocolServer p] ->
|
||||
(ProtoServerWithAuth p -> AM a) ->
|
||||
AM a
|
||||
withNextSrv c userId srvsSel triedHosts usedSrvs action = do
|
||||
|
||||
@@ -52,7 +52,6 @@ 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 qualified Data.List.NonEmpty as L
|
||||
@@ -68,8 +67,10 @@ import Numeric.Natural
|
||||
import Simplex.FileTransfer.Client (XFTPClientConfig (..), defaultXFTPClientConfig)
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Agent.Store.SQLite
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
|
||||
import Simplex.Messaging.Agent.Store (createStore)
|
||||
import Simplex.Messaging.Agent.Store.Common (DBStore)
|
||||
import Simplex.Messaging.Agent.Store.Interface (DBOpts)
|
||||
import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..), MigrationError (..))
|
||||
import Simplex.Messaging.Client
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.Ratchet (VersionRangeE2E, supportedE2EEncryptVRange)
|
||||
@@ -95,7 +96,8 @@ data InitialAgentServers = InitialAgentServers
|
||||
{ smp :: Map UserId (NonEmpty (ServerCfg 'PSMP)),
|
||||
ntf :: [NtfServer],
|
||||
xftp :: Map UserId (NonEmpty (ServerCfg 'PXFTP)),
|
||||
netCfg :: NetworkConfig
|
||||
netCfg :: NetworkConfig,
|
||||
presetDomains :: [HostName]
|
||||
}
|
||||
|
||||
data ServerCfg p = ServerCfg
|
||||
@@ -254,7 +256,7 @@ defaultAgentConfig =
|
||||
|
||||
data Env = Env
|
||||
{ config :: AgentConfig,
|
||||
store :: SQLiteStore,
|
||||
store :: DBStore,
|
||||
random :: TVar ChaChaDRG,
|
||||
randomServer :: TVar StdGen,
|
||||
ntfSupervisor :: NtfSupervisor,
|
||||
@@ -262,7 +264,7 @@ data Env = Env
|
||||
multicastSubscribers :: TMVar Int
|
||||
}
|
||||
|
||||
newSMPAgentEnv :: AgentConfig -> SQLiteStore -> IO Env
|
||||
newSMPAgentEnv :: AgentConfig -> DBStore -> IO Env
|
||||
newSMPAgentEnv config store = do
|
||||
random <- C.newRandom
|
||||
randomServer <- newTVarIO =<< liftIO newStdGen
|
||||
@@ -271,8 +273,8 @@ newSMPAgentEnv config store = do
|
||||
multicastSubscribers <- newTMVarIO 0
|
||||
pure Env {config, store, random, randomServer, ntfSupervisor, xftpAgent, multicastSubscribers}
|
||||
|
||||
createAgentStore :: FilePath -> ScrubbedBytes -> Bool -> MigrationConfirmation -> IO (Either MigrationError SQLiteStore)
|
||||
createAgentStore dbFilePath dbKey keepKey = createSQLiteStore dbFilePath dbKey keepKey Migrations.app
|
||||
createAgentStore :: DBOpts -> MigrationConfirmation -> IO (Either MigrationError DBStore)
|
||||
createAgentStore = createStore
|
||||
|
||||
data NtfSupervisor = NtfSupervisor
|
||||
{ ntfTkn :: TVar (Maybe NtfToken),
|
||||
|
||||
@@ -6,6 +6,7 @@ module Simplex.Messaging.Agent.Lock
|
||||
withLock',
|
||||
withGetLock,
|
||||
withGetLocks,
|
||||
getPutLock,
|
||||
)
|
||||
where
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Agent.Stats
|
||||
import Simplex.Messaging.Agent.Store
|
||||
import Simplex.Messaging.Agent.Store.SQLite
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import Simplex.Messaging.Agent.Store.AgentStore
|
||||
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Notifications.Types
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DerivingStrategies #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
@@ -19,6 +21,7 @@
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
|
||||
-- |
|
||||
-- Module : Simplex.Messaging.Agent.Protocol
|
||||
@@ -100,16 +103,31 @@ module Simplex.Messaging.Agent.Protocol
|
||||
ConnectionMode (..),
|
||||
SConnectionMode (..),
|
||||
AConnectionMode (..),
|
||||
cmInvitation,
|
||||
cmContact,
|
||||
ConnectionModeI (..),
|
||||
ConnectionRequestUri (..),
|
||||
AConnectionRequestUri (..),
|
||||
ConnReqUriData (..),
|
||||
CRClientData,
|
||||
ServiceScheme,
|
||||
FixedLinkData (..),
|
||||
ConnLinkData (..),
|
||||
OwnerAuth (..),
|
||||
OwnerId,
|
||||
ConnectionLink (..),
|
||||
AConnectionLink (..),
|
||||
ConnShortLink (..),
|
||||
AConnShortLink (..),
|
||||
CreatedConnLink (..),
|
||||
ACreatedConnLink (..),
|
||||
ContactConnType (..),
|
||||
ShortLinkScheme (..),
|
||||
LinkKey (..),
|
||||
sameConnReqContact,
|
||||
sameShortLinkContact,
|
||||
simplexChat,
|
||||
connReqUriP',
|
||||
simplexConnReqUri,
|
||||
simplexShortLink,
|
||||
AgentErrorType (..),
|
||||
CommandErrorType (..),
|
||||
ConnectionErrorType (..),
|
||||
@@ -129,6 +147,7 @@ module Simplex.Messaging.Agent.Protocol
|
||||
AgentMsgId,
|
||||
NotificationsMode (..),
|
||||
NotificationInfo (..),
|
||||
ConnMsgReq (..),
|
||||
|
||||
-- * Encode/decode
|
||||
serializeCommand,
|
||||
@@ -139,18 +158,26 @@ module Simplex.Messaging.Agent.Protocol
|
||||
serializeQueueStatus,
|
||||
queueStatusT,
|
||||
agentMessageType,
|
||||
aMessageType,
|
||||
extraSMPServerHosts,
|
||||
updateSMPServerHosts,
|
||||
shortenShortLink,
|
||||
restoreShortLink,
|
||||
linkUserData,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Applicative (optional, (<|>))
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..), Value (..), (.:), (.:?))
|
||||
import qualified Data.Aeson.TH as J
|
||||
import qualified Data.Aeson.Types as JT
|
||||
import Data.Attoparsec.ByteString.Char8 (Parser)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import qualified Data.ByteString.Base64.URL as B64
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Char (toLower, toUpper)
|
||||
import Data.Foldable (find)
|
||||
import Data.Functor (($>))
|
||||
import Data.Int (Int64)
|
||||
import Data.Kind (Type)
|
||||
@@ -164,10 +191,9 @@ import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Data.Time.Clock.System (SystemTime)
|
||||
import Data.Type.Equality
|
||||
import Data.Typeable ()
|
||||
import Data.Typeable (Typeable)
|
||||
import Data.Word (Word16, Word32)
|
||||
import Database.SQLite.Simple.FromField
|
||||
import Database.SQLite.Simple.ToField
|
||||
import Simplex.Messaging.Agent.Store.DB (Binary (..), FromField (..), ToField (..), blobFieldDecoder, fromTextField_)
|
||||
import Simplex.FileTransfer.Description
|
||||
import Simplex.FileTransfer.Protocol (FileParty (..))
|
||||
import Simplex.FileTransfer.Transport (XFTPErrorType)
|
||||
@@ -197,6 +223,7 @@ import Simplex.Messaging.Protocol
|
||||
MsgId,
|
||||
NMsgMeta,
|
||||
ProtocolServer (..),
|
||||
QueueMode (..),
|
||||
SMPClientVersion,
|
||||
SMPServer,
|
||||
SMPServerWithAuth,
|
||||
@@ -212,6 +239,8 @@ import Simplex.Messaging.Protocol
|
||||
sameSrvAddr,
|
||||
sndAuthKeySMPClientVersion,
|
||||
srvHostnamesSMPClientVersion,
|
||||
shortLinksSMPClientVersion,
|
||||
senderCanSecure,
|
||||
pattern ProtoServerWithAuth,
|
||||
pattern SMPServer,
|
||||
)
|
||||
@@ -231,6 +260,7 @@ import UnliftIO.Exception (Exception)
|
||||
-- 4 - delivery receipts (7/13/2023)
|
||||
-- 5 - post-quantum double ratchet (3/14/2024)
|
||||
-- 6 - secure reply queues with provided keys (6/14/2024)
|
||||
-- 7 - initialize ratchet on processing confirmation (7/18/2024)
|
||||
|
||||
data SMPAgentVersion
|
||||
|
||||
@@ -358,8 +388,8 @@ data AEvent (e :: AEntity) where
|
||||
MSGNTF :: MsgId -> Maybe UTCTime -> AEvent AEConn
|
||||
RCVD :: MsgMeta -> NonEmpty MsgReceipt -> AEvent AEConn
|
||||
QCONT :: AEvent AEConn
|
||||
DEL_RCVQ :: SMPServer -> SMP.RecipientId -> Maybe AgentErrorType -> AEvent AEConn
|
||||
DEL_CONN :: AEvent AEConn
|
||||
DEL_RCVQS :: NonEmpty (ConnId, SMPServer, SMP.RecipientId, Maybe AgentErrorType) -> AEvent AEConn
|
||||
DEL_CONNS :: NonEmpty ConnId -> AEvent AEConn
|
||||
DEL_USER :: Int64 -> AEvent AENone
|
||||
STAT :: ConnectionStats -> AEvent AEConn
|
||||
OK :: AEvent AEConn
|
||||
@@ -429,8 +459,8 @@ data AEventTag (e :: AEntity) where
|
||||
MSGNTF_ :: AEventTag AEConn
|
||||
RCVD_ :: AEventTag AEConn
|
||||
QCONT_ :: AEventTag AEConn
|
||||
DEL_RCVQ_ :: AEventTag AEConn
|
||||
DEL_CONN_ :: AEventTag AEConn
|
||||
DEL_RCVQS_ :: AEventTag AEConn
|
||||
DEL_CONNS_ :: AEventTag AEConn
|
||||
DEL_USER_ :: AEventTag AENone
|
||||
STAT_ :: AEventTag AEConn
|
||||
OK_ :: AEventTag AEConn
|
||||
@@ -484,8 +514,8 @@ aEventTag = \case
|
||||
MSGNTF {} -> MSGNTF_
|
||||
RCVD {} -> RCVD_
|
||||
QCONT -> QCONT_
|
||||
DEL_RCVQ {} -> DEL_RCVQ_
|
||||
DEL_CONN -> DEL_CONN_
|
||||
DEL_RCVQS _ -> DEL_RCVQS_
|
||||
DEL_CONNS _ -> DEL_CONNS_
|
||||
DEL_USER _ -> DEL_USER_
|
||||
STAT _ -> STAT_
|
||||
OK -> OK_
|
||||
@@ -649,11 +679,21 @@ instance FromField NotificationsMode where fromField = blobFieldDecoder $ parseA
|
||||
|
||||
data NotificationInfo = NotificationInfo
|
||||
{ ntfConnId :: ConnId,
|
||||
ntfDbQueueId :: Int64,
|
||||
ntfTs :: SystemTime,
|
||||
-- Nothing means that the message failed to decrypt or to decode,
|
||||
-- we can still show event notification
|
||||
ntfMsgMeta :: Maybe NMsgMeta
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data ConnMsgReq = ConnMsgReq
|
||||
{ msgConnId :: ConnId,
|
||||
msgDbQueueId :: Int64,
|
||||
msgTs :: Maybe UTCTime
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data ConnectionMode = CMInvitation | CMContact
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -675,21 +715,17 @@ data AConnectionMode = forall m. ConnectionModeI m => ACM (SConnectionMode m)
|
||||
instance Eq AConnectionMode where
|
||||
ACM m == ACM m' = isJust $ testEquality m m'
|
||||
|
||||
cmInvitation :: AConnectionMode
|
||||
cmInvitation = ACM SCMInvitation
|
||||
|
||||
cmContact :: AConnectionMode
|
||||
cmContact = ACM SCMContact
|
||||
|
||||
deriving instance Show AConnectionMode
|
||||
|
||||
connMode :: SConnectionMode m -> ConnectionMode
|
||||
connMode SCMInvitation = CMInvitation
|
||||
connMode SCMContact = CMContact
|
||||
{-# INLINE connMode #-}
|
||||
|
||||
connMode' :: ConnectionMode -> AConnectionMode
|
||||
connMode' CMInvitation = cmInvitation
|
||||
connMode' CMContact = cmContact
|
||||
connMode' CMInvitation = ACM SCMInvitation
|
||||
connMode' CMContact = ACM SCMContact
|
||||
{-# INLINE connMode' #-}
|
||||
|
||||
class ConnectionModeI (m :: ConnectionMode) where sConnectionMode :: SConnectionMode m
|
||||
|
||||
@@ -855,20 +891,7 @@ agentMessageType = \case
|
||||
AgentConnInfo _ -> AM_CONN_INFO
|
||||
AgentConnInfoReply {} -> AM_CONN_INFO_REPLY
|
||||
AgentRatchetInfo _ -> AM_RATCHET_INFO
|
||||
AgentMessage _ aMsg -> case aMsg of
|
||||
-- HELLO is used both in v1 and in v2, but differently.
|
||||
-- - in v1 (and, possibly, in v2 for simplex connections) can be sent multiple times,
|
||||
-- until the queue is secured - the OK response from the server instead of initial AUTH errors confirms it.
|
||||
-- - in v2 duplexHandshake it is sent only once, when it is known that the queue was secured.
|
||||
HELLO -> AM_HELLO_
|
||||
A_MSG _ -> AM_A_MSG_
|
||||
A_RCVD {} -> AM_A_RCVD_
|
||||
A_QCONT _ -> AM_QCONT_
|
||||
QADD _ -> AM_QADD_
|
||||
QKEY _ -> AM_QKEY_
|
||||
QUSE _ -> AM_QUSE_
|
||||
QTEST _ -> AM_QTEST_
|
||||
EREADY _ -> AM_EREADY_
|
||||
AgentMessage _ aMsg -> aMessageType aMsg
|
||||
|
||||
data APrivHeader = APrivHeader
|
||||
{ -- | sequential ID assigned by the sending agent
|
||||
@@ -946,6 +969,22 @@ data AMessage
|
||||
EREADY AgentMsgId
|
||||
deriving (Show)
|
||||
|
||||
aMessageType :: AMessage -> AgentMessageType
|
||||
aMessageType = \case
|
||||
-- HELLO is used both in v1 and in v2, but differently.
|
||||
-- - in v1 (and, possibly, in v2 for simplex connections) can be sent multiple times,
|
||||
-- until the queue is secured - the OK response from the server instead of initial AUTH errors confirms it.
|
||||
-- - in v2 duplexHandshake it is sent only once, when it is known that the queue was secured.
|
||||
HELLO -> AM_HELLO_
|
||||
A_MSG _ -> AM_A_MSG_
|
||||
A_RCVD {} -> AM_A_RCVD_
|
||||
A_QCONT _ -> AM_QCONT_
|
||||
QADD _ -> AM_QADD_
|
||||
QKEY _ -> AM_QKEY_
|
||||
QUSE _ -> AM_QUSE_
|
||||
QTEST _ -> AM_QTEST_
|
||||
EREADY _ -> AM_EREADY_
|
||||
|
||||
-- | this type is used to send as part of the protocol between different clients
|
||||
-- TODO possibly, rename fields and types referring to external and internal IDs to make them different
|
||||
data AMessageReceipt = AMessageReceipt
|
||||
@@ -1010,6 +1049,10 @@ instance Encoding AMessage where
|
||||
QTEST_ -> QTEST <$> smpP
|
||||
EREADY_ -> EREADY <$> smpP
|
||||
|
||||
instance ToField AMessage where toField = toField . Binary . smpEncode
|
||||
|
||||
instance FromField AMessage where fromField = blobFieldDecoder smpDecode
|
||||
|
||||
instance Encoding AMessageReceipt where
|
||||
smpEncode AMessageReceipt {agentMsgId, msgHash, rcptInfo} =
|
||||
smpEncode (agentMsgId, msgHash, Large rcptInfo)
|
||||
@@ -1034,6 +1077,36 @@ instance ConnectionModeI m => StrEncoding (ConnectionRequestUri m) where
|
||||
<> maybe [] (\cd -> [("data", encodeUtf8 cd)]) crClientData
|
||||
strP = connReqUriP' (Just SSSimplex)
|
||||
|
||||
instance ConnectionModeI m => Encoding (ConnectionRequestUri m) where
|
||||
smpEncode = \case
|
||||
CRInvitationUri crData e2eParams -> smpEncode (CMInvitation, crData, e2eParams)
|
||||
CRContactUri crData -> smpEncode (CMContact, crData)
|
||||
smpP = (\(ACR _ cr) -> checkConnMode cr) <$?> smpP
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
instance Encoding AConnectionRequestUri where
|
||||
smpEncode (ACR _ cr) = smpEncode cr
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP =
|
||||
smpP >>= \case
|
||||
CMInvitation -> ACR SCMInvitation <$> (CRInvitationUri <$> smpP <*> smpP)
|
||||
CMContact -> ACR SCMContact . CRContactUri <$> smpP
|
||||
|
||||
instance Encoding ConnReqUriData where
|
||||
smpEncode ConnReqUriData {crAgentVRange, crSmpQueues, crClientData} =
|
||||
smpEncode (crAgentVRange, crSmpQueues, Large . encodeUtf8 <$> crClientData)
|
||||
smpP = do
|
||||
(crAgentVRange, smpQueues, clientData) <- smpP
|
||||
-- This patch to compensate for the fact that queueMode QMContact won't be included in queue encoding,
|
||||
-- until min SMP client version is >= 3 (sndAuthKeySMPClientVersion).
|
||||
-- This is possible because SMP encoding of ConnReqUriData was not used prior to SMP client version 4.
|
||||
let crSmpQueues = L.map patchQueueMode smpQueues
|
||||
pure ConnReqUriData {crScheme = SSSimplex, crAgentVRange, crSmpQueues, crClientData = safeDecodeUtf8 . unLarge <$> clientData}
|
||||
where
|
||||
patchQueueMode q@SMPQueueUri {queueAddress = a} = case a of
|
||||
SMPQueueAddress {queueMode = Nothing} -> q {queueAddress = a {queueMode = Just QMContact}} :: SMPQueueUri
|
||||
_ -> q
|
||||
|
||||
connReqUriP' :: forall m. ConnectionModeI m => Maybe ServiceScheme -> Parser (ConnectionRequestUri m)
|
||||
connReqUriP' overrideScheme = do
|
||||
ACR m cr <- connReqUriP overrideScheme
|
||||
@@ -1083,6 +1156,13 @@ instance ToJSON AConnectionRequestUri where
|
||||
toJSON = strToJSON
|
||||
toEncoding = strToJEncoding
|
||||
|
||||
instance ConnectionModeI m => FromJSON (ConnShortLink m) where
|
||||
parseJSON = strParseJSON "ConnShortLink"
|
||||
|
||||
instance ConnectionModeI m => ToJSON (ConnShortLink m) where
|
||||
toJSON = strToJSON
|
||||
toEncoding = strToJEncoding
|
||||
|
||||
-- debug :: Show a => String -> a -> a
|
||||
-- debug name value = unsafePerformIO (putStrLn $ name <> ": " <> show value) `seq` value
|
||||
-- {-# INLINE debug #-}
|
||||
@@ -1097,6 +1177,16 @@ instance StrEncoding AConnectionMode where
|
||||
strEncode (ACM cMode) = strEncode $ connMode cMode
|
||||
strP = connMode' <$> strP
|
||||
|
||||
instance Encoding ConnectionMode where
|
||||
smpEncode = \case
|
||||
CMInvitation -> "I"
|
||||
CMContact -> "C"
|
||||
smpP =
|
||||
A.anyChar >>= \case
|
||||
'I' -> pure CMInvitation
|
||||
'C' -> pure CMContact
|
||||
_ -> fail "bad connection mode"
|
||||
|
||||
connModeT :: Text -> Maybe ConnectionMode
|
||||
connModeT = \case
|
||||
"INV" -> Just CMInvitation
|
||||
@@ -1144,16 +1234,20 @@ data SMPQueueInfo = SMPQueueInfo {clientVersion :: VersionSMPC, queueAddress ::
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance Encoding SMPQueueInfo where
|
||||
smpEncode (SMPQueueInfo clientVersion SMPQueueAddress {smpServer, senderId, dhPublicKey, sndSecure})
|
||||
| clientVersion >= sndAuthKeySMPClientVersion && sndSecure = smpEncode (clientVersion, smpServer, senderId, dhPublicKey, sndSecure)
|
||||
| clientVersion > initialSMPClientVersion = smpEncode (clientVersion, smpServer, senderId, dhPublicKey)
|
||||
smpEncode (SMPQueueInfo clientVersion SMPQueueAddress {smpServer, senderId, dhPublicKey, queueMode})
|
||||
| clientVersion >= shortLinksSMPClientVersion = addrEnc <> maybe "" smpEncode queueMode
|
||||
| clientVersion >= sndAuthKeySMPClientVersion && sndSecure = addrEnc <> smpEncode sndSecure
|
||||
| clientVersion > initialSMPClientVersion = addrEnc
|
||||
| otherwise = smpEncode clientVersion <> legacyEncodeServer smpServer <> smpEncode (senderId, dhPublicKey)
|
||||
where
|
||||
addrEnc = smpEncode (clientVersion, smpServer, senderId, dhPublicKey)
|
||||
sndSecure = senderCanSecure queueMode
|
||||
smpP = do
|
||||
clientVersion <- smpP
|
||||
smpServer <- if clientVersion > initialSMPClientVersion then smpP else updateSMPServerHosts <$> legacyServerP
|
||||
(senderId, dhPublicKey) <- smpP
|
||||
sndSecure <- fromMaybe False <$> optional smpP
|
||||
pure $ SMPQueueInfo clientVersion SMPQueueAddress {smpServer, senderId, dhPublicKey, sndSecure}
|
||||
queueMode <- queueModeP
|
||||
pure $ SMPQueueInfo clientVersion SMPQueueAddress {smpServer, senderId, dhPublicKey, queueMode}
|
||||
|
||||
-- This instance seems contrived and there was a temptation to split a common part of both types.
|
||||
-- But this is created to allow backward and forward compatibility where SMPQueueUri
|
||||
@@ -1180,7 +1274,7 @@ data SMPQueueAddress = SMPQueueAddress
|
||||
{ smpServer :: SMPServer,
|
||||
senderId :: SMP.SenderId,
|
||||
dhPublicKey :: C.PublicKeyX25519,
|
||||
sndSecure :: Bool
|
||||
queueMode :: Maybe QueueMode
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -1207,42 +1301,60 @@ sameQAddress (srv, qId) (srv', qId') = sameSrvAddr srv srv' && qId == qId'
|
||||
{-# INLINE sameQAddress #-}
|
||||
|
||||
instance StrEncoding SMPQueueUri where
|
||||
strEncode (SMPQueueUri vr SMPQueueAddress {smpServer = srv, senderId = qId, dhPublicKey, sndSecure})
|
||||
strEncode (SMPQueueUri vr SMPQueueAddress {smpServer = srv, senderId = qId, dhPublicKey, queueMode})
|
||||
| minVersion vr >= srvHostnamesSMPClientVersion = strEncode srv <> "/" <> strEncode qId <> "#/?" <> query queryParams
|
||||
| otherwise = legacyStrEncodeServer srv <> "/" <> strEncode qId <> "#/?" <> query (queryParams <> srvParam)
|
||||
where
|
||||
query = strEncode . QSP QEscape
|
||||
queryParams = [("v", strEncode vr), ("dh", strEncode dhPublicKey)] <> [("k", "s") | sndSecure]
|
||||
queryParams = [("v", strEncode vr), ("dh", strEncode dhPublicKey)] <> queueModeParam <> sndSecureParam
|
||||
where
|
||||
queueModeParam = case queueMode of
|
||||
Just QMMessaging -> [("q", "m")]
|
||||
Just QMContact -> [("q", "c")]
|
||||
Nothing -> []
|
||||
sndSecureParam = [("k", "s") | senderCanSecure queueMode && minVersion vr < shortLinksSMPClientVersion]
|
||||
srvParam = [("srv", strEncode $ TransportHosts_ hs) | not (null hs)]
|
||||
hs = L.tail $ host srv
|
||||
strP = do
|
||||
srv@ProtocolServer {host = h :| host} <- strP <* A.char '/'
|
||||
senderId <- strP <* optional (A.char '/') <* A.char '#'
|
||||
(vr, hs, dhPublicKey, sndSecure) <- versioned <|> unversioned
|
||||
(vr, hs, dhPublicKey, queueMode) <- versioned <|> unversioned
|
||||
let srv' = srv {host = h :| host <> hs}
|
||||
smpServer = if maxVersion vr < srvHostnamesSMPClientVersion then updateSMPServerHosts srv' else srv'
|
||||
pure $ SMPQueueUri vr SMPQueueAddress {smpServer, senderId, dhPublicKey, sndSecure}
|
||||
pure $ SMPQueueUri vr SMPQueueAddress {smpServer, senderId, dhPublicKey, queueMode}
|
||||
where
|
||||
unversioned = (versionToRange initialSMPClientVersion,[],,False) <$> strP <* A.endOfInput
|
||||
unversioned = (versionToRange initialSMPClientVersion,[],,Nothing) <$> strP <* A.endOfInput
|
||||
versioned = do
|
||||
dhKey_ <- optional strP
|
||||
query <- optional (A.char '/') *> A.char '?' *> strP
|
||||
vr <- queryParam "v" query
|
||||
dhKey <- maybe (queryParam "dh" query) pure dhKey_
|
||||
hs_ <- queryParam_ "srv" query
|
||||
let sndSecure = queryParamStr "k" query == Just "s"
|
||||
pure (vr, maybe [] thList_ hs_, dhKey, sndSecure)
|
||||
let queueMode = case queryParamStr "q" query of
|
||||
Just "m" -> Just QMMessaging
|
||||
Just "c" -> Just QMContact
|
||||
_ | queryParamStr "k" query == Just "s" -> Just QMMessaging
|
||||
_ -> Nothing
|
||||
pure (vr, maybe [] thList_ hs_, dhKey, queueMode)
|
||||
|
||||
instance Encoding SMPQueueUri where
|
||||
smpEncode (SMPQueueUri clientVRange SMPQueueAddress {smpServer, senderId, dhPublicKey, sndSecure})
|
||||
| maxVersion clientVRange >= sndAuthKeySMPClientVersion && sndSecure =
|
||||
smpEncode (clientVRange, smpServer, senderId, dhPublicKey, sndSecure)
|
||||
| otherwise =
|
||||
smpEncode (clientVRange, smpServer, senderId, dhPublicKey)
|
||||
smpEncode (SMPQueueUri clientVRange@(VersionRange minV maxV) SMPQueueAddress {smpServer, senderId, dhPublicKey, queueMode})
|
||||
-- The condition is for minVersion as earlier clients won't be able to support it.
|
||||
-- The alternative would be to encode both queueMode and sndSecure
|
||||
| minV >= shortLinksSMPClientVersion = addrEnc <> maybe "" smpEncode queueMode
|
||||
-- Earlier versions won't be able to ignore sndSecure, so we don't include it when it is False
|
||||
| minV >= sndAuthKeySMPClientVersion || (maxV >= sndAuthKeySMPClientVersion && sndSecure) = addrEnc <> smpEncode sndSecure
|
||||
| otherwise = addrEnc
|
||||
where
|
||||
addrEnc = smpEncode (clientVRange, smpServer, senderId, dhPublicKey)
|
||||
sndSecure = senderCanSecure queueMode
|
||||
smpP = do
|
||||
(clientVRange, smpServer, senderId, dhPublicKey) <- smpP
|
||||
sndSecure <- fromMaybe False <$> optional smpP
|
||||
pure $ SMPQueueUri clientVRange SMPQueueAddress {smpServer, senderId, dhPublicKey, sndSecure}
|
||||
queueMode <- queueModeP
|
||||
pure $ SMPQueueUri clientVRange SMPQueueAddress {smpServer, senderId, dhPublicKey, queueMode}
|
||||
|
||||
queueModeP :: Parser (Maybe QueueMode)
|
||||
queueModeP = Just <$> smpP <|> optional ((\case True -> QMMessaging; _ -> QMContact) <$> smpP)
|
||||
|
||||
data ConnectionRequestUri (m :: ConnectionMode) where
|
||||
CRInvitationUri :: ConnReqUriData -> RcvE2ERatchetParamsUri 'C.X448 -> ConnectionRequestUri CMInvitation
|
||||
@@ -1250,6 +1362,11 @@ data ConnectionRequestUri (m :: ConnectionMode) where
|
||||
-- they are passed in AgentInvitation message
|
||||
CRContactUri :: ConnReqUriData -> ConnectionRequestUri CMContact
|
||||
|
||||
simplexConnReqUri :: ConnectionRequestUri m -> ConnectionRequestUri m
|
||||
simplexConnReqUri = \case
|
||||
CRInvitationUri crData e2eParams -> CRInvitationUri crData {crScheme = SSSimplex} e2eParams
|
||||
CRContactUri crData -> CRContactUri crData {crScheme = SSSimplex}
|
||||
|
||||
deriving instance Eq (ConnectionRequestUri m)
|
||||
|
||||
deriving instance Show (ConnectionRequestUri m)
|
||||
@@ -1263,6 +1380,229 @@ instance Eq AConnectionRequestUri where
|
||||
|
||||
deriving instance Show AConnectionRequestUri
|
||||
|
||||
data ShortLinkScheme = SLSSimplex | SLSServer deriving (Eq, Show)
|
||||
|
||||
data ConnShortLink (m :: ConnectionMode) where
|
||||
CSLInvitation :: ShortLinkScheme -> SMPServer -> SMP.LinkId -> LinkKey -> ConnShortLink 'CMInvitation
|
||||
CSLContact :: ShortLinkScheme -> ContactConnType -> SMPServer -> LinkKey -> ConnShortLink 'CMContact
|
||||
|
||||
deriving instance Eq (ConnShortLink m)
|
||||
|
||||
deriving instance Show (ConnShortLink m)
|
||||
|
||||
simplexShortLink :: ConnShortLink m -> ConnShortLink m
|
||||
simplexShortLink = \case
|
||||
CSLInvitation _ srv lnkId k -> CSLInvitation SLSSimplex srv lnkId k
|
||||
CSLContact _ ct srv k -> CSLContact SLSSimplex ct srv k
|
||||
|
||||
newtype LinkKey = LinkKey ByteString -- sha3-256(fixed_data)
|
||||
deriving (Eq, Show)
|
||||
deriving newtype (FromField, StrEncoding)
|
||||
|
||||
instance ToField LinkKey where toField (LinkKey s) = toField $ Binary s
|
||||
|
||||
instance ConnectionModeI c => ToField (ConnectionLink c) where toField = toField . Binary . strEncode
|
||||
|
||||
instance (Typeable c, ConnectionModeI c) => FromField (ConnectionLink c) where fromField = blobFieldDecoder strDecode
|
||||
|
||||
instance ConnectionModeI c => ToField (ConnShortLink c) where toField = toField . Binary . strEncode
|
||||
|
||||
instance (Typeable c, ConnectionModeI c) => FromField (ConnShortLink c) where fromField = blobFieldDecoder strDecode
|
||||
|
||||
data ContactConnType = CCTContact | CCTChannel | CCTGroup deriving (Eq, Show)
|
||||
|
||||
data AConnShortLink = forall m. ConnectionModeI m => ACSL (SConnectionMode m) (ConnShortLink m)
|
||||
|
||||
data ConnectionLink m = CLFull (ConnectionRequestUri m) | CLShort (ConnShortLink m)
|
||||
deriving (Eq, Show)
|
||||
|
||||
data CreatedConnLink m = CCLink {connFullLink :: ConnectionRequestUri m, connShortLink :: Maybe (ConnShortLink m)}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data ACreatedConnLink = forall m. ConnectionModeI m => ACCL (SConnectionMode m) (CreatedConnLink m)
|
||||
|
||||
deriving instance Show ACreatedConnLink
|
||||
|
||||
data AConnectionLink = forall m. ConnectionModeI m => ACL (SConnectionMode m) (ConnectionLink m)
|
||||
|
||||
instance Eq AConnectionLink where
|
||||
ACL m cl == ACL m' cl' = case testEquality m m' of
|
||||
Just Refl -> cl == cl'
|
||||
_ -> False
|
||||
|
||||
deriving instance Show AConnectionLink
|
||||
|
||||
instance ConnectionModeI m => StrEncoding (ConnectionLink m) where
|
||||
strEncode = \case
|
||||
CLFull cr -> strEncode cr
|
||||
CLShort sl -> strEncode sl
|
||||
strP = (\(ACL _ cl) -> checkConnMode cl) <$?> strP
|
||||
{-# INLINE strP #-}
|
||||
|
||||
instance StrEncoding AConnectionLink where
|
||||
strEncode (ACL _ cl) = strEncode cl
|
||||
{-# INLINE strEncode #-}
|
||||
strP =
|
||||
(\(ACR m cr) -> ACL m (CLFull cr)) <$> strP
|
||||
<|> (\(ACSL m sl) -> ACL m (CLShort sl)) <$> strP
|
||||
|
||||
instance ConnectionModeI m => ToJSON (ConnectionLink m) where
|
||||
toEncoding = strToJEncoding
|
||||
toJSON = strToJSON
|
||||
|
||||
instance ConnectionModeI m => FromJSON (ConnectionLink m) where
|
||||
parseJSON = strParseJSON "ConnectionLink"
|
||||
|
||||
instance ToJSON AConnectionLink where
|
||||
toEncoding = strToJEncoding
|
||||
toJSON = strToJSON
|
||||
|
||||
instance FromJSON AConnectionLink where
|
||||
parseJSON = strParseJSON "AConnectionLink"
|
||||
|
||||
instance ConnectionModeI m => StrEncoding (ConnShortLink m) where
|
||||
strEncode = \case
|
||||
CSLInvitation sch srv (SMP.EntityId lnkId) (LinkKey k) -> slEncode sch srv 'i' lnkId k
|
||||
CSLContact sch ct srv (LinkKey k) -> slEncode sch srv (toLower $ ctTypeChar ct) "" k
|
||||
where
|
||||
slEncode sch (SMPServer (h :| hs) port (C.KeyHash kh)) linkType lnkId k =
|
||||
B.concat [authority, "/", B.singleton linkType, "#", lnkIdStr, B64.encodeUnpadded k, queryStr]
|
||||
where
|
||||
(authority, paramHosts) = case sch of
|
||||
SLSSimplex -> ("simplex:", h : hs)
|
||||
SLSServer -> ("https://" <> strEncode h, hs)
|
||||
lnkIdStr = if B.null lnkId then "" else B64.encodeUnpadded lnkId <> "/"
|
||||
queryStr = if B.null query then "" else "?" <> query
|
||||
query =
|
||||
strEncode . QSP QEscape $
|
||||
[("h", strEncode (TransportHosts_ paramHosts)) | not (null paramHosts)]
|
||||
<> [("p", B.pack port) | not (null port)]
|
||||
<> [("c", B64.encodeUnpadded kh) | not (B.null kh)]
|
||||
strP = (\(ACSL _ l) -> checkConnMode l) <$?> strP
|
||||
{-# INLINE strP #-}
|
||||
|
||||
instance StrEncoding AConnShortLink where
|
||||
strEncode (ACSL _ l) = strEncode l
|
||||
{-# INLINE strEncode #-}
|
||||
strP = do
|
||||
(sch, h_) <- authorityP <* A.char '/'
|
||||
ct_ <- contactTypeP <* optional (A.char '/') <* A.char '#'
|
||||
case ct_ of
|
||||
Nothing -> do
|
||||
lnkId <- strP <* A.char '/'
|
||||
k <- strP
|
||||
srv <- serverQueryP h_
|
||||
pure $ ACSL SCMInvitation $ CSLInvitation sch srv (SMP.EntityId lnkId) (LinkKey k)
|
||||
Just ct -> do
|
||||
k <- strP
|
||||
srv <- serverQueryP h_
|
||||
pure $ ACSL SCMContact $ CSLContact sch ct srv (LinkKey k)
|
||||
where
|
||||
authorityP =
|
||||
"simplex:" $> (SLSSimplex, Nothing)
|
||||
<|> "https://" *> ((SLSServer,) . Just <$> strP)
|
||||
<|> fail "bad short link scheme"
|
||||
contactTypeP = do
|
||||
Just <$> (A.anyChar >>= ctTypeP . toUpper)
|
||||
<|> A.char 'i' $> Nothing
|
||||
<|> fail "unknown short link type"
|
||||
serverQueryP h_ =
|
||||
optional (A.char '?' *> strP) >>= \case
|
||||
Nothing -> maybe noServer (pure . SMPServerOnlyHost) h_
|
||||
Just query -> do
|
||||
hs <- maybe noServer pure . L.nonEmpty . maybe id (:) h_ . maybe [] thList_ =<< queryParam_ "h" query
|
||||
p <- maybe "" show <$> queryParam_ @Word16 "p" query
|
||||
kh <- fromMaybe (C.KeyHash "") <$> queryParam_ "c" query
|
||||
pure $ SMPServer hs p kh
|
||||
noServer = fail "short link without server"
|
||||
|
||||
instance ConnectionModeI m => Encoding (ConnShortLink m) where
|
||||
smpEncode = \case
|
||||
CSLInvitation _ srv lnkId (LinkKey k) -> smpEncode (CMInvitation, srv, lnkId, k)
|
||||
CSLContact _ ct srv (LinkKey k) -> smpEncode (CMContact, ctTypeChar ct, srv, k)
|
||||
smpP = (\(ACSL _ l) -> checkConnMode l) <$?> smpP
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
instance Encoding AConnShortLink where
|
||||
smpEncode (ACSL _ l) = smpEncode l
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP =
|
||||
smpP >>= \case
|
||||
CMInvitation -> do
|
||||
(srv, lnkId, k) <- smpP
|
||||
pure $ ACSL SCMInvitation $ CSLInvitation SLSServer srv lnkId (LinkKey k)
|
||||
CMContact -> do
|
||||
ct <- ctTypeP =<< A.anyChar
|
||||
(srv, k) <- smpP
|
||||
pure $ ACSL SCMContact $ CSLContact SLSServer ct srv (LinkKey k)
|
||||
|
||||
ctTypeP :: Char -> Parser ContactConnType
|
||||
ctTypeP = \case
|
||||
'A' -> pure CCTContact
|
||||
'C' -> pure CCTChannel
|
||||
'G' -> pure CCTGroup
|
||||
_ -> fail "unknown contact address type"
|
||||
{-# INLINE ctTypeP #-}
|
||||
|
||||
ctTypeChar :: ContactConnType -> Char
|
||||
ctTypeChar = \case
|
||||
CCTContact -> 'A'
|
||||
CCTChannel -> 'C'
|
||||
CCTGroup -> 'G'
|
||||
{-# INLINE ctTypeChar #-}
|
||||
|
||||
-- the servers passed to this function should be all preset servers, not servers configured by the user.
|
||||
shortenShortLink :: NonEmpty SMPServer -> ConnShortLink m -> ConnShortLink m
|
||||
shortenShortLink presetSrvs = \case
|
||||
CSLInvitation sch srv lnkId linkKey -> CSLInvitation sch (shortServer srv) lnkId linkKey
|
||||
CSLContact sch ct srv linkKey -> CSLContact sch ct (shortServer srv) linkKey
|
||||
where
|
||||
shortServer srv@(SMPServer hs@(h :| _) p kh) =
|
||||
if isPresetServer then SMPServerOnlyHost h else srv
|
||||
where
|
||||
isPresetServer = case findPresetServer srv presetSrvs of
|
||||
Just (SMPServer hs' p' kh') ->
|
||||
all (`elem` hs') hs
|
||||
&& (p == p' || (null p' && (p == "443" || p == "5223")))
|
||||
&& kh == kh'
|
||||
Nothing -> False
|
||||
|
||||
-- explicit bidirectional is used for ghc 8.10.7 compatibility, [h]/[] patterns are not reversible.
|
||||
pattern SMPServerOnlyHost :: TransportHost -> SMPServer
|
||||
pattern SMPServerOnlyHost h <- SMPServer [h] "" (C.KeyHash "")
|
||||
where
|
||||
SMPServerOnlyHost h = SMPServer [h] "" (C.KeyHash "")
|
||||
|
||||
-- the servers passed to this function should be all preset servers, not servers configured by the user.
|
||||
restoreShortLink :: NonEmpty SMPServer -> ConnShortLink m -> ConnShortLink m
|
||||
restoreShortLink presetSrvs = \case
|
||||
CSLInvitation sch srv lnkId linkKey -> CSLInvitation sch (fullServer srv) lnkId linkKey
|
||||
CSLContact sch ct srv linkKey -> CSLContact sch ct (fullServer srv) linkKey
|
||||
where
|
||||
fullServer = \case
|
||||
s@(SMPServerOnlyHost _) -> fromMaybe s $ findPresetServer s presetSrvs
|
||||
s -> s
|
||||
|
||||
findPresetServer :: SMPServer -> NonEmpty SMPServer -> Maybe SMPServer
|
||||
findPresetServer ProtocolServer {host = h :| _} = find (\ProtocolServer {host = h' :| _} -> h == h')
|
||||
{-# INLINE findPresetServer #-}
|
||||
|
||||
sameConnReqContact :: ConnectionRequestUri 'CMContact -> ConnectionRequestUri 'CMContact -> Bool
|
||||
sameConnReqContact (CRContactUri ConnReqUriData {crSmpQueues = qs}) (CRContactUri ConnReqUriData {crSmpQueues = qs'}) =
|
||||
L.length qs == L.length qs' && all same (L.zip qs qs')
|
||||
where
|
||||
same (q, q') = sameQAddress (qAddress q) (qAddress q')
|
||||
|
||||
sameShortLinkContact :: ConnShortLink 'CMContact -> ConnShortLink 'CMContact -> Bool
|
||||
sameShortLinkContact (CSLContact _ ct srv k) (CSLContact _ ct' srv' k') =
|
||||
ct == ct' && sameSrvAddr srv srv' && k == k'
|
||||
|
||||
checkConnMode :: forall t m m'. (ConnectionModeI m, ConnectionModeI m') => t m' -> Either String (t m)
|
||||
checkConnMode c = case testEquality (sConnectionMode @m) (sConnectionMode @m') of
|
||||
Just Refl -> Right c
|
||||
Nothing -> Left "bad connection mode"
|
||||
{-# INLINE checkConnMode #-}
|
||||
|
||||
data ConnReqUriData = ConnReqUriData
|
||||
{ crScheme :: ServiceScheme,
|
||||
crAgentVRange :: VersionRangeSMPA,
|
||||
@@ -1273,6 +1613,86 @@ data ConnReqUriData = ConnReqUriData
|
||||
|
||||
type CRClientData = Text
|
||||
|
||||
data FixedLinkData c = FixedLinkData
|
||||
{ agentVRange :: VersionRangeSMPA,
|
||||
rootKey :: C.PublicKeyEd25519,
|
||||
connReq :: ConnectionRequestUri c
|
||||
}
|
||||
|
||||
data ConnLinkData c where
|
||||
InvitationLinkData :: VersionRangeSMPA -> ConnInfo -> ConnLinkData 'CMInvitation
|
||||
ContactLinkData ::
|
||||
{ agentVRange :: VersionRangeSMPA,
|
||||
-- direct connection via connReq in fixed data is allowed.
|
||||
direct :: Bool,
|
||||
-- additional owner keys to sign changes of mutable data.
|
||||
owners :: [OwnerAuth],
|
||||
-- alternative addresses of chat relays that receive requests for this contact address.
|
||||
relays :: [ConnShortLink 'CMContact],
|
||||
userData :: ConnInfo
|
||||
} -> ConnLinkData 'CMContact
|
||||
|
||||
data AConnLinkData = forall m. ConnectionModeI m => ACLD (SConnectionMode m) (ConnLinkData m)
|
||||
|
||||
linkUserData :: ConnLinkData c -> ConnInfo
|
||||
linkUserData = \case
|
||||
InvitationLinkData _ d -> d
|
||||
ContactLinkData {userData} -> userData
|
||||
|
||||
type OwnerId = ByteString
|
||||
|
||||
data OwnerAuth = OwnerAuth
|
||||
{ ownerId :: OwnerId, -- unique in the list, application specific - e.g., MemberId
|
||||
ownerKey :: C.PublicKeyEd25519,
|
||||
-- sender ID signed with ownerKey,
|
||||
-- confirms that the owner accepts being the owner.
|
||||
-- sender ID is used here as it is immutable for the queue, link data can be removed.
|
||||
ownerSig :: C.Signature 'C.Ed25519,
|
||||
-- null for root key authorization
|
||||
authOwnerId :: OwnerId,
|
||||
-- owner authorization, sig(ownerId || ownerKey, key(authOwnerId)),
|
||||
-- where authOwnerId is either null for a root key or some other owner authorized by root key, etc.
|
||||
-- Owner validation should detect and reject loops.
|
||||
authOwnerSig :: C.Signature 'C.Ed25519
|
||||
}
|
||||
|
||||
instance Encoding OwnerAuth where
|
||||
smpEncode OwnerAuth {ownerId, ownerKey, ownerSig, authOwnerId, authOwnerSig} =
|
||||
smpEncode (ownerId, ownerKey, C.signatureBytes ownerSig, authOwnerId, C.signatureBytes authOwnerSig)
|
||||
smpP = do
|
||||
(ownerId, ownerKey, ownerSig, authOwnerId, authOwnerSig) <- smpP
|
||||
pure OwnerAuth {ownerId, ownerKey, ownerSig, authOwnerId, authOwnerSig}
|
||||
|
||||
instance ConnectionModeI c => Encoding (FixedLinkData c) where
|
||||
smpEncode FixedLinkData {agentVRange, rootKey, connReq} =
|
||||
smpEncode (agentVRange, rootKey, connReq)
|
||||
smpP = do
|
||||
(agentVRange, rootKey, connReq) <- smpP
|
||||
pure FixedLinkData {agentVRange, rootKey, connReq}
|
||||
|
||||
instance ConnectionModeI c => Encoding (ConnLinkData c) where
|
||||
smpEncode = \case
|
||||
InvitationLinkData vr userData -> smpEncode (CMInvitation, vr, userData)
|
||||
ContactLinkData {agentVRange, direct, owners, relays, userData} ->
|
||||
B.concat [smpEncode (CMContact, agentVRange, direct), smpEncodeList owners, smpEncodeList relays, smpEncode userData]
|
||||
smpP = (\(ACLD _ d) -> checkConnMode d) <$?> smpP
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
instance Encoding AConnLinkData where
|
||||
smpEncode (ACLD _ d) = smpEncode d
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP =
|
||||
smpP >>= \case
|
||||
CMInvitation -> do
|
||||
(vr, userData) <- smpP
|
||||
pure $ ACLD SCMInvitation $ InvitationLinkData vr userData
|
||||
CMContact -> do
|
||||
(agentVRange, direct) <- smpP
|
||||
owners <- smpListP
|
||||
relays <- smpListP
|
||||
userData <- smpP
|
||||
pure $ ACLD SCMContact ContactLinkData {agentVRange, direct, owners, relays, userData}
|
||||
|
||||
-- | SMP queue status.
|
||||
data QueueStatus
|
||||
= -- | queue is created
|
||||
@@ -1405,6 +1825,8 @@ data SMPAgentError
|
||||
A_PROHIBITED {prohibitedErr :: String}
|
||||
| -- | incompatible version of SMP client, agent or encryption protocols
|
||||
A_VERSION
|
||||
| -- | failed signature, hash or senderId verification of retrieved link data
|
||||
A_LINK {linkErr :: String}
|
||||
| -- | cannot decrypt message
|
||||
A_CRYPTO {cryptoErr :: AgentCryptoError}
|
||||
| -- | duplicate message - this error is detected by ratchet decryption - this message will be ignored and not shown
|
||||
@@ -1517,3 +1939,22 @@ $(J.deriveJSON (sumTypeJSON id) ''AgentErrorType)
|
||||
$(J.deriveJSON (enumJSON $ dropPrefix "QD") ''QueueDirection)
|
||||
|
||||
$(J.deriveJSON (enumJSON $ dropPrefix "SP") ''SwitchPhase)
|
||||
|
||||
instance ConnectionModeI m => FromJSON (CreatedConnLink m) where
|
||||
parseJSON = $(J.mkParseJSON defaultJSON ''CreatedConnLink)
|
||||
|
||||
instance ConnectionModeI m => ToJSON (CreatedConnLink m) where
|
||||
toEncoding = $(J.mkToEncoding defaultJSON ''CreatedConnLink)
|
||||
toJSON = $(J.mkToJSON defaultJSON ''CreatedConnLink)
|
||||
|
||||
instance FromJSON ACreatedConnLink where
|
||||
parseJSON (Object v) = do
|
||||
ACR m cReq <- v .: "connFullLink"
|
||||
shortLink <- v .:? "connShortLink"
|
||||
pure $ ACCL m $ CCLink cReq shortLink
|
||||
parseJSON invalid =
|
||||
JT.prependFailure "bad ACreatedConnLink, " (JT.typeMismatch "Object" invalid)
|
||||
|
||||
instance ToJSON ACreatedConnLink where
|
||||
toEncoding (ACCL _ ccLink) = toEncoding ccLink
|
||||
toJSON (ACCL _ ccLink) = toJSON ccLink
|
||||
|
||||
@@ -10,11 +10,10 @@ import qualified Data.Aeson.TH as J
|
||||
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, NtfServer)
|
||||
import Simplex.Messaging.Agent.Store.DB (FromField (..), ToField (..), fromTextField_)
|
||||
import Simplex.Messaging.Parsers (defaultJSON)
|
||||
import Simplex.Messaging.Protocol (NtfServer, SMPServer, XFTPServer)
|
||||
import Simplex.Messaging.Util (decodeJSON, encodeJSON)
|
||||
import UnliftIO.STM
|
||||
|
||||
|
||||
@@ -29,8 +29,12 @@ import Data.Time (UTCTime)
|
||||
import Data.Type.Equality
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.RetryInterval (RI2State)
|
||||
import Simplex.Messaging.Agent.Store.Common
|
||||
import Simplex.Messaging.Agent.Store.Interface (createDBStore)
|
||||
import Simplex.Messaging.Agent.Store.Migrations.App (appMigrations)
|
||||
import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..), MigrationError (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.Ratchet (PQEncryption, PQSupport, RatchetX448)
|
||||
import Simplex.Messaging.Crypto.Ratchet (MsgEncryptKeyX448, PQEncryption, PQSupport, RatchetX448)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol
|
||||
( MsgBody,
|
||||
@@ -39,16 +43,19 @@ import Simplex.Messaging.Protocol
|
||||
NotifierId,
|
||||
NtfPrivateAuthKey,
|
||||
NtfPublicAuthKey,
|
||||
QueueMode,
|
||||
RcvDhSecret,
|
||||
RcvNtfDhSecret,
|
||||
RcvPrivateAuthKey,
|
||||
SndPrivateAuthKey,
|
||||
SndPublicAuthKey,
|
||||
SenderCanSecure,
|
||||
VersionSMPC,
|
||||
)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
|
||||
createStore :: DBOpts -> MigrationConfirmation -> IO (Either MigrationError DBStore)
|
||||
createStore dbOpts = createDBStore dbOpts appMigrations
|
||||
|
||||
-- * Queue types
|
||||
|
||||
data QueueStored = QSStored | QSNew
|
||||
@@ -85,7 +92,9 @@ data StoredRcvQueue (q :: QueueStored) = RcvQueue
|
||||
-- | sender queue ID
|
||||
sndId :: SMP.SenderId,
|
||||
-- | sender can secure the queue
|
||||
sndSecure :: SenderCanSecure,
|
||||
queueMode :: Maybe QueueMode,
|
||||
-- | short link ID and credentials
|
||||
shortLink :: Maybe ShortLinkCreds,
|
||||
-- | queue status
|
||||
status :: QueueStatus,
|
||||
-- | database queue ID (within connection)
|
||||
@@ -103,6 +112,14 @@ data StoredRcvQueue (q :: QueueStored) = RcvQueue
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data ShortLinkCreds = ShortLinkCreds
|
||||
{ shortLinkId :: SMP.LinkId,
|
||||
shortLinkKey :: LinkKey,
|
||||
linkPrivSigKey :: C.PrivateKeyEd25519,
|
||||
linkEncFixedData :: SMP.EncFixedDataBytes
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
rcvQueueInfo :: RcvQueue -> RcvQueueInfo
|
||||
rcvQueueInfo rq@RcvQueue {server, rcvSwchStatus} =
|
||||
RcvQueueInfo {rcvServer = server, rcvSwitchStatus = rcvSwchStatus, canAbortSwitch = canAbortRcvSwitch rq}
|
||||
@@ -130,6 +147,19 @@ data ClientNtfCreds = ClientNtfCreds
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
-- This record is stored in inv_short_links table.
|
||||
-- It is needed only for 1-time invitation links because of "secure-on-read" property of link data,
|
||||
-- that prevents undetected access to link data from link observers.
|
||||
data InvShortLink = InvShortLink
|
||||
{ server :: SMPServer,
|
||||
linkId :: SMP.LinkId,
|
||||
linkKey :: LinkKey,
|
||||
sndPrivateKey :: SndPrivateAuthKey, -- stored to allow retries
|
||||
sndPublicKey :: SndPublicAuthKey,
|
||||
sndId :: Maybe SMP.SenderId
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
type SndQueue = StoredSndQueue 'QSStored
|
||||
|
||||
type NewSndQueue = StoredSndQueue 'QSNew
|
||||
@@ -142,7 +172,7 @@ data StoredSndQueue (q :: QueueStored) = SndQueue
|
||||
-- | sender queue ID
|
||||
sndId :: SMP.SenderId,
|
||||
-- | sender can secure the queue
|
||||
sndSecure :: SenderCanSecure,
|
||||
queueMode :: Maybe QueueMode,
|
||||
-- | key pair used by the sender to authorize transmissions
|
||||
-- TODO combine keys to key pair so that types match
|
||||
sndPublicKey :: SndPublicAuthKey,
|
||||
@@ -536,9 +566,17 @@ data SndMsgData = SndMsgData
|
||||
msgBody :: MsgBody,
|
||||
pqEncryption :: PQEncryption,
|
||||
internalHash :: MsgHash,
|
||||
prevMsgHash :: MsgHash
|
||||
prevMsgHash :: MsgHash,
|
||||
sndMsgPrepData_ :: Maybe SndMsgPrepData
|
||||
}
|
||||
|
||||
data SndMsgPrepData = SndMsgPrepData
|
||||
{ encryptKey :: MsgEncryptKeyX448,
|
||||
paddedLen :: Int,
|
||||
sndMsgBodyId :: Int64
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data SndMsg = SndMsg
|
||||
{ internalId :: InternalId,
|
||||
internalSndId :: InternalSndId,
|
||||
@@ -554,7 +592,17 @@ data PendingMsgData = PendingMsgData
|
||||
msgBody :: MsgBody,
|
||||
pqEncryption :: PQEncryption,
|
||||
msgRetryState :: Maybe RI2State,
|
||||
internalTs :: InternalTs
|
||||
internalTs :: InternalTs,
|
||||
internalSndId :: InternalSndId,
|
||||
prevMsgHash :: PrevSndMsgHash,
|
||||
pendingMsgPrepData_ :: Maybe PendingMsgPrepData
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data PendingMsgPrepData = PendingMsgPrepData
|
||||
{ encryptKey :: MsgEncryptKeyX448,
|
||||
paddedLen :: Int,
|
||||
sndMsgBody :: AMessage
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
@@ -623,7 +671,7 @@ data StoreError
|
||||
| -- | Confirmation not found.
|
||||
SEConfirmationNotFound
|
||||
| -- | Invitation not found
|
||||
SEInvitationNotFound
|
||||
SEInvitationNotFound String InvitationId
|
||||
| -- | Message not found
|
||||
SEMsgNotFound
|
||||
| -- | Command not found
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.Common
|
||||
#if defined(dbPostgres)
|
||||
( module Simplex.Messaging.Agent.Store.Postgres.Common,
|
||||
)
|
||||
where
|
||||
import Simplex.Messaging.Agent.Store.Postgres.Common
|
||||
#else
|
||||
( module Simplex.Messaging.Agent.Store.SQLite.Common,
|
||||
)
|
||||
where
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Common
|
||||
#endif
|
||||
@@ -0,0 +1,18 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.DB
|
||||
#if defined(dbPostgres)
|
||||
( module Simplex.Messaging.Agent.Store.Postgres.DB,
|
||||
FromField (..),
|
||||
ToField (..),
|
||||
)
|
||||
where
|
||||
import Simplex.Messaging.Agent.Store.Postgres.DB
|
||||
#else
|
||||
( module Simplex.Messaging.Agent.Store.SQLite.DB,
|
||||
FromField (..),
|
||||
ToField (..),
|
||||
)
|
||||
where
|
||||
import Simplex.Messaging.Agent.Store.SQLite.DB
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.Interface
|
||||
#if defined(dbPostgres)
|
||||
( module Simplex.Messaging.Agent.Store.Postgres,
|
||||
)
|
||||
where
|
||||
import Simplex.Messaging.Agent.Store.Postgres
|
||||
#else
|
||||
( module Simplex.Messaging.Agent.Store.SQLite,
|
||||
)
|
||||
where
|
||||
import Simplex.Messaging.Agent.Store.SQLite
|
||||
#endif
|
||||
@@ -0,0 +1,78 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.Migrations
|
||||
( Migration (..),
|
||||
MigrationsToRun (..),
|
||||
DownMigration (..),
|
||||
DBMigrate (..),
|
||||
sharedMigrateSchema,
|
||||
-- for tests
|
||||
migrationsToRun,
|
||||
toDownMigration,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad
|
||||
import Data.Char (toLower)
|
||||
import Data.Functor (($>))
|
||||
import Data.Maybe (isNothing, mapMaybe)
|
||||
import Simplex.Messaging.Agent.Store.Shared
|
||||
import System.Exit (exitFailure)
|
||||
import System.IO (hFlush, stdout)
|
||||
|
||||
migrationsToRun :: [Migration] -> [Migration] -> Either MTRError MigrationsToRun
|
||||
migrationsToRun [] [] = Right MTRNone
|
||||
migrationsToRun appMs [] = Right $ MTRUp appMs
|
||||
migrationsToRun [] dbMs
|
||||
| length dms == length dbMs = Right $ MTRDown dms
|
||||
| otherwise = Left $ MTRENoDown $ mapMaybe nameNoDown dbMs
|
||||
where
|
||||
dms = mapMaybe toDownMigration dbMs
|
||||
nameNoDown m = if isNothing (down m) then Just $ name m else Nothing
|
||||
migrationsToRun (a : as) (d : ds)
|
||||
| name a == name d = migrationsToRun as ds
|
||||
| otherwise = Left $ MTREDifferent (name a) (name d)
|
||||
|
||||
data DBMigrate = DBMigrate
|
||||
{ initialize :: IO (),
|
||||
getCurrent :: IO [Migration],
|
||||
run :: MigrationsToRun -> IO (),
|
||||
backup :: IO ()
|
||||
}
|
||||
|
||||
sharedMigrateSchema :: DBMigrate -> Bool -> [Migration] -> MigrationConfirmation -> IO (Either MigrationError ())
|
||||
sharedMigrateSchema dbm dbNew' migrations confirmMigrations = do
|
||||
initialize dbm
|
||||
currentMs <- getCurrent dbm
|
||||
case migrationsToRun migrations currentMs of
|
||||
Left e -> do
|
||||
when (confirmMigrations == MCConsole) $ confirmOrExit ("Database state error: " <> mtrErrorDescription e)
|
||||
pure . Left $ MigrationError e
|
||||
Right MTRNone -> pure $ Right ()
|
||||
Right ms@(MTRUp ums)
|
||||
| dbNew' -> run dbm ms $> Right ()
|
||||
| otherwise -> case confirmMigrations of
|
||||
MCYesUp -> runWithBackup ms
|
||||
MCYesUpDown -> runWithBackup ms
|
||||
MCConsole -> confirm err >> runWithBackup ms
|
||||
MCError -> pure $ Left err
|
||||
where
|
||||
err = MEUpgrade $ map upMigration ums -- "The app has a newer version than the database.\nConfirm to back up and upgrade using these migrations: " <> intercalate ", " (map name ums)
|
||||
Right ms@(MTRDown dms) -> case confirmMigrations of
|
||||
MCYesUpDown -> runWithBackup ms
|
||||
MCConsole -> confirm err >> runWithBackup ms
|
||||
MCYesUp -> pure $ Left err
|
||||
MCError -> pure $ Left err
|
||||
where
|
||||
err = MEDowngrade $ map downName dms
|
||||
where
|
||||
runWithBackup ms = backup dbm >> run dbm ms $> Right ()
|
||||
confirm err = confirmOrExit $ migrationErrorDescription err
|
||||
|
||||
confirmOrExit :: String -> IO ()
|
||||
confirmOrExit s = do
|
||||
putStrLn s
|
||||
putStr "Continue (y/N): "
|
||||
hFlush stdout
|
||||
ok <- getLine
|
||||
when (map toLower ok /= "y") exitFailure
|
||||
@@ -0,0 +1,14 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.Migrations.App
|
||||
#if defined(dbPostgres)
|
||||
( module Simplex.Messaging.Agent.Store.Postgres.Migrations.App,
|
||||
)
|
||||
where
|
||||
import Simplex.Messaging.Agent.Store.Postgres.Migrations.App
|
||||
#else
|
||||
( module Simplex.Messaging.Agent.Store.SQLite.Migrations.App,
|
||||
)
|
||||
where
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.App
|
||||
#endif
|
||||
@@ -0,0 +1,133 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.Postgres
|
||||
( DBOpts (..),
|
||||
Migrations.getCurrentMigrations,
|
||||
checkSchemaExists,
|
||||
createDBStore,
|
||||
closeDBStore,
|
||||
reopenDBStore,
|
||||
execSQL,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Control.Exception (finally, onException, throwIO, uninterruptibleMask_)
|
||||
import Control.Logger.Simple (logError)
|
||||
import Control.Monad
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Functor (($>))
|
||||
import Data.Text (Text)
|
||||
import Database.PostgreSQL.Simple (Only (..))
|
||||
import Database.PostgreSQL.Simple.Types (Query (..))
|
||||
import qualified Database.PostgreSQL.Simple as PSQL
|
||||
import Database.PostgreSQL.Simple.SqlQQ (sql)
|
||||
import Simplex.Messaging.Agent.Store.Migrations (DBMigrate (..), sharedMigrateSchema)
|
||||
import qualified Simplex.Messaging.Agent.Store.Postgres.Migrations as Migrations
|
||||
import Simplex.Messaging.Agent.Store.Postgres.Common
|
||||
import qualified Simplex.Messaging.Agent.Store.Postgres.DB as DB
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..), MigrationConfirmation (..), MigrationError (..))
|
||||
import Simplex.Messaging.Util (ifM, safeDecodeUtf8)
|
||||
import System.Exit (exitFailure)
|
||||
|
||||
-- | Create a new Postgres DBStore with the given connection string, schema name and migrations.
|
||||
-- If passed schema does not exist in connectInfo database, it will be created.
|
||||
-- Applies necessary migrations to schema.
|
||||
createDBStore :: DBOpts -> [Migration] -> MigrationConfirmation -> IO (Either MigrationError DBStore)
|
||||
createDBStore opts migrations confirmMigrations = do
|
||||
st <- connectPostgresStore opts
|
||||
r <- migrateSchema st `onException` closeDBStore st
|
||||
case r of
|
||||
Right () -> pure $ Right st
|
||||
Left e -> closeDBStore st $> Left e
|
||||
where
|
||||
migrateSchema st =
|
||||
let initialize = Migrations.initialize st
|
||||
getCurrent = withTransaction st Migrations.getCurrentMigrations
|
||||
dbm = DBMigrate {initialize, getCurrent, run = Migrations.run st, backup = pure ()}
|
||||
in sharedMigrateSchema dbm (dbNew st) migrations confirmMigrations
|
||||
|
||||
connectPostgresStore :: DBOpts -> IO DBStore
|
||||
connectPostgresStore DBOpts {connstr, schema, poolSize, createSchema} = do
|
||||
dbPriorityPool <- newDBStorePool poolSize
|
||||
dbPool <- newDBStorePool poolSize
|
||||
dbClosed <- newTVarIO True
|
||||
let st = DBStore {dbConnstr = connstr, dbSchema = schema, dbPoolSize = fromIntegral poolSize, dbPriorityPool, dbPool, dbNew = False, dbClosed}
|
||||
dbNew <- connectStore st createSchema
|
||||
pure st {dbNew}
|
||||
|
||||
-- uninterruptibleMask_ here and below is used here so that it is not interrupted half-way,
|
||||
-- it relies on the assumption that when dbClosed = True, the queue is empty,
|
||||
-- and when it is False, the queue is full (or will have connections returned to it by the threads that use them).
|
||||
connectStore :: DBStore -> Bool -> IO Bool
|
||||
connectStore DBStore {dbConnstr, dbSchema, dbPoolSize, dbPriorityPool, dbPool, dbClosed} createSchema = uninterruptibleMask_ $ do
|
||||
(conn, dbNew) <- connectDB dbConnstr dbSchema createSchema -- TODO [postgres] analogue for dbBusyLoop?
|
||||
writeConns dbPriorityPool . (conn :) =<< mkConns (dbPoolSize - 1)
|
||||
writeConns dbPool =<< mkConns dbPoolSize
|
||||
atomically $ writeTVar dbClosed False
|
||||
pure dbNew
|
||||
where
|
||||
writeConns pool conns = mapM_ (atomically . writeTBQueue (dbPoolConns pool)) conns
|
||||
mkConns n = replicateM n $ fst <$> connectDB dbConnstr dbSchema False
|
||||
|
||||
connectDB :: ByteString -> ByteString -> Bool -> IO (DB.Connection, Bool)
|
||||
connectDB connstr schema createSchema = do
|
||||
db <- PSQL.connectPostgreSQL connstr
|
||||
dbNew <- prepare db `onException` PSQL.close db
|
||||
pure (db, dbNew)
|
||||
where
|
||||
prepare db = do
|
||||
void $ PSQL.execute_ db "SET client_min_messages TO WARNING"
|
||||
dbNew <- not <$> doesSchemaExist db schema
|
||||
when dbNew $
|
||||
if createSchema
|
||||
then void $ PSQL.execute_ db $ Query $ "CREATE SCHEMA " <> schema
|
||||
else do
|
||||
logError $ "connectPostgresStore, schema " <> safeDecodeUtf8 schema <> " does not exist, exiting."
|
||||
PSQL.close db
|
||||
exitFailure
|
||||
void $ PSQL.execute_ db $ Query $ "SET search_path TO " <> schema
|
||||
pure dbNew
|
||||
|
||||
checkSchemaExists :: ByteString -> ByteString -> IO Bool
|
||||
checkSchemaExists connstr schema = do
|
||||
db <- PSQL.connectPostgreSQL connstr
|
||||
doesSchemaExist db schema `finally` DB.close db
|
||||
|
||||
doesSchemaExist :: DB.Connection -> ByteString -> IO Bool
|
||||
doesSchemaExist db schema = do
|
||||
[Only schemaExists] <-
|
||||
PSQL.query
|
||||
db
|
||||
[sql|
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM pg_catalog.pg_namespace
|
||||
WHERE nspname = ?
|
||||
)
|
||||
|]
|
||||
(Only schema)
|
||||
pure schemaExists
|
||||
|
||||
closeDBStore :: DBStore -> IO ()
|
||||
closeDBStore DBStore {dbPoolSize, dbPriorityPool, dbPool, dbClosed} =
|
||||
ifM (readTVarIO dbClosed) (putStrLn "closeDBStore: already closed") $ uninterruptibleMask_ $ do
|
||||
closePool dbPriorityPool
|
||||
closePool dbPool
|
||||
atomically $ writeTVar dbClosed True
|
||||
where
|
||||
closePool pool = replicateM_ dbPoolSize $ atomically (readTBQueue $ dbPoolConns pool) >>= DB.close
|
||||
|
||||
reopenDBStore :: DBStore -> IO ()
|
||||
reopenDBStore st =
|
||||
ifM
|
||||
(readTVarIO $ dbClosed st)
|
||||
(void $ connectStore st False)
|
||||
(putStrLn "reopenDBStore: already opened")
|
||||
|
||||
-- not used with postgres client (used for ExecAgentStoreSQL, ExecChatStoreSQL)
|
||||
execSQL :: PSQL.Connection -> Text -> IO [Text]
|
||||
execSQL _db _query = throwIO (userError "not implemented")
|
||||
@@ -0,0 +1,88 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.Postgres.Common
|
||||
( DBStore (..),
|
||||
DBStorePool (..),
|
||||
DBOpts (..),
|
||||
newDBStorePool,
|
||||
withConnection,
|
||||
withConnection',
|
||||
withTransaction,
|
||||
withTransaction',
|
||||
withTransactionPriority,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent.MVar
|
||||
import Control.Concurrent.STM
|
||||
import Control.Exception (bracket)
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Database.PostgreSQL.Simple as PSQL
|
||||
import Numeric.Natural (Natural)
|
||||
import Simplex.Messaging.Agent.Store.Postgres.Options
|
||||
|
||||
-- TODO [postgres] use log_min_duration_statement instead of custom slow queries (SQLite's Connection type)
|
||||
data DBStore = DBStore
|
||||
{ dbConnstr :: ByteString,
|
||||
dbSchema :: ByteString,
|
||||
dbPoolSize :: Int,
|
||||
dbPriorityPool :: DBStorePool,
|
||||
dbPool :: DBStorePool,
|
||||
-- dbPoolSize :: Int,
|
||||
-- dbPool :: TBQueue PSQL.Connection,
|
||||
-- -- MVar is needed for fair pool distribution, without STM retry contention.
|
||||
-- -- Only one thread can be blocked on STM read.
|
||||
-- dbSem :: MVar (),
|
||||
dbClosed :: TVar Bool,
|
||||
dbNew :: Bool
|
||||
}
|
||||
|
||||
newDBStorePool :: Natural -> IO DBStorePool
|
||||
newDBStorePool poolSize = do
|
||||
dbSem <- newMVar ()
|
||||
dbPoolConns <- newTBQueueIO poolSize
|
||||
pure DBStorePool {dbSem, dbPoolConns}
|
||||
|
||||
data DBStorePool = DBStorePool
|
||||
{ dbPoolConns :: TBQueue PSQL.Connection,
|
||||
-- MVar is needed for fair pool distribution, without STM retry contention.
|
||||
-- Only one thread can be blocked on STM read.
|
||||
dbSem :: MVar ()
|
||||
}
|
||||
|
||||
withConnectionPriority :: DBStore -> Bool -> (PSQL.Connection -> IO a) -> IO a
|
||||
withConnectionPriority DBStore {dbPriorityPool, dbPool} priority =
|
||||
withConnectionPool $ if priority then dbPriorityPool else dbPool
|
||||
{-# INLINE withConnectionPriority #-}
|
||||
|
||||
withConnectionPool :: DBStorePool -> (PSQL.Connection -> IO a) -> IO a
|
||||
withConnectionPool DBStorePool {dbPoolConns, dbSem} =
|
||||
bracket
|
||||
(withMVar dbSem $ \_ -> atomically $ readTBQueue dbPoolConns)
|
||||
(atomically . writeTBQueue dbPoolConns)
|
||||
|
||||
withConnection :: DBStore -> (PSQL.Connection -> IO a) -> IO a
|
||||
withConnection st = withConnectionPriority st False
|
||||
{-# INLINE withConnection #-}
|
||||
|
||||
withConnection' :: DBStore -> (PSQL.Connection -> IO a) -> IO a
|
||||
withConnection' = withConnection
|
||||
{-# INLINE withConnection' #-}
|
||||
|
||||
withTransaction' :: DBStore -> (PSQL.Connection -> IO a) -> IO a
|
||||
withTransaction' = withTransaction
|
||||
{-# INLINE withTransaction' #-}
|
||||
|
||||
withTransaction :: DBStore -> (PSQL.Connection -> IO a) -> IO a
|
||||
withTransaction st = withTransactionPriority st False
|
||||
{-# INLINE withTransaction #-}
|
||||
|
||||
-- TODO [postgres] analogue for dbBusyLoop?
|
||||
withTransactionPriority :: DBStore -> Bool -> (PSQL.Connection -> IO a) -> IO a
|
||||
withTransactionPriority st priority action = withConnectionPriority st priority transaction
|
||||
where
|
||||
transaction conn = PSQL.withTransaction conn $ action conn
|
||||
@@ -0,0 +1,89 @@
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.Postgres.DB
|
||||
( BoolInt (..),
|
||||
PSQL.Binary (..),
|
||||
PSQL.Connection,
|
||||
FromField (..),
|
||||
ToField (..),
|
||||
PSQL.connect,
|
||||
PSQL.close,
|
||||
execute,
|
||||
execute_,
|
||||
executeMany,
|
||||
PSQL.query,
|
||||
PSQL.query_,
|
||||
blobFieldDecoder,
|
||||
fromTextField_,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad (void)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import Data.Int (Int64)
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (decodeUtf8)
|
||||
import Data.Typeable (Typeable)
|
||||
import Data.Word (Word16, Word32)
|
||||
import Database.PostgreSQL.Simple (ResultError (..))
|
||||
import qualified Database.PostgreSQL.Simple as PSQL
|
||||
import Database.PostgreSQL.Simple.FromField (Field (..), FieldParser, FromField (..), returnError)
|
||||
import Database.PostgreSQL.Simple.ToField (ToField (..))
|
||||
import Database.PostgreSQL.Simple.TypeInfo.Static (textOid, varcharOid)
|
||||
|
||||
newtype BoolInt = BI {unBI :: Bool}
|
||||
|
||||
instance FromField BoolInt where
|
||||
fromField field dat = BI . (/= (0 :: Int)) <$> fromField field dat
|
||||
{-# INLINE fromField #-}
|
||||
|
||||
instance ToField BoolInt where
|
||||
toField (BI b) = toField ((if b then 1 else 0) :: Int)
|
||||
{-# INLINE toField #-}
|
||||
|
||||
execute :: PSQL.ToRow q => PSQL.Connection -> PSQL.Query -> q -> IO ()
|
||||
execute db q qs = void $ PSQL.execute db q qs
|
||||
{-# INLINE execute #-}
|
||||
|
||||
execute_ :: PSQL.Connection -> PSQL.Query -> IO ()
|
||||
execute_ db q = void $ PSQL.execute_ db q
|
||||
{-# INLINE execute_ #-}
|
||||
|
||||
executeMany :: PSQL.ToRow q => PSQL.Connection -> PSQL.Query -> [q] -> IO ()
|
||||
executeMany db q qs = void $ PSQL.executeMany db q qs
|
||||
{-# INLINE executeMany #-}
|
||||
|
||||
-- orphan instances
|
||||
|
||||
-- used in FileSize
|
||||
instance FromField Word32 where
|
||||
fromField field dat = do
|
||||
i :: Int64 <- fromField field dat
|
||||
if i >= 0 && i <= fromIntegral (maxBound :: Word32)
|
||||
then pure (fromIntegral i :: Word32)
|
||||
else returnError ConversionFailed field "Negative value can't be converted to Word32"
|
||||
|
||||
-- used in Version
|
||||
instance FromField Word16 where
|
||||
fromField field dat = do
|
||||
i :: Int64 <- fromField field dat
|
||||
if i >= 0 && i <= fromIntegral (maxBound :: Word16)
|
||||
then pure (fromIntegral i :: Word16)
|
||||
else returnError ConversionFailed field "Negative value can't be converted to Word16"
|
||||
|
||||
blobFieldDecoder :: Typeable k => (ByteString -> Either String k) -> FieldParser k
|
||||
blobFieldDecoder dec f val = do
|
||||
x <- fromField f val
|
||||
case dec x of
|
||||
Right k -> pure k
|
||||
Left e -> returnError ConversionFailed f ("couldn't parse field: " ++ e)
|
||||
|
||||
fromTextField_ :: Typeable a => (Text -> Maybe a) -> FieldParser a
|
||||
fromTextField_ fromText f val =
|
||||
if typeOid f `elem` [textOid, varcharOid]
|
||||
then case val of
|
||||
Just t -> case fromText $ decodeUtf8 t of
|
||||
Just x -> pure x
|
||||
_ -> returnError ConversionFailed f "invalid text value"
|
||||
Nothing -> returnError UnexpectedNull f "NULL value found for non-NULL field"
|
||||
else returnError Incompatible f "expecting TEXT or VARCHAR column type"
|
||||
@@ -0,0 +1,68 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.Postgres.Migrations
|
||||
( initialize,
|
||||
run,
|
||||
getCurrentMigrations,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Exception (throwIO)
|
||||
import Control.Monad (void)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as TE
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import qualified Database.PostgreSQL.LibPQ as LibPQ
|
||||
import Database.PostgreSQL.Simple (Only (..))
|
||||
import qualified Database.PostgreSQL.Simple as PSQL
|
||||
import Database.PostgreSQL.Simple.Internal (Connection (..))
|
||||
import Database.PostgreSQL.Simple.SqlQQ (sql)
|
||||
import Simplex.Messaging.Agent.Store.Postgres.Common
|
||||
import Simplex.Messaging.Agent.Store.Shared
|
||||
import Simplex.Messaging.Util (($>>=))
|
||||
import UnliftIO.MVar
|
||||
|
||||
initialize :: DBStore -> IO ()
|
||||
initialize st = withTransaction' st $ \db ->
|
||||
void $
|
||||
PSQL.execute_
|
||||
db
|
||||
[sql|
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
name TEXT NOT NULL,
|
||||
ts TIMESTAMP NOT NULL,
|
||||
down TEXT,
|
||||
PRIMARY KEY (name)
|
||||
)
|
||||
|]
|
||||
|
||||
run :: DBStore -> MigrationsToRun -> IO ()
|
||||
run st = \case
|
||||
MTRUp [] -> pure ()
|
||||
MTRUp ms -> mapM_ runUp ms
|
||||
MTRDown ms -> mapM_ runDown $ reverse ms
|
||||
MTRNone -> pure ()
|
||||
where
|
||||
runUp Migration {name, up, down} = withTransaction' st $ \db -> do
|
||||
insert db
|
||||
execSQL db up
|
||||
where
|
||||
insert db = void $ PSQL.execute db "INSERT INTO migrations (name, down, ts) VALUES (?,?,?)" . (name,down,) =<< getCurrentTime
|
||||
runDown DownMigration {downName, downQuery} = withTransaction' st $ \db -> do
|
||||
execSQL db downQuery
|
||||
void $ PSQL.execute db "DELETE FROM migrations WHERE name = ?" (Only downName)
|
||||
execSQL db query =
|
||||
withMVar (connectionHandle db) $ \pqConn ->
|
||||
LibPQ.exec pqConn (TE.encodeUtf8 query) $>>= LibPQ.resultErrorMessage >>= \case
|
||||
Just e | not (B.null e) -> throwIO $ userError $ B.unpack e
|
||||
_ -> pure ()
|
||||
|
||||
getCurrentMigrations :: PSQL.Connection -> IO [Migration]
|
||||
getCurrentMigrations db = map toMigration <$> PSQL.query_ db "SELECT name, down FROM migrations ORDER BY name ASC;"
|
||||
where
|
||||
toMigration (name, down) = Migration {name, up = T.pack "", down}
|
||||
@@ -0,0 +1,23 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.Postgres.Migrations.App (appMigrations) where
|
||||
|
||||
import Data.List (sortOn)
|
||||
import Data.Text (Text)
|
||||
import Simplex.Messaging.Agent.Store.Postgres.Migrations.M20241210_initial
|
||||
import Simplex.Messaging.Agent.Store.Postgres.Migrations.M20250203_msg_bodies
|
||||
import Simplex.Messaging.Agent.Store.Postgres.Migrations.M20250322_short_links
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Text, Maybe Text)]
|
||||
schemaMigrations =
|
||||
[ ("20241210_initial", m20241210_initial, Nothing),
|
||||
("20250203_msg_bodies", m20250203_msg_bodies, Just down_m20250203_msg_bodies),
|
||||
("20250322_short_links", m20250322_short_links, Just down_m20250322_short_links)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
appMigrations :: [Migration]
|
||||
appMigrations = sortOn name $ map migration schemaMigrations
|
||||
where
|
||||
migration (name, up, down) = Migration {name, up, down = down}
|
||||
@@ -0,0 +1,545 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.Postgres.Migrations.M20241210_initial where
|
||||
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Text.RawString.QQ (r)
|
||||
|
||||
m20241210_initial :: Text
|
||||
m20241210_initial =
|
||||
T.pack
|
||||
[r|
|
||||
CREATE TABLE users(
|
||||
user_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
deleted SMALLINT NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE servers(
|
||||
host TEXT NOT NULL,
|
||||
port TEXT NOT NULL,
|
||||
key_hash BYTEA NOT NULL,
|
||||
PRIMARY KEY(host, port)
|
||||
);
|
||||
CREATE TABLE connections(
|
||||
conn_id BYTEA NOT NULL PRIMARY KEY,
|
||||
conn_mode TEXT NOT NULL,
|
||||
last_internal_msg_id BIGINT NOT NULL DEFAULT 0,
|
||||
last_internal_rcv_msg_id BIGINT NOT NULL DEFAULT 0,
|
||||
last_internal_snd_msg_id BIGINT NOT NULL DEFAULT 0,
|
||||
last_external_snd_msg_id BIGINT NOT NULL DEFAULT 0,
|
||||
last_rcv_msg_hash BYTEA NOT NULL DEFAULT ''::BYTEA,
|
||||
last_snd_msg_hash BYTEA NOT NULL DEFAULT ''::BYTEA,
|
||||
smp_agent_version INTEGER NOT NULL DEFAULT 1,
|
||||
duplex_handshake SMALLINT NULL DEFAULT 0,
|
||||
enable_ntfs SMALLINT,
|
||||
deleted SMALLINT NOT NULL DEFAULT 0,
|
||||
user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
ratchet_sync_state TEXT NOT NULL DEFAULT 'ok',
|
||||
deleted_at_wait_delivery TIMESTAMPTZ,
|
||||
pq_support SMALLINT NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE rcv_queues(
|
||||
host TEXT NOT NULL,
|
||||
port TEXT NOT NULL,
|
||||
rcv_id BYTEA NOT NULL,
|
||||
conn_id BYTEA NOT NULL REFERENCES connections ON DELETE CASCADE,
|
||||
rcv_private_key BYTEA NOT NULL,
|
||||
rcv_dh_secret BYTEA NOT NULL,
|
||||
e2e_priv_key BYTEA NOT NULL,
|
||||
e2e_dh_secret BYTEA,
|
||||
snd_id BYTEA NOT NULL,
|
||||
snd_key BYTEA,
|
||||
status TEXT NOT NULL,
|
||||
smp_server_version INTEGER NOT NULL DEFAULT 1,
|
||||
smp_client_version INTEGER,
|
||||
ntf_public_key BYTEA,
|
||||
ntf_private_key BYTEA,
|
||||
ntf_id BYTEA,
|
||||
rcv_ntf_dh_secret BYTEA,
|
||||
rcv_queue_id BIGINT NOT NULL,
|
||||
rcv_primary SMALLINT NOT NULL,
|
||||
replace_rcv_queue_id BIGINT NULL,
|
||||
delete_errors BIGINT NOT NULL DEFAULT 0,
|
||||
server_key_hash BYTEA,
|
||||
switch_status TEXT,
|
||||
deleted SMALLINT NOT NULL DEFAULT 0,
|
||||
snd_secure SMALLINT NOT NULL DEFAULT 0,
|
||||
last_broker_ts TIMESTAMPTZ,
|
||||
PRIMARY KEY(host, port, rcv_id),
|
||||
FOREIGN KEY(host, port) REFERENCES servers
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
UNIQUE(host, port, snd_id)
|
||||
);
|
||||
CREATE TABLE snd_queues(
|
||||
host TEXT NOT NULL,
|
||||
port TEXT NOT NULL,
|
||||
snd_id BYTEA NOT NULL,
|
||||
conn_id BYTEA NOT NULL REFERENCES connections ON DELETE CASCADE,
|
||||
snd_private_key BYTEA NOT NULL,
|
||||
e2e_dh_secret BYTEA NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
smp_server_version INTEGER NOT NULL DEFAULT 1,
|
||||
smp_client_version INTEGER NOT NULL DEFAULT 1,
|
||||
snd_public_key BYTEA,
|
||||
e2e_pub_key BYTEA,
|
||||
snd_queue_id BIGINT NOT NULL,
|
||||
snd_primary SMALLINT NOT NULL,
|
||||
replace_snd_queue_id BIGINT NULL,
|
||||
server_key_hash BYTEA,
|
||||
switch_status TEXT,
|
||||
snd_secure SMALLINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY(host, port, snd_id),
|
||||
FOREIGN KEY(host, port) REFERENCES servers
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
CREATE TABLE messages(
|
||||
conn_id BYTEA NOT NULL REFERENCES connections(conn_id)
|
||||
ON DELETE CASCADE,
|
||||
internal_id BIGINT NOT NULL,
|
||||
internal_ts TIMESTAMPTZ NOT NULL,
|
||||
internal_rcv_id BIGINT,
|
||||
internal_snd_id BIGINT,
|
||||
msg_type BYTEA NOT NULL,
|
||||
msg_body BYTEA NOT NULL DEFAULT ''::BYTEA,
|
||||
msg_flags TEXT NULL,
|
||||
pq_encryption SMALLINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY(conn_id, internal_id)
|
||||
);
|
||||
CREATE TABLE rcv_messages(
|
||||
conn_id BYTEA NOT NULL,
|
||||
internal_rcv_id BIGINT NOT NULL,
|
||||
internal_id BIGINT NOT NULL,
|
||||
external_snd_id BIGINT NOT NULL,
|
||||
broker_id BYTEA NOT NULL,
|
||||
broker_ts TIMESTAMPTZ NOT NULL,
|
||||
internal_hash BYTEA NOT NULL,
|
||||
external_prev_snd_hash BYTEA NOT NULL,
|
||||
integrity BYTEA NOT NULL,
|
||||
user_ack SMALLINT NULL DEFAULT 0,
|
||||
rcv_queue_id BIGINT NOT NULL,
|
||||
PRIMARY KEY(conn_id, internal_rcv_id),
|
||||
FOREIGN KEY(conn_id, internal_id) REFERENCES messages
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
ALTER TABLE messages
|
||||
ADD CONSTRAINT fk_messages_rcv_messages
|
||||
FOREIGN KEY (conn_id, internal_rcv_id) REFERENCES rcv_messages
|
||||
ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
|
||||
CREATE TABLE snd_messages(
|
||||
conn_id BYTEA NOT NULL,
|
||||
internal_snd_id BIGINT NOT NULL,
|
||||
internal_id BIGINT NOT NULL,
|
||||
internal_hash BYTEA NOT NULL,
|
||||
previous_msg_hash BYTEA NOT NULL DEFAULT ''::BYTEA,
|
||||
retry_int_slow BIGINT,
|
||||
retry_int_fast BIGINT,
|
||||
rcpt_internal_id BIGINT,
|
||||
rcpt_status TEXT,
|
||||
PRIMARY KEY(conn_id, internal_snd_id),
|
||||
FOREIGN KEY(conn_id, internal_id) REFERENCES messages
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
ALTER TABLE messages
|
||||
ADD CONSTRAINT fk_messages_snd_messages
|
||||
FOREIGN KEY (conn_id, internal_snd_id) REFERENCES snd_messages
|
||||
ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
|
||||
CREATE TABLE conn_confirmations(
|
||||
confirmation_id BYTEA NOT NULL PRIMARY KEY,
|
||||
conn_id BYTEA NOT NULL REFERENCES connections ON DELETE CASCADE,
|
||||
e2e_snd_pub_key BYTEA NOT NULL,
|
||||
sender_key BYTEA,
|
||||
ratchet_state BYTEA NOT NULL,
|
||||
sender_conn_info BYTEA NOT NULL,
|
||||
accepted SMALLINT NOT NULL,
|
||||
own_conn_info BYTEA,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
smp_reply_queues BYTEA NULL,
|
||||
smp_client_version INTEGER
|
||||
);
|
||||
CREATE TABLE conn_invitations(
|
||||
invitation_id BYTEA NOT NULL PRIMARY KEY,
|
||||
contact_conn_id BYTEA NOT NULL REFERENCES connections ON DELETE CASCADE,
|
||||
cr_invitation BYTEA NOT NULL,
|
||||
recipient_conn_info BYTEA NOT NULL,
|
||||
accepted SMALLINT NOT NULL DEFAULT 0,
|
||||
own_conn_info BYTEA,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT (now())
|
||||
);
|
||||
CREATE TABLE ratchets(
|
||||
conn_id BYTEA NOT NULL PRIMARY KEY REFERENCES connections
|
||||
ON DELETE CASCADE,
|
||||
x3dh_priv_key_1 BYTEA,
|
||||
x3dh_priv_key_2 BYTEA,
|
||||
ratchet_state BYTEA,
|
||||
e2e_version INTEGER NOT NULL DEFAULT 1,
|
||||
x3dh_pub_key_1 BYTEA,
|
||||
x3dh_pub_key_2 BYTEA,
|
||||
pq_priv_kem BYTEA,
|
||||
pq_pub_kem BYTEA
|
||||
);
|
||||
CREATE TABLE skipped_messages(
|
||||
skipped_message_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
conn_id BYTEA NOT NULL REFERENCES ratchets
|
||||
ON DELETE CASCADE,
|
||||
header_key BYTEA NOT NULL,
|
||||
msg_n BIGINT NOT NULL,
|
||||
msg_key BYTEA NOT NULL
|
||||
);
|
||||
CREATE TABLE ntf_servers(
|
||||
ntf_host TEXT NOT NULL,
|
||||
ntf_port TEXT NOT NULL,
|
||||
ntf_key_hash BYTEA NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
PRIMARY KEY(ntf_host, ntf_port)
|
||||
);
|
||||
CREATE TABLE ntf_tokens(
|
||||
provider TEXT NOT NULL,
|
||||
device_token TEXT NOT NULL,
|
||||
ntf_host TEXT NOT NULL,
|
||||
ntf_port TEXT NOT NULL,
|
||||
tkn_id BYTEA,
|
||||
tkn_pub_key BYTEA NOT NULL,
|
||||
tkn_priv_key BYTEA NOT NULL,
|
||||
tkn_pub_dh_key BYTEA NOT NULL,
|
||||
tkn_priv_dh_key BYTEA NOT NULL,
|
||||
tkn_dh_secret BYTEA,
|
||||
tkn_status TEXT NOT NULL,
|
||||
tkn_action BYTEA,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
ntf_mode BYTEA NULL,
|
||||
PRIMARY KEY(provider, device_token, ntf_host, ntf_port),
|
||||
FOREIGN KEY(ntf_host, ntf_port) REFERENCES ntf_servers
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
CREATE TABLE ntf_subscriptions(
|
||||
conn_id BYTEA NOT NULL,
|
||||
smp_host TEXT NULL,
|
||||
smp_port TEXT NULL,
|
||||
smp_ntf_id BYTEA,
|
||||
ntf_host TEXT NOT NULL,
|
||||
ntf_port TEXT NOT NULL,
|
||||
ntf_sub_id BYTEA,
|
||||
ntf_sub_status TEXT NOT NULL,
|
||||
ntf_sub_action BYTEA,
|
||||
ntf_sub_smp_action BYTEA,
|
||||
ntf_sub_action_ts TIMESTAMPTZ,
|
||||
updated_by_supervisor SMALLINT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
smp_server_key_hash BYTEA,
|
||||
ntf_failed SMALLINT DEFAULT 0,
|
||||
smp_failed SMALLINT DEFAULT 0,
|
||||
PRIMARY KEY(conn_id),
|
||||
FOREIGN KEY(smp_host, smp_port) REFERENCES servers(host, port)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
FOREIGN KEY(ntf_host, ntf_port) REFERENCES ntf_servers
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
CREATE TABLE commands(
|
||||
command_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
conn_id BYTEA NOT NULL REFERENCES connections ON DELETE CASCADE,
|
||||
host TEXT,
|
||||
port TEXT,
|
||||
corr_id BYTEA NOT NULL,
|
||||
command_tag BYTEA NOT NULL,
|
||||
command BYTEA NOT NULL,
|
||||
agent_version INTEGER NOT NULL DEFAULT 1,
|
||||
server_key_hash BYTEA,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT '1970-01-01 00:00:00',
|
||||
failed SMALLINT DEFAULT 0,
|
||||
FOREIGN KEY(host, port) REFERENCES servers
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
CREATE TABLE snd_message_deliveries(
|
||||
snd_message_delivery_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
conn_id BYTEA NOT NULL REFERENCES connections ON DELETE CASCADE,
|
||||
snd_queue_id BIGINT NOT NULL,
|
||||
internal_id BIGINT NOT NULL,
|
||||
failed SMALLINT DEFAULT 0,
|
||||
FOREIGN KEY(conn_id, internal_id) REFERENCES messages ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
CREATE TABLE xftp_servers(
|
||||
xftp_server_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
xftp_host TEXT NOT NULL,
|
||||
xftp_port TEXT NOT NULL,
|
||||
xftp_key_hash BYTEA NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
UNIQUE(xftp_host, xftp_port, xftp_key_hash)
|
||||
);
|
||||
CREATE TABLE rcv_files(
|
||||
rcv_file_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
rcv_file_entity_id BYTEA NOT NULL,
|
||||
user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
size BIGINT NOT NULL,
|
||||
digest BYTEA NOT NULL,
|
||||
key BYTEA NOT NULL,
|
||||
nonce BYTEA NOT NULL,
|
||||
chunk_size BIGINT NOT NULL,
|
||||
prefix_path TEXT NOT NULL,
|
||||
tmp_path TEXT,
|
||||
save_path TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
deleted SMALLINT NOT NULL DEFAULT 0,
|
||||
error TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
save_file_key BYTEA,
|
||||
save_file_nonce BYTEA,
|
||||
failed SMALLINT DEFAULT 0,
|
||||
redirect_id BIGINT REFERENCES rcv_files ON DELETE SET NULL,
|
||||
redirect_entity_id BYTEA,
|
||||
redirect_size BIGINT,
|
||||
redirect_digest BYTEA,
|
||||
approved_relays SMALLINT NOT NULL DEFAULT 0,
|
||||
UNIQUE(rcv_file_entity_id)
|
||||
);
|
||||
CREATE TABLE rcv_file_chunks(
|
||||
rcv_file_chunk_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
rcv_file_id BIGINT NOT NULL REFERENCES rcv_files ON DELETE CASCADE,
|
||||
chunk_no BIGINT NOT NULL,
|
||||
chunk_size BIGINT NOT NULL,
|
||||
digest BYTEA NOT NULL,
|
||||
tmp_path TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT (now())
|
||||
);
|
||||
CREATE TABLE rcv_file_chunk_replicas(
|
||||
rcv_file_chunk_replica_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
rcv_file_chunk_id BIGINT NOT NULL REFERENCES rcv_file_chunks ON DELETE CASCADE,
|
||||
replica_number BIGINT NOT NULL,
|
||||
xftp_server_id BIGINT NOT NULL REFERENCES xftp_servers ON DELETE CASCADE,
|
||||
replica_id BYTEA NOT NULL,
|
||||
replica_key BYTEA NOT NULL,
|
||||
received SMALLINT NOT NULL DEFAULT 0,
|
||||
delay BIGINT,
|
||||
retries BIGINT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT (now())
|
||||
);
|
||||
CREATE TABLE snd_files(
|
||||
snd_file_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
snd_file_entity_id BYTEA NOT NULL,
|
||||
user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
num_recipients BIGINT NOT NULL,
|
||||
digest BYTEA,
|
||||
key BYTEA NOT NUll,
|
||||
nonce BYTEA NOT NUll,
|
||||
path TEXT NOT NULL,
|
||||
prefix_path TEXT,
|
||||
status TEXT NOT NULL,
|
||||
deleted SMALLINT NOT NULL DEFAULT 0,
|
||||
error TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
src_file_key BYTEA,
|
||||
src_file_nonce BYTEA,
|
||||
failed SMALLINT DEFAULT 0,
|
||||
redirect_size BIGINT,
|
||||
redirect_digest BYTEA
|
||||
);
|
||||
CREATE TABLE snd_file_chunks(
|
||||
snd_file_chunk_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
snd_file_id BIGINT NOT NULL REFERENCES snd_files ON DELETE CASCADE,
|
||||
chunk_no BIGINT NOT NULL,
|
||||
chunk_offset BIGINT NOT NULL,
|
||||
chunk_size BIGINT NOT NULL,
|
||||
digest BYTEA NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT (now())
|
||||
);
|
||||
CREATE TABLE snd_file_chunk_replicas(
|
||||
snd_file_chunk_replica_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
snd_file_chunk_id BIGINT NOT NULL REFERENCES snd_file_chunks ON DELETE CASCADE,
|
||||
replica_number BIGINT NOT NULL,
|
||||
xftp_server_id BIGINT NOT NULL REFERENCES xftp_servers ON DELETE CASCADE,
|
||||
replica_id BYTEA NOT NULL,
|
||||
replica_key BYTEA NOT NULL,
|
||||
replica_status TEXT NOT NULL,
|
||||
delay BIGINT,
|
||||
retries BIGINT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT (now())
|
||||
);
|
||||
CREATE TABLE snd_file_chunk_replica_recipients(
|
||||
snd_file_chunk_replica_recipient_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
snd_file_chunk_replica_id BIGINT NOT NULL REFERENCES snd_file_chunk_replicas ON DELETE CASCADE,
|
||||
rcv_replica_id BYTEA NOT NULL,
|
||||
rcv_replica_key BYTEA NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT (now())
|
||||
);
|
||||
CREATE TABLE deleted_snd_chunk_replicas(
|
||||
deleted_snd_chunk_replica_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
user_id BIGINT NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
xftp_server_id BIGINT NOT NULL REFERENCES xftp_servers ON DELETE CASCADE,
|
||||
replica_id BYTEA NOT NULL,
|
||||
replica_key BYTEA NOT NULL,
|
||||
chunk_digest BYTEA NOT NULL,
|
||||
delay BIGINT,
|
||||
retries BIGINT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
failed SMALLINT DEFAULT 0
|
||||
);
|
||||
CREATE TABLE encrypted_rcv_message_hashes(
|
||||
encrypted_rcv_message_hash_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
conn_id BYTEA NOT NULL REFERENCES connections ON DELETE CASCADE,
|
||||
hash BYTEA NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT (now())
|
||||
);
|
||||
CREATE TABLE processed_ratchet_key_hashes(
|
||||
processed_ratchet_key_hash_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
conn_id BYTEA NOT NULL REFERENCES connections ON DELETE CASCADE,
|
||||
hash BYTEA NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT (now())
|
||||
);
|
||||
CREATE TABLE servers_stats(
|
||||
servers_stats_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
servers_stats TEXT,
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT (now()),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT (now())
|
||||
);
|
||||
INSERT INTO servers_stats DEFAULT VALUES;
|
||||
CREATE TABLE ntf_tokens_to_delete(
|
||||
ntf_token_to_delete_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
ntf_host TEXT NOT NULL,
|
||||
ntf_port TEXT NOT NULL,
|
||||
ntf_key_hash BYTEA NOT NULL,
|
||||
tkn_id BYTEA NOT NULL,
|
||||
tkn_priv_key BYTEA NOT NULL,
|
||||
del_failed SMALLINT DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT (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);
|
||||
CREATE INDEX idx_snd_message_deliveries ON snd_message_deliveries(
|
||||
conn_id,
|
||||
snd_queue_id
|
||||
);
|
||||
CREATE INDEX idx_connections_user ON connections(user_id);
|
||||
CREATE INDEX idx_commands_conn_id ON commands(conn_id);
|
||||
CREATE INDEX idx_commands_host_port ON commands(host, port);
|
||||
CREATE INDEX idx_conn_confirmations_conn_id ON conn_confirmations(conn_id);
|
||||
CREATE INDEX idx_conn_invitations_contact_conn_id ON conn_invitations(
|
||||
contact_conn_id
|
||||
);
|
||||
CREATE INDEX idx_messages_conn_id_internal_snd_id ON messages(
|
||||
conn_id,
|
||||
internal_snd_id
|
||||
);
|
||||
CREATE INDEX idx_messages_conn_id_internal_rcv_id ON messages(
|
||||
conn_id,
|
||||
internal_rcv_id
|
||||
);
|
||||
CREATE INDEX idx_messages_conn_id ON messages(conn_id);
|
||||
CREATE INDEX idx_ntf_subscriptions_ntf_host_ntf_port ON ntf_subscriptions(
|
||||
ntf_host,
|
||||
ntf_port
|
||||
);
|
||||
CREATE INDEX idx_ntf_subscriptions_smp_host_smp_port ON ntf_subscriptions(
|
||||
smp_host,
|
||||
smp_port
|
||||
);
|
||||
CREATE INDEX idx_ntf_tokens_ntf_host_ntf_port ON ntf_tokens(
|
||||
ntf_host,
|
||||
ntf_port
|
||||
);
|
||||
CREATE INDEX idx_ratchets_conn_id ON ratchets(conn_id);
|
||||
CREATE INDEX idx_rcv_messages_conn_id_internal_id ON rcv_messages(
|
||||
conn_id,
|
||||
internal_id
|
||||
);
|
||||
CREATE INDEX idx_skipped_messages_conn_id ON skipped_messages(conn_id);
|
||||
CREATE INDEX idx_snd_message_deliveries_conn_id_internal_id ON snd_message_deliveries(
|
||||
conn_id,
|
||||
internal_id
|
||||
);
|
||||
CREATE INDEX idx_snd_messages_conn_id_internal_id ON snd_messages(
|
||||
conn_id,
|
||||
internal_id
|
||||
);
|
||||
CREATE INDEX idx_snd_queues_host_port ON snd_queues(host, port);
|
||||
CREATE INDEX idx_rcv_files_user_id ON rcv_files(user_id);
|
||||
CREATE INDEX idx_rcv_file_chunks_rcv_file_id ON rcv_file_chunks(rcv_file_id);
|
||||
CREATE INDEX idx_rcv_file_chunk_replicas_rcv_file_chunk_id ON rcv_file_chunk_replicas(
|
||||
rcv_file_chunk_id
|
||||
);
|
||||
CREATE INDEX idx_rcv_file_chunk_replicas_xftp_server_id ON rcv_file_chunk_replicas(
|
||||
xftp_server_id
|
||||
);
|
||||
CREATE INDEX idx_snd_files_user_id ON snd_files(user_id);
|
||||
CREATE INDEX idx_snd_file_chunks_snd_file_id ON snd_file_chunks(snd_file_id);
|
||||
CREATE INDEX idx_snd_file_chunk_replicas_snd_file_chunk_id ON snd_file_chunk_replicas(
|
||||
snd_file_chunk_id
|
||||
);
|
||||
CREATE INDEX idx_snd_file_chunk_replicas_xftp_server_id ON snd_file_chunk_replicas(
|
||||
xftp_server_id
|
||||
);
|
||||
CREATE INDEX idx_snd_file_chunk_replica_recipients_snd_file_chunk_replica_id ON snd_file_chunk_replica_recipients(
|
||||
snd_file_chunk_replica_id
|
||||
);
|
||||
CREATE INDEX idx_deleted_snd_chunk_replicas_user_id ON deleted_snd_chunk_replicas(
|
||||
user_id
|
||||
);
|
||||
CREATE INDEX idx_deleted_snd_chunk_replicas_xftp_server_id ON deleted_snd_chunk_replicas(
|
||||
xftp_server_id
|
||||
);
|
||||
CREATE INDEX idx_rcv_file_chunk_replicas_pending ON rcv_file_chunk_replicas(
|
||||
received,
|
||||
replica_number
|
||||
);
|
||||
CREATE INDEX idx_snd_file_chunk_replicas_pending ON snd_file_chunk_replicas(
|
||||
replica_status,
|
||||
replica_number
|
||||
);
|
||||
CREATE INDEX idx_deleted_snd_chunk_replicas_pending ON deleted_snd_chunk_replicas(
|
||||
created_at
|
||||
);
|
||||
CREATE INDEX idx_encrypted_rcv_message_hashes_hash ON encrypted_rcv_message_hashes(
|
||||
conn_id,
|
||||
hash
|
||||
);
|
||||
CREATE INDEX idx_processed_ratchet_key_hashes_hash ON processed_ratchet_key_hashes(
|
||||
conn_id,
|
||||
hash
|
||||
);
|
||||
CREATE INDEX idx_snd_messages_rcpt_internal_id ON snd_messages(
|
||||
conn_id,
|
||||
rcpt_internal_id
|
||||
);
|
||||
CREATE INDEX idx_processed_ratchet_key_hashes_created_at ON processed_ratchet_key_hashes(
|
||||
created_at
|
||||
);
|
||||
CREATE INDEX idx_encrypted_rcv_message_hashes_created_at ON encrypted_rcv_message_hashes(
|
||||
created_at
|
||||
);
|
||||
CREATE INDEX idx_messages_internal_ts ON messages(internal_ts);
|
||||
CREATE INDEX idx_commands_server_commands ON commands(
|
||||
host,
|
||||
port,
|
||||
created_at,
|
||||
command_id
|
||||
);
|
||||
CREATE INDEX idx_rcv_files_status_created_at ON rcv_files(status, created_at);
|
||||
CREATE INDEX idx_snd_files_status_created_at ON snd_files(status, created_at);
|
||||
CREATE INDEX idx_snd_files_snd_file_entity_id ON snd_files(snd_file_entity_id);
|
||||
CREATE INDEX idx_messages_snd_expired ON messages(
|
||||
conn_id,
|
||||
internal_snd_id,
|
||||
internal_ts
|
||||
);
|
||||
CREATE INDEX idx_snd_message_deliveries_expired ON snd_message_deliveries(
|
||||
conn_id,
|
||||
snd_queue_id,
|
||||
failed,
|
||||
internal_id
|
||||
);
|
||||
CREATE INDEX idx_rcv_files_redirect_id on rcv_files(redirect_id);
|
||||
|]
|
||||
@@ -0,0 +1,37 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.Postgres.Migrations.M20250203_msg_bodies where
|
||||
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Text.RawString.QQ (r)
|
||||
|
||||
m20250203_msg_bodies :: Text
|
||||
m20250203_msg_bodies =
|
||||
T.pack
|
||||
[r|
|
||||
ALTER TABLE snd_messages ADD COLUMN msg_encrypt_key BYTEA;
|
||||
ALTER TABLE snd_messages ADD COLUMN padded_msg_len BIGINT;
|
||||
|
||||
|
||||
CREATE TABLE snd_message_bodies (
|
||||
snd_message_body_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
agent_msg BYTEA NOT NULL DEFAULT ''::BYTEA
|
||||
);
|
||||
ALTER TABLE snd_messages ADD COLUMN snd_message_body_id BIGINT REFERENCES snd_message_bodies ON DELETE SET NULL;
|
||||
CREATE INDEX idx_snd_messages_snd_message_body_id ON snd_messages(snd_message_body_id);
|
||||
|]
|
||||
|
||||
|
||||
down_m20250203_msg_bodies :: Text
|
||||
down_m20250203_msg_bodies =
|
||||
T.pack
|
||||
[r|
|
||||
DROP INDEX idx_snd_messages_snd_message_body_id;
|
||||
ALTER TABLE snd_messages DROP COLUMN snd_message_body_id;
|
||||
DROP TABLE snd_message_bodies;
|
||||
|
||||
|
||||
ALTER TABLE snd_messages DROP COLUMN msg_encrypt_key;
|
||||
ALTER TABLE snd_messages DROP COLUMN padded_msg_len;
|
||||
|]
|
||||
@@ -0,0 +1,63 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.Postgres.Migrations.M20250322_short_links where
|
||||
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Text.RawString.QQ (r)
|
||||
|
||||
m20250322_short_links :: Text
|
||||
m20250322_short_links =
|
||||
T.pack
|
||||
[r|
|
||||
ALTER TABLE rcv_queues ADD COLUMN link_id BYTEA;
|
||||
ALTER TABLE rcv_queues ADD COLUMN link_key BYTEA;
|
||||
ALTER TABLE rcv_queues ADD COLUMN link_priv_sig_key BYTEA;
|
||||
ALTER TABLE rcv_queues ADD COLUMN link_enc_fixed_data BYTEA;
|
||||
|
||||
CREATE UNIQUE INDEX idx_rcv_queues_link_id ON rcv_queues(host, port, link_id);
|
||||
|
||||
ALTER TABLE rcv_queues ADD COLUMN queue_mode TEXT;
|
||||
UPDATE rcv_queues SET queue_mode = 'M' WHERE snd_secure = 1;
|
||||
ALTER TABLE rcv_queues DROP COLUMN snd_secure;
|
||||
|
||||
ALTER TABLE snd_queues ADD COLUMN queue_mode TEXT;
|
||||
UPDATE snd_queues SET queue_mode = 'M' WHERE snd_secure = 1;
|
||||
ALTER TABLE snd_queues DROP COLUMN snd_secure;
|
||||
|
||||
CREATE TABLE inv_short_links(
|
||||
inv_short_link_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
host TEXT NOT NULL,
|
||||
port TEXT NOT NULL,
|
||||
server_key_hash BYTEA,
|
||||
link_id BYTEA NOT NULL,
|
||||
link_key BYTEA NOT NULL,
|
||||
snd_private_key BYTEA NOT NULL,
|
||||
snd_id BYTEA,
|
||||
FOREIGN KEY(host, port) REFERENCES servers ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_inv_short_links_link_id ON inv_short_links(host, port, link_id);
|
||||
|]
|
||||
|
||||
down_m20250322_short_links :: Text
|
||||
down_m20250322_short_links =
|
||||
T.pack
|
||||
[r|
|
||||
DROP INDEX idx_rcv_queues_link_id;
|
||||
ALTER TABLE rcv_queues DROP COLUMN link_id;
|
||||
ALTER TABLE rcv_queues DROP COLUMN link_key;
|
||||
ALTER TABLE rcv_queues DROP COLUMN link_priv_sig_key;
|
||||
ALTER TABLE rcv_queues DROP COLUMN link_enc_fixed_data;
|
||||
|
||||
DROP INDEX idx_inv_short_links_link_id;
|
||||
DROP TABLE inv_short_links;
|
||||
|
||||
ALTER TABLE rcv_queues ADD COLUMN snd_secure INTEGER NOT NULL DEFAULT 0;
|
||||
UPDATE rcv_queues SET snd_secure = 1 WHERE queue_mode = 'M';
|
||||
ALTER TABLE rcv_queues DROP COLUMN queue_mode;
|
||||
|
||||
ALTER TABLE snd_queues ADD COLUMN snd_secure INTEGER NOT NULL DEFAULT 0;
|
||||
UPDATE snd_queues SET snd_secure = 1 WHERE queue_mode = 'M';
|
||||
ALTER TABLE snd_queues DROP COLUMN queue_mode;
|
||||
|]
|
||||
@@ -0,0 +1,12 @@
|
||||
module Simplex.Messaging.Agent.Store.Postgres.Options where
|
||||
|
||||
import Data.ByteString (ByteString)
|
||||
import Numeric.Natural
|
||||
|
||||
data DBOpts = DBOpts
|
||||
{ connstr :: ByteString,
|
||||
schema :: ByteString,
|
||||
poolSize :: Natural,
|
||||
createSchema :: Bool
|
||||
}
|
||||
deriving (Show)
|
||||
@@ -0,0 +1,100 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.Postgres.Util
|
||||
( createDBAndUserIfNotExists,
|
||||
dropSchema,
|
||||
dropAllSchemasExceptSystem,
|
||||
dropDatabaseAndUser,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Exception (bracket)
|
||||
import Control.Monad (forM_, unless, void, when)
|
||||
import Data.String (fromString)
|
||||
import Database.PostgreSQL.Simple (ConnectInfo (..), Only (..), defaultConnectInfo)
|
||||
import qualified Database.PostgreSQL.Simple as PSQL
|
||||
import Database.PostgreSQL.Simple.SqlQQ (sql)
|
||||
|
||||
createDBAndUserIfNotExists :: ConnectInfo -> IO ()
|
||||
createDBAndUserIfNotExists ConnectInfo {connectUser = user, connectDatabase = dbName} = do
|
||||
-- connect to the default "postgres" maintenance database
|
||||
bracket (PSQL.connect defaultConnectInfo {connectUser = "postgres", connectDatabase = "postgres"}) PSQL.close $
|
||||
\postgresDB -> do
|
||||
void $ PSQL.execute_ postgresDB "SET client_min_messages TO WARNING"
|
||||
-- check if the user exists, create if not
|
||||
[Only userExists] <-
|
||||
PSQL.query
|
||||
postgresDB
|
||||
[sql|
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM pg_catalog.pg_roles
|
||||
WHERE rolname = ?
|
||||
)
|
||||
|]
|
||||
(Only user)
|
||||
unless userExists $ void $ PSQL.execute_ postgresDB (fromString $ "CREATE USER " <> user)
|
||||
-- check if the database exists, create if not
|
||||
dbExists <- checkDBExists postgresDB dbName
|
||||
unless dbExists $ void $ PSQL.execute_ postgresDB (fromString $ "CREATE DATABASE " <> dbName <> " OWNER " <> user)
|
||||
|
||||
checkDBExists :: PSQL.Connection -> String -> IO Bool
|
||||
checkDBExists postgresDB dbName = do
|
||||
[Only dbExists] <-
|
||||
PSQL.query
|
||||
postgresDB
|
||||
[sql|
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM pg_catalog.pg_database
|
||||
WHERE datname = ?
|
||||
)
|
||||
|]
|
||||
(Only dbName)
|
||||
pure dbExists
|
||||
|
||||
dropSchema :: ConnectInfo -> String -> IO ()
|
||||
dropSchema connectInfo schema =
|
||||
bracket (PSQL.connect connectInfo) PSQL.close $
|
||||
\db -> do
|
||||
void $ PSQL.execute_ db "SET client_min_messages TO WARNING"
|
||||
void $ PSQL.execute_ db (fromString $ "DROP SCHEMA IF EXISTS " <> schema <> " CASCADE")
|
||||
|
||||
dropAllSchemasExceptSystem :: ConnectInfo -> IO ()
|
||||
dropAllSchemasExceptSystem connectInfo =
|
||||
bracket (PSQL.connect connectInfo) PSQL.close $
|
||||
\db -> do
|
||||
void $ PSQL.execute_ db "SET client_min_messages TO WARNING"
|
||||
schemaNames :: [Only String] <-
|
||||
PSQL.query_
|
||||
db
|
||||
[sql|
|
||||
SELECT schema_name
|
||||
FROM information_schema.schemata
|
||||
WHERE schema_name NOT IN ('public', 'pg_catalog', 'information_schema')
|
||||
|]
|
||||
forM_ schemaNames $ \(Only schema) ->
|
||||
PSQL.execute_ db (fromString $ "DROP SCHEMA " <> schema <> " CASCADE")
|
||||
|
||||
dropDatabaseAndUser :: ConnectInfo -> IO ()
|
||||
dropDatabaseAndUser ConnectInfo {connectUser = user, connectDatabase = dbName} =
|
||||
bracket (PSQL.connect defaultConnectInfo {connectUser = "postgres", connectDatabase = "postgres"}) PSQL.close $
|
||||
\postgresDB -> do
|
||||
void $ PSQL.execute_ postgresDB "SET client_min_messages TO WARNING"
|
||||
dbExists <- checkDBExists postgresDB dbName
|
||||
when dbExists $ do
|
||||
void $ PSQL.execute_ postgresDB (fromString $ "ALTER DATABASE " <> dbName <> " WITH ALLOW_CONNECTIONS false")
|
||||
-- terminate all connections to the database
|
||||
_r :: [Only Bool] <-
|
||||
PSQL.query
|
||||
postgresDB
|
||||
[sql|
|
||||
SELECT pg_terminate_backend(pg_stat_activity.pid)
|
||||
FROM pg_stat_activity
|
||||
WHERE datname = ?
|
||||
AND pid <> pg_backend_pid()
|
||||
|]
|
||||
(Only dbName)
|
||||
void $ PSQL.execute_ postgresDB (fromString $ "DROP DATABASE " <> dbName)
|
||||
void $ PSQL.execute_ postgresDB (fromString $ "DROP USER IF EXISTS " <> user)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,8 @@
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.SQLite.Common
|
||||
( SQLiteStore (..),
|
||||
( DBStore (..),
|
||||
DBOpts (..),
|
||||
withConnection,
|
||||
withConnection',
|
||||
withTransaction,
|
||||
@@ -30,7 +31,7 @@ import UnliftIO.STM
|
||||
storeKey :: ScrubbedBytes -> Bool -> Maybe ScrubbedBytes
|
||||
storeKey key keepKey = if keepKey || BA.null key then Just key else Nothing
|
||||
|
||||
data SQLiteStore = SQLiteStore
|
||||
data DBStore = DBStore
|
||||
{ dbFilePath :: FilePath,
|
||||
dbKey :: TVar (Maybe ScrubbedBytes),
|
||||
dbSem :: TVar Int,
|
||||
@@ -39,8 +40,16 @@ data SQLiteStore = SQLiteStore
|
||||
dbNew :: Bool
|
||||
}
|
||||
|
||||
withConnectionPriority :: SQLiteStore -> Bool -> (DB.Connection -> IO a) -> IO a
|
||||
withConnectionPriority SQLiteStore {dbSem, dbConnection} priority action
|
||||
data DBOpts = DBOpts
|
||||
{ dbFilePath :: FilePath,
|
||||
dbKey :: ScrubbedBytes,
|
||||
keepKey :: Bool,
|
||||
vacuum :: Bool,
|
||||
track :: DB.TrackQueries
|
||||
}
|
||||
|
||||
withConnectionPriority :: DBStore -> Bool -> (DB.Connection -> IO a) -> IO a
|
||||
withConnectionPriority DBStore {dbSem, dbConnection} priority action
|
||||
| priority = E.bracket_ signal release $ withMVar dbConnection action
|
||||
| otherwise = lowPriority
|
||||
where
|
||||
@@ -50,20 +59,20 @@ withConnectionPriority SQLiteStore {dbSem, dbConnection} priority action
|
||||
wait = unlessM free $ atomically $ unlessM ((0 ==) <$> readTVar dbSem) retry
|
||||
free = (0 ==) <$> readTVarIO dbSem
|
||||
|
||||
withConnection :: SQLiteStore -> (DB.Connection -> IO a) -> IO a
|
||||
withConnection :: DBStore -> (DB.Connection -> IO a) -> IO a
|
||||
withConnection st = withConnectionPriority st False
|
||||
|
||||
withConnection' :: SQLiteStore -> (SQL.Connection -> IO a) -> IO a
|
||||
withConnection' :: DBStore -> (SQL.Connection -> IO a) -> IO a
|
||||
withConnection' st action = withConnection st $ action . DB.conn
|
||||
|
||||
withTransaction' :: SQLiteStore -> (SQL.Connection -> IO a) -> IO a
|
||||
withTransaction' :: DBStore -> (SQL.Connection -> IO a) -> IO a
|
||||
withTransaction' st action = withTransaction st $ action . DB.conn
|
||||
|
||||
withTransaction :: SQLiteStore -> (DB.Connection -> IO a) -> IO a
|
||||
withTransaction :: DBStore -> (DB.Connection -> IO a) -> IO a
|
||||
withTransaction st = withTransactionPriority st False
|
||||
{-# INLINE withTransaction #-}
|
||||
|
||||
withTransactionPriority :: SQLiteStore -> Bool -> (DB.Connection -> IO a) -> IO a
|
||||
withTransactionPriority :: DBStore -> Bool -> (DB.Connection -> IO a) -> IO a
|
||||
withTransactionPriority st priority action = withConnectionPriority st priority $ dbBusyLoop . transaction
|
||||
where
|
||||
transaction db@DB.Connection {conn} = SQL.withImmediateTransaction conn $ action db
|
||||
|
||||
@@ -1,45 +1,69 @@
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DerivingStrategies #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE StrictData #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.SQLite.DB
|
||||
( Connection (..),
|
||||
( BoolInt (..),
|
||||
Binary (..),
|
||||
Connection (..),
|
||||
SlowQueryStats (..),
|
||||
TrackQueries (..),
|
||||
FromField (..),
|
||||
ToField (..),
|
||||
open,
|
||||
close,
|
||||
execute,
|
||||
execute_,
|
||||
executeNamed,
|
||||
executeMany,
|
||||
query,
|
||||
query_,
|
||||
queryNamed,
|
||||
blobFieldDecoder,
|
||||
fromTextField_,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad (when)
|
||||
import Control.Exception
|
||||
import Control.Monad (when)
|
||||
import qualified Data.Aeson.TH as J
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Int (Int64)
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time (diffUTCTime, getCurrentTime)
|
||||
import Database.SQLite.Simple (FromRow, NamedParam, Query, ToRow)
|
||||
import Data.Typeable (Typeable)
|
||||
import Database.SQLite.Simple (FromRow, ResultError (..), Query, SQLData (..), ToRow)
|
||||
import qualified Database.SQLite.Simple as SQL
|
||||
import Database.SQLite.Simple.FromField (FieldParser, FromField (..), returnError)
|
||||
import Database.SQLite.Simple.Internal (Field (..))
|
||||
import Database.SQLite.Simple.Ok (Ok (Ok))
|
||||
import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import Simplex.Messaging.Parsers (defaultJSON)
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Util (diffToMilliseconds, tshow)
|
||||
import Simplex.Messaging.Util (diffToMicroseconds, tshow)
|
||||
|
||||
newtype BoolInt = BI {unBI :: Bool}
|
||||
deriving newtype (FromField, ToField)
|
||||
|
||||
newtype Binary = Binary {fromBinary :: ByteString}
|
||||
deriving newtype (FromField, ToField)
|
||||
|
||||
data Connection = Connection
|
||||
{ conn :: SQL.Connection,
|
||||
track :: TrackQueries,
|
||||
slow :: TMap Query SlowQueryStats
|
||||
}
|
||||
|
||||
data TrackQueries = TQAll | TQSlow Int64 | TQOff
|
||||
deriving (Eq)
|
||||
|
||||
data SlowQueryStats = SlowQueryStats
|
||||
{ count :: Int64,
|
||||
timeMax :: Int64,
|
||||
@@ -48,21 +72,29 @@ data SlowQueryStats = SlowQueryStats
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
timeIt :: TMap Query SlowQueryStats -> Query -> IO a -> IO a
|
||||
timeIt slow sql a = do
|
||||
t <- getCurrentTime
|
||||
r <- a `catch` \e -> do
|
||||
atomically $ TM.alter (Just . updateQueryErrors e) sql slow
|
||||
throwIO e
|
||||
t' <- getCurrentTime
|
||||
let diff = diffToMilliseconds $ diffUTCTime t' t
|
||||
when (diff > 1) $ atomically $ TM.alter (updateQueryStats diff) sql slow
|
||||
pure r
|
||||
timeIt :: Connection -> Query -> IO a -> IO a
|
||||
timeIt Connection {slow, track} sql a
|
||||
| track == TQOff = makeQuery
|
||||
| otherwise = do
|
||||
t <- getCurrentTime
|
||||
r <- makeQuery
|
||||
t' <- getCurrentTime
|
||||
let diff = diffToMicroseconds $ diffUTCTime t' t
|
||||
when (trackQuery diff) $ atomically $ TM.alter (updateQueryStats diff) sql slow
|
||||
pure r
|
||||
where
|
||||
makeQuery =
|
||||
a `catch` \e -> do
|
||||
atomically $ TM.alter (Just . updateQueryErrors e) sql slow
|
||||
throwIO e
|
||||
trackQuery diff = case track of
|
||||
TQOff -> False
|
||||
TQSlow t -> diff > t
|
||||
TQAll -> True
|
||||
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}
|
||||
updateQueryErrors e (Just st@SlowQueryStats {errs}) =
|
||||
st {errs = M.alter (Just . maybe 1 (+ 1)) (tshow e) errs}
|
||||
updateQueryStats :: Int64 -> Maybe SlowQueryStats -> Maybe SlowQueryStats
|
||||
updateQueryStats diff Nothing = Just $ SlowQueryStats 1 diff diff M.empty
|
||||
updateQueryStats diff (Just SlowQueryStats {count, timeMax, timeAvg, errs}) =
|
||||
@@ -74,41 +106,49 @@ timeIt slow sql a = do
|
||||
errs
|
||||
}
|
||||
|
||||
open :: String -> IO Connection
|
||||
open f = do
|
||||
open :: String -> TrackQueries -> IO Connection
|
||||
open f track = do
|
||||
conn <- SQL.open f
|
||||
slow <- TM.emptyIO
|
||||
pure Connection {conn, slow}
|
||||
pure Connection {conn, slow, track}
|
||||
|
||||
close :: Connection -> IO ()
|
||||
close = SQL.close . conn
|
||||
|
||||
execute :: ToRow q => Connection -> Query -> q -> IO ()
|
||||
execute Connection {conn, slow} sql = timeIt slow sql . SQL.execute conn sql
|
||||
execute c sql = timeIt c sql . SQL.execute (conn c) sql
|
||||
{-# INLINE execute #-}
|
||||
|
||||
execute_ :: Connection -> Query -> IO ()
|
||||
execute_ Connection {conn, slow} sql = timeIt slow sql $ SQL.execute_ conn sql
|
||||
execute_ c sql = timeIt c sql $ SQL.execute_ (conn c) sql
|
||||
{-# INLINE execute_ #-}
|
||||
|
||||
executeNamed :: Connection -> Query -> [NamedParam] -> IO ()
|
||||
executeNamed Connection {conn, slow} sql = timeIt slow sql . SQL.executeNamed conn sql
|
||||
{-# INLINE executeNamed #-}
|
||||
|
||||
executeMany :: ToRow q => Connection -> Query -> [q] -> IO ()
|
||||
executeMany Connection {conn, slow} sql = timeIt slow sql . SQL.executeMany conn sql
|
||||
executeMany c sql = timeIt c sql . SQL.executeMany (conn c) sql
|
||||
{-# INLINE executeMany #-}
|
||||
|
||||
query :: (ToRow q, FromRow r) => Connection -> Query -> q -> IO [r]
|
||||
query Connection {conn, slow} sql = timeIt slow sql . SQL.query conn sql
|
||||
query c sql = timeIt c sql . SQL.query (conn c) sql
|
||||
{-# INLINE query #-}
|
||||
|
||||
query_ :: FromRow r => Connection -> Query -> IO [r]
|
||||
query_ Connection {conn, slow} sql = timeIt slow sql $ SQL.query_ conn sql
|
||||
query_ c sql = timeIt c sql $ SQL.query_ (conn c) sql
|
||||
{-# INLINE query_ #-}
|
||||
|
||||
queryNamed :: FromRow r => Connection -> Query -> [NamedParam] -> IO [r]
|
||||
queryNamed Connection {conn, slow} sql = timeIt slow sql . SQL.queryNamed conn sql
|
||||
{-# INLINE queryNamed #-}
|
||||
blobFieldDecoder :: Typeable k => (ByteString -> Either String k) -> FieldParser k
|
||||
blobFieldDecoder dec = \case
|
||||
f@(Field (SQLBlob b) _) ->
|
||||
case dec b of
|
||||
Right k -> Ok k
|
||||
Left e -> returnError ConversionFailed f ("couldn't parse field: " ++ e)
|
||||
f -> returnError ConversionFailed f "expecting SQLBlob column type"
|
||||
|
||||
fromTextField_ :: Typeable a => (Text -> Maybe a) -> Field -> Ok a
|
||||
fromTextField_ fromText = \case
|
||||
f@(Field (SQLText t) _) ->
|
||||
case fromText t of
|
||||
Just x -> Ok x
|
||||
_ -> returnError ConversionFailed f ("invalid text: " <> T.unpack t)
|
||||
f -> returnError ConversionFailed f "expecting SQLText column type"
|
||||
|
||||
$(J.deriveJSON defaultJSON ''SlowQueryStats)
|
||||
|
||||
@@ -5,142 +5,44 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StrictData #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.SQLite.Migrations
|
||||
( Migration (..),
|
||||
MigrationsToRun (..),
|
||||
MTRError (..),
|
||||
DownMigration (..),
|
||||
app,
|
||||
initialize,
|
||||
get,
|
||||
( initialize,
|
||||
run,
|
||||
getCurrent,
|
||||
mtrErrorDescription,
|
||||
-- for unit tests
|
||||
migrationsToRun,
|
||||
toDownMigration,
|
||||
getCurrentMigrations,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad (forM_, when)
|
||||
import qualified Data.Aeson.TH as J
|
||||
import Data.List (intercalate, sortOn)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (isNothing, mapMaybe)
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (decodeLatin1)
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import Database.SQLite.Simple (Connection, Only (..), Query (..))
|
||||
import qualified Database.SQLite.Simple as DB
|
||||
import Database.SQLite.Simple (Only (..), Query (..))
|
||||
import qualified Database.SQLite.Simple as SQL
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
import qualified Database.SQLite3 as SQLite3
|
||||
import Simplex.Messaging.Agent.Protocol (extraSMPServerHosts)
|
||||
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Common
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220101_initial
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220301_snd_queue_keys
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220322_notifications
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220608_v2
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220625_v2_ntf_mode
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220811_onion_hosts
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220817_connection_ntfs
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220905_commands
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220915_connection_queues
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230110_users
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230117_fkey_indexes
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230120_delete_errors
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230217_server_key_hash
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230223_files
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230320_retry_state
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230401_snd_files
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230510_files_pending_replicas_indexes
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230516_encrypted_rcv_message_hashes
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230531_switch_status
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230615_ratchet_sync
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230701_delivery_receipts
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230720_delete_expired_messages
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230722_indexes
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230814_indexes
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230829_crypto_files
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20231222_command_created_at
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20231225_failed_work_items
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240121_message_delivery_indexes
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240124_file_redirect
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240223_connections_wait_delivery
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240225_ratchet_kem
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240624_snd_secure
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240702_servers_stats
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240930_ntf_tokens_to_delete
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20241007_rcv_queues_last_broker_ts
|
||||
import Simplex.Messaging.Agent.Store.Shared
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||
|
||||
data Migration = Migration {name :: String, up :: Text, down :: Maybe Text}
|
||||
deriving (Eq, Show)
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
schemaMigrations =
|
||||
[ ("20220101_initial", m20220101_initial, Nothing),
|
||||
("20220301_snd_queue_keys", m20220301_snd_queue_keys, Nothing),
|
||||
("20220322_notifications", m20220322_notifications, Nothing),
|
||||
("20220607_v2", m20220608_v2, Nothing),
|
||||
("m20220625_v2_ntf_mode", m20220625_v2_ntf_mode, Nothing),
|
||||
("m20220811_onion_hosts", m20220811_onion_hosts, Nothing),
|
||||
("m20220817_connection_ntfs", m20220817_connection_ntfs, Nothing),
|
||||
("m20220905_commands", m20220905_commands, Nothing),
|
||||
("m20220915_connection_queues", m20220915_connection_queues, Nothing),
|
||||
("m20230110_users", m20230110_users, Nothing),
|
||||
("m20230117_fkey_indexes", m20230117_fkey_indexes, Nothing),
|
||||
("m20230120_delete_errors", m20230120_delete_errors, Nothing),
|
||||
("m20230217_server_key_hash", m20230217_server_key_hash, Nothing),
|
||||
("m20230223_files", m20230223_files, Just down_m20230223_files),
|
||||
("m20230320_retry_state", m20230320_retry_state, Just down_m20230320_retry_state),
|
||||
("m20230401_snd_files", m20230401_snd_files, Just down_m20230401_snd_files),
|
||||
("m20230510_files_pending_replicas_indexes", m20230510_files_pending_replicas_indexes, Just down_m20230510_files_pending_replicas_indexes),
|
||||
("m20230516_encrypted_rcv_message_hashes", m20230516_encrypted_rcv_message_hashes, Just down_m20230516_encrypted_rcv_message_hashes),
|
||||
("m20230531_switch_status", m20230531_switch_status, Just down_m20230531_switch_status),
|
||||
("m20230615_ratchet_sync", m20230615_ratchet_sync, Just down_m20230615_ratchet_sync),
|
||||
("m20230701_delivery_receipts", m20230701_delivery_receipts, Just down_m20230701_delivery_receipts),
|
||||
("m20230720_delete_expired_messages", m20230720_delete_expired_messages, Just down_m20230720_delete_expired_messages),
|
||||
("m20230722_indexes", m20230722_indexes, Just down_m20230722_indexes),
|
||||
("m20230814_indexes", m20230814_indexes, Just down_m20230814_indexes),
|
||||
("m20230829_crypto_files", m20230829_crypto_files, Just down_m20230829_crypto_files),
|
||||
("m20231222_command_created_at", m20231222_command_created_at, Just down_m20231222_command_created_at),
|
||||
("m20231225_failed_work_items", m20231225_failed_work_items, Just down_m20231225_failed_work_items),
|
||||
("m20240121_message_delivery_indexes", m20240121_message_delivery_indexes, Just down_m20240121_message_delivery_indexes),
|
||||
("m20240124_file_redirect", m20240124_file_redirect, Just down_m20240124_file_redirect),
|
||||
("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),
|
||||
("m20240702_servers_stats", m20240702_servers_stats, Just down_m20240702_servers_stats),
|
||||
("m20240930_ntf_tokens_to_delete", m20240930_ntf_tokens_to_delete, Just down_m20240930_ntf_tokens_to_delete),
|
||||
("m20241007_rcv_queues_last_broker_ts", m20241007_rcv_queues_last_broker_ts, Just down_m20241007_rcv_queues_last_broker_ts)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
app :: [Migration]
|
||||
app = sortOn name $ map migration schemaMigrations
|
||||
where
|
||||
migration (name, up, down) = Migration {name, up = fromQuery up, down = fromQuery <$> down}
|
||||
|
||||
get :: SQLiteStore -> [Migration] -> IO (Either MTRError MigrationsToRun)
|
||||
get st migrations = migrationsToRun migrations <$> withTransaction' st getCurrent
|
||||
|
||||
getCurrent :: Connection -> IO [Migration]
|
||||
getCurrent db = map toMigration <$> DB.query_ db "SELECT name, down FROM migrations ORDER BY name ASC;"
|
||||
getCurrentMigrations :: DB.Connection -> IO [Migration]
|
||||
getCurrentMigrations DB.Connection {DB.conn} = map toMigration <$> SQL.query_ conn "SELECT name, down FROM migrations ORDER BY name ASC;"
|
||||
where
|
||||
toMigration (name, down) = Migration {name, up = "", down}
|
||||
|
||||
run :: SQLiteStore -> MigrationsToRun -> IO ()
|
||||
run st = \case
|
||||
run :: DBStore -> Bool -> MigrationsToRun -> IO ()
|
||||
run st vacuum = \case
|
||||
MTRUp [] -> pure ()
|
||||
MTRUp ms -> mapM_ runUp ms >> withConnection' st (`execSQL` "VACUUM;")
|
||||
MTRUp ms -> do
|
||||
mapM_ runUp ms
|
||||
when vacuum $ withConnection' st (`execSQL` "VACUUM;")
|
||||
MTRDown ms -> mapM_ runDown $ reverse ms
|
||||
MTRNone -> pure ()
|
||||
where
|
||||
@@ -148,27 +50,27 @@ run st = \case
|
||||
when (name == "m20220811_onion_hosts") $ updateServers db
|
||||
insert db >> execSQL db up'
|
||||
where
|
||||
insert db = DB.execute db "INSERT INTO migrations (name, down, ts) VALUES (?,?,?)" . (name,down,) =<< getCurrentTime
|
||||
insert db = SQL.execute db "INSERT INTO migrations (name, down, ts) VALUES (?,?,?)" . (name,down,) =<< getCurrentTime
|
||||
up'
|
||||
| dbNew st && name == "m20230110_users" = fromQuery new_m20230110_users
|
||||
| otherwise = up
|
||||
updateServers db = forM_ (M.assocs extraSMPServerHosts) $ \(h, h') ->
|
||||
let hs = decodeLatin1 . strEncode $ ([h, h'] :: NonEmpty TransportHost)
|
||||
in DB.execute db "UPDATE servers SET host = ? WHERE host = ?" (hs, decodeLatin1 $ strEncode h)
|
||||
in SQL.execute db "UPDATE servers SET host = ? WHERE host = ?" (hs, decodeLatin1 $ strEncode h)
|
||||
runDown DownMigration {downName, downQuery} = withTransaction' st $ \db -> do
|
||||
execSQL db downQuery
|
||||
DB.execute db "DELETE FROM migrations WHERE name = ?" (Only downName)
|
||||
execSQL db = SQLite3.exec $ DB.connectionHandle db
|
||||
SQL.execute db "DELETE FROM migrations WHERE name = ?" (Only downName)
|
||||
execSQL db = SQLite3.exec $ SQL.connectionHandle db
|
||||
|
||||
initialize :: SQLiteStore -> IO ()
|
||||
initialize :: DBStore -> IO ()
|
||||
initialize st = withTransaction' st $ \db -> do
|
||||
cs :: [Text] <- map fromOnly <$> DB.query_ db "SELECT name FROM pragma_table_info('migrations')"
|
||||
cs :: [Text] <- map fromOnly <$> SQL.query_ db "SELECT name FROM pragma_table_info('migrations')"
|
||||
case cs of
|
||||
[] -> createMigrations db
|
||||
_ -> when ("down" `notElem` cs) $ DB.execute_ db "ALTER TABLE migrations ADD COLUMN down TEXT"
|
||||
_ -> when ("down" `notElem` cs) $ SQL.execute_ db "ALTER TABLE migrations ADD COLUMN down TEXT"
|
||||
where
|
||||
createMigrations db =
|
||||
DB.execute_
|
||||
SQL.execute_
|
||||
db
|
||||
[sql|
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
@@ -178,37 +80,3 @@ initialize st = withTransaction' st $ \db -> do
|
||||
PRIMARY KEY (name)
|
||||
);
|
||||
|]
|
||||
|
||||
data DownMigration = DownMigration {downName :: String, downQuery :: Text}
|
||||
deriving (Eq, Show)
|
||||
|
||||
toDownMigration :: Migration -> Maybe DownMigration
|
||||
toDownMigration Migration {name, down} = DownMigration name <$> down
|
||||
|
||||
data MigrationsToRun = MTRUp [Migration] | MTRDown [DownMigration] | MTRNone
|
||||
deriving (Eq, Show)
|
||||
|
||||
data MTRError
|
||||
= MTRENoDown {dbMigrations :: [String]}
|
||||
| MTREDifferent {appMigration :: String, dbMigration :: String}
|
||||
deriving (Eq, Show)
|
||||
|
||||
mtrErrorDescription :: MTRError -> String
|
||||
mtrErrorDescription = \case
|
||||
MTRENoDown ms -> "database version is newer than the app, but no down migration for: " <> intercalate ", " ms
|
||||
MTREDifferent a d -> "different migration in the app/database: " <> a <> " / " <> d
|
||||
|
||||
migrationsToRun :: [Migration] -> [Migration] -> Either MTRError MigrationsToRun
|
||||
migrationsToRun [] [] = Right MTRNone
|
||||
migrationsToRun appMs [] = Right $ MTRUp appMs
|
||||
migrationsToRun [] dbMs
|
||||
| length dms == length dbMs = Right $ MTRDown dms
|
||||
| otherwise = Left $ MTRENoDown $ mapMaybe nameNoDown dbMs
|
||||
where
|
||||
dms = mapMaybe toDownMigration dbMs
|
||||
nameNoDown m = if isNothing (down m) then Just $ name m else Nothing
|
||||
migrationsToRun (a : as) (d : ds)
|
||||
| name a == name d = migrationsToRun as ds
|
||||
| otherwise = Left $ MTREDifferent (name a) (name d)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON $ dropPrefix "MTRE") ''MTRError)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.SQLite.Migrations.App (appMigrations) where
|
||||
|
||||
import Data.List (sortOn)
|
||||
import Database.SQLite.Simple (Query (..))
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220101_initial
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220301_snd_queue_keys
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220322_notifications
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220608_v2
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220625_v2_ntf_mode
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220811_onion_hosts
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220817_connection_ntfs
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220905_commands
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220915_connection_queues
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230110_users
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230117_fkey_indexes
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230120_delete_errors
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230217_server_key_hash
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230223_files
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230320_retry_state
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230401_snd_files
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230510_files_pending_replicas_indexes
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230516_encrypted_rcv_message_hashes
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230531_switch_status
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230615_ratchet_sync
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230701_delivery_receipts
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230720_delete_expired_messages
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230722_indexes
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230814_indexes
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230829_crypto_files
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20231222_command_created_at
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20231225_failed_work_items
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240121_message_delivery_indexes
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240124_file_redirect
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240223_connections_wait_delivery
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240225_ratchet_kem
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240417_rcv_files_approved_relays
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240624_snd_secure
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240702_servers_stats
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240930_ntf_tokens_to_delete
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20241007_rcv_queues_last_broker_ts
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20241224_ratchet_e2e_snd_params
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20250203_msg_bodies
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20250322_short_links
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
schemaMigrations =
|
||||
[ ("20220101_initial", m20220101_initial, Nothing),
|
||||
("20220301_snd_queue_keys", m20220301_snd_queue_keys, Nothing),
|
||||
("20220322_notifications", m20220322_notifications, Nothing),
|
||||
("20220607_v2", m20220608_v2, Nothing),
|
||||
("m20220625_v2_ntf_mode", m20220625_v2_ntf_mode, Nothing),
|
||||
("m20220811_onion_hosts", m20220811_onion_hosts, Nothing),
|
||||
("m20220817_connection_ntfs", m20220817_connection_ntfs, Nothing),
|
||||
("m20220905_commands", m20220905_commands, Nothing),
|
||||
("m20220915_connection_queues", m20220915_connection_queues, Nothing),
|
||||
("m20230110_users", m20230110_users, Nothing),
|
||||
("m20230117_fkey_indexes", m20230117_fkey_indexes, Nothing),
|
||||
("m20230120_delete_errors", m20230120_delete_errors, Nothing),
|
||||
("m20230217_server_key_hash", m20230217_server_key_hash, Nothing),
|
||||
("m20230223_files", m20230223_files, Just down_m20230223_files),
|
||||
("m20230320_retry_state", m20230320_retry_state, Just down_m20230320_retry_state),
|
||||
("m20230401_snd_files", m20230401_snd_files, Just down_m20230401_snd_files),
|
||||
("m20230510_files_pending_replicas_indexes", m20230510_files_pending_replicas_indexes, Just down_m20230510_files_pending_replicas_indexes),
|
||||
("m20230516_encrypted_rcv_message_hashes", m20230516_encrypted_rcv_message_hashes, Just down_m20230516_encrypted_rcv_message_hashes),
|
||||
("m20230531_switch_status", m20230531_switch_status, Just down_m20230531_switch_status),
|
||||
("m20230615_ratchet_sync", m20230615_ratchet_sync, Just down_m20230615_ratchet_sync),
|
||||
("m20230701_delivery_receipts", m20230701_delivery_receipts, Just down_m20230701_delivery_receipts),
|
||||
("m20230720_delete_expired_messages", m20230720_delete_expired_messages, Just down_m20230720_delete_expired_messages),
|
||||
("m20230722_indexes", m20230722_indexes, Just down_m20230722_indexes),
|
||||
("m20230814_indexes", m20230814_indexes, Just down_m20230814_indexes),
|
||||
("m20230829_crypto_files", m20230829_crypto_files, Just down_m20230829_crypto_files),
|
||||
("m20231222_command_created_at", m20231222_command_created_at, Just down_m20231222_command_created_at),
|
||||
("m20231225_failed_work_items", m20231225_failed_work_items, Just down_m20231225_failed_work_items),
|
||||
("m20240121_message_delivery_indexes", m20240121_message_delivery_indexes, Just down_m20240121_message_delivery_indexes),
|
||||
("m20240124_file_redirect", m20240124_file_redirect, Just down_m20240124_file_redirect),
|
||||
("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),
|
||||
("m20240702_servers_stats", m20240702_servers_stats, Just down_m20240702_servers_stats),
|
||||
("m20240930_ntf_tokens_to_delete", m20240930_ntf_tokens_to_delete, Just down_m20240930_ntf_tokens_to_delete),
|
||||
("m20241007_rcv_queues_last_broker_ts", m20241007_rcv_queues_last_broker_ts, Just down_m20241007_rcv_queues_last_broker_ts),
|
||||
("m20241224_ratchet_e2e_snd_params", m20241224_ratchet_e2e_snd_params, Just down_m20241224_ratchet_e2e_snd_params),
|
||||
("m20250203_msg_bodies", m20250203_msg_bodies, Just down_m20250203_msg_bodies),
|
||||
("m20250322_short_links", m20250322_short_links, Just down_m20250322_short_links)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
appMigrations :: [Migration]
|
||||
appMigrations = sortOn name $ map migration schemaMigrations
|
||||
where
|
||||
migration (name, up, down) = Migration {name, up = fromQuery up, down = fromQuery <$> down}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20241224_ratchet_e2e_snd_params where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20241224_ratchet_e2e_snd_params :: Query
|
||||
m20241224_ratchet_e2e_snd_params =
|
||||
[sql|
|
||||
ALTER TABLE ratchets ADD COLUMN pq_pub_kem BLOB;
|
||||
|]
|
||||
|
||||
down_m20241224_ratchet_e2e_snd_params :: Query
|
||||
down_m20241224_ratchet_e2e_snd_params =
|
||||
[sql|
|
||||
ALTER TABLE ratchets DROP COLUMN pq_pub_kem;
|
||||
|]
|
||||
@@ -0,0 +1,33 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20250203_msg_bodies where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20250203_msg_bodies :: Query
|
||||
m20250203_msg_bodies =
|
||||
[sql|
|
||||
ALTER TABLE snd_messages ADD COLUMN msg_encrypt_key BLOB;
|
||||
ALTER TABLE snd_messages ADD COLUMN padded_msg_len INTEGER;
|
||||
|
||||
|
||||
CREATE TABLE snd_message_bodies (
|
||||
snd_message_body_id INTEGER PRIMARY KEY,
|
||||
agent_msg BLOB NOT NULL DEFAULT x''
|
||||
);
|
||||
ALTER TABLE snd_messages ADD COLUMN snd_message_body_id INTEGER REFERENCES snd_message_bodies ON DELETE SET NULL;
|
||||
CREATE INDEX idx_snd_messages_snd_message_body_id ON snd_messages(snd_message_body_id);
|
||||
|]
|
||||
|
||||
down_m20250203_msg_bodies :: Query
|
||||
down_m20250203_msg_bodies =
|
||||
[sql|
|
||||
DROP INDEX idx_snd_messages_snd_message_body_id;
|
||||
ALTER TABLE snd_messages DROP COLUMN snd_message_body_id;
|
||||
DROP TABLE snd_message_bodies;
|
||||
|
||||
|
||||
ALTER TABLE snd_messages DROP COLUMN msg_encrypt_key;
|
||||
ALTER TABLE snd_messages DROP COLUMN padded_msg_len;
|
||||
|]
|
||||
@@ -0,0 +1,60 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20250322_short_links where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20250322_short_links :: Query
|
||||
m20250322_short_links =
|
||||
[sql|
|
||||
ALTER TABLE rcv_queues ADD COLUMN link_id BLOB;
|
||||
ALTER TABLE rcv_queues ADD COLUMN link_key BLOB;
|
||||
ALTER TABLE rcv_queues ADD COLUMN link_priv_sig_key BLOB;
|
||||
ALTER TABLE rcv_queues ADD COLUMN link_enc_fixed_data BLOB;
|
||||
|
||||
CREATE UNIQUE INDEX idx_rcv_queues_link_id ON rcv_queues(host, port, link_id);
|
||||
|
||||
ALTER TABLE rcv_queues ADD COLUMN queue_mode TEXT;
|
||||
UPDATE rcv_queues SET queue_mode = 'M' WHERE snd_secure = 1;
|
||||
ALTER TABLE rcv_queues DROP COLUMN snd_secure;
|
||||
|
||||
ALTER TABLE snd_queues ADD COLUMN queue_mode TEXT;
|
||||
UPDATE snd_queues SET queue_mode = 'M' WHERE snd_secure = 1;
|
||||
ALTER TABLE snd_queues DROP COLUMN snd_secure;
|
||||
|
||||
CREATE TABLE inv_short_links(
|
||||
inv_short_link_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
host TEXT NOT NULL,
|
||||
port TEXT NOT NULL,
|
||||
server_key_hash BLOB,
|
||||
link_id BLOB NOT NULL,
|
||||
link_key BLOB NOT NULL,
|
||||
snd_private_key BLOB NOT NULL,
|
||||
snd_id BLOB,
|
||||
FOREIGN KEY(host, port) REFERENCES servers ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_inv_short_links_link_id ON inv_short_links(host, port, link_id);
|
||||
|]
|
||||
|
||||
down_m20250322_short_links :: Query
|
||||
down_m20250322_short_links =
|
||||
[sql|
|
||||
DROP INDEX idx_rcv_queues_link_id;
|
||||
ALTER TABLE rcv_queues DROP COLUMN link_id;
|
||||
ALTER TABLE rcv_queues DROP COLUMN link_key;
|
||||
ALTER TABLE rcv_queues DROP COLUMN link_priv_sig_key;
|
||||
ALTER TABLE rcv_queues DROP COLUMN link_enc_fixed_data;
|
||||
|
||||
DROP INDEX idx_inv_short_links_link_id;
|
||||
DROP TABLE inv_short_links;
|
||||
|
||||
ALTER TABLE rcv_queues ADD COLUMN snd_secure INTEGER NOT NULL DEFAULT 0;
|
||||
UPDATE rcv_queues SET snd_secure = 1 WHERE queue_mode = 'M';
|
||||
ALTER TABLE rcv_queues DROP COLUMN queue_mode;
|
||||
|
||||
ALTER TABLE snd_queues ADD COLUMN snd_secure INTEGER NOT NULL DEFAULT 0;
|
||||
UPDATE snd_queues SET snd_secure = 1 WHERE queue_mode = 'M';
|
||||
ALTER TABLE snd_queues DROP COLUMN queue_mode;
|
||||
|]
|
||||
@@ -55,8 +55,12 @@ CREATE TABLE rcv_queues(
|
||||
server_key_hash BLOB,
|
||||
switch_status TEXT,
|
||||
deleted INTEGER NOT NULL DEFAULT 0,
|
||||
snd_secure INTEGER NOT NULL DEFAULT 0,
|
||||
last_broker_ts TEXT,
|
||||
link_id BLOB,
|
||||
link_key BLOB,
|
||||
link_priv_sig_key BLOB,
|
||||
link_enc_fixed_data BLOB,
|
||||
queue_mode TEXT,
|
||||
PRIMARY KEY(host, port, rcv_id),
|
||||
FOREIGN KEY(host, port) REFERENCES servers
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
@@ -79,7 +83,7 @@ CREATE TABLE snd_queues(
|
||||
replace_snd_queue_id INTEGER NULL,
|
||||
server_key_hash BLOB,
|
||||
switch_status TEXT,
|
||||
snd_secure INTEGER NOT NULL DEFAULT 0,
|
||||
queue_mode TEXT,
|
||||
PRIMARY KEY(host, port, snd_id),
|
||||
FOREIGN KEY(host, port) REFERENCES servers
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
@@ -127,6 +131,9 @@ CREATE TABLE snd_messages(
|
||||
retry_int_fast INTEGER,
|
||||
rcpt_internal_id INTEGER,
|
||||
rcpt_status TEXT,
|
||||
msg_encrypt_key BLOB,
|
||||
padded_msg_len INTEGER,
|
||||
snd_message_body_id INTEGER REFERENCES snd_message_bodies ON DELETE SET NULL,
|
||||
PRIMARY KEY(conn_id, internal_snd_id),
|
||||
FOREIGN KEY(conn_id, internal_id) REFERENCES messages
|
||||
ON DELETE CASCADE
|
||||
@@ -166,7 +173,8 @@ CREATE TABLE ratchets(
|
||||
,
|
||||
x3dh_pub_key_1 BLOB,
|
||||
x3dh_pub_key_2 BLOB,
|
||||
pq_priv_kem BLOB
|
||||
pq_priv_kem BLOB,
|
||||
pq_pub_kem BLOB
|
||||
) WITHOUT ROWID;
|
||||
CREATE TABLE skipped_messages(
|
||||
skipped_message_id INTEGER PRIMARY KEY,
|
||||
@@ -414,6 +422,21 @@ CREATE TABLE ntf_tokens_to_delete(
|
||||
del_failed INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE TABLE snd_message_bodies(
|
||||
snd_message_body_id INTEGER PRIMARY KEY,
|
||||
agent_msg BLOB NOT NULL DEFAULT x''
|
||||
);
|
||||
CREATE TABLE inv_short_links(
|
||||
inv_short_link_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
host TEXT NOT NULL,
|
||||
port TEXT NOT NULL,
|
||||
server_key_hash BLOB,
|
||||
link_id BLOB NOT NULL,
|
||||
link_key BLOB NOT NULL,
|
||||
snd_private_key BLOB NOT NULL,
|
||||
snd_id BLOB,
|
||||
FOREIGN KEY(host, port) REFERENCES servers ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
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);
|
||||
@@ -540,3 +563,12 @@ CREATE INDEX idx_snd_message_deliveries_expired ON snd_message_deliveries(
|
||||
internal_id
|
||||
);
|
||||
CREATE INDEX idx_rcv_files_redirect_id on rcv_files(redirect_id);
|
||||
CREATE INDEX idx_snd_messages_snd_message_body_id ON snd_messages(
|
||||
snd_message_body_id
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_rcv_queues_link_id ON rcv_queues(host, port, link_id);
|
||||
CREATE UNIQUE INDEX idx_inv_short_links_link_id ON inv_short_links(
|
||||
host,
|
||||
port,
|
||||
link_id
|
||||
);
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
module Simplex.Messaging.Agent.Store.Shared
|
||||
( Migration (..),
|
||||
MigrationsToRun (..),
|
||||
DownMigration (..),
|
||||
MTRError (..),
|
||||
mtrErrorDescription,
|
||||
MigrationConfirmation (..),
|
||||
MigrationError (..),
|
||||
UpMigration (..),
|
||||
migrationErrorDescription,
|
||||
-- for tests
|
||||
toDownMigration,
|
||||
upMigration,
|
||||
)
|
||||
where
|
||||
|
||||
import qualified Data.Aeson.TH as J
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.List (intercalate)
|
||||
import Data.Maybe (isJust)
|
||||
import Data.Text (Text)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, sumTypeJSON)
|
||||
|
||||
data Migration = Migration {name :: String, up :: Text, down :: Maybe Text}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data DownMigration = DownMigration {downName :: String, downQuery :: Text}
|
||||
deriving (Eq, Show)
|
||||
|
||||
toDownMigration :: Migration -> Maybe DownMigration
|
||||
toDownMigration Migration {name, down} = DownMigration name <$> down
|
||||
|
||||
data MigrationsToRun = MTRUp [Migration] | MTRDown [DownMigration] | MTRNone
|
||||
deriving (Eq, Show)
|
||||
|
||||
data MTRError
|
||||
= MTRENoDown {dbMigrations :: [String]}
|
||||
| MTREDifferent {appMigration :: String, dbMigration :: String}
|
||||
deriving (Eq, Show)
|
||||
|
||||
mtrErrorDescription :: MTRError -> String
|
||||
mtrErrorDescription = \case
|
||||
MTRENoDown ms -> "database version is newer than the app, but no down migration for: " <> intercalate ", " ms
|
||||
MTREDifferent a d -> "different migration in the app/database: " <> a <> " / " <> d
|
||||
|
||||
data MigrationError
|
||||
= MEUpgrade {upMigrations :: [UpMigration]}
|
||||
| MEDowngrade {downMigrations :: [String]}
|
||||
| MigrationError {mtrError :: MTRError}
|
||||
deriving (Eq, Show)
|
||||
|
||||
migrationErrorDescription :: MigrationError -> String
|
||||
migrationErrorDescription = \case
|
||||
MEUpgrade ums ->
|
||||
"The app has a newer version than the database.\nConfirm to back up and upgrade using these migrations: " <> intercalate ", " (map upName ums)
|
||||
MEDowngrade dms ->
|
||||
"Database version is newer than the app.\nConfirm to back up and downgrade using these migrations: " <> intercalate ", " dms
|
||||
MigrationError err -> mtrErrorDescription err
|
||||
|
||||
data UpMigration = UpMigration {upName :: String, withDown :: Bool}
|
||||
deriving (Eq, Show)
|
||||
|
||||
upMigration :: Migration -> UpMigration
|
||||
upMigration Migration {name, down} = UpMigration name $ isJust down
|
||||
|
||||
data MigrationConfirmation = MCYesUp | MCYesUpDown | MCConsole | MCError
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding MigrationConfirmation where
|
||||
strEncode = \case
|
||||
MCYesUp -> "yesUp"
|
||||
MCYesUpDown -> "yesUpDown"
|
||||
MCConsole -> "console"
|
||||
MCError -> "error"
|
||||
strP =
|
||||
A.takeByteString >>= \case
|
||||
"yesUp" -> pure MCYesUp
|
||||
"yesUpDown" -> pure MCYesUpDown
|
||||
"console" -> pure MCConsole
|
||||
"error" -> pure MCError
|
||||
_ -> fail "invalid MigrationConfirmation"
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON $ dropPrefix "MTRE") ''MTRError)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''UpMigration)
|
||||
|
||||
$(J.deriveToJSON (sumTypeJSON $ dropPrefix "ME") ''MigrationError)
|
||||
+126
-30
@@ -49,6 +49,12 @@ module Simplex.Messaging.Client
|
||||
secureSMPQueue,
|
||||
secureSndSMPQueue,
|
||||
proxySecureSndSMPQueue,
|
||||
addSMPQueueLink,
|
||||
deleteSMPQueueLink,
|
||||
secureGetSMPQueueLink,
|
||||
proxySecureGetSMPQueueLink,
|
||||
getSMPQueueLink,
|
||||
proxyGetSMPQueueLink,
|
||||
enableSMPQueueNotifications,
|
||||
disableSMPQueueNotifications,
|
||||
enableSMPQueuesNtfs,
|
||||
@@ -78,6 +84,7 @@ module Simplex.Messaging.Client
|
||||
SocksMode (..),
|
||||
SMPProxyMode (..),
|
||||
SMPProxyFallback (..),
|
||||
SMPWebPortServers (..),
|
||||
defaultClientConfig,
|
||||
defaultSMPClientConfig,
|
||||
defaultNetworkConfig,
|
||||
@@ -101,11 +108,12 @@ module Simplex.Messaging.Client
|
||||
TBQueueInfo (..),
|
||||
getTBQueueInfo,
|
||||
getProtocolClientQueuesInfo,
|
||||
nonBlockingWriteTBQueue,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import Control.Concurrent (ThreadId, forkFinally, killThread, mkWeakThreadId)
|
||||
import Control.Concurrent (ThreadId, forkFinally, forkIO, killThread, mkWeakThreadId)
|
||||
import Control.Concurrent.Async
|
||||
import Control.Concurrent.STM
|
||||
import Control.Exception
|
||||
@@ -122,7 +130,7 @@ import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import Data.Functor (($>))
|
||||
import Data.Int (Int64)
|
||||
import Data.List (find)
|
||||
import Data.List (find, isSuffixOf)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Maybe (catMaybes, fromMaybe)
|
||||
@@ -131,7 +139,7 @@ import qualified Data.Text as T
|
||||
import Data.Time.Clock (UTCTime (..), diffUTCTime, getCurrentTime)
|
||||
import qualified Data.X509 as X
|
||||
import qualified Data.X509.Validation as XV
|
||||
import Network.Socket (ServiceName)
|
||||
import Network.Socket (HostName, ServiceName)
|
||||
import Network.Socks5 (SocksCredentials (..))
|
||||
import Numeric.Natural
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
@@ -145,8 +153,7 @@ import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Transport.Client (SocksAuth (..), SocksProxyWithAuth (..), TransportClientConfig (..), TransportHost (..), defaultSMPPort, defaultTcpConnectTimeout, runTransportClient)
|
||||
import Simplex.Messaging.Transport.KeepAlive
|
||||
import Simplex.Messaging.Transport.WebSockets (WS)
|
||||
import Simplex.Messaging.Util (bshow, diffToMicroseconds, ifM, liftEitherWith, raceAny_, threadDelay', tshow, whenM)
|
||||
import Simplex.Messaging.Util (bshow, diffToMicroseconds, ifM, liftEitherWith, raceAny_, threadDelay', tryWriteTBQueue, tshow, whenM)
|
||||
import Simplex.Messaging.Version
|
||||
import System.Mem.Weak (Weak, deRefWeak)
|
||||
import System.Timeout (timeout)
|
||||
@@ -172,7 +179,7 @@ data PClient v err msg = PClient
|
||||
timeoutErrorCount :: TVar Int,
|
||||
clientCorrId :: TVar ChaChaDRG,
|
||||
sentCommands :: TMap CorrId (Request err msg),
|
||||
sndQ :: TBQueue (Maybe (TVar Bool), ByteString),
|
||||
sndQ :: TBQueue (Maybe (Request err msg), ByteString),
|
||||
rcvQ :: TBQueue (NonEmpty (SignedTransmission err msg)),
|
||||
msgQ :: Maybe (TBQueue (ServerTransmissionBatch v err msg))
|
||||
}
|
||||
@@ -285,7 +292,7 @@ data NetworkConfig = NetworkConfig
|
||||
-- | Fallback to direct connection when destination SMP relay does not support SMP proxy protocol extensions
|
||||
smpProxyFallback :: SMPProxyFallback,
|
||||
-- | use web port 443 for SMP protocol
|
||||
smpWebPort :: Bool,
|
||||
smpWebPortServers :: SMPWebPortServers,
|
||||
-- | timeout for the initial client TCP/TLS connection (microseconds)
|
||||
tcpConnectTimeout :: Int,
|
||||
-- | timeout of protocol commands (microseconds)
|
||||
@@ -321,6 +328,12 @@ data SMPProxyFallback
|
||||
| SPFProhibit -- prohibit direct connection to destination relay.
|
||||
deriving (Eq, Show)
|
||||
|
||||
data SMPWebPortServers
|
||||
= SWPAll
|
||||
| SWPPreset
|
||||
| SWPOff
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding SMPProxyMode where
|
||||
strEncode = \case
|
||||
SPMAlways -> "always"
|
||||
@@ -347,6 +360,18 @@ instance StrEncoding SMPProxyFallback where
|
||||
"no" -> pure SPFProhibit
|
||||
_ -> fail "Invalid SMP proxy fallback mode"
|
||||
|
||||
instance StrEncoding SMPWebPortServers where
|
||||
strEncode = \case
|
||||
SWPAll -> "all"
|
||||
SWPPreset -> "preset"
|
||||
SWPOff -> "off"
|
||||
strP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"all" -> pure SWPAll
|
||||
"preset" -> pure SWPPreset
|
||||
"off" -> pure SWPOff
|
||||
_ -> fail "Invalid SMP wep port setting"
|
||||
|
||||
defaultNetworkConfig :: NetworkConfig
|
||||
defaultNetworkConfig =
|
||||
NetworkConfig
|
||||
@@ -357,7 +382,7 @@ defaultNetworkConfig =
|
||||
sessionMode = TSMSession,
|
||||
smpProxyMode = SPMNever,
|
||||
smpProxyFallback = SPFAllow,
|
||||
smpWebPort = False,
|
||||
smpWebPortServers = SWPPreset,
|
||||
tcpConnectTimeout = defaultTcpConnectTimeout,
|
||||
tcpTimeout = 15_000_000,
|
||||
tcpTimeoutPerKb = 5_000,
|
||||
@@ -407,6 +432,8 @@ data ProtocolClientConfig v = ProtocolClientConfig
|
||||
serverVRange :: VersionRange v,
|
||||
-- | agree shared session secret (used in SMP proxy for additional encryption layer)
|
||||
agreeSecret :: Bool,
|
||||
-- | Whether connecting client is a proxy server. See comment in ClientHandshake
|
||||
proxyServer :: Bool,
|
||||
-- | send SNI to server, False for SMP
|
||||
useSNI :: Bool
|
||||
}
|
||||
@@ -421,6 +448,7 @@ defaultClientConfig clientALPN useSNI serverVRange =
|
||||
clientALPN,
|
||||
serverVRange,
|
||||
agreeSecret = False,
|
||||
proxyServer = False,
|
||||
useSNI
|
||||
}
|
||||
{-# INLINE defaultClientConfig #-}
|
||||
@@ -489,15 +517,15 @@ type TransportSession msg = (UserId, ProtoServer msg, Maybe ByteString)
|
||||
--
|
||||
-- A single queue can be used for multiple 'SMPClient' instances,
|
||||
-- as 'SMPServerTransmission' includes server information.
|
||||
getProtocolClient :: forall v err msg. Protocol v err msg => TVar ChaChaDRG -> TransportSession msg -> ProtocolClientConfig v -> Maybe (TBQueue (ServerTransmissionBatch v err msg)) -> UTCTime -> (ProtocolClient v err msg -> IO ()) -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg))
|
||||
getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, clientALPN, serverVRange, agreeSecret, useSNI} msgQ proxySessTs disconnected = do
|
||||
getProtocolClient :: forall v err msg. Protocol v err msg => TVar ChaChaDRG -> TransportSession msg -> ProtocolClientConfig v -> [HostName] -> Maybe (TBQueue (ServerTransmissionBatch v err msg)) -> UTCTime -> (ProtocolClient v err msg -> IO ()) -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg))
|
||||
getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, clientALPN, serverVRange, agreeSecret, proxyServer, useSNI} presetDomains msgQ proxySessTs disconnected = do
|
||||
case chooseTransportHost networkConfig (host srv) of
|
||||
Right useHost ->
|
||||
(getCurrentTime >>= mkProtocolClient useHost >>= runClient useTransport useHost)
|
||||
`catch` \(e :: IOException) -> pure . Left $ PCEIOError e
|
||||
Left e -> pure $ Left e
|
||||
where
|
||||
NetworkConfig {smpWebPort, tcpConnectTimeout, tcpTimeout, smpPingInterval} = networkConfig
|
||||
NetworkConfig {smpWebPortServers, tcpConnectTimeout, tcpTimeout, smpPingInterval} = networkConfig
|
||||
mkProtocolClient :: TransportHost -> UTCTime -> IO (PClient v err msg)
|
||||
mkProtocolClient transportHost ts = do
|
||||
connected <- newTVarIO False
|
||||
@@ -544,13 +572,19 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
|
||||
"" -> case protocolTypeI @(ProtoType msg) of
|
||||
SPSMP | smpWebPort -> ("443", transport @TLS)
|
||||
_ -> defaultTransport cfg
|
||||
"80" -> ("80", transport @WS)
|
||||
p -> (p, transport @TLS)
|
||||
where
|
||||
smpWebPort = case smpWebPortServers of
|
||||
SWPAll -> True
|
||||
SWPPreset -> case srv of
|
||||
ProtocolServer {host = THDomainName h :| _} -> any (`isSuffixOf` h) presetDomains
|
||||
_ -> False
|
||||
SWPOff -> False
|
||||
|
||||
client :: forall c. Transport c => TProxy c -> PClient v err msg -> TMVar (Either (ProtocolClientError err) (ProtocolClient v err msg)) -> c -> IO ()
|
||||
client _ c cVar h = do
|
||||
ks <- if agreeSecret then Just <$> atomically (C.generateKeyPair g) else pure Nothing
|
||||
runExceptT (protocolClientHandshake @v @err @msg h ks (keyHash srv) serverVRange) >>= \case
|
||||
runExceptT (protocolClientHandshake @v @err @msg h ks (keyHash srv) serverVRange proxyServer) >>= \case
|
||||
Left e -> atomically . putTMVar cVar . Left $ PCETransportError e
|
||||
Right th@THandle {params} -> do
|
||||
sessionTs <- getCurrentTime
|
||||
@@ -565,9 +599,12 @@ getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize
|
||||
send :: Transport c => ProtocolClient v err msg -> THandle v c 'TClient -> IO ()
|
||||
send ProtocolClient {client_ = PClient {sndQ}} h = forever $ atomically (readTBQueue sndQ) >>= sendPending
|
||||
where
|
||||
sendPending (Nothing, s) = send_ s
|
||||
sendPending (Just pending, s) = whenM (readTVarIO pending) $ send_ s
|
||||
send_ = void . tPutLog h
|
||||
sendPending (r, s) = case r of
|
||||
Nothing -> void $ tPutLog h s
|
||||
Just Request {pending, responseVar} ->
|
||||
whenM (readTVarIO pending) $ tPutLog h s >>= either responseErr pure
|
||||
where
|
||||
responseErr = atomically . putTMVar responseVar . Left . PCETransportError
|
||||
|
||||
receive :: Transport c => ProtocolClient v err msg -> THandle v c 'TClient -> IO ()
|
||||
receive ProtocolClient {client_ = PClient {rcvQ, lastReceived, timeoutErrorCount}} h = forever $ do
|
||||
@@ -702,14 +739,17 @@ smpProxyError = \case
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#create-queue-command
|
||||
createSMPQueue ::
|
||||
SMPClient ->
|
||||
Maybe C.CbNonce -> -- used as correlation ID to allow deriving SenderId from it for short links
|
||||
C.AAuthKeyPair -> -- SMP v6 - signature key pair, SMP v7 - DH key pair
|
||||
RcvPublicDhKey ->
|
||||
Maybe BasicAuth ->
|
||||
SubscriptionMode ->
|
||||
Bool ->
|
||||
QueueReqData ->
|
||||
-- TODO [notifications]
|
||||
-- Maybe NewNtfCreds ->
|
||||
ExceptT SMPClientError IO QueueIdsKeys
|
||||
createSMPQueue c (rKey, rpKey) dhKey auth subMode sndSecure =
|
||||
sendSMPCommand c (Just rpKey) NoEntity (NEW rKey dhKey auth subMode sndSecure) >>= \case
|
||||
createSMPQueue c nonce_ (rKey, rpKey) dhKey auth subMode qrd =
|
||||
sendProtocolCommand_ c nonce_ Nothing (Just rpKey) NoEntity (Cmd SRecipient $ NEW $ NewQueueReq rKey dhKey auth subMode (Just qrd)) >>= \case
|
||||
IDS qik -> pure qik
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
@@ -761,6 +801,7 @@ getSMPMessage c rpKey rId =
|
||||
OK -> pure Nothing
|
||||
cmd@(MSG msg) -> liftIO (writeSMPMessage c rId cmd) $> Just msg
|
||||
r -> throwE $ unexpectedResponse r
|
||||
{-# INLINE getSMPMessage #-}
|
||||
|
||||
-- | Subscribe to the SMP queue notifications.
|
||||
--
|
||||
@@ -795,9 +836,47 @@ secureSndSMPQueue c spKey sId senderKey = okSMPCommand (SKEY senderKey) c spKey
|
||||
{-# INLINE secureSndSMPQueue #-}
|
||||
|
||||
proxySecureSndSMPQueue :: SMPClient -> ProxiedRelay -> SndPrivateAuthKey -> SenderId -> SndPublicAuthKey -> ExceptT SMPClientError IO (Either ProxyClientError ())
|
||||
proxySecureSndSMPQueue c proxiedRelay spKey sId senderKey = proxySMPCommand c proxiedRelay (Just spKey) sId (SKEY senderKey)
|
||||
proxySecureSndSMPQueue c proxiedRelay spKey sId senderKey = proxyOKSMPCommand c proxiedRelay (Just spKey) sId (SKEY senderKey)
|
||||
{-# INLINE proxySecureSndSMPQueue #-}
|
||||
|
||||
-- | Add or update date for queue link
|
||||
addSMPQueueLink :: SMPClient -> RcvPrivateAuthKey -> RecipientId -> LinkId -> QueueLinkData -> ExceptT SMPClientError IO ()
|
||||
addSMPQueueLink c rpKey rId lnkId d = okSMPCommand (LSET lnkId d) c rpKey rId
|
||||
{-# INLINE addSMPQueueLink #-}
|
||||
|
||||
-- | Delete queue link
|
||||
deleteSMPQueueLink :: SMPClient -> RcvPrivateAuthKey -> RecipientId -> ExceptT SMPClientError IO ()
|
||||
deleteSMPQueueLink = okSMPCommand LDEL
|
||||
{-# INLINE deleteSMPQueueLink #-}
|
||||
|
||||
-- | Get 1-time inviation SMP queue link data and secure the queue via queue link ID.
|
||||
secureGetSMPQueueLink :: SMPClient -> SndPrivateAuthKey -> LinkId -> SndPublicAuthKey -> ExceptT SMPClientError IO (SenderId, QueueLinkData)
|
||||
secureGetSMPQueueLink c spKey lnkId senderKey =
|
||||
sendSMPCommand c (Just spKey) lnkId (LKEY senderKey) >>= \case
|
||||
LNK sId d -> pure (sId, d)
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
proxySecureGetSMPQueueLink :: SMPClient -> ProxiedRelay -> SndPrivateAuthKey -> LinkId -> SndPublicAuthKey -> ExceptT SMPClientError IO (Either ProxyClientError (SenderId, QueueLinkData))
|
||||
proxySecureGetSMPQueueLink c proxiedRelay spKey lnkId senderKey =
|
||||
proxySMPCommand c proxiedRelay (Just spKey) lnkId (LKEY senderKey) >>= \case
|
||||
Right (LNK sId d) -> pure $ Right (sId, d)
|
||||
Right r -> throwE $ unexpectedResponse r
|
||||
Left e -> pure $ Left e
|
||||
|
||||
-- | Get contact address SMP queue link data.
|
||||
getSMPQueueLink :: SMPClient -> LinkId -> ExceptT SMPClientError IO (SenderId, QueueLinkData)
|
||||
getSMPQueueLink c lnkId =
|
||||
sendSMPCommand c Nothing lnkId LGET >>= \case
|
||||
LNK sId d -> pure (sId, d)
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
proxyGetSMPQueueLink :: SMPClient -> ProxiedRelay -> LinkId -> ExceptT SMPClientError IO (Either ProxyClientError (SenderId, QueueLinkData))
|
||||
proxyGetSMPQueueLink c proxiedRelay lnkId =
|
||||
proxySMPCommand c proxiedRelay Nothing lnkId LGET >>= \case
|
||||
Right (LNK sId d) -> pure $ Right (sId, d)
|
||||
Right r -> throwE $ unexpectedResponse r
|
||||
Left e -> pure $ Left e
|
||||
|
||||
-- | Enable notifications for the queue for push notifications server.
|
||||
--
|
||||
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#enable-notifications-command
|
||||
@@ -839,7 +918,7 @@ sendSMPMessage c spKey sId flags msg =
|
||||
r -> throwE $ unexpectedResponse r
|
||||
|
||||
proxySMPMessage :: SMPClient -> ProxiedRelay -> Maybe SndPrivateAuthKey -> SenderId -> MsgFlags -> MsgBody -> ExceptT SMPClientError IO (Either ProxyClientError ())
|
||||
proxySMPMessage c proxiedRelay spKey sId flags msg = proxySMPCommand c proxiedRelay spKey sId (SEND flags msg)
|
||||
proxySMPMessage c proxiedRelay spKey sId flags msg = proxyOKSMPCommand c proxiedRelay spKey sId (SEND flags msg)
|
||||
|
||||
-- | Acknowledge message delivery (server deletes the message).
|
||||
--
|
||||
@@ -951,15 +1030,24 @@ instance StrEncoding ProxyClientError where
|
||||
-- - other errors from the client running on proxy and connected to relay in PREProxiedRelayError
|
||||
|
||||
-- This function proxies Sender commands that return OK or ERR
|
||||
proxyOKSMPCommand :: SMPClient -> ProxiedRelay -> Maybe SndPrivateAuthKey -> SenderId -> Command 'Sender -> ExceptT SMPClientError IO (Either ProxyClientError ())
|
||||
proxyOKSMPCommand c proxiedRelay spKey sId command =
|
||||
proxySMPCommand c proxiedRelay spKey sId command >>= \case
|
||||
Right OK -> pure $ Right ()
|
||||
Right r -> throwE $ unexpectedResponse r
|
||||
Left e -> pure $ Left e
|
||||
|
||||
proxySMPCommand ::
|
||||
forall p.
|
||||
PartyI p =>
|
||||
SMPClient ->
|
||||
-- proxy session from PKEY
|
||||
ProxiedRelay ->
|
||||
-- message to deliver
|
||||
Maybe SndPrivateAuthKey ->
|
||||
SenderId ->
|
||||
Command 'Sender ->
|
||||
ExceptT SMPClientError IO (Either ProxyClientError ())
|
||||
Command p ->
|
||||
ExceptT SMPClientError IO (Either ProxyClientError BrokerMsg)
|
||||
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
|
||||
@@ -968,7 +1056,7 @@ proxySMPCommand c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c
|
||||
let cmdSecret = C.dh' serverKey cmdPrivKey
|
||||
nonce@(C.CbNonce corrId) <- liftIO . atomically $ C.randomCbNonce g
|
||||
-- encode
|
||||
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth serverThParams (CorrId corrId, sId, Cmd SSender command)
|
||||
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth serverThParams (CorrId corrId, sId, Cmd (sParty @p) command)
|
||||
auth <- liftEitherWith PCETransportError $ authTransmission serverThAuth spKey nonce tForAuth
|
||||
b <- case batchTransmissions (batch serverThParams) (blockSize serverThParams) [Right (auth, tToSend)] of
|
||||
[] -> throwE $ PCETransportError TELargeMsg
|
||||
@@ -986,9 +1074,8 @@ proxySMPCommand c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c
|
||||
case tParse serverThParams t' of
|
||||
t'' :| [] -> case tDecodeParseValidate serverThParams t'' of
|
||||
(_auth, _signed, (_c, _e, cmd)) -> case cmd of
|
||||
Right OK -> pure $ Right ()
|
||||
Right (ERR e) -> throwE $ PCEProtocolError e -- this is the error from the destination relay
|
||||
Right r' -> throwE $ unexpectedResponse r'
|
||||
Right r' -> pure $ Right r'
|
||||
Left e -> throwE $ PCEResponseError e
|
||||
_ -> throwE $ PCETransportError TEBadBlock
|
||||
ERR e -> pure . Left $ ProxyProtocolError e -- this will not happen, this error is returned via Left
|
||||
@@ -1082,11 +1169,11 @@ sendBatch c@ProtocolClient {client_ = PClient {sndQ}} b = do
|
||||
pure [Response entityId $ Left $ PCETransportError e]
|
||||
TBTransmissions s n rs
|
||||
| n > 0 -> do
|
||||
atomically $ writeTBQueue sndQ (Nothing, s) -- do not expire batched responses
|
||||
nonBlockingWriteTBQueue sndQ (Nothing, s) -- do not expire batched responses
|
||||
mapConcurrently (getResponse c Nothing) rs
|
||||
| otherwise -> pure []
|
||||
TBTransmission s r -> do
|
||||
atomically $ writeTBQueue sndQ (Nothing, s)
|
||||
nonBlockingWriteTBQueue sndQ (Nothing, s)
|
||||
(: []) <$> getResponse c Nothing r
|
||||
|
||||
-- | Send Protocol command
|
||||
@@ -1097,24 +1184,31 @@ sendProtocolCommand c = sendProtocolCommand_ c Nothing Nothing
|
||||
-- This is to reflect the fact that we send subscriptions only as batches, and also because we do not track a separate timeout for the whole batch, so it is not obvious when should we expire it.
|
||||
-- We could expire a batch of deletes, for example, either when the first response expires or when the last one does.
|
||||
-- But a better solution is to process delayed delete responses.
|
||||
--
|
||||
-- Please note: if nonce is passed it is also used as a correlation ID
|
||||
sendProtocolCommand_ :: forall v err msg. Protocol v err msg => ProtocolClient v err msg -> Maybe C.CbNonce -> Maybe Int -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg
|
||||
sendProtocolCommand_ c@ProtocolClient {client_ = PClient {sndQ}, thParams = THandleParams {batch, blockSize}} nonce_ tOut pKey entId cmd =
|
||||
ExceptT $ uncurry sendRecv =<< mkTransmission_ c nonce_ (pKey, entId, cmd)
|
||||
where
|
||||
-- two separate "atomically" needed to avoid blocking
|
||||
sendRecv :: Either TransportError SentRawTransmission -> Request err msg -> IO (Either (ProtocolClientError err) msg)
|
||||
sendRecv t_ r@Request {pending} = case t_ of
|
||||
sendRecv t_ r = case t_ of
|
||||
Left e -> pure . Left $ PCETransportError e
|
||||
Right t
|
||||
| B.length s > blockSize - 2 -> pure . Left $ PCETransportError TELargeMsg
|
||||
| otherwise -> do
|
||||
atomically $ writeTBQueue sndQ (Just pending, s)
|
||||
nonBlockingWriteTBQueue sndQ (Just r, s)
|
||||
response <$> getResponse c tOut r
|
||||
where
|
||||
s
|
||||
| batch = tEncodeBatch1 t
|
||||
| otherwise = tEncode t
|
||||
|
||||
nonBlockingWriteTBQueue :: TBQueue a -> a -> IO ()
|
||||
nonBlockingWriteTBQueue q x = do
|
||||
sent <- atomically $ tryWriteTBQueue q x
|
||||
unless sent $ void $ forkIO $ atomically $ writeTBQueue q x
|
||||
|
||||
getResponse :: ProtocolClient v err msg -> Maybe Int -> Request err msg -> IO (Response err msg)
|
||||
getResponse ProtocolClient {client_ = PClient {tcpTimeout, timeoutErrorCount}} tOut Request {entityId, pending, responseVar} = do
|
||||
r <- fromMaybe tcpTimeout tOut `timeout` atomically (takeTMVar responseVar)
|
||||
@@ -1195,6 +1289,8 @@ $(J.deriveJSON (enumJSON $ dropPrefix "SPM") ''SMPProxyMode)
|
||||
|
||||
$(J.deriveJSON (enumJSON $ dropPrefix "SPF") ''SMPProxyFallback)
|
||||
|
||||
$(J.deriveJSON (enumJSON $ dropPrefix "SWP") ''SMPWebPortServers)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''NetworkConfig)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON $ dropPrefix "Proxy") ''ProxyClientError)
|
||||
|
||||
@@ -42,7 +42,7 @@ import Simplex.Messaging.Session
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Util (catchAll_, ifM, toChunks, whenM, ($>>=), (<$$>))
|
||||
import Simplex.Messaging.Util (catchAll_, ifM, safeDecodeUtf8, toChunks, tshow, whenM, ($>>=), (<$$>))
|
||||
import System.Timeout (timeout)
|
||||
import UnliftIO (async)
|
||||
import qualified UnliftIO.Exception as E
|
||||
@@ -198,7 +198,7 @@ isOwnServer SMPClientAgent {agentCfg} ProtocolServer {host} =
|
||||
-- | Run an SMP client for SMPClientVar
|
||||
connectClient :: SMPClientAgent -> SMPServer -> SMPClientVar -> IO (Either SMPClientError SMPClient)
|
||||
connectClient ca@SMPClientAgent {agentCfg, smpClients, smpSessions, msgQ, randomDrg, startedAt} srv v =
|
||||
getProtocolClient randomDrg (1, srv, Nothing) (smpCfg agentCfg) (Just msgQ) startedAt clientDisconnected
|
||||
getProtocolClient randomDrg (1, srv, Nothing) (smpCfg agentCfg) [] (Just msgQ) startedAt clientDisconnected
|
||||
where
|
||||
clientDisconnected :: SMPClient -> IO ()
|
||||
clientDisconnected smp = do
|
||||
@@ -321,7 +321,7 @@ withSMP ca srv action = (getSMPServerClient' ca srv >>= action) `catchE` logSMPE
|
||||
where
|
||||
logSMPError :: SMPClientError -> ExceptT SMPClientError IO a
|
||||
logSMPError e = do
|
||||
liftIO $ putStrLn $ "SMP error (" <> show srv <> "): " <> show e
|
||||
logInfo $ "SMP error (" <> safeDecodeUtf8 (strEncode $ host srv) <> "): " <> tshow e
|
||||
throwE e
|
||||
|
||||
subscribeQueuesSMP :: SMPClientAgent -> SMPServer -> NonEmpty (RecipientId, RcvPrivateAuthKey) -> IO ()
|
||||
@@ -412,14 +412,22 @@ removeSubscription :: SMPClientAgent -> SMPServer -> SMPSub -> STM ()
|
||||
removeSubscription = removeSub_ . srvSubs
|
||||
{-# INLINE removeSubscription #-}
|
||||
|
||||
removePendingSub :: SMPClientAgent -> SMPServer -> SMPSub -> STM ()
|
||||
removePendingSub = removeSub_ . pendingSrvSubs
|
||||
{-# INLINE removePendingSub #-}
|
||||
|
||||
removeSub_ :: TMap SMPServer (TMap SMPSub s) -> SMPServer -> SMPSub -> STM ()
|
||||
removeSub_ subs srv s = TM.lookup srv subs >>= mapM_ (TM.delete s)
|
||||
|
||||
removeSubscriptions :: SMPClientAgent -> SMPServer -> SMPSubParty -> [QueueId] -> STM ()
|
||||
removeSubscriptions = removeSubs_ . srvSubs
|
||||
{-# INLINE removeSubscriptions #-}
|
||||
|
||||
removePendingSubs :: SMPClientAgent -> SMPServer -> SMPSubParty -> [QueueId] -> STM ()
|
||||
removePendingSubs = removeSubs_ . pendingSrvSubs
|
||||
{-# INLINE removePendingSubs #-}
|
||||
|
||||
removeSubs_ :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey) -> SMPServer -> SMPSubParty -> [QueueId] -> STM ()
|
||||
removeSubs_ :: TMap SMPServer (TMap SMPSub s) -> 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
|
||||
|
||||
+113
-31
@@ -1,11 +1,14 @@
|
||||
{-# LANGUAGE AllowAmbiguousTypes #-}
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE ConstraintKinds #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DerivingStrategies #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
@@ -139,6 +142,8 @@ module Simplex.Messaging.Crypto
|
||||
cbDecryptNoPad,
|
||||
sbDecrypt_,
|
||||
sbEncrypt_,
|
||||
sbEncryptNoPad,
|
||||
sbDecryptNoPad,
|
||||
cbNonce,
|
||||
randomCbNonce,
|
||||
reverseNonce,
|
||||
@@ -157,6 +162,7 @@ module Simplex.Messaging.Crypto
|
||||
SbKeyNonce,
|
||||
sbcInit,
|
||||
sbcHkdf,
|
||||
hkdf,
|
||||
|
||||
-- * pseudo-random bytes
|
||||
randomBytes,
|
||||
@@ -164,8 +170,11 @@ module Simplex.Messaging.Crypto
|
||||
-- * digests
|
||||
sha256Hash,
|
||||
sha512Hash,
|
||||
sha3_256,
|
||||
sha3_384,
|
||||
|
||||
-- * Message padding / un-padding
|
||||
canPad,
|
||||
pad,
|
||||
unPad,
|
||||
|
||||
@@ -203,7 +212,7 @@ import Crypto.Cipher.AES (AES256)
|
||||
import qualified Crypto.Cipher.Types as AES
|
||||
import qualified Crypto.Cipher.XSalsa as XSalsa
|
||||
import qualified Crypto.Error as CE
|
||||
import Crypto.Hash (Digest, SHA256 (..), SHA512 (..), hash, hashDigestSize)
|
||||
import Crypto.Hash (Digest, SHA3_256, SHA3_384, SHA256 (..), SHA512 (..), hash, hashDigestSize)
|
||||
import qualified Crypto.KDF.HKDF as H
|
||||
import qualified Crypto.MAC.Poly1305 as Poly1305
|
||||
import qualified Crypto.PubKey.Curve25519 as X25519
|
||||
@@ -233,13 +242,12 @@ import Data.Typeable (Proxy (Proxy), Typeable)
|
||||
import Data.Word (Word32)
|
||||
import Data.X509
|
||||
import Data.X509.Validation (Fingerprint (..), getFingerprint)
|
||||
import Database.SQLite.Simple.FromField (FromField (..))
|
||||
import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import GHC.TypeLits (ErrorMessage (..), KnownNat, Nat, TypeError, natVal, type (+))
|
||||
import Network.Transport.Internal (decodeWord16, encodeWord16)
|
||||
import Simplex.Messaging.Agent.Store.DB (Binary (..), FromField (..), ToField (..), blobFieldDecoder)
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (blobFieldDecoder, parseAll, parseString)
|
||||
import Simplex.Messaging.Parsers (parseAll, parseString)
|
||||
import Simplex.Messaging.Util ((<$?>))
|
||||
|
||||
-- | Cryptographic algorithms.
|
||||
@@ -721,23 +729,23 @@ generateKeyPair_ = case sAlgorithm @a of
|
||||
let k = X448.toPublic pk
|
||||
in pure (PublicKeyX448 k, PrivateKeyX448 pk k)
|
||||
|
||||
instance ToField APrivateSignKey where toField = toField . encodePrivKey
|
||||
instance ToField APrivateSignKey where toField = toField . Binary . encodePrivKey
|
||||
|
||||
instance ToField APublicVerifyKey where toField = toField . encodePubKey
|
||||
instance ToField APublicVerifyKey where toField = toField . Binary . encodePubKey
|
||||
|
||||
instance ToField APrivateAuthKey where toField = toField . encodePrivKey
|
||||
instance ToField APrivateAuthKey where toField = toField . Binary . encodePrivKey
|
||||
|
||||
instance ToField APublicAuthKey where toField = toField . encodePubKey
|
||||
instance ToField APublicAuthKey where toField = toField . Binary . encodePubKey
|
||||
|
||||
instance ToField APrivateDhKey where toField = toField . encodePrivKey
|
||||
instance ToField APrivateDhKey where toField = toField . Binary . encodePrivKey
|
||||
|
||||
instance ToField APublicDhKey where toField = toField . encodePubKey
|
||||
instance ToField APublicDhKey where toField = toField . Binary . encodePubKey
|
||||
|
||||
instance AlgorithmI a => ToField (PrivateKey a) where toField = toField . encodePrivKey
|
||||
instance AlgorithmI a => ToField (PrivateKey a) where toField = toField . Binary . encodePrivKey
|
||||
|
||||
instance AlgorithmI a => ToField (PublicKey a) where toField = toField . encodePubKey
|
||||
instance AlgorithmI a => ToField (PublicKey a) where toField = toField . Binary . encodePubKey
|
||||
|
||||
instance ToField (DhSecret a) where toField = toField . dhBytes'
|
||||
instance ToField (DhSecret a) where toField = toField . Binary . dhBytes'
|
||||
|
||||
instance FromField APrivateSignKey where fromField = blobFieldDecoder decodePrivKey
|
||||
|
||||
@@ -783,10 +791,19 @@ class CryptoSignature s where
|
||||
|
||||
instance CryptoSignature (Signature s) => StrEncoding (Signature s) where
|
||||
strEncode = serializeSignature
|
||||
{-# INLINE strEncode #-}
|
||||
strDecode = decodeSignature
|
||||
{-# INLINE strDecode #-}
|
||||
|
||||
instance CryptoSignature (Signature s) => Encoding (Signature s) where
|
||||
smpEncode = smpEncode . signatureBytes
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP = decodeSignature <$?> smpP
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
instance CryptoSignature ASignature where
|
||||
signatureBytes (ASignature _ sig) = signatureBytes sig
|
||||
{-# INLINE signatureBytes #-}
|
||||
decodeSignature s
|
||||
| B.length s == Ed25519.signatureSize =
|
||||
ASignature SEd25519 . SignatureEd25519 <$> ed Ed25519.signature s
|
||||
@@ -798,6 +815,7 @@ instance CryptoSignature ASignature where
|
||||
|
||||
instance CryptoSignature (Maybe ASignature) where
|
||||
signatureBytes = maybe "" signatureBytes
|
||||
{-# INLINE signatureBytes #-}
|
||||
decodeSignature s
|
||||
| B.null s = Right Nothing
|
||||
| otherwise = Just <$> decodeSignature s
|
||||
@@ -806,6 +824,7 @@ instance AlgorithmI a => CryptoSignature (Signature a) where
|
||||
signatureBytes = \case
|
||||
SignatureEd25519 s -> BA.convert s
|
||||
SignatureEd448 s -> BA.convert s
|
||||
{-# INLINE signatureBytes #-}
|
||||
decodeSignature s = do
|
||||
ASignature _ sig <- decodeSignature s
|
||||
checkAlgorithm sig
|
||||
@@ -816,25 +835,31 @@ instance SignatureSize (Signature a) where
|
||||
signatureSize = \case
|
||||
SignatureEd25519 _ -> Ed25519.signatureSize
|
||||
SignatureEd448 _ -> Ed448.signatureSize
|
||||
{-# INLINE signatureSize #-}
|
||||
|
||||
instance SignatureSize ASignature where
|
||||
signatureSize (ASignature _ s) = signatureSize s
|
||||
{-# INLINE signatureSize #-}
|
||||
|
||||
instance SignatureSize APrivateSignKey where
|
||||
signatureSize (APrivateSignKey _ k) = signatureSize k
|
||||
{-# INLINE signatureSize #-}
|
||||
|
||||
instance SignatureSize APublicVerifyKey where
|
||||
signatureSize (APublicVerifyKey _ k) = signatureSize k
|
||||
{-# INLINE signatureSize #-}
|
||||
|
||||
instance SignatureAlgorithm a => SignatureSize (PrivateKey a) where
|
||||
signatureSize = \case
|
||||
PrivateKeyEd25519 _ _ -> Ed25519.signatureSize
|
||||
PrivateKeyEd448 _ _ -> Ed448.signatureSize
|
||||
{-# INLINE signatureSize #-}
|
||||
|
||||
instance SignatureAlgorithm a => SignatureSize (PublicKey a) where
|
||||
signatureSize = \case
|
||||
PublicKeyEd25519 _ -> Ed25519.signatureSize
|
||||
PublicKeyEd448 _ -> Ed448.signatureSize
|
||||
{-# INLINE signatureSize #-}
|
||||
|
||||
-- | Various cryptographic or related errors.
|
||||
data CryptoError
|
||||
@@ -884,14 +909,14 @@ x448_size = 448 `quot` 8
|
||||
validSignatureSize :: Int -> Bool
|
||||
validSignatureSize n =
|
||||
n == Ed25519.signatureSize || n == Ed448.signatureSize
|
||||
{-# INLINE validSignatureSize #-}
|
||||
|
||||
-- | AES key newtype.
|
||||
newtype Key = Key {unKey :: ByteString}
|
||||
deriving (Eq, Ord, Show)
|
||||
deriving newtype (FromField)
|
||||
|
||||
instance ToField Key where toField = toField . unKey
|
||||
|
||||
instance FromField Key where fromField f = Key <$> fromField f
|
||||
instance ToField Key where toField (Key s) = toField $ Binary s
|
||||
|
||||
instance ToJSON Key where
|
||||
toJSON = strToJSON . unKey
|
||||
@@ -952,17 +977,29 @@ instance FromJSON KeyHash where
|
||||
instance IsString KeyHash where
|
||||
fromString = parseString $ parseAll strP
|
||||
|
||||
instance ToField KeyHash where toField = toField . strEncode
|
||||
instance ToField KeyHash where toField = toField . Binary . strEncode
|
||||
|
||||
instance FromField KeyHash where fromField = blobFieldDecoder $ parseAll strP
|
||||
|
||||
-- | SHA256 digest.
|
||||
sha256Hash :: ByteString -> ByteString
|
||||
sha256Hash = BA.convert . (hash :: ByteString -> Digest SHA256)
|
||||
{-# INLINE sha256Hash #-}
|
||||
|
||||
-- | SHA512 digest.
|
||||
sha512Hash :: ByteString -> ByteString
|
||||
sha512Hash = BA.convert . (hash :: ByteString -> Digest SHA512)
|
||||
{-# INLINE sha512Hash #-}
|
||||
|
||||
-- | SHA3-256 digest.
|
||||
sha3_256 :: ByteString -> ByteString
|
||||
sha3_256 = BA.convert . (hash :: ByteString -> Digest SHA3_256)
|
||||
{-# INLINE sha3_256 #-}
|
||||
|
||||
-- | SHA3-384 digest.
|
||||
sha3_384 :: ByteString -> ByteString
|
||||
sha3_384 = BA.convert . (hash :: ByteString -> Digest SHA3_384)
|
||||
{-# INLINE sha3_384 #-}
|
||||
|
||||
-- | AEAD-GCM encryption with associated data.
|
||||
--
|
||||
@@ -979,6 +1016,7 @@ encryptAEAD aesKey ivBytes paddedLen ad msg = do
|
||||
-- This function requires 12 bytes IV, it does not transform IV.
|
||||
encryptAESNoPad :: Key -> GCMIV -> ByteString -> ExceptT CryptoError IO (AuthTag, ByteString)
|
||||
encryptAESNoPad key iv = encryptAEADNoPad key iv ""
|
||||
{-# INLINE encryptAESNoPad #-}
|
||||
|
||||
encryptAEADNoPad :: Key -> GCMIV -> ByteString -> ByteString -> ExceptT CryptoError IO (AuthTag, ByteString)
|
||||
encryptAEADNoPad aesKey ivBytes ad msg = do
|
||||
@@ -1000,6 +1038,7 @@ decryptAEAD aesKey ivBytes ad msg (AuthTag authTag) = do
|
||||
-- This function requires 12 bytes IV, it does not transform IV.
|
||||
decryptAESNoPad :: Key -> GCMIV -> ByteString -> AuthTag -> ExceptT CryptoError IO ByteString
|
||||
decryptAESNoPad key iv = decryptAEADNoPad key iv ""
|
||||
{-# INLINE decryptAESNoPad #-}
|
||||
|
||||
decryptAEADNoPad :: Key -> GCMIV -> ByteString -> ByteString -> AuthTag -> ExceptT CryptoError IO ByteString
|
||||
decryptAEADNoPad aesKey iv ad msg (AuthTag tag) = do
|
||||
@@ -1009,6 +1048,11 @@ decryptAEADNoPad aesKey iv ad msg (AuthTag tag) = do
|
||||
maxMsgLen :: Int
|
||||
maxMsgLen = 2 ^ (16 :: Int) - 3
|
||||
|
||||
canPad :: Int -> Int -> Bool
|
||||
canPad msgLen paddedLen = msgLen <= maxMsgLen && padLen >= 0
|
||||
where
|
||||
padLen = paddedLen - msgLen - 2
|
||||
|
||||
pad :: ByteString -> Int -> Either CryptoError ByteString
|
||||
pad msg paddedLen
|
||||
| len <= maxMsgLen && padLen >= 0 = Right $ encodeWord16 (fromIntegral len) <> msg <> B.replicate padLen '#'
|
||||
@@ -1047,6 +1091,7 @@ maxLenBS s
|
||||
|
||||
unsafeMaxLenBS :: forall i. KnownNat i => ByteString -> MaxLenBS i
|
||||
unsafeMaxLenBS = MLBS
|
||||
{-# INLINE unsafeMaxLenBS #-}
|
||||
|
||||
padMaxLenBS :: forall i. KnownNat i => MaxLenBS i -> MaxLenBS (i + 2)
|
||||
padMaxLenBS (MLBS msg) = MLBS $ encodeWord16 (fromIntegral len) <> msg <> B.replicate padLen '#'
|
||||
@@ -1059,6 +1104,7 @@ appendMaxLenBS (MLBS s1) (MLBS s2) = MLBS $ s1 <> s2
|
||||
|
||||
maxLength :: forall i. KnownNat i => Int
|
||||
maxLength = fromIntegral (natVal $ Proxy @i)
|
||||
{-# INLINE maxLength #-}
|
||||
|
||||
-- this function requires 16 bytes IV, it transforms IV in cryptonite_aes_gcm_init here:
|
||||
-- https://github.com/haskell-crypto/cryptonite/blob/master/cbits/cryptonite_aes.c
|
||||
@@ -1079,12 +1125,15 @@ initAEADGCM (Key aesKey) (GCMIV ivBytes) = cryptoFailable $ do
|
||||
-- | Random AES256 key.
|
||||
randomAesKey :: TVar ChaChaDRG -> STM Key
|
||||
randomAesKey = fmap Key . randomBytes aesKeySize
|
||||
{-# INLINE randomAesKey #-}
|
||||
|
||||
randomGCMIV :: TVar ChaChaDRG -> STM GCMIV
|
||||
randomGCMIV = fmap GCMIV . randomBytes gcmIVSize
|
||||
{-# INLINE randomGCMIV #-}
|
||||
|
||||
ivSize :: forall c. AES.BlockCipher c => Int
|
||||
ivSize = AES.blockSize (undefined :: c)
|
||||
{-# INLINE ivSize #-}
|
||||
|
||||
gcmIVSize :: Int
|
||||
gcmIVSize = 12
|
||||
@@ -1094,6 +1143,7 @@ makeIV bs = maybeError CryptoIVError $ AES.makeIV bs
|
||||
|
||||
maybeError :: CryptoError -> Maybe a -> ExceptT CryptoError IO a
|
||||
maybeError e = maybe (throwE e) return
|
||||
{-# INLINE maybeError #-}
|
||||
|
||||
cryptoFailable :: CE.CryptoFailable a -> ExceptT CryptoError IO a
|
||||
cryptoFailable = liftEither . first AESCipherError . CE.eitherCryptoError
|
||||
@@ -1104,12 +1154,15 @@ cryptoFailable = liftEither . first AESCipherError . CE.eitherCryptoError
|
||||
sign' :: SignatureAlgorithm a => PrivateKey a -> ByteString -> Signature a
|
||||
sign' (PrivateKeyEd25519 pk k) msg = SignatureEd25519 $ Ed25519.sign pk k msg
|
||||
sign' (PrivateKeyEd448 pk k) msg = SignatureEd448 $ Ed448.sign pk k msg
|
||||
{-# INLINE sign' #-}
|
||||
|
||||
sign :: APrivateSignKey -> ByteString -> ASignature
|
||||
sign (APrivateSignKey a k) = ASignature a . sign' k
|
||||
{-# INLINE sign #-}
|
||||
|
||||
signCertificate :: APrivateSignKey -> Certificate -> SignedCertificate
|
||||
signCertificate = signX509
|
||||
{-# INLINE signCertificate #-}
|
||||
|
||||
signX509 :: (ASN1Object o, Eq o, Show o) => APrivateSignKey -> o -> SignedExact o
|
||||
signX509 key = fst . objectToSignedExact f
|
||||
@@ -1134,6 +1187,7 @@ verifyX509 key exact = do
|
||||
|
||||
certificateFingerprint :: SignedCertificate -> KeyHash
|
||||
certificateFingerprint = signedFingerprint
|
||||
{-# INLINE certificateFingerprint #-}
|
||||
|
||||
signedFingerprint :: (ASN1Object o, Eq o, Show o) => SignedExact o -> KeyHash
|
||||
signedFingerprint o = KeyHash fp
|
||||
@@ -1147,25 +1201,33 @@ instance SignatureAlgorithm a => SignatureAlgorithmX509 (SAlgorithm a) where
|
||||
signatureAlgorithmX509 = \case
|
||||
SEd25519 -> SignatureALG_IntrinsicHash PubKeyALG_Ed25519
|
||||
SEd448 -> SignatureALG_IntrinsicHash PubKeyALG_Ed448
|
||||
{-# INLINE signatureAlgorithmX509 #-}
|
||||
|
||||
instance SignatureAlgorithmX509 APrivateSignKey where
|
||||
signatureAlgorithmX509 (APrivateSignKey a _) = signatureAlgorithmX509 a
|
||||
{-# INLINE signatureAlgorithmX509 #-}
|
||||
|
||||
instance SignatureAlgorithmX509 APublicVerifyKey where
|
||||
signatureAlgorithmX509 (APublicVerifyKey a _) = signatureAlgorithmX509 a
|
||||
{-# INLINE signatureAlgorithmX509 #-}
|
||||
|
||||
-- | An instance for 'ASignatureKeyPair' / ('PublicKeyType' pk, pk), without touching its type family.
|
||||
instance SignatureAlgorithmX509 pk => SignatureAlgorithmX509 (a, pk) where
|
||||
signatureAlgorithmX509 = signatureAlgorithmX509 . snd
|
||||
{-# INLINE signatureAlgorithmX509 #-}
|
||||
|
||||
-- | A wrapper to marshall signed ASN1 objects, like certificates.
|
||||
newtype SignedObject a = SignedObject {getSignedExact :: SignedExact a}
|
||||
|
||||
instance (Typeable a, Eq a, Show a, ASN1Object a) => FromField (SignedObject a) where
|
||||
#if defined(dbPostgres)
|
||||
fromField f dat = SignedObject <$> blobFieldDecoder decodeSignedObject f dat
|
||||
#else
|
||||
fromField = fmap SignedObject . blobFieldDecoder decodeSignedObject
|
||||
#endif
|
||||
|
||||
instance (Eq a, Show a, ASN1Object a) => ToField (SignedObject a) where
|
||||
toField (SignedObject s) = toField $ encodeSignedObject s
|
||||
toField (SignedObject s) = toField . Binary $ encodeSignedObject s
|
||||
|
||||
instance (Eq a, Show a, ASN1Object a) => Encoding (SignedObject a) where
|
||||
smpEncode (SignedObject exact) = smpEncode . Large $ encodeSignedObject exact
|
||||
@@ -1187,6 +1249,7 @@ certChainP = do
|
||||
verify' :: SignatureAlgorithm a => PublicKey a -> Signature a -> ByteString -> Bool
|
||||
verify' (PublicKeyEd25519 k) (SignatureEd25519 sig) msg = Ed25519.verify k msg sig
|
||||
verify' (PublicKeyEd448 k) (SignatureEd448 sig) msg = Ed448.verify k msg sig
|
||||
{-# INLINE verify' #-}
|
||||
|
||||
verify :: APublicVerifyKey -> ASignature -> ByteString -> Bool
|
||||
verify (APublicVerifyKey a k) (ASignature a' sig) msg = case testEquality a a' of
|
||||
@@ -1196,25 +1259,35 @@ verify (APublicVerifyKey a k) (ASignature a' sig) msg = case testEquality a a' o
|
||||
dh' :: DhAlgorithm a => PublicKey a -> PrivateKey a -> DhSecret a
|
||||
dh' (PublicKeyX25519 k) (PrivateKeyX25519 pk _) = DhSecretX25519 $ X25519.dh k pk
|
||||
dh' (PublicKeyX448 k) (PrivateKeyX448 pk _) = DhSecretX448 $ X448.dh k pk
|
||||
{-# INLINE dh' #-}
|
||||
|
||||
-- | NaCl @crypto_box@ encrypt with padding with a shared DH secret and 192-bit nonce.
|
||||
cbEncrypt :: DhSecret X25519 -> CbNonce -> ByteString -> Int -> Either CryptoError ByteString
|
||||
cbEncrypt (DhSecretX25519 secret) = sbEncrypt_ secret
|
||||
{-# INLINE cbEncrypt #-}
|
||||
|
||||
-- | NaCl @crypto_box@ encrypt with a shared DH secret and 192-bit nonce (without padding).
|
||||
cbEncryptNoPad :: DhSecret X25519 -> CbNonce -> ByteString -> ByteString
|
||||
cbEncryptNoPad (DhSecretX25519 secret) (CbNonce nonce) = cryptoBox secret nonce
|
||||
{-# INLINE cbEncryptNoPad #-}
|
||||
|
||||
-- | NaCl @secret_box@ encrypt with a symmetric 256-bit key and 192-bit nonce.
|
||||
sbEncrypt :: SbKey -> CbNonce -> ByteString -> Int -> Either CryptoError ByteString
|
||||
sbEncrypt (SbKey key) = sbEncrypt_ key
|
||||
{-# INLINE sbEncrypt #-}
|
||||
|
||||
sbEncrypt_ :: ByteArrayAccess key => key -> CbNonce -> ByteString -> Int -> Either CryptoError ByteString
|
||||
sbEncrypt_ secret (CbNonce nonce) msg paddedLen = cryptoBox secret nonce <$> pad msg paddedLen
|
||||
{-# INLINE sbEncrypt_ #-}
|
||||
|
||||
sbEncryptNoPad :: SbKey -> CbNonce -> ByteString -> ByteString
|
||||
sbEncryptNoPad (SbKey key) (CbNonce nonce) = cryptoBox key nonce
|
||||
{-# INLINE sbEncryptNoPad #-}
|
||||
|
||||
-- | NaCl @crypto_box@ encrypt with a shared DH secret and 192-bit nonce.
|
||||
cbEncryptMaxLenBS :: KnownNat i => DhSecret X25519 -> CbNonce -> MaxLenBS i -> ByteString
|
||||
cbEncryptMaxLenBS (DhSecretX25519 secret) (CbNonce nonce) = cryptoBox secret nonce . unMaxLenBS . padMaxLenBS
|
||||
{-# INLINE cbEncryptMaxLenBS #-}
|
||||
|
||||
cryptoBox :: ByteArrayAccess key => key -> ByteString -> ByteString -> ByteString
|
||||
cryptoBox secret nonce s = BA.convert tag <> c
|
||||
@@ -1225,18 +1298,26 @@ cryptoBox secret nonce s = BA.convert tag <> c
|
||||
-- | NaCl @crypto_box@ decrypt with a shared DH secret and 192-bit nonce.
|
||||
cbDecrypt :: DhSecret X25519 -> CbNonce -> ByteString -> Either CryptoError ByteString
|
||||
cbDecrypt (DhSecretX25519 secret) = sbDecrypt_ secret
|
||||
{-# INLINE cbDecrypt #-}
|
||||
|
||||
-- | NaCl @crypto_box@ decrypt with a shared DH secret and 192-bit nonce (without unpadding).
|
||||
cbDecryptNoPad :: DhSecret X25519 -> CbNonce -> ByteString -> Either CryptoError ByteString
|
||||
cbDecryptNoPad (DhSecretX25519 secret) = sbDecryptNoPad_ secret
|
||||
{-# INLINE cbDecryptNoPad #-}
|
||||
|
||||
-- | NaCl @secret_box@ decrypt with a symmetric 256-bit key and 192-bit nonce.
|
||||
sbDecrypt :: SbKey -> CbNonce -> ByteString -> Either CryptoError ByteString
|
||||
sbDecrypt (SbKey key) = sbDecrypt_ key
|
||||
{-# INLINE sbDecrypt #-}
|
||||
|
||||
-- | NaCl @crypto_box@ decrypt with a shared DH secret and 192-bit nonce.
|
||||
sbDecrypt_ :: ByteArrayAccess key => key -> CbNonce -> ByteString -> Either CryptoError ByteString
|
||||
sbDecrypt_ secret nonce = unPad <=< sbDecryptNoPad_ secret nonce
|
||||
{-# INLINE sbDecrypt_ #-}
|
||||
|
||||
sbDecryptNoPad :: SbKey -> CbNonce -> ByteString -> Either CryptoError ByteString
|
||||
sbDecryptNoPad (SbKey key) = sbDecryptNoPad_ key
|
||||
{-# INLINE sbDecryptNoPad #-}
|
||||
|
||||
-- | NaCl @crypto_box@ decrypt with a shared DH secret and 192-bit nonce (without unpadding).
|
||||
sbDecryptNoPad_ :: ByteArrayAccess key => key -> CbNonce -> ByteString -> Either CryptoError ByteString
|
||||
@@ -1265,6 +1346,9 @@ cbVerify k pk nonce (CbAuthenticator s) authorized = cbDecryptNoPad (dh' k pk) n
|
||||
|
||||
newtype CbNonce = CryptoBoxNonce {unCbNonce :: ByteString}
|
||||
deriving (Eq, Show)
|
||||
deriving newtype (FromField)
|
||||
|
||||
instance ToField CbNonce where toField (CryptoBoxNonce s) = toField $ Binary s
|
||||
|
||||
pattern CbNonce :: ByteString -> CbNonce
|
||||
pattern CbNonce s <- CryptoBoxNonce s
|
||||
@@ -1282,10 +1366,6 @@ instance ToJSON CbNonce where
|
||||
instance FromJSON CbNonce where
|
||||
parseJSON = strParseJSON "CbNonce"
|
||||
|
||||
instance FromField CbNonce where fromField f = CryptoBoxNonce <$> fromField f
|
||||
|
||||
instance ToField CbNonce where toField (CryptoBoxNonce s) = toField s
|
||||
|
||||
cbNonce :: ByteString -> CbNonce
|
||||
cbNonce s
|
||||
| len == 24 = CryptoBoxNonce s
|
||||
@@ -1309,6 +1389,9 @@ instance Encoding CbNonce where
|
||||
|
||||
newtype SbKey = SecretBoxKey {unSbKey :: ByteString}
|
||||
deriving (Eq, Show)
|
||||
deriving newtype (FromField)
|
||||
|
||||
instance ToField SbKey where toField (SecretBoxKey s) = toField $ Binary s
|
||||
|
||||
pattern SbKey :: ByteString -> SbKey
|
||||
pattern SbKey s <- SecretBoxKey s
|
||||
@@ -1326,10 +1409,6 @@ instance ToJSON SbKey where
|
||||
instance FromJSON SbKey where
|
||||
parseJSON = strParseJSON "SbKey"
|
||||
|
||||
instance FromField SbKey where fromField f = SecretBoxKey <$> fromField f
|
||||
|
||||
instance ToField SbKey where toField (SecretBoxKey s) = toField s
|
||||
|
||||
sbKey :: ByteString -> Either String SbKey
|
||||
sbKey s
|
||||
| B.length s == 32 = Right $ SecretBoxKey s
|
||||
@@ -1347,20 +1426,23 @@ newtype SbChainKey = SecretBoxChainKey {unSbChainKey :: ByteString}
|
||||
sbcInit :: ByteArrayAccess secret => ByteString -> secret -> (SbChainKey, SbChainKey)
|
||||
sbcInit salt secret = (SecretBoxChainKey ck1, SecretBoxChainKey ck2)
|
||||
where
|
||||
prk = H.extract salt secret :: H.PRK SHA512
|
||||
out = H.expand prk ("SimpleXSbChainInit" :: ByteString) 64
|
||||
(ck1, ck2) = B.splitAt 32 out
|
||||
(ck1, ck2) = B.splitAt 32 $ hkdf salt secret "SimpleXSbChainInit" 64
|
||||
|
||||
type SbKeyNonce = (SbKey, CbNonce)
|
||||
|
||||
sbcHkdf :: SbChainKey -> (SbKeyNonce, SbChainKey)
|
||||
sbcHkdf (SecretBoxChainKey ck) = ((SecretBoxKey sk, CryptoBoxNonce nonce), SecretBoxChainKey ck')
|
||||
where
|
||||
prk = H.extract B.empty ck :: H.PRK SHA512
|
||||
out = H.expand prk ("SimpleXSbChain" :: ByteString) 88 -- = 32 (new chain key) + 32 (secret_box key) + 24 (nonce)
|
||||
out = hkdf "" ck "SimpleXSbChain" 88 -- = 32 (new chain key) + 32 (secret_box key) + 24 (nonce)
|
||||
(ck', rest) = B.splitAt 32 out
|
||||
(sk, nonce) = B.splitAt 32 rest
|
||||
|
||||
hkdf :: ByteArrayAccess secret => ByteString -> secret -> ByteString -> Int -> ByteString
|
||||
hkdf salt ikm info n =
|
||||
let prk = H.extract salt ikm :: H.PRK SHA512
|
||||
in H.expand prk info n
|
||||
{-# INLINE hkdf #-}
|
||||
|
||||
xSalsa20 :: ByteArrayAccess key => key -> ByteString -> ByteString -> (ByteString, ByteString)
|
||||
xSalsa20 secret nonce msg = (rs, msg')
|
||||
where
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
@@ -20,6 +21,8 @@
|
||||
module Simplex.Messaging.Crypto.Ratchet
|
||||
( Ratchet (..),
|
||||
RatchetX448,
|
||||
MsgEncryptKey (..),
|
||||
MsgEncryptKeyX448,
|
||||
SkippedMsgDiff (..),
|
||||
SkippedMsgKeys,
|
||||
InitialKeys (..),
|
||||
@@ -63,7 +66,9 @@ module Simplex.Messaging.Crypto.Ratchet
|
||||
pqX3dhRcv,
|
||||
initSndRatchet,
|
||||
initRcvRatchet,
|
||||
rcEncrypt,
|
||||
rcCheckCanPad,
|
||||
rcEncryptHeader,
|
||||
rcEncryptMsg,
|
||||
rcDecrypt,
|
||||
-- used in tests
|
||||
MsgHeader (..),
|
||||
@@ -84,12 +89,11 @@ module Simplex.Messaging.Crypto.Ratchet
|
||||
where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import Control.Monad (unless)
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
import Control.Monad.Trans.Except
|
||||
import Crypto.Cipher.AES (AES256)
|
||||
import Crypto.Hash (SHA512)
|
||||
import qualified Crypto.KDF.HKDF as H
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||
import qualified Data.Aeson as J
|
||||
@@ -109,14 +113,13 @@ import Data.Maybe (fromMaybe, isJust)
|
||||
import Data.Type.Equality
|
||||
import Data.Typeable (Typeable)
|
||||
import Data.Word (Word16, Word32)
|
||||
import Database.SQLite.Simple.FromField (FromField (..))
|
||||
import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import Simplex.Messaging.Agent.QueryString
|
||||
import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..), FromField (..), ToField (..), blobFieldDecoder)
|
||||
import Simplex.Messaging.Crypto
|
||||
import Simplex.Messaging.Crypto.SNTRUP761.Bindings
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (blobFieldDecoder, defaultJSON, parseE, parseE')
|
||||
import Simplex.Messaging.Parsers (defaultJSON, parseE, parseE')
|
||||
import Simplex.Messaging.Util (($>>=), (<$?>))
|
||||
import Simplex.Messaging.Version
|
||||
import Simplex.Messaging.Version.Internal
|
||||
@@ -206,6 +209,10 @@ instance Encoding ARKEMParams where
|
||||
'A' -> ARKP SRKSAccepted .: RKParamsAccepted <$> smpP <*> smpP
|
||||
_ -> fail "bad ratchet KEM params"
|
||||
|
||||
instance ToField ARKEMParams where toField = toField . Binary . smpEncode
|
||||
|
||||
instance FromField ARKEMParams where fromField = blobFieldDecoder smpDecode
|
||||
|
||||
data E2ERatchetParams (s :: RatchetKEMState) (a :: Algorithm)
|
||||
= E2ERatchetParams VersionE2E (PublicKey a) (PublicKey a) (Maybe (RKEMParams s))
|
||||
deriving (Show)
|
||||
@@ -299,11 +306,13 @@ instance (RatchetKEMStateI s, AlgorithmI a) => StrEncoding (E2ERatchetParamsUri
|
||||
| otherwise = case kem of
|
||||
RKParamsProposed k -> [("kem_key", strEncode k)]
|
||||
RKParamsAccepted ct k -> [("kem_ct", strEncode ct), ("kem_key", strEncode k)]
|
||||
strP = toParamsURI <$?> strP
|
||||
where
|
||||
toParamsURI = \case
|
||||
AE2ERatchetParamsUri _ (E2ERatchetParamsUri vr k1 k2 Nothing) -> Right $ E2ERatchetParamsUri vr k1 k2 Nothing
|
||||
AE2ERatchetParamsUri _ ps -> checkRatchetKEMState ps
|
||||
strP = toE2ERatchetParamsUri <$?> strP
|
||||
{-# INLINE strP #-}
|
||||
|
||||
toE2ERatchetParamsUri :: RatchetKEMStateI s => AE2ERatchetParamsUri a -> Either String (E2ERatchetParamsUri s a)
|
||||
toE2ERatchetParamsUri = \case
|
||||
AE2ERatchetParamsUri _ (E2ERatchetParamsUri vr k1 k2 Nothing) -> Right $ E2ERatchetParamsUri vr k1 k2 Nothing
|
||||
AE2ERatchetParamsUri _ ps -> checkRatchetKEMState ps
|
||||
|
||||
instance AlgorithmI a => StrEncoding (AE2ERatchetParamsUri a) where
|
||||
strEncode (AE2ERatchetParamsUri _ ps) = strEncode ps
|
||||
@@ -333,6 +342,33 @@ instance StrEncoding AnyE2ERatchetParamsUri where
|
||||
Nothing -> ARKP SRKSProposed $ RKParamsProposed k
|
||||
Just ct -> ARKP SRKSAccepted $ RKParamsAccepted ct k
|
||||
|
||||
instance (RatchetKEMStateI s, AlgorithmI a) => Encoding (E2ERatchetParamsUri s a) where
|
||||
smpEncode (E2ERatchetParamsUri vr k1 k2 kem_) = smpEncode (vr, k1, k2, kem_)
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP = toE2ERatchetParamsUri <$?> smpP
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
instance AlgorithmI a => Encoding (AE2ERatchetParamsUri a) where
|
||||
smpEncode (AE2ERatchetParamsUri _ ps) = smpEncode ps
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP = (\(AnyE2ERatchetParamsUri s _ ps) -> AE2ERatchetParamsUri s <$> checkAlgorithm ps) <$?> smpP
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
instance Encoding AnyE2ERatchetParamsUri where
|
||||
smpEncode (AnyE2ERatchetParamsUri _ _ ps) = smpEncode ps
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP = do
|
||||
vr <- smpP @VersionRangeE2E
|
||||
APublicDhKey a k1 <- smpP
|
||||
APublicDhKey a' k2 <- smpP
|
||||
case testEquality a a' of
|
||||
Nothing -> fail "bad e2e params: different key algorithms"
|
||||
Just Refl ->
|
||||
let result = \case
|
||||
Just (ARKP s kem) -> AnyE2ERatchetParamsUri s a $ E2ERatchetParamsUri vr k1 k2 (Just kem)
|
||||
Nothing -> AnyE2ERatchetParamsUri SRKSProposed a $ E2ERatchetParamsUri vr k1 k2 Nothing
|
||||
in result <$> smpP
|
||||
|
||||
type RcvE2ERatchetParams a = E2ERatchetParams 'RKSProposed a
|
||||
|
||||
type SndE2ERatchetParams a = AE2ERatchetParams a
|
||||
@@ -359,7 +395,7 @@ instance Encoding APrivRKEMParams where
|
||||
'A' -> APRKP SRKSAccepted .:. PrivateRKParamsAccepted <$> smpP <*> smpP <*> smpP
|
||||
_ -> fail "bad APrivRKEMParams"
|
||||
|
||||
instance RatchetKEMStateI s => ToField (PrivRKEMParams s) where toField = toField . smpEncode
|
||||
instance RatchetKEMStateI s => ToField (PrivRKEMParams s) where toField = toField . Binary . smpEncode
|
||||
|
||||
instance (Typeable s, RatchetKEMStateI s) => FromField (PrivRKEMParams s) where fromField = blobFieldDecoder smpDecode
|
||||
|
||||
@@ -560,6 +596,7 @@ applySMDiff smks = \case
|
||||
type HeaderKey = Key
|
||||
|
||||
data MessageKey = MessageKey Key IV
|
||||
deriving (Show)
|
||||
|
||||
instance Encoding MessageKey where
|
||||
smpEncode (MessageKey (Key key) (IV iv)) = smpEncode (key, iv)
|
||||
@@ -576,7 +613,7 @@ instance ToJSON RatchetKey where
|
||||
instance FromJSON RatchetKey where
|
||||
parseJSON = fmap RatchetKey . strParseJSON "Key"
|
||||
|
||||
instance ToField MessageKey where toField = toField . smpEncode
|
||||
instance ToField MessageKey where toField = toField . Binary . smpEncode
|
||||
|
||||
instance FromField MessageKey where fromField = blobFieldDecoder smpDecode
|
||||
|
||||
@@ -841,9 +878,13 @@ connPQEncryption = \case
|
||||
IKUsePQ -> PQSupportOn
|
||||
IKNoPQ pq -> pq -- default for creating connection is IKNoPQ PQEncOn
|
||||
|
||||
rcEncrypt :: AlgorithmI a => Ratchet a -> Int -> ByteString -> Maybe PQEncryption -> VersionE2E -> ExceptT CryptoError IO (ByteString, Ratchet a)
|
||||
rcEncrypt Ratchet {rcSnd = Nothing} _ _ _ _ = throwE CERatchetState
|
||||
rcEncrypt rc@Ratchet {rcSnd = Just sr@SndRatchet {rcCKs, rcHKs}, rcDHRs, rcKEM, rcNs, rcPN, rcAD = Str rcAD, rcSupportKEM, rcEnableKEM, rcVersion} paddedMsgLen msg pqEnc_ supportedE2EVersion = do
|
||||
rcCheckCanPad :: Int -> ByteString -> ExceptT CryptoError IO ()
|
||||
rcCheckCanPad paddedMsgLen msg =
|
||||
unless (canPad (B.length msg) paddedMsgLen) $ throwE CryptoLargeMsgError
|
||||
|
||||
rcEncryptHeader :: AlgorithmI a => Ratchet a -> Maybe PQEncryption -> VersionE2E -> ExceptT CryptoError IO (MsgEncryptKey a, Ratchet a)
|
||||
rcEncryptHeader Ratchet {rcSnd = Nothing} _ _ = throwE CERatchetState
|
||||
rcEncryptHeader rc@Ratchet {rcSnd = Just sr@SndRatchet {rcCKs, rcHKs}, rcDHRs, rcKEM, rcNs, rcPN, rcAD = Str rcAD, rcSupportKEM, rcEnableKEM, rcVersion} pqEnc_ supportedE2EVersion = do
|
||||
-- state.CKs, mk = KDF_CK(state.CKs)
|
||||
let (ck', mk, iv, ehIV) = chainKdf rcCKs
|
||||
v = current rcVersion
|
||||
@@ -858,11 +899,15 @@ rcEncrypt rc@Ratchet {rcSnd = Just sr@SndRatchet {rcCKs, rcHKs}, rcDHRs, rcKEM,
|
||||
rcVersion' = rcVersion {maxSupported = maxSupported'}
|
||||
-- enc_header = HENCRYPT(state.HKs, header)
|
||||
(ehAuthTag, ehBody) <- encryptAEAD rcHKs ehIV (paddedHeaderLen v rcSupportKEM') rcAD (msgHeader v maxSupported')
|
||||
-- return enc_header, ENCRYPT(mk, plaintext, CONCAT(AD, enc_header))
|
||||
-- return enc_header
|
||||
let emHeader = smpEncode EncMessageHeader {ehVersion = v, ehBody, ehAuthTag, ehIV}
|
||||
(emAuthTag, emBody) <- encryptAEAD mk iv paddedMsgLen (rcAD <> emHeader) msg
|
||||
let msg' = encodeEncRatchetMessage v EncRatchetMessage {emHeader, emBody, emAuthTag}
|
||||
-- state.Ns += 1
|
||||
msgEncryptKey =
|
||||
MsgEncryptKey
|
||||
{ msgRcVersion = v,
|
||||
msgKey = MessageKey mk iv,
|
||||
msgRcAD = rcAD,
|
||||
msgEncHeader = emHeader
|
||||
}
|
||||
rc' =
|
||||
rc
|
||||
{ rcSnd = Just sr {rcCKs = ck'},
|
||||
@@ -872,7 +917,7 @@ rcEncrypt rc@Ratchet {rcSnd = Just sr@SndRatchet {rcCKs, rcHKs}, rcDHRs, rcKEM,
|
||||
rcVersion = rcVersion',
|
||||
rcKEM = if pqEnc_ == Just PQEncOff then (\rck -> rck {rcKEMs = Nothing}) <$> rcKEM else rcKEM
|
||||
}
|
||||
pure (msg', rc')
|
||||
pure (msgEncryptKey, rc')
|
||||
where
|
||||
-- header = HEADER_PQ2(
|
||||
-- dh = state.DHRs.public,
|
||||
@@ -895,6 +940,23 @@ rcEncrypt rc@Ratchet {rcSnd = Just sr@SndRatchet {rcCKs, rcHKs}, rcDHRs, rcKEM,
|
||||
Nothing -> ARKP SRKSProposed $ RKParamsProposed k
|
||||
Just RatchetKEMAccepted {rcPQRct} -> ARKP SRKSAccepted $ RKParamsAccepted rcPQRct k
|
||||
|
||||
type MsgEncryptKeyX448 = MsgEncryptKey 'X448
|
||||
|
||||
data MsgEncryptKey a = MsgEncryptKey
|
||||
{ msgRcVersion :: VersionE2E,
|
||||
msgKey :: MessageKey,
|
||||
msgRcAD :: ByteString,
|
||||
msgEncHeader :: ByteString
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
rcEncryptMsg :: AlgorithmI a => MsgEncryptKey a -> Int -> ByteString -> ExceptT CryptoError IO ByteString
|
||||
rcEncryptMsg MsgEncryptKey {msgKey = MessageKey mk iv, msgRcAD, msgEncHeader, msgRcVersion = v} paddedMsgLen msg = do
|
||||
-- return ENCRYPT(mk, plaintext, CONCAT(AD, enc_header))
|
||||
(emAuthTag, emBody) <- encryptAEAD mk iv paddedMsgLen (msgRcAD <> msgEncHeader) msg
|
||||
let msg' = encodeEncRatchetMessage v EncRatchetMessage {emHeader = msgEncHeader, emBody, emAuthTag}
|
||||
pure msg'
|
||||
|
||||
data SkippedMessage a
|
||||
= SMMessage (DecryptResult a)
|
||||
| SMHeader (Maybe RatchetStep) (MsgHeader a)
|
||||
@@ -1095,8 +1157,7 @@ chainKdf (RatchetKey ck) =
|
||||
hkdf3 :: ByteString -> ByteString -> ByteString -> (ByteString, ByteString, ByteString)
|
||||
hkdf3 salt ikm info = (s1, s2, s3)
|
||||
where
|
||||
prk = H.extract salt ikm :: H.PRK SHA512
|
||||
out = H.expand prk info 96
|
||||
out = hkdf salt ikm info 96
|
||||
(s1, rest) = B.splitAt 32 out
|
||||
(s2, s3) = B.splitAt 32 rest
|
||||
|
||||
@@ -1120,14 +1181,35 @@ instance AlgorithmI a => ToJSON (Ratchet a) where
|
||||
instance AlgorithmI a => FromJSON (Ratchet a) where
|
||||
parseJSON = $(JQ.mkParseJSON defaultJSON ''Ratchet)
|
||||
|
||||
instance AlgorithmI a => ToField (Ratchet a) where toField = toField . LB.toStrict . J.encode
|
||||
instance AlgorithmI a => ToField (Ratchet a) where toField = toField . Binary . LB.toStrict . J.encode
|
||||
|
||||
instance (AlgorithmI a, Typeable a) => FromField (Ratchet a) where fromField = blobFieldDecoder J.eitherDecodeStrict'
|
||||
|
||||
instance ToField PQEncryption where toField (PQEncryption pqEnc) = toField pqEnc
|
||||
instance ToField PQEncryption where toField (PQEncryption pqEnc) = toField (BI pqEnc)
|
||||
|
||||
instance FromField PQEncryption where fromField f = PQEncryption <$> fromField f
|
||||
instance FromField PQEncryption where
|
||||
#if defined(dbPostgres)
|
||||
fromField f dat = PQEncryption . unBI <$> fromField f dat
|
||||
#else
|
||||
fromField f = PQEncryption . unBI <$> fromField f
|
||||
#endif
|
||||
|
||||
instance ToField PQSupport where toField (PQSupport pqEnc) = toField pqEnc
|
||||
instance ToField PQSupport where toField (PQSupport pqEnc) = toField (BI pqEnc)
|
||||
|
||||
instance FromField PQSupport where fromField f = PQSupport <$> fromField f
|
||||
instance FromField PQSupport where
|
||||
#if defined(dbPostgres)
|
||||
fromField f dat = PQSupport . unBI <$> fromField f dat
|
||||
#else
|
||||
fromField f = PQSupport . unBI <$> fromField f
|
||||
#endif
|
||||
|
||||
instance Encoding (MsgEncryptKey a) where
|
||||
smpEncode MsgEncryptKey {msgRcVersion = v, msgKey, msgRcAD, msgEncHeader} =
|
||||
smpEncode (v, msgRcAD, msgKey, Large msgEncHeader)
|
||||
smpP = do
|
||||
(v, msgRcAD, msgKey, Large msgEncHeader) <- smpP
|
||||
pure MsgEncryptKey {msgRcVersion = v, msgRcAD, msgKey, msgEncHeader}
|
||||
|
||||
instance AlgorithmI a => ToField (MsgEncryptKey a) where toField = toField . Binary . smpEncode
|
||||
|
||||
instance (AlgorithmI a, Typeable a) => FromField (MsgEncryptKey a) where fromField = blobFieldDecoder smpDecode
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Simplex.Messaging.Crypto.SNTRUP761.Bindings where
|
||||
@@ -9,9 +10,8 @@ import Data.Bifunctor (bimap)
|
||||
import Data.ByteArray (ScrubbedBytes)
|
||||
import qualified Data.ByteArray as BA
|
||||
import Data.ByteString (ByteString)
|
||||
import Database.SQLite.Simple.FromField
|
||||
import Database.SQLite.Simple.ToField
|
||||
import Foreign (nullPtr)
|
||||
import Simplex.Messaging.Agent.Store.DB (FromField (..), ToField (..))
|
||||
import Simplex.Messaging.Crypto.SNTRUP761.Bindings.Defines
|
||||
import Simplex.Messaging.Crypto.SNTRUP761.Bindings.FFI
|
||||
import Simplex.Messaging.Crypto.SNTRUP761.Bindings.RNG (withDRG)
|
||||
@@ -121,7 +121,11 @@ instance ToField KEMSharedKey where
|
||||
toField (KEMSharedKey k) = toField (BA.convert k :: ByteString)
|
||||
|
||||
instance FromField KEMSharedKey where
|
||||
#if defined(dbPostgres)
|
||||
fromField f dat = KEMSharedKey . BA.convert @ByteString <$> fromField f dat
|
||||
#else
|
||||
fromField f = KEMSharedKey . BA.convert @ByteString <$> fromField f
|
||||
#endif
|
||||
|
||||
instance ToJSON KEMSharedKey where
|
||||
toJSON = strToJSON
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Simplex.Messaging.Crypto.ShortLink
|
||||
( contactShortLinkKdf,
|
||||
invShortLinkKdf,
|
||||
encodeSignLinkData,
|
||||
encodeSignUserData,
|
||||
encryptLinkData,
|
||||
encryptUserData,
|
||||
decryptLinkData,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Class
|
||||
import Crypto.Random (ChaChaDRG)
|
||||
import Data.Bifunctor (first)
|
||||
import Data.Bitraversable (bimapM)
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as B
|
||||
import Simplex.Messaging.Agent.Client (cryptoError)
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Protocol (EntityId (..), LinkId, EncDataBytes (..), QueueLinkData)
|
||||
import Simplex.Messaging.Util (liftEitherWith)
|
||||
|
||||
fixedDataPaddedLength :: Int
|
||||
fixedDataPaddedLength = 2008 -- 2048 - 24 (nonce) - 16 (auth tag)
|
||||
|
||||
userDataPaddedLength :: Int
|
||||
userDataPaddedLength = 13784 -- 13824 - 24 - 16
|
||||
|
||||
contactShortLinkKdf :: LinkKey -> (LinkId, C.SbKey)
|
||||
contactShortLinkKdf (LinkKey k) =
|
||||
let (lnkId, sbKey) = B.splitAt 24 $ C.hkdf "" k "SimpleXContactLink" 56
|
||||
in (EntityId lnkId, C.unsafeSbKey sbKey)
|
||||
|
||||
invShortLinkKdf :: LinkKey -> C.SbKey
|
||||
invShortLinkKdf (LinkKey k) = C.unsafeSbKey $ C.hkdf "" k "SimpleXInvLink" 32
|
||||
|
||||
encodeSignLinkData :: forall c. ConnectionModeI c => C.KeyPair 'C.Ed25519 -> VersionRangeSMPA -> ConnectionRequestUri c -> ConnInfo -> (LinkKey, (ByteString, ByteString))
|
||||
encodeSignLinkData (rootKey, pk) agentVRange connReq userData =
|
||||
let fd = smpEncode FixedLinkData {agentVRange, rootKey, connReq}
|
||||
md = smpEncode $ connLinkData @c agentVRange userData
|
||||
in (LinkKey (C.sha3_256 fd), (encodeSign pk fd, encodeSign pk md))
|
||||
|
||||
encodeSignUserData :: C.PrivateKeyEd25519 -> VersionRangeSMPA -> ConnInfo -> ByteString
|
||||
encodeSignUserData pk agentVRange userData =
|
||||
encodeSign pk $ smpEncode $ connLinkData @'CMContact agentVRange userData
|
||||
|
||||
connLinkData :: forall c. ConnectionModeI c => VersionRangeSMPA -> ConnInfo -> ConnLinkData c
|
||||
connLinkData agentVRange userData = case sConnectionMode @c of
|
||||
SCMInvitation -> InvitationLinkData agentVRange userData
|
||||
SCMContact -> ContactLinkData {agentVRange, direct = True, owners = [], relays = [], userData}
|
||||
|
||||
encodeSign :: C.PrivateKeyEd25519 -> ByteString -> ByteString
|
||||
encodeSign pk s = smpEncode (C.sign' pk s) <> s
|
||||
|
||||
encryptLinkData :: TVar ChaChaDRG -> C.SbKey -> (ByteString, ByteString) -> ExceptT AgentErrorType IO QueueLinkData
|
||||
encryptLinkData g k = bimapM (encrypt fixedDataPaddedLength) (encrypt userDataPaddedLength)
|
||||
where
|
||||
encrypt len = encryptData g k len
|
||||
|
||||
encryptUserData :: TVar ChaChaDRG -> C.SbKey -> ByteString -> ExceptT AgentErrorType IO EncDataBytes
|
||||
encryptUserData g k s = encryptData g k userDataPaddedLength s
|
||||
|
||||
encryptData :: TVar ChaChaDRG -> C.SbKey -> Int -> ByteString -> ExceptT AgentErrorType IO EncDataBytes
|
||||
encryptData g k len s = do
|
||||
nonce <- liftIO $ atomically $ C.randomCbNonce g
|
||||
ct <- liftEitherWith cryptoError $ C.sbEncrypt k nonce s len
|
||||
pure $ EncDataBytes $ smpEncode nonce <> ct
|
||||
|
||||
decryptLinkData :: forall c. ConnectionModeI c => LinkKey -> C.SbKey -> QueueLinkData -> Either AgentErrorType (ConnectionRequestUri c, ConnLinkData c)
|
||||
decryptLinkData linkKey k (encFD, encMD) = do
|
||||
(sig1, fd) <- decrypt encFD
|
||||
(sig2, md) <- decrypt encMD
|
||||
FixedLinkData {rootKey, connReq} <- decode fd
|
||||
md' <- decode @(ConnLinkData c) md
|
||||
if
|
||||
| LinkKey (C.sha3_256 fd) /= linkKey -> linkErr "link data hash"
|
||||
| not (C.verify' rootKey sig1 fd) -> linkErr "link data signature"
|
||||
| not (C.verify' rootKey sig2 md) -> linkErr "user data signature"
|
||||
| otherwise -> Right (connReq, md')
|
||||
where
|
||||
decrypt (EncDataBytes d) = do
|
||||
(nonce, Tail ct) <- decode d
|
||||
(sig, Tail s) <- decode =<< first cryptoError (C.sbDecrypt k nonce ct)
|
||||
pure (sig, s)
|
||||
decode :: Encoding a => ByteString -> Either AgentErrorType a
|
||||
decode = msgErr . smpDecode
|
||||
msgErr = first (const $ AGENT A_MESSAGE)
|
||||
linkErr = Left . AGENT . A_LINK
|
||||
@@ -143,7 +143,7 @@ instance Encoding Large where
|
||||
instance Encoding SystemTime where
|
||||
smpEncode = smpEncode . systemSeconds
|
||||
{-# INLINE smpEncode #-}
|
||||
smpP = MkSystemTime <$> smpP <*> pure 0
|
||||
smpP = (`MkSystemTime` 0) <$> smpP
|
||||
{-# INLINE smpP #-}
|
||||
|
||||
_smpP :: Encoding a => Parser a
|
||||
|
||||
@@ -140,11 +140,11 @@ instance StrEncoding Int64 where
|
||||
|
||||
instance StrEncoding SystemTime where
|
||||
strEncode = strEncode . systemSeconds
|
||||
strP = MkSystemTime <$> strP <*> pure 0
|
||||
strP = (`MkSystemTime` 0) <$> strP
|
||||
|
||||
instance StrEncoding UTCTime where
|
||||
strEncode = B.pack . iso8601Show
|
||||
strP = maybe (Left "bad UTCTime") Right . iso8601ParseM . B.unpack <$?> A.takeTill (\c -> c == ' ' || c == '\n')
|
||||
strP = maybe (Left "bad UTCTime") Right . iso8601ParseM . B.unpack <$?> A.takeTill (\c -> c == ' ' || c == '\n' || c == ',' || c == ';')
|
||||
|
||||
-- lists encode/parse as comma-separated strings
|
||||
strEncodeList :: StrEncoding a => [a] -> ByteString
|
||||
|
||||
@@ -49,8 +49,9 @@ ntfReplaceToken c pKey tknId token = okNtfCommand (TRPL token) c pKey tknId
|
||||
ntfDeleteToken :: NtfClient -> C.APrivateAuthKey -> NtfTokenId -> ExceptT NtfClientError IO ()
|
||||
ntfDeleteToken = okNtfCommand TDEL
|
||||
|
||||
ntfEnableCron :: NtfClient -> C.APrivateAuthKey -> NtfTokenId -> Word16 -> ExceptT NtfClientError IO ()
|
||||
ntfEnableCron c pKey tknId int = okNtfCommand (TCRN int) c pKey tknId
|
||||
-- set to 0 to disable
|
||||
ntfSetCronInterval :: NtfClient -> C.APrivateAuthKey -> NtfTokenId -> Word16 -> ExceptT NtfClientError IO ()
|
||||
ntfSetCronInterval c pKey tknId int = okNtfCommand (TCRN int) c pKey tknId
|
||||
|
||||
ntfCreateSubscription :: NtfClient -> C.APrivateAuthKey -> NewNtfEntity 'Subscription -> ExceptT NtfClientError IO NtfSubscriptionId
|
||||
ntfCreateSubscription c pKey newSub =
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
module Simplex.Messaging.Notifications.Protocol where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import Control.Applicative (optional, (<|>))
|
||||
import Data.Aeson (FromJSON (..), ToJSON (..), (.:), (.=))
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.Encoding as JE
|
||||
@@ -27,14 +27,12 @@ import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||||
import Data.Time.Clock.System
|
||||
import Data.Type.Equality
|
||||
import Data.Word (Word16)
|
||||
import Database.SQLite.Simple.FromField (FromField (..))
|
||||
import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import Simplex.Messaging.Agent.Protocol (updateSMPServerHosts)
|
||||
import Simplex.Messaging.Agent.Store.DB (FromField (..), ToField (..), fromTextField_)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Notifications.Transport (NTFVersion, ntfClientHandshake)
|
||||
import Simplex.Messaging.Parsers (fromTextField_)
|
||||
import Simplex.Messaging.Notifications.Transport (NTFVersion, invalidReasonNTFVersion, ntfClientHandshake)
|
||||
import Simplex.Messaging.Protocol hiding (Command (..), CommandTag (..))
|
||||
import Simplex.Messaging.Util (eitherToMaybe, (<$?>))
|
||||
|
||||
@@ -297,12 +295,18 @@ data NtfResponse
|
||||
|
||||
instance ProtocolEncoding NTFVersion ErrorType NtfResponse where
|
||||
type Tag NtfResponse = NtfResponseTag
|
||||
encodeProtocol _v = \case
|
||||
encodeProtocol v = \case
|
||||
NRTknId entId dhKey -> e (NRTknId_, ' ', entId, dhKey)
|
||||
NRSubId entId -> e (NRSubId_, ' ', entId)
|
||||
NROk -> e NROk_
|
||||
NRErr err -> e (NRErr_, ' ', err)
|
||||
NRTkn stat -> e (NRTkn_, ' ', stat)
|
||||
NRTkn stat -> e (NRTkn_, ' ', stat')
|
||||
where
|
||||
stat'
|
||||
| v >= invalidReasonNTFVersion = stat
|
||||
| otherwise = case stat of
|
||||
NTInvalid _ -> NTInvalid Nothing
|
||||
_ -> stat
|
||||
NRSub stat -> e (NRSub_, ' ', stat)
|
||||
NRPong -> e NRPong_
|
||||
where
|
||||
@@ -513,7 +517,9 @@ instance Encoding NtfSubStatus where
|
||||
|
||||
instance StrEncoding NtfSubStatus where
|
||||
strEncode = smpEncode
|
||||
{-# INLINE strEncode #-}
|
||||
strP = smpP
|
||||
{-# INLINE strP #-}
|
||||
|
||||
data NtfTknStatus
|
||||
= -- | Token created in DB
|
||||
@@ -521,7 +527,7 @@ data NtfTknStatus
|
||||
| -- | state after registration (TNEW)
|
||||
NTRegistered
|
||||
| -- | if initial notification failed (push provider error) or verification failed
|
||||
NTInvalid
|
||||
NTInvalid (Maybe NTInvalidReason)
|
||||
| -- | Token confirmed via notification (accepted by push provider or verification code received by client)
|
||||
NTConfirmed
|
||||
| -- | after successful verification (TVFY)
|
||||
@@ -530,24 +536,65 @@ data NtfTknStatus
|
||||
NTExpired
|
||||
deriving (Eq, Show)
|
||||
|
||||
allowTokenVerification :: NtfTknStatus -> Bool
|
||||
allowTokenVerification = \case
|
||||
NTNew -> False
|
||||
NTRegistered -> True
|
||||
NTInvalid _ -> False
|
||||
NTConfirmed -> True
|
||||
NTActive -> True
|
||||
NTExpired -> False
|
||||
|
||||
allowNtfSubCommands :: NtfTknStatus -> Bool
|
||||
allowNtfSubCommands = \case
|
||||
NTNew -> False
|
||||
NTRegistered -> False
|
||||
-- TODO we could have separate statuses to show whether it became invalid
|
||||
-- after verification (allow commands) or before (do not allow)
|
||||
NTInvalid _ -> True
|
||||
NTConfirmed -> False
|
||||
NTActive -> True
|
||||
NTExpired -> True
|
||||
|
||||
instance Encoding NtfTknStatus where
|
||||
smpEncode = \case
|
||||
NTNew -> "NEW"
|
||||
NTRegistered -> "REGISTERED"
|
||||
NTInvalid -> "INVALID"
|
||||
NTInvalid r_ -> "INVALID" <> maybe "" (\r -> ',' `B.cons` strEncode r) r_
|
||||
NTConfirmed -> "CONFIRMED"
|
||||
NTActive -> "ACTIVE"
|
||||
NTExpired -> "EXPIRED"
|
||||
smpP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
A.takeTill (\c -> c == ' ' || c == ',') >>= \case
|
||||
"NEW" -> pure NTNew
|
||||
"REGISTERED" -> pure NTRegistered
|
||||
"INVALID" -> pure NTInvalid
|
||||
"INVALID" -> NTInvalid <$> optional (A.char ',' *> strP)
|
||||
"CONFIRMED" -> pure NTConfirmed
|
||||
"ACTIVE" -> pure NTActive
|
||||
"EXPIRED" -> pure NTExpired
|
||||
_ -> fail "bad NtfTknStatus"
|
||||
|
||||
instance StrEncoding NTInvalidReason where
|
||||
strEncode = smpEncode
|
||||
strP = smpP
|
||||
|
||||
data NTInvalidReason = NTIRBadToken | NTIRTokenNotForTopic | NTIRExpiredToken | NTIRUnregistered
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance Encoding NTInvalidReason where
|
||||
smpEncode = \case
|
||||
NTIRBadToken -> "BAD"
|
||||
NTIRTokenNotForTopic -> "TOPIC"
|
||||
NTIRExpiredToken -> "EXPIRED"
|
||||
NTIRUnregistered -> "UNREGISTERED"
|
||||
smpP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"BAD" -> pure NTIRBadToken
|
||||
"TOPIC" -> pure NTIRTokenNotForTopic
|
||||
"EXPIRED" -> pure NTIRExpiredToken
|
||||
"UNREGISTERED" -> pure NTIRUnregistered
|
||||
_ -> fail "bad NTInvalidReason"
|
||||
|
||||
instance StrEncoding NtfTknStatus where
|
||||
strEncode = smpEncode
|
||||
strP = smpP
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
@@ -8,33 +9,39 @@
|
||||
module Simplex.Messaging.Notifications.Server.Env where
|
||||
|
||||
import Control.Concurrent (ThreadId)
|
||||
import Control.Concurrent.Async (Async)
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import Crypto.Random
|
||||
import Data.Int (Int64)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import Data.Time.Clock.System (SystemTime)
|
||||
import Data.Word (Word16)
|
||||
import Data.X509.Validation (Fingerprint (..))
|
||||
import Network.Socket
|
||||
import qualified Network.TLS as T
|
||||
import qualified Network.TLS as TLS
|
||||
import Numeric.Natural
|
||||
import Simplex.Messaging.Client.Agent
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Notifications.Server.Push.APNS
|
||||
import Simplex.Messaging.Notifications.Server.Stats
|
||||
import Simplex.Messaging.Notifications.Server.Store
|
||||
import Simplex.Messaging.Notifications.Server.StoreLog
|
||||
import Simplex.Messaging.Notifications.Server.Store (newNtfSTMStore)
|
||||
import Simplex.Messaging.Notifications.Server.Store.Postgres
|
||||
import Simplex.Messaging.Notifications.Server.Store.Types
|
||||
import Simplex.Messaging.Notifications.Server.StoreLog (readWriteNtfSTMStore)
|
||||
import Simplex.Messaging.Notifications.Transport (NTFVersion, VersionRangeNTF)
|
||||
import Simplex.Messaging.Protocol (BasicAuth, CorrId, SMPServer, Transmission)
|
||||
import Simplex.Messaging.Server.Env.STM (StartOptions (..))
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Server.QueueStore.Postgres.Config (PostgresStoreCfg (..))
|
||||
import Simplex.Messaging.Server.StoreLog (closeStoreLog)
|
||||
import Simplex.Messaging.Session
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport (ATransport, THandleParams, TransportPeer (..))
|
||||
import Simplex.Messaging.Transport.Server (AddHTTP, ServerCredentials, TransportServerConfig, loadFingerprint, loadServerCredential)
|
||||
import System.IO (IOMode (..))
|
||||
import System.Exit (exitFailure)
|
||||
import System.Mem.Weak (Weak)
|
||||
import UnliftIO.STM
|
||||
|
||||
@@ -46,22 +53,25 @@ data NtfServerConfig = NtfServerConfig
|
||||
subIdBytes :: Int,
|
||||
regCodeBytes :: Int,
|
||||
clientQSize :: Natural,
|
||||
subQSize :: Natural,
|
||||
pushQSize :: Natural,
|
||||
smpAgentCfg :: SMPClientAgentConfig,
|
||||
apnsConfig :: APNSPushClientConfig,
|
||||
subsBatchSize :: Int,
|
||||
inactiveClientExpiration :: Maybe ExpirationConfig,
|
||||
storeLogFile :: Maybe FilePath,
|
||||
storeLastNtfsFile :: Maybe FilePath,
|
||||
dbStoreConfig :: PostgresStoreCfg,
|
||||
ntfCredentials :: ServerCredentials,
|
||||
periodicNtfsInterval :: Int, -- seconds
|
||||
-- stats config - see SMP server config
|
||||
logStatsInterval :: Maybe Int64,
|
||||
logStatsStartTime :: Int64,
|
||||
serverStatsLogFile :: FilePath,
|
||||
serverStatsBackupFile :: Maybe FilePath,
|
||||
-- | interval and file to save prometheus metrics
|
||||
prometheusInterval :: Maybe Int,
|
||||
prometheusMetricsFile :: FilePath,
|
||||
ntfServerVRange :: VersionRangeNTF,
|
||||
transportConfig :: TransportServerConfig
|
||||
transportConfig :: TransportServerConfig,
|
||||
startOptions :: StartOptions
|
||||
}
|
||||
|
||||
defaultInactiveClientExpiration :: ExpirationConfig
|
||||
@@ -75,71 +85,65 @@ data NtfEnv = NtfEnv
|
||||
{ config :: NtfServerConfig,
|
||||
subscriber :: NtfSubscriber,
|
||||
pushServer :: NtfPushServer,
|
||||
store :: NtfStore,
|
||||
storeLog :: Maybe (StoreLog 'WriteMode),
|
||||
store :: NtfPostgresStore,
|
||||
random :: TVar ChaChaDRG,
|
||||
tlsServerCreds :: T.Credential,
|
||||
tlsServerCreds :: TLS.Credential,
|
||||
serverIdentity :: C.KeyHash,
|
||||
serverStats :: NtfServerStats
|
||||
}
|
||||
|
||||
newNtfServerEnv :: NtfServerConfig -> IO NtfEnv
|
||||
newNtfServerEnv config@NtfServerConfig {subQSize, pushQSize, smpAgentCfg, apnsConfig, storeLogFile, ntfCredentials} = do
|
||||
newNtfServerEnv config@NtfServerConfig {pushQSize, smpAgentCfg, apnsConfig, dbStoreConfig, ntfCredentials, startOptions} = do
|
||||
when (compactLog startOptions) $ compactDbStoreLog $ dbStoreLogPath dbStoreConfig
|
||||
random <- C.newRandom
|
||||
store <- newNtfStore
|
||||
logInfo "restoring subscriptions..."
|
||||
storeLog <- mapM (`readWriteNtfStore` store) storeLogFile
|
||||
logInfo "restored subscriptions"
|
||||
subscriber <- newNtfSubscriber subQSize smpAgentCfg random
|
||||
store <- newNtfDbStore dbStoreConfig
|
||||
subscriber <- newNtfSubscriber smpAgentCfg random
|
||||
pushServer <- newNtfPushServer pushQSize apnsConfig
|
||||
tlsServerCreds <- loadServerCredential ntfCredentials
|
||||
Fingerprint fp <- loadFingerprint ntfCredentials
|
||||
serverStats <- newNtfServerStats =<< getCurrentTime
|
||||
pure NtfEnv {config, subscriber, pushServer, store, storeLog, random, tlsServerCreds, serverIdentity = C.KeyHash fp, serverStats}
|
||||
pure NtfEnv {config, subscriber, pushServer, store, random, tlsServerCreds, serverIdentity = C.KeyHash fp, serverStats}
|
||||
where
|
||||
compactDbStoreLog = \case
|
||||
Just f -> do
|
||||
logNote $ "compacting store log " <> T.pack f
|
||||
newNtfSTMStore >>= readWriteNtfSTMStore False f >>= closeStoreLog
|
||||
Nothing -> do
|
||||
logError "Error: `--compact-log` used without `enable: on` option in STORE_LOG section of INI file"
|
||||
exitFailure
|
||||
|
||||
data NtfSubscriber = NtfSubscriber
|
||||
{ smpSubscribers :: TMap SMPServer SMPSubscriber,
|
||||
newSubQ :: TBQueue [NtfEntityRec 'Subscription],
|
||||
{ smpSubscribers :: TMap SMPServer SMPSubscriberVar,
|
||||
subscriberSeq :: TVar Int,
|
||||
smpAgent :: SMPClientAgent
|
||||
}
|
||||
|
||||
newNtfSubscriber :: Natural -> SMPClientAgentConfig -> TVar ChaChaDRG -> IO NtfSubscriber
|
||||
newNtfSubscriber qSize smpAgentCfg random = do
|
||||
type SMPSubscriberVar = SessionVar SMPSubscriber
|
||||
|
||||
newNtfSubscriber :: SMPClientAgentConfig -> TVar ChaChaDRG -> IO NtfSubscriber
|
||||
newNtfSubscriber smpAgentCfg random = do
|
||||
smpSubscribers <- TM.emptyIO
|
||||
newSubQ <- newTBQueueIO qSize
|
||||
subscriberSeq <- newTVarIO 0
|
||||
smpAgent <- newSMPClientAgent smpAgentCfg random
|
||||
pure NtfSubscriber {smpSubscribers, newSubQ, smpAgent}
|
||||
pure NtfSubscriber {smpSubscribers, subscriberSeq, smpAgent}
|
||||
|
||||
data SMPSubscriber = SMPSubscriber
|
||||
{ newSubQ :: TQueue (NonEmpty (NtfEntityRec 'Subscription)),
|
||||
subThreadId :: TVar (Maybe (Weak ThreadId))
|
||||
{ smpServer :: SMPServer,
|
||||
subscriberSubQ :: TQueue (NonEmpty ServerNtfSub),
|
||||
subThreadId :: Weak ThreadId
|
||||
}
|
||||
|
||||
newSMPSubscriber :: IO SMPSubscriber
|
||||
newSMPSubscriber = do
|
||||
newSubQ <- newTQueueIO
|
||||
subThreadId <- newTVarIO Nothing
|
||||
pure SMPSubscriber {newSubQ, subThreadId}
|
||||
|
||||
data NtfPushServer = NtfPushServer
|
||||
{ pushQ :: TBQueue (NtfTknData, PushNotification),
|
||||
{ pushQ :: TBQueue (NtfTknRec, PushNotification),
|
||||
pushClients :: TMap PushProvider PushProviderClient,
|
||||
intervalNotifiers :: TMap NtfTokenId IntervalNotifier,
|
||||
apnsConfig :: APNSPushClientConfig
|
||||
}
|
||||
|
||||
data IntervalNotifier = IntervalNotifier
|
||||
{ action :: Async (),
|
||||
token :: NtfTknData,
|
||||
interval :: Word16
|
||||
}
|
||||
|
||||
newNtfPushServer :: Natural -> APNSPushClientConfig -> IO NtfPushServer
|
||||
newNtfPushServer qSize apnsConfig = do
|
||||
pushQ <- newTBQueueIO qSize
|
||||
pushClients <- TM.emptyIO
|
||||
intervalNotifiers <- TM.emptyIO
|
||||
pure NtfPushServer {pushQ, pushClients, intervalNotifiers, apnsConfig}
|
||||
pure NtfPushServer {pushQ, pushClients, apnsConfig}
|
||||
|
||||
newPushClient :: NtfPushServer -> PushProvider -> IO PushProviderClient
|
||||
newPushClient NtfPushServer {apnsConfig, pushClients} pp = do
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{-# LANGUAGE ApplicativeDo #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
@@ -9,30 +10,48 @@
|
||||
|
||||
module Simplex.Messaging.Notifications.Server.Main where
|
||||
|
||||
import Control.Logger.Simple (setLogLevel)
|
||||
import Control.Monad ((<$!>))
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Functor (($>))
|
||||
import Data.Ini (lookupValue, readIniFile)
|
||||
import Data.Int (Int64)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Set (Set)
|
||||
import qualified Data.Set as S
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import qualified Data.Text.IO as T
|
||||
import Network.Socket (HostName)
|
||||
import Network.Socket (HostName, ServiceName)
|
||||
import Options.Applicative
|
||||
import Simplex.Messaging.Agent.Store.Postgres (checkSchemaExists)
|
||||
import Simplex.Messaging.Agent.Store.Postgres.Options (DBOpts (..))
|
||||
import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..))
|
||||
import Simplex.Messaging.Client (HostMode (..), NetworkConfig (..), ProtocolClientConfig (..), SocksMode (..), defaultNetworkConfig, textToHostMode)
|
||||
import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Notifications.Server (runNtfServer)
|
||||
import Simplex.Messaging.Notifications.Protocol (NtfTokenId)
|
||||
import Simplex.Messaging.Notifications.Server (runNtfServer, restoreServerLastNtfs)
|
||||
import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..), defaultInactiveClientExpiration)
|
||||
import Simplex.Messaging.Notifications.Server.Push.APNS (defaultAPNSPushClientConfig)
|
||||
import Simplex.Messaging.Notifications.Server.Store (newNtfSTMStore)
|
||||
import Simplex.Messaging.Notifications.Server.Store.Postgres (exportNtfDbStore, importNtfSTMStore, newNtfDbStore)
|
||||
import Simplex.Messaging.Notifications.Server.StoreLog (readWriteNtfSTMStore)
|
||||
import Simplex.Messaging.Notifications.Transport (supportedServerNTFVRange)
|
||||
import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), pattern NtfServer)
|
||||
import Simplex.Messaging.Server.CLI
|
||||
import Simplex.Messaging.Server.Env.STM (StartOptions (..))
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Transport (simplexMQVersion)
|
||||
import Simplex.Messaging.Server.Main (strParse)
|
||||
import Simplex.Messaging.Server.Main.Init (iniDbOpts)
|
||||
import Simplex.Messaging.Server.QueueStore.Postgres.Config (PostgresStoreCfg (..))
|
||||
import Simplex.Messaging.Server.StoreLog (closeStoreLog)
|
||||
import Simplex.Messaging.Transport (ATransport, simplexMQVersion)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
import Simplex.Messaging.Transport.Server (ServerCredentials (..), TransportServerConfig (..), defaultTransportServerConfig)
|
||||
import Simplex.Messaging.Util (tshow)
|
||||
import System.Directory (createDirectoryIfMissing, doesFileExist)
|
||||
import Simplex.Messaging.Transport.Server (AddHTTP, ServerCredentials (..), TransportServerConfig (..), defaultTransportServerConfig)
|
||||
import Simplex.Messaging.Util (eitherToMaybe, ifM, tshow)
|
||||
import System.Directory (createDirectoryIfMissing, doesFileExist, renameFile)
|
||||
import System.Exit (exitFailure)
|
||||
import System.FilePath (combine)
|
||||
import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
|
||||
import Text.Read (readMaybe)
|
||||
@@ -44,14 +63,8 @@ ntfServerCLI cfgPath logPath =
|
||||
doesFileExist iniFile >>= \case
|
||||
True -> exitError $ "Error: server is already initialized (" <> iniFile <> " exists).\nRun `" <> executableName <> " start`."
|
||||
_ -> initializeServer opts
|
||||
OnlineCert certOpts ->
|
||||
doesFileExist iniFile >>= \case
|
||||
True -> genOnline cfgPath certOpts
|
||||
_ -> exitError $ "Error: server is not initialized (" <> iniFile <> " does not exist).\nRun `" <> executableName <> " init`."
|
||||
Start ->
|
||||
doesFileExist iniFile >>= \case
|
||||
True -> readIniFile iniFile >>= either exitError runServer
|
||||
_ -> exitError $ "Error: server is not initialized (" <> iniFile <> " does not exist).\nRun `" <> executableName <> " init`."
|
||||
OnlineCert certOpts -> withIniFile $ \_ -> genOnline cfgPath certOpts
|
||||
Start opts -> withIniFile $ runServer opts
|
||||
Delete -> do
|
||||
confirmOrExit
|
||||
"WARNING: deleting the server will make all queues inaccessible, because the server identity (certificate fingerprint) will change.\nTHIS CANNOT BE UNDONE!"
|
||||
@@ -59,13 +72,75 @@ ntfServerCLI cfgPath logPath =
|
||||
deleteDirIfExists cfgPath
|
||||
deleteDirIfExists logPath
|
||||
putStrLn "Deleted configuration and log files"
|
||||
Database cmd dbOpts@DBOpts {connstr, schema} -> withIniFile $ \ini -> do
|
||||
schemaExists <- checkSchemaExists connstr schema
|
||||
storeLogExists <- doesFileExist storeLogFilePath
|
||||
lastNtfsExists <- doesFileExist defaultLastNtfsFile
|
||||
case cmd of
|
||||
SCImport skipTokens
|
||||
| schemaExists && (storeLogExists || lastNtfsExists) -> exitConfigureNtfStore connstr schema
|
||||
| schemaExists -> do
|
||||
putStrLn $ "Schema " <> B.unpack schema <> " already exists in PostrgreSQL database: " <> B.unpack connstr
|
||||
exitFailure
|
||||
| not storeLogExists -> do
|
||||
putStrLn $ storeLogFilePath <> " file does not exist."
|
||||
exitFailure
|
||||
| not lastNtfsExists -> do
|
||||
putStrLn $ defaultLastNtfsFile <> " file does not exist."
|
||||
exitFailure
|
||||
| otherwise -> do
|
||||
storeLogFile <- getRequiredStoreLogFile ini
|
||||
confirmOrExit
|
||||
("WARNING: store log file " <> storeLogFile <> " will be compacted and imported to PostrgreSQL database: " <> B.unpack connstr <> ", schema: " <> B.unpack schema)
|
||||
"Notification server store not imported"
|
||||
stmStore <- newNtfSTMStore
|
||||
sl <- readWriteNtfSTMStore True storeLogFile stmStore
|
||||
closeStoreLog sl
|
||||
restoreServerLastNtfs stmStore defaultLastNtfsFile
|
||||
let storeCfg = PostgresStoreCfg {dbOpts = dbOpts {createSchema = True}, dbStoreLogPath = Nothing, confirmMigrations = MCConsole, deletedTTL = iniDeletedTTL ini}
|
||||
ps <- newNtfDbStore storeCfg
|
||||
(tCnt, sCnt, nCnt) <- importNtfSTMStore ps stmStore skipTokens
|
||||
renameFile storeLogFile $ storeLogFile <> ".bak"
|
||||
putStrLn $ "Import completed: " <> show tCnt <> " tokens, " <> show sCnt <> " subscriptions, " <> show nCnt <> " last token notifications."
|
||||
putStrLn "Configure database options in INI file."
|
||||
SCExport
|
||||
| schemaExists && storeLogExists -> exitConfigureNtfStore connstr schema
|
||||
| not schemaExists -> do
|
||||
putStrLn $ "Schema " <> B.unpack schema <> " does not exist in PostrgreSQL database: " <> B.unpack connstr
|
||||
exitFailure
|
||||
| storeLogExists -> do
|
||||
putStrLn $ storeLogFilePath <> " file already exists."
|
||||
exitFailure
|
||||
| lastNtfsExists -> do
|
||||
putStrLn $ defaultLastNtfsFile <> " file already exists."
|
||||
exitFailure
|
||||
| otherwise -> do
|
||||
confirmOrExit
|
||||
("WARNING: PostrgreSQL database schema " <> B.unpack schema <> " (database: " <> B.unpack connstr <> ") will be exported to store log file " <> storeLogFilePath)
|
||||
"Notification server store not imported"
|
||||
let storeCfg = PostgresStoreCfg {dbOpts, dbStoreLogPath = Just storeLogFilePath, confirmMigrations = MCConsole, deletedTTL = iniDeletedTTL ini}
|
||||
st <- newNtfDbStore storeCfg
|
||||
(tCnt, sCnt, nCnt) <- exportNtfDbStore st defaultLastNtfsFile
|
||||
putStrLn $ "Export completed: " <> show tCnt <> " tokens, " <> show sCnt <> " subscriptions, " <> show nCnt <> " last token notifications."
|
||||
where
|
||||
withIniFile a =
|
||||
doesFileExist iniFile >>= \case
|
||||
True -> readIniFile iniFile >>= either exitError a
|
||||
_ -> exitError $ "Error: server is not initialized (" <> iniFile <> " does not exist).\nRun `" <> executableName <> " init`."
|
||||
getRequiredStoreLogFile ini = do
|
||||
case enableStoreLog' ini $> storeLogFilePath of
|
||||
Just storeLogFile -> do
|
||||
ifM
|
||||
(doesFileExist storeLogFile)
|
||||
(pure storeLogFile)
|
||||
(putStrLn ("Store log file " <> storeLogFile <> " not found") >> exitFailure)
|
||||
Nothing -> putStrLn "Store log disabled, see `[STORE_LOG] enable`" >> exitFailure
|
||||
iniFile = combine cfgPath "ntf-server.ini"
|
||||
serverVersion = "SMP notifications server v" <> simplexMQVersion
|
||||
defaultServerPort = "443"
|
||||
executableName = "ntf-server"
|
||||
storeLogFilePath = combine logPath "ntf-server-store.log"
|
||||
initializeServer InitOptions {enableStoreLog, signAlgorithm, ip, fqdn} = do
|
||||
initializeServer InitOptions {enableStoreLog, dbOptions, signAlgorithm, ip, fqdn} = do
|
||||
clearDirIfExists cfgPath
|
||||
clearDirIfExists logPath
|
||||
createDirectoryIfMissing True cfgPath
|
||||
@@ -87,9 +162,10 @@ ntfServerCLI cfgPath logPath =
|
||||
\# and restoring it when the server is started.\n\
|
||||
\# Log is compacted on start (deleted objects are removed).\n"
|
||||
<> ("enable: " <> onOff enableStoreLog <> "\n\n")
|
||||
<> "# Last notifications are optionally saved and restored when the server restarts,\n\
|
||||
\# they are preserved in the .bak file until the next restart.\n"
|
||||
<> ("restore_last_notifications: " <> onOff enableStoreLog <> "\n\n")
|
||||
<> "# Database connection settings for PostgreSQL database.\n"
|
||||
<> iniDbOpts dbOptions defaultNtfDBOpts
|
||||
<> "Time to retain deleted entities in the database, days.\n"
|
||||
<> ("# db_deleted_ttl: " <> tshow defaultDeletedTTL <> "\n\n")
|
||||
<> "log_stats: off\n\n\
|
||||
\[AUTH]\n\
|
||||
\# control_port_admin_password:\n\
|
||||
@@ -124,26 +200,30 @@ ntfServerCLI cfgPath logPath =
|
||||
\disconnect: off\n"
|
||||
<> ("# ttl: " <> tshow (ttl defaultInactiveClientExpiration) <> "\n")
|
||||
<> ("# check_interval: " <> tshow (checkInterval defaultInactiveClientExpiration) <> "\n")
|
||||
runServer ini = do
|
||||
enableStoreLog' = settingIsOn "STORE_LOG" "enable"
|
||||
runServer startOptions ini = do
|
||||
setLogLevel $ logLevel startOptions
|
||||
hSetBuffering stdout LineBuffering
|
||||
hSetBuffering stderr LineBuffering
|
||||
fp <- checkSavedFingerprint cfgPath defaultX509Config
|
||||
let host = either (const "<hostnames>") T.unpack $ lookupValue "TRANSPORT" "host" ini
|
||||
port = T.unpack $ strictIni "TRANSPORT" "port" ini
|
||||
cfg@NtfServerConfig {transports, storeLogFile} = serverConfig
|
||||
cfg@NtfServerConfig {transports} = serverConfig
|
||||
srv = ProtoServerWithAuth (NtfServer [THDomainName host] (if port == "443" then "" else port) (C.KeyHash fp)) Nothing
|
||||
printServiceInfo serverVersion srv
|
||||
printServerConfig transports storeLogFile
|
||||
printNtfServerConfig transports dbStoreConfig
|
||||
runNtfServer cfg
|
||||
where
|
||||
enableStoreLog = settingIsOn "STORE_LOG" "enable" ini
|
||||
logStats = settingIsOn "STORE_LOG" "log_stats" ini
|
||||
c = combine cfgPath . ($ defaultX509Config)
|
||||
restoreLastNtfsFile path = case iniOnOff "STORE_LOG" "restore_last_notifications" ini of
|
||||
Just True -> Just path
|
||||
Just False -> Nothing
|
||||
-- if the setting is not set, it is enabled when store log is enabled
|
||||
_ -> enableStoreLog $> path
|
||||
dbStoreLogPath = enableStoreLog' ini $> storeLogFilePath
|
||||
dbStoreConfig =
|
||||
PostgresStoreCfg
|
||||
{ dbOpts = iniDBOptions ini defaultNtfDBOpts,
|
||||
dbStoreLogPath,
|
||||
confirmMigrations = MCYesUp,
|
||||
deletedTTL = iniDeletedTTL ini
|
||||
}
|
||||
serverConfig =
|
||||
NtfServerConfig
|
||||
{ transports = iniTransports ini,
|
||||
@@ -153,8 +233,7 @@ ntfServerCLI cfgPath logPath =
|
||||
subIdBytes = 24,
|
||||
regCodeBytes = 32,
|
||||
clientQSize = 64,
|
||||
subQSize = 512,
|
||||
pushQSize = 1048,
|
||||
pushQSize = 32768,
|
||||
smpAgentCfg =
|
||||
defaultSMPClientAgentConfig
|
||||
{ smpCfg =
|
||||
@@ -179,57 +258,108 @@ ntfServerCLI cfgPath logPath =
|
||||
{ ttl = readStrictIni "INACTIVE_CLIENTS" "ttl" ini,
|
||||
checkInterval = readStrictIni "INACTIVE_CLIENTS" "check_interval" ini
|
||||
},
|
||||
storeLogFile = enableStoreLog $> storeLogFilePath,
|
||||
storeLastNtfsFile = restoreLastNtfsFile $ combine logPath "ntf-server-last-notifications.log",
|
||||
dbStoreConfig,
|
||||
ntfCredentials =
|
||||
ServerCredentials
|
||||
{ caCertificateFile = Just $ c caCrtFile,
|
||||
privateKeyFile = c serverKeyFile,
|
||||
certificateFile = c serverCrtFile
|
||||
},
|
||||
periodicNtfsInterval = 5 * 60, -- 5 minutes
|
||||
logStatsInterval = logStats $> 86400, -- seconds
|
||||
logStatsStartTime = 0, -- seconds from 00:00 UTC
|
||||
serverStatsLogFile = combine logPath "ntf-server-stats.daily.log",
|
||||
serverStatsBackupFile = logStats $> combine logPath "ntf-server-stats.log",
|
||||
prometheusInterval = eitherToMaybe $ read . T.unpack <$> lookupValue "STORE_LOG" "prometheus_interval" ini,
|
||||
prometheusMetricsFile = combine logPath "ntf-server-metrics.txt",
|
||||
ntfServerVRange = supportedServerNTFVRange,
|
||||
transportConfig =
|
||||
defaultTransportServerConfig
|
||||
{ logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini
|
||||
}
|
||||
},
|
||||
startOptions
|
||||
}
|
||||
iniDeletedTTL ini = readIniDefault (86400 * defaultDeletedTTL) "STORE_LOG" "db_deleted_ttl" ini
|
||||
defaultLastNtfsFile = combine logPath "ntf-server-last-notifications.log"
|
||||
exitConfigureNtfStore connstr schema = do
|
||||
putStrLn $ "Error: both " <> storeLogFilePath <> " file and " <> B.unpack schema <> " schema are present (database: " <> B.unpack connstr <> ")."
|
||||
putStrLn "Configure notification server storage."
|
||||
exitFailure
|
||||
|
||||
printNtfServerConfig :: [(ServiceName, ATransport, AddHTTP)] -> PostgresStoreCfg -> IO ()
|
||||
printNtfServerConfig transports PostgresStoreCfg {dbOpts = DBOpts {connstr, schema}, dbStoreLogPath} = do
|
||||
B.putStrLn $ "PostgreSQL database: " <> connstr <> ", schema: " <> schema
|
||||
printServerConfig "NTF" transports dbStoreLogPath
|
||||
|
||||
data CliCommand
|
||||
= Init InitOptions
|
||||
| OnlineCert CertOptions
|
||||
| Start
|
||||
| Start StartOptions
|
||||
| Delete
|
||||
| Database StoreCmd DBOpts
|
||||
|
||||
data StoreCmd = SCImport (Set NtfTokenId) | SCExport
|
||||
|
||||
data InitOptions = InitOptions
|
||||
{ enableStoreLog :: Bool,
|
||||
dbOptions :: DBOpts,
|
||||
signAlgorithm :: SignAlgorithm,
|
||||
ip :: HostName,
|
||||
fqdn :: Maybe HostName
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
defaultNtfDBOpts :: DBOpts
|
||||
defaultNtfDBOpts =
|
||||
DBOpts
|
||||
{ connstr = "postgresql://ntf@/ntf_server_store",
|
||||
schema = "ntf_server",
|
||||
poolSize = 10,
|
||||
createSchema = False
|
||||
}
|
||||
|
||||
-- time to retain deleted tokens and subscriptions in the database (days), for debugging
|
||||
defaultDeletedTTL :: Int64
|
||||
defaultDeletedTTL = 21
|
||||
|
||||
cliCommandP :: FilePath -> FilePath -> FilePath -> Parser CliCommand
|
||||
cliCommandP cfgPath logPath iniFile =
|
||||
hsubparser
|
||||
( command "init" (info (Init <$> initP) (progDesc $ "Initialize server - creates " <> cfgPath <> " and " <> logPath <> " directories and configuration files"))
|
||||
<> command "cert" (info (OnlineCert <$> certOptionsP) (progDesc $ "Generate new online TLS server credentials (configuration: " <> iniFile <> ")"))
|
||||
<> command "start" (info (pure Start) (progDesc $ "Start server (configuration: " <> iniFile <> ")"))
|
||||
<> command "start" (info (Start <$> startOptionsP) (progDesc $ "Start server (configuration: " <> iniFile <> ")"))
|
||||
<> command "delete" (info (pure Delete) (progDesc "Delete configuration and log files"))
|
||||
<> command "database" (info (Database <$> databaseCmdP <*> dbOptsP defaultNtfDBOpts) (progDesc "Import/export notifications server store to/from PostgreSQL database"))
|
||||
)
|
||||
where
|
||||
initP :: Parser InitOptions
|
||||
initP =
|
||||
InitOptions
|
||||
<$> switch
|
||||
( long "store-log"
|
||||
<> short 'l'
|
||||
<> help "Enable store log for persistence"
|
||||
databaseCmdP =
|
||||
hsubparser
|
||||
( command "import" (info (SCImport <$> skipTokensP) (progDesc $ "Import store logs into a new PostgreSQL database schema"))
|
||||
<> command "export" (info (pure SCExport) (progDesc $ "Export PostgreSQL database schema to store logs"))
|
||||
)
|
||||
skipTokensP :: Parser (Set NtfTokenId)
|
||||
skipTokensP =
|
||||
option
|
||||
strParse
|
||||
( long "skip-tokens"
|
||||
<> help "Skip tokens during import"
|
||||
<> value S.empty
|
||||
)
|
||||
<*> option
|
||||
initP :: Parser InitOptions
|
||||
initP = do
|
||||
enableStoreLog <-
|
||||
flag' False
|
||||
( long "disable-store-log"
|
||||
<> help "Disable store log for persistence (enabled by default)"
|
||||
)
|
||||
<|> flag True True
|
||||
( long "store-log"
|
||||
<> short 'l'
|
||||
<> help "Enable store log for persistence (DEPRECATED, enabled by default)"
|
||||
)
|
||||
dbOptions <- dbOptsP defaultNtfDBOpts
|
||||
signAlgorithm <-
|
||||
option
|
||||
(maybeReader readMaybe)
|
||||
( long "sign-algorithm"
|
||||
<> short 'a'
|
||||
@@ -238,7 +368,8 @@ cliCommandP cfgPath logPath iniFile =
|
||||
<> showDefault
|
||||
<> metavar "ALG"
|
||||
)
|
||||
<*> strOption
|
||||
ip <-
|
||||
strOption
|
||||
( long "ip"
|
||||
<> help
|
||||
"Server IP address, used as Common Name for TLS online certificate if FQDN is not supplied"
|
||||
@@ -246,10 +377,12 @@ cliCommandP cfgPath logPath iniFile =
|
||||
<> showDefault
|
||||
<> metavar "IP"
|
||||
)
|
||||
<*> (optional . strOption)
|
||||
fqdn <-
|
||||
(optional . strOption)
|
||||
( long "fqdn"
|
||||
<> short 'n'
|
||||
<> help "Server FQDN used as Common Name for TLS online certificate"
|
||||
<> showDefault
|
||||
<> metavar "FQDN"
|
||||
)
|
||||
pure InitOptions {enableStoreLog, dbOptions, signAlgorithm, ip, fqdn}
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# OPTIONS_GHC -fno-warn-unrecognised-pragmas #-}
|
||||
|
||||
module Simplex.Messaging.Notifications.Server.Prometheus where
|
||||
|
||||
import Data.Int (Int64)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock (UTCTime (..), diffUTCTime)
|
||||
import Data.Time.Clock.System (systemEpochDay)
|
||||
import Data.Time.Format.ISO8601 (iso8601Show)
|
||||
import Numeric.Natural (Natural)
|
||||
import Simplex.Messaging.Notifications.Server.Stats
|
||||
import Simplex.Messaging.Server.Stats (PeriodStatCounts (..))
|
||||
import Simplex.Messaging.Transport (simplexMQVersion)
|
||||
|
||||
data NtfServerMetrics = NtfServerMetrics
|
||||
{ statsData :: NtfServerStatsData,
|
||||
activeTokensCounts :: PeriodStatCounts,
|
||||
activeSubsCounts :: PeriodStatCounts,
|
||||
tokenCount :: Int64,
|
||||
approxSubCount :: Int64,
|
||||
lastNtfCount :: Int64,
|
||||
rtsOptions :: Text
|
||||
}
|
||||
|
||||
rtsOptionsEnv :: Text
|
||||
rtsOptionsEnv = "NTF_RTS_OPTIONS"
|
||||
|
||||
data NtfRealTimeMetrics = NtfRealTimeMetrics
|
||||
{ threadsCount :: Int,
|
||||
srvSubscribers :: NtfSMPWorkerMetrics,
|
||||
srvClients :: NtfSMPWorkerMetrics,
|
||||
srvSubWorkers :: NtfSMPWorkerMetrics,
|
||||
ntfActiveSubs :: NtfSMPSubMetrics,
|
||||
ntfPendingSubs :: NtfSMPSubMetrics,
|
||||
smpSessionCount :: Int,
|
||||
apnsPushQLength :: Natural
|
||||
}
|
||||
|
||||
data NtfSMPWorkerMetrics = NtfSMPWorkerMetrics {ownServers :: [Text], otherServers :: Int}
|
||||
|
||||
data NtfSMPSubMetrics = NtfSMPSubMetrics {ownSrvSubs :: M.Map Text Int, otherServers :: Int, otherSrvSubCount :: Int}
|
||||
|
||||
{-# FOURMOLU_DISABLE\n#-}
|
||||
ntfPrometheusMetrics :: NtfServerMetrics -> NtfRealTimeMetrics -> UTCTime -> Text
|
||||
ntfPrometheusMetrics sm rtm ts =
|
||||
time <> tokens <> subscriptions <> notifications <> info
|
||||
where
|
||||
NtfServerMetrics {statsData, activeTokensCounts = psTkns, activeSubsCounts = psSubs, tokenCount, approxSubCount, lastNtfCount, rtsOptions} = sm
|
||||
NtfRealTimeMetrics
|
||||
{ threadsCount,
|
||||
srvSubscribers,
|
||||
srvClients,
|
||||
srvSubWorkers,
|
||||
ntfActiveSubs,
|
||||
ntfPendingSubs,
|
||||
smpSessionCount,
|
||||
apnsPushQLength
|
||||
} = rtm
|
||||
NtfServerStatsData
|
||||
{ _fromTime,
|
||||
_tknCreated,
|
||||
_tknVerified,
|
||||
_tknDeleted,
|
||||
_tknReplaced,
|
||||
_subCreated,
|
||||
_subDeleted,
|
||||
_ntfReceived,
|
||||
_ntfDelivered,
|
||||
_ntfFailed,
|
||||
_ntfCronDelivered,
|
||||
_ntfCronFailed,
|
||||
_ntfVrfQueued,
|
||||
_ntfVrfDelivered,
|
||||
_ntfVrfFailed,
|
||||
_ntfVrfInvalidTkn
|
||||
} = statsData
|
||||
time =
|
||||
"# Recorded at: " <> T.pack (iso8601Show ts) <> "\n\
|
||||
\# Stats from: " <> T.pack (iso8601Show _fromTime) <> "\n\
|
||||
\\n"
|
||||
tokens =
|
||||
"# Tokens\n\
|
||||
\# ------\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_tokens_created Created tokens\n\
|
||||
\# TYPE simplex_ntf_tokens_created counter\n\
|
||||
\simplex_ntf_tokens_created " <> mshow _tknCreated <> "\n# tknCreated\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_tokens_verified Verified tokens\n\
|
||||
\# TYPE simplex_ntf_tokens_verified counter\n\
|
||||
\simplex_ntf_tokens_verified " <> mshow _tknVerified <> "\n# tknVerified\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_tokens_deleted Deleted tokens\n\
|
||||
\# TYPE simplex_ntf_tokens_deleted counter\n\
|
||||
\simplex_ntf_tokens_deleted " <> mshow _tknDeleted <> "\n# tknDeleted\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_tokens_replaced Deleted tokens\n\
|
||||
\# TYPE simplex_ntf_tokens_replaced counter\n\
|
||||
\simplex_ntf_tokens_replaced " <> mshow _tknReplaced <> "\n# tknReplaced\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_tokens_count_daily Daily active tokens\n\
|
||||
\# TYPE simplex_ntf_tokens_count_daily gauge\n\
|
||||
\simplex_ntf_tokens_count_daily " <> mstr (dayCount psTkns) <> "\n# dayCountTkn\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_tokens_count_weekly Weekly active tokens\n\
|
||||
\# TYPE simplex_ntf_tokens_count_weekly gauge\n\
|
||||
\simplex_ntf_tokens_count_weekly " <> mstr (weekCount psTkns) <> "\n# weekCountTkn\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_tokens_count_monthly Monthly active tokens\n\
|
||||
\# TYPE simplex_ntf_tokens_count_monthly gauge\n\
|
||||
\simplex_ntf_tokens_count_monthly " <> mstr (monthCount psTkns) <> "\n# monthCountTkn\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_tokens_total Total number of tokens stored.\n\
|
||||
\# TYPE simplex_ntf_tokens_total gauge\n\
|
||||
\simplex_ntf_tokens_total " <> mshow tokenCount <> "\n# tokenCount\n\
|
||||
\\n"
|
||||
subscriptions =
|
||||
"# Subscriptions\n\
|
||||
\# -------------\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_subscriptions_created Created subscriptions\n\
|
||||
\# TYPE simplex_ntf_subscriptions_created counter\n\
|
||||
\simplex_ntf_subscriptions_created " <> mshow _subCreated <> "\n# subCreated\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_subscriptions_deleted Deleted subscriptions\n\
|
||||
\# TYPE simplex_ntf_subscriptions_deleted counter\n\
|
||||
\simplex_ntf_subscriptions_deleted " <> mshow _subDeleted <> "\n# subDeleted\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_subscriptions_count_daily Daily subscriptions count\n\
|
||||
\# TYPE simplex_ntf_subscriptions_count_daily gauge\n\
|
||||
\simplex_ntf_subscriptions_count_daily " <> mstr (dayCount psSubs) <> "\n# dayCountSub\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_subscriptions_count_weekly Weekly subscriptions count\n\
|
||||
\# TYPE simplex_ntf_subscriptions_count_weekly gauge\n\
|
||||
\simplex_ntf_subscriptions_count_weekly " <> mstr (weekCount psSubs) <> "\n# weekCountSub\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_subscriptions_count_monthly Monthly subscriptions count\n\
|
||||
\# TYPE simplex_ntf_subscriptions_count_monthly gauge\n\
|
||||
\simplex_ntf_subscriptions_count_monthly " <> mstr (monthCount psSubs) <> "\n# monthCountSub\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_subscriptions_approx_total Approximate total number of subscriptions stored.\n\
|
||||
\# TYPE simplex_ntf_subscriptions_approx_total gauge\n\
|
||||
\simplex_ntf_subscriptions_approx_total " <> mshow approxSubCount <> "\n# approxSubCount\n\
|
||||
\\n"
|
||||
<> showSubMetric ntfActiveSubs "simplex_ntf_smp_subscription_active_" "Active"
|
||||
<> showSubMetric ntfPendingSubs "simplex_ntf_smp_subscription_pending_" "Pending"
|
||||
notifications =
|
||||
"# Notifications\n\
|
||||
\# -------------\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_notifications_received Received notifications\n\
|
||||
\# TYPE simplex_ntf_notifications_received counter\n\
|
||||
\simplex_ntf_notifications_received " <> mshow _ntfReceived <> "\n# ntfReceived\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_notifications_delivered Delivered notifications\n\
|
||||
\# TYPE simplex_ntf_notifications_delivered counter\n\
|
||||
\simplex_ntf_notifications_delivered " <> mshow _ntfDelivered <> "\n# ntfDelivered\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_notifications_failed Failed notifications\n\
|
||||
\# TYPE simplex_ntf_notifications_failed counter\n\
|
||||
\simplex_ntf_notifications_failed " <> mshow _ntfFailed <> "\n# ntfFailed\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_notifications_periodic_delivered Delivered periodic notifications\n\
|
||||
\# TYPE simplex_ntf_notifications_periodic_delivered counter\n\
|
||||
\simplex_ntf_notifications_periodic_delivered " <> mshow _ntfCronDelivered <> "\n# ntfCronDelivered\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_notifications_periodic_failed Failed periodic notifications\n\
|
||||
\# TYPE simplex_ntf_notifications_periodic_failed counter\n\
|
||||
\simplex_ntf_notifications_periodic_failed " <> mshow _ntfCronFailed <> "\n# ntfCronFailed\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_notifications_verification_queued Token verifications queued\n\
|
||||
\# TYPE simplex_ntf_notifications_verification_queued counter\n\
|
||||
\simplex_ntf_notifications_verification_queued " <> mshow _ntfVrfQueued <> "\n# ntfVrfQueued\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_notifications_verification_delivered Delivered token verifications\n\
|
||||
\# TYPE simplex_ntf_notifications_verification_delivered counter\n\
|
||||
\simplex_ntf_notifications_verification_delivered " <> mshow _ntfVrfDelivered <> "\n# ntfVrfDelivered\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_notifications_verification_failed Failed token verification deliveries\n\
|
||||
\# TYPE simplex_ntf_notifications_verification_failed counter\n\
|
||||
\simplex_ntf_notifications_verification_failed " <> mshow _ntfVrfFailed <> "\n# ntfVrfFailed\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_notifications_verification_invalid_tkn Invalid token errors while delivering verifications\n\
|
||||
\# TYPE simplex_ntf_notifications_verification_invalid_tkn counter\n\
|
||||
\simplex_ntf_notifications_verification_invalid_tkn " <> mshow _ntfVrfInvalidTkn <> "\n# ntfVrfInvalidTkn\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_notifications_total Total number of last notifications stored.\n\
|
||||
\# TYPE simplex_ntf_notifications_total gauge\n\
|
||||
\simplex_ntf_notifications_total " <> mshow lastNtfCount <> "\n# lastNtfCount\n\
|
||||
\\n"
|
||||
info =
|
||||
"# Info\n\
|
||||
\# ----\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_info Server information. RTS options have to be passed via " <> rtsOptionsEnv <> " env var\n\
|
||||
\# TYPE simplex_ntf_info gauge\n\
|
||||
\simplex_ntf_info{version=\"" <> T.pack simplexMQVersion <> "\",rts_options=\"" <> rtsOptions <> "\"} 1\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_threads_total Thread count\n\
|
||||
\# TYPE simplex_ntf_threads_total gauge\n\
|
||||
\simplex_ntf_threads_total " <> mshow threadsCount <> "\n# threadsCount\n\
|
||||
\\n"
|
||||
<> showWorkerMetric srvSubscribers "simplex_ntf_smp_subscribers_" "SMP subcscribers"
|
||||
<> showWorkerMetric srvClients "simplex_ntf_smp_agent_clients_" "SMP agent clients"
|
||||
<> showWorkerMetric srvSubWorkers "simplex_ntf_smp_agent_sub_workers_" "SMP agent subscription workers"
|
||||
<> "# HELP simplex_ntf_smp_sessions_count SMP sessions count\n\
|
||||
\# TYPE simplex_ntf_smp_sessions_count gauge\n\
|
||||
\simplex_ntf_smp_sessions_count " <> mshow smpSessionCount <> "\n# smpSessionCount\n\
|
||||
\\n\
|
||||
\# HELP simplex_ntf_apns_push_queue_length Count of notifications in push queue\n\
|
||||
\# TYPE simplex_ntf_apns_push_queue_length gauge\n\
|
||||
\simplex_ntf_apns_push_queue_length " <> mshow apnsPushQLength <> "\n# apnsPushQLength\n\
|
||||
\\n"
|
||||
showSubMetric NtfSMPSubMetrics {ownSrvSubs, otherServers, otherSrvSubCount} mPfx descrPfx =
|
||||
showOwnSrvSubs <> showOtherSrvSubs
|
||||
where
|
||||
showOwnSrvSubs
|
||||
| M.null ownSrvSubs = showOwn_ "" 0 0
|
||||
| otherwise = T.concat $ map (\(host, cnt) -> showOwn_ (metricHost host) 1 cnt) $ M.assocs ownSrvSubs
|
||||
showOwn_ param srvCnt subCnt =
|
||||
gaugeMetric (mPfx <> "server_count_own") param srvCnt (descrPfx <> " SMP subscriptions, own server count") "ownSrvSubs server"
|
||||
<> gaugeMetric (mPfx <> "sub_count_own") param subCnt (descrPfx <> " SMP subscriptions count for own servers") "ownSrvSubs count"
|
||||
showOtherSrvSubs =
|
||||
gaugeMetric (mPfx <> "server_count_other") "" otherServers (descrPfx <> " SMP subscriptions, other server count") "otherServers"
|
||||
<> gaugeMetric (mPfx <> "sub_count_other") "" otherSrvSubCount (descrPfx <> " SMP subscriptions count for other servers") "otherSrvSubCount"
|
||||
showWorkerMetric NtfSMPWorkerMetrics {ownServers, otherServers} mPfx descrPfx =
|
||||
showOwnServers <> showOtherServers
|
||||
where
|
||||
showOwnServers
|
||||
| null ownServers = showOwn_ "" 0
|
||||
| otherwise = T.concat $ map (\host -> showOwn_ (metricHost host) 1) ownServers
|
||||
showOwn_ param cnt = gaugeMetric (mPfx <> "count_own") param cnt (descrPfx <> " count for own servers") "ownServers"
|
||||
showOtherServers = gaugeMetric (mPfx <> "count_other") "" otherServers (descrPfx <> " count for other servers") "otherServers"
|
||||
gaugeMetric :: Text -> Text -> Int -> Text -> Text -> Text
|
||||
gaugeMetric name param value descr codeRef =
|
||||
"# HELP " <> name <> " " <> descr <> "\n\
|
||||
\# TYPE " <> name <> " gauge\n\
|
||||
\" <> name <> param <> " " <> mshow value <> "\n# " <> codeRef <> "\n\
|
||||
\\n"
|
||||
metricHost host = "{server=\"" <> host <> "\"}"
|
||||
mstr a = T.pack a <> " " <> tsEpoch
|
||||
mshow :: Show a => a -> Text
|
||||
mshow = mstr . show
|
||||
tsEpoch = T.pack $ show @Int64 $ floor @Double $ realToFrac (ts `diffUTCTime` epoch) * 1000
|
||||
epoch = UTCTime systemEpochDay 0
|
||||
{-# FOURMOLU_ENABLE\n#-}
|
||||
@@ -33,15 +33,19 @@ import qualified Data.ByteString.Base64.URL as U
|
||||
import Data.ByteString.Builder (lazyByteString)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import Data.Int (Int64)
|
||||
import Data.List (find)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import Data.Map.Strict (Map)
|
||||
import Data.Maybe (isNothing)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Time.Clock.System
|
||||
import qualified Data.X509 as X
|
||||
import qualified Data.X509.CertificateStore as XS
|
||||
import Network.HPACK.Token as HT
|
||||
import Network.HTTP.Types (Status)
|
||||
import qualified Network.HTTP.Types as N
|
||||
import Network.HTTP2.Client (Request)
|
||||
@@ -50,7 +54,7 @@ import Network.Socket (HostName, ServiceName)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Notifications.Server.Push.APNS.Internal
|
||||
import Simplex.Messaging.Notifications.Server.Store (NtfTknData (..))
|
||||
import Simplex.Messaging.Notifications.Server.Store.Types (NtfTknRec (..))
|
||||
import Simplex.Messaging.Parsers (defaultJSON)
|
||||
import Simplex.Messaging.Transport.HTTP2 (HTTP2Body (..))
|
||||
import Simplex.Messaging.Transport.HTTP2.Client
|
||||
@@ -263,8 +267,8 @@ disconnectApnsHTTP2Client APNSPushClient {https2Client} =
|
||||
ntfCategoryCheckMessage :: Text
|
||||
ntfCategoryCheckMessage = "NTF_CAT_CHECK_MESSAGE"
|
||||
|
||||
apnsNotification :: NtfTknData -> C.CbNonce -> Int -> PushNotification -> Either C.CryptoError APNSNotification
|
||||
apnsNotification NtfTknData {tknDhSecret} nonce paddedLen = \case
|
||||
apnsNotification :: NtfTknRec -> C.CbNonce -> Int -> PushNotification -> Either C.CryptoError APNSNotification
|
||||
apnsNotification NtfTknRec {tknDhSecret} nonce paddedLen = \case
|
||||
PNVerification (NtfRegCode code) ->
|
||||
encrypt code $ \code' ->
|
||||
apn APNSBackground {contentAvailable = 1} . Just $ J.object ["nonce" .= nonce, "verification" .= code']
|
||||
@@ -308,12 +312,12 @@ data PushProviderError
|
||||
= PPConnection HTTP2ClientError
|
||||
| PPCryptoError C.CryptoError
|
||||
| PPResponseError (Maybe Status) Text
|
||||
| PPTokenInvalid
|
||||
| PPTokenInvalid NTInvalidReason
|
||||
| PPRetryLater
|
||||
| PPPermanentError
|
||||
deriving (Show, Exception)
|
||||
|
||||
type PushProviderClient = NtfTknData -> PushNotification -> ExceptT PushProviderError IO ()
|
||||
type PushProviderClient = NtfTknRec -> PushNotification -> ExceptT PushProviderError IO ()
|
||||
|
||||
-- this is not a newtype on purpose to have a correct JSON encoding as a record
|
||||
data APNSErrorResponse = APNSErrorResponse {reason :: Text}
|
||||
@@ -321,7 +325,7 @@ data APNSErrorResponse = APNSErrorResponse {reason :: Text}
|
||||
$(JQ.deriveFromJSON defaultJSON ''APNSErrorResponse)
|
||||
|
||||
apnsPushProviderClient :: APNSPushClient -> PushProviderClient
|
||||
apnsPushProviderClient c@APNSPushClient {nonceDrg, apnsCfg} tkn@NtfTknData {token = DeviceToken _ tknStr} pn = do
|
||||
apnsPushProviderClient c@APNSPushClient {nonceDrg, apnsCfg} tkn@NtfTknRec {token = DeviceToken _ tknStr} pn = do
|
||||
http2 <- liftHTTPS2 $ getApnsHTTP2Client c
|
||||
nonce <- atomically $ C.randomCbNonce nonceDrg
|
||||
apnsNtf <- liftEither $ first PPCryptoError $ apnsNotification tkn nonce (paddedNtfLength apnsCfg) pn
|
||||
@@ -330,26 +334,34 @@ apnsPushProviderClient c@APNSPushClient {nonceDrg, apnsCfg} tkn@NtfTknData {toke
|
||||
HTTP2Response {response, respBody = HTTP2Body {bodyHead}} <- liftHTTPS2 $ sendRequest http2 req Nothing
|
||||
let status = H.responseStatus response
|
||||
reason' = maybe "" reason $ J.decodeStrict' bodyHead
|
||||
logDebug $ "APNS response: " <> T.pack (show status) <> " " <> reason'
|
||||
if status == Just N.ok200
|
||||
then logDebug $ "APNS response: ok" <> apnsIds response
|
||||
else logWarn $ "APNS error: " <> T.pack (show status) <> " " <> reason' <> apnsIds response
|
||||
result status reason'
|
||||
where
|
||||
apnsIds response = headerStr "apns-id" <> headerStr "apns-unique-id"
|
||||
where
|
||||
headerStr name =
|
||||
maybe "" (\(_, v) -> ", " <> name <> ": " <> safeDecodeUtf8 v) $
|
||||
find (\(t, _) -> HT.tokenKey t == CI.mk (encodeUtf8 name)) (fst (H.responseHeaders response))
|
||||
result :: Maybe Status -> Text -> ExceptT PushProviderError IO ()
|
||||
result status reason'
|
||||
| status == Just N.ok200 = pure ()
|
||||
| status == Just N.badRequest400 =
|
||||
case reason' of
|
||||
"BadDeviceToken" -> throwE PPTokenInvalid
|
||||
"DeviceTokenNotForTopic" -> throwE PPTokenInvalid
|
||||
"TopicDisallowed" -> throwE PPPermanentError
|
||||
_ -> err status reason'
|
||||
| status == Just N.forbidden403 = case reason' of
|
||||
"ExpiredProviderToken" -> throwE PPPermanentError -- there should be no point retrying it as the token was refreshed
|
||||
"InvalidProviderToken" -> throwE PPPermanentError
|
||||
_ -> err status reason'
|
||||
| status == Just N.gone410 = throwE PPTokenInvalid
|
||||
throwE $ case reason' of
|
||||
"BadDeviceToken" -> PPTokenInvalid NTIRBadToken
|
||||
"DeviceTokenNotForTopic" -> PPTokenInvalid NTIRTokenNotForTopic
|
||||
"TopicDisallowed" -> PPPermanentError
|
||||
_ -> PPResponseError status reason'
|
||||
| status == Just N.forbidden403 = throwE $ case reason' of
|
||||
"ExpiredProviderToken" -> PPPermanentError -- there should be no point retrying it as the token was refreshed
|
||||
"InvalidProviderToken" -> PPPermanentError
|
||||
_ -> PPResponseError status reason'
|
||||
| status == Just N.gone410 = throwE $ case reason' of
|
||||
"ExpiredToken" -> PPTokenInvalid NTIRExpiredToken
|
||||
"Unregistered" -> PPTokenInvalid NTIRUnregistered
|
||||
_ -> PPRetryLater
|
||||
| status == Just N.serviceUnavailable503 = liftIO (disconnectApnsHTTP2Client c) >> throwE PPRetryLater
|
||||
-- Just tooManyRequests429 -> TooManyRequests - too many requests for the same token
|
||||
| otherwise = err status reason'
|
||||
err :: Maybe Status -> Text -> ExceptT PushProviderError IO ()
|
||||
err s r = throwE $ PPResponseError s r
|
||||
| otherwise = throwE $ PPResponseError status reason'
|
||||
liftHTTPS2 a = ExceptT $ first PPConnection <$> a
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
module Simplex.Messaging.Notifications.Server.Stats where
|
||||
|
||||
import Control.Applicative (optional)
|
||||
import Control.Applicative (optional, (<|>))
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.IORef
|
||||
@@ -17,10 +17,18 @@ data NtfServerStats = NtfServerStats
|
||||
tknCreated :: IORef Int,
|
||||
tknVerified :: IORef Int,
|
||||
tknDeleted :: IORef Int,
|
||||
tknReplaced :: IORef Int,
|
||||
subCreated :: IORef Int,
|
||||
subDeleted :: IORef Int,
|
||||
ntfReceived :: IORef Int,
|
||||
ntfDelivered :: IORef Int,
|
||||
ntfFailed :: IORef Int,
|
||||
ntfCronDelivered :: IORef Int,
|
||||
ntfCronFailed :: IORef Int,
|
||||
ntfVrfQueued :: IORef Int,
|
||||
ntfVrfDelivered :: IORef Int,
|
||||
ntfVrfFailed :: IORef Int,
|
||||
ntfVrfInvalidTkn :: IORef Int,
|
||||
activeTokens :: PeriodStats,
|
||||
activeSubs :: PeriodStats
|
||||
}
|
||||
@@ -30,10 +38,18 @@ data NtfServerStatsData = NtfServerStatsData
|
||||
_tknCreated :: Int,
|
||||
_tknVerified :: Int,
|
||||
_tknDeleted :: Int,
|
||||
_tknReplaced :: Int,
|
||||
_subCreated :: Int,
|
||||
_subDeleted :: Int,
|
||||
_ntfReceived :: Int,
|
||||
_ntfDelivered :: Int,
|
||||
_ntfFailed :: Int,
|
||||
_ntfCronDelivered :: Int,
|
||||
_ntfCronFailed :: Int,
|
||||
_ntfVrfQueued :: Int,
|
||||
_ntfVrfDelivered :: Int,
|
||||
_ntfVrfFailed :: Int,
|
||||
_ntfVrfInvalidTkn :: Int,
|
||||
_activeTokens :: PeriodStatsData,
|
||||
_activeSubs :: PeriodStatsData
|
||||
}
|
||||
@@ -44,13 +60,41 @@ newNtfServerStats ts = do
|
||||
tknCreated <- newIORef 0
|
||||
tknVerified <- newIORef 0
|
||||
tknDeleted <- newIORef 0
|
||||
tknReplaced <- newIORef 0
|
||||
subCreated <- newIORef 0
|
||||
subDeleted <- newIORef 0
|
||||
ntfReceived <- newIORef 0
|
||||
ntfDelivered <- newIORef 0
|
||||
ntfFailed <- newIORef 0
|
||||
ntfCronDelivered <- newIORef 0
|
||||
ntfCronFailed <- newIORef 0
|
||||
ntfVrfQueued <- newIORef 0
|
||||
ntfVrfDelivered <- newIORef 0
|
||||
ntfVrfFailed <- newIORef 0
|
||||
ntfVrfInvalidTkn <- newIORef 0
|
||||
activeTokens <- newPeriodStats
|
||||
activeSubs <- newPeriodStats
|
||||
pure NtfServerStats {fromTime, tknCreated, tknVerified, tknDeleted, subCreated, subDeleted, ntfReceived, ntfDelivered, activeTokens, activeSubs}
|
||||
pure
|
||||
NtfServerStats
|
||||
{ fromTime,
|
||||
tknCreated,
|
||||
tknVerified,
|
||||
tknDeleted,
|
||||
tknReplaced,
|
||||
subCreated,
|
||||
subDeleted,
|
||||
ntfReceived,
|
||||
ntfDelivered,
|
||||
ntfFailed,
|
||||
ntfCronDelivered,
|
||||
ntfCronFailed,
|
||||
ntfVrfQueued,
|
||||
ntfVrfDelivered,
|
||||
ntfVrfFailed,
|
||||
ntfVrfInvalidTkn,
|
||||
activeTokens,
|
||||
activeSubs
|
||||
}
|
||||
|
||||
getNtfServerStatsData :: NtfServerStats -> IO NtfServerStatsData
|
||||
getNtfServerStatsData s@NtfServerStats {fromTime} = do
|
||||
@@ -58,13 +102,41 @@ getNtfServerStatsData s@NtfServerStats {fromTime} = do
|
||||
_tknCreated <- readIORef $ tknCreated s
|
||||
_tknVerified <- readIORef $ tknVerified s
|
||||
_tknDeleted <- readIORef $ tknDeleted s
|
||||
_tknReplaced <- readIORef $ tknReplaced s
|
||||
_subCreated <- readIORef $ subCreated s
|
||||
_subDeleted <- readIORef $ subDeleted s
|
||||
_ntfReceived <- readIORef $ ntfReceived s
|
||||
_ntfDelivered <- readIORef $ ntfDelivered s
|
||||
_ntfFailed <- readIORef $ ntfFailed s
|
||||
_ntfCronDelivered <- readIORef $ ntfCronDelivered s
|
||||
_ntfCronFailed <- readIORef $ ntfCronFailed s
|
||||
_ntfVrfQueued <- readIORef $ ntfVrfQueued s
|
||||
_ntfVrfDelivered <- readIORef $ ntfVrfDelivered s
|
||||
_ntfVrfFailed <- readIORef $ ntfVrfFailed s
|
||||
_ntfVrfInvalidTkn <- readIORef $ ntfVrfInvalidTkn s
|
||||
_activeTokens <- getPeriodStatsData $ activeTokens s
|
||||
_activeSubs <- getPeriodStatsData $ activeSubs s
|
||||
pure NtfServerStatsData {_fromTime, _tknCreated, _tknVerified, _tknDeleted, _subCreated, _subDeleted, _ntfReceived, _ntfDelivered, _activeTokens, _activeSubs}
|
||||
pure
|
||||
NtfServerStatsData
|
||||
{ _fromTime,
|
||||
_tknCreated,
|
||||
_tknVerified,
|
||||
_tknDeleted,
|
||||
_tknReplaced,
|
||||
_subCreated,
|
||||
_subDeleted,
|
||||
_ntfReceived,
|
||||
_ntfDelivered,
|
||||
_ntfFailed,
|
||||
_ntfCronDelivered,
|
||||
_ntfCronFailed,
|
||||
_ntfVrfQueued,
|
||||
_ntfVrfDelivered,
|
||||
_ntfVrfFailed,
|
||||
_ntfVrfInvalidTkn,
|
||||
_activeTokens,
|
||||
_activeSubs
|
||||
}
|
||||
|
||||
-- this function is not thread safe, it is used on server start only
|
||||
setNtfServerStats :: NtfServerStats -> NtfServerStatsData -> IO ()
|
||||
@@ -73,24 +145,60 @@ setNtfServerStats s@NtfServerStats {fromTime} d@NtfServerStatsData {_fromTime} =
|
||||
writeIORef (tknCreated s) $! _tknCreated d
|
||||
writeIORef (tknVerified s) $! _tknVerified d
|
||||
writeIORef (tknDeleted s) $! _tknDeleted d
|
||||
writeIORef (tknReplaced s) $! _tknReplaced d
|
||||
writeIORef (subCreated s) $! _subCreated d
|
||||
writeIORef (subDeleted s) $! _subDeleted d
|
||||
writeIORef (ntfReceived s) $! _ntfReceived d
|
||||
writeIORef (ntfDelivered s) $! _ntfDelivered d
|
||||
writeIORef (ntfFailed s) $! _ntfFailed d
|
||||
writeIORef (ntfCronDelivered s) $! _ntfCronDelivered d
|
||||
writeIORef (ntfCronFailed s) $! _ntfCronFailed d
|
||||
writeIORef (ntfVrfQueued s) $! _ntfVrfQueued d
|
||||
writeIORef (ntfVrfDelivered s) $! _ntfVrfDelivered d
|
||||
writeIORef (ntfVrfFailed s) $! _ntfVrfFailed d
|
||||
writeIORef (ntfVrfInvalidTkn s) $! _ntfVrfInvalidTkn d
|
||||
setPeriodStats (activeTokens s) (_activeTokens d)
|
||||
setPeriodStats (activeSubs s) (_activeSubs d)
|
||||
|
||||
instance StrEncoding NtfServerStatsData where
|
||||
strEncode NtfServerStatsData {_fromTime, _tknCreated, _tknVerified, _tknDeleted, _subCreated, _subDeleted, _ntfReceived, _ntfDelivered, _activeTokens, _activeSubs} =
|
||||
strEncode
|
||||
NtfServerStatsData
|
||||
{ _fromTime,
|
||||
_tknCreated,
|
||||
_tknVerified,
|
||||
_tknDeleted,
|
||||
_tknReplaced,
|
||||
_subCreated,
|
||||
_subDeleted,
|
||||
_ntfReceived,
|
||||
_ntfDelivered,
|
||||
_ntfFailed,
|
||||
_ntfCronDelivered,
|
||||
_ntfCronFailed,
|
||||
_ntfVrfQueued,
|
||||
_ntfVrfDelivered,
|
||||
_ntfVrfFailed,
|
||||
_ntfVrfInvalidTkn,
|
||||
_activeTokens,
|
||||
_activeSubs
|
||||
} =
|
||||
B.unlines
|
||||
[ "fromTime=" <> strEncode _fromTime,
|
||||
"tknCreated=" <> strEncode _tknCreated,
|
||||
"tknVerified=" <> strEncode _tknVerified,
|
||||
"tknDeleted=" <> strEncode _tknDeleted,
|
||||
"tknReplaced=" <> strEncode _tknReplaced,
|
||||
"subCreated=" <> strEncode _subCreated,
|
||||
"subDeleted=" <> strEncode _subDeleted,
|
||||
"ntfReceived=" <> strEncode _ntfReceived,
|
||||
"ntfDelivered=" <> strEncode _ntfDelivered,
|
||||
"ntfFailed=" <> strEncode _ntfFailed,
|
||||
"ntfCronDelivered=" <> strEncode _ntfCronDelivered,
|
||||
"ntfCronFailed=" <> strEncode _ntfCronFailed,
|
||||
"ntfVrfQueued=" <> strEncode _ntfVrfQueued,
|
||||
"ntfVrfDelivered=" <> strEncode _ntfVrfDelivered,
|
||||
"ntfVrfFailed=" <> strEncode _ntfVrfFailed,
|
||||
"ntfVrfInvalidTkn=" <> strEncode _ntfVrfInvalidTkn,
|
||||
"activeTokens:",
|
||||
strEncode _activeTokens,
|
||||
"activeSubs:",
|
||||
@@ -101,12 +209,42 @@ instance StrEncoding NtfServerStatsData where
|
||||
_tknCreated <- "tknCreated=" *> strP <* A.endOfLine
|
||||
_tknVerified <- "tknVerified=" *> strP <* A.endOfLine
|
||||
_tknDeleted <- "tknDeleted=" *> strP <* A.endOfLine
|
||||
_tknReplaced <- opt "tknReplaced="
|
||||
_subCreated <- "subCreated=" *> strP <* A.endOfLine
|
||||
_subDeleted <- "subDeleted=" *> strP <* A.endOfLine
|
||||
_ntfReceived <- "ntfReceived=" *> strP <* A.endOfLine
|
||||
_ntfDelivered <- "ntfDelivered=" *> strP <* A.endOfLine
|
||||
_ntfFailed <- opt "ntfFailed="
|
||||
_ntfCronDelivered <- opt "ntfCronDelivered="
|
||||
_ntfCronFailed <- opt "ntfCronFailed="
|
||||
_ntfVrfQueued <- opt "ntfVrfQueued="
|
||||
_ntfVrfDelivered <- opt "ntfVrfDelivered="
|
||||
_ntfVrfFailed <- opt "ntfVrfFailed="
|
||||
_ntfVrfInvalidTkn <- opt "ntfVrfInvalidTkn="
|
||||
_ <- "activeTokens:" <* A.endOfLine
|
||||
_activeTokens <- strP <* A.endOfLine
|
||||
_ <- "activeSubs:" <* A.endOfLine
|
||||
_activeSubs <- strP <* optional A.endOfLine
|
||||
pure NtfServerStatsData {_fromTime, _tknCreated, _tknVerified, _tknDeleted, _subCreated, _subDeleted, _ntfReceived, _ntfDelivered, _activeTokens, _activeSubs}
|
||||
pure
|
||||
NtfServerStatsData
|
||||
{ _fromTime,
|
||||
_tknCreated,
|
||||
_tknVerified,
|
||||
_tknDeleted,
|
||||
_tknReplaced,
|
||||
_subCreated,
|
||||
_subDeleted,
|
||||
_ntfReceived,
|
||||
_ntfDelivered,
|
||||
_ntfFailed,
|
||||
_ntfCronDelivered,
|
||||
_ntfCronFailed,
|
||||
_ntfVrfQueued,
|
||||
_ntfVrfDelivered,
|
||||
_ntfVrfFailed,
|
||||
_ntfVrfInvalidTkn,
|
||||
_activeTokens,
|
||||
_activeSubs
|
||||
}
|
||||
where
|
||||
opt s = A.string s *> strP <* A.endOfLine <|> pure 0
|
||||
|
||||
@@ -25,11 +25,12 @@ import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Notifications.Protocol
|
||||
import Simplex.Messaging.Protocol (NtfPrivateAuthKey, NtfPublicAuthKey, SMPServer)
|
||||
import Simplex.Messaging.Server.QueueStore (RoundedSystemTime)
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Util (whenM, ($>>=))
|
||||
|
||||
data NtfStore = NtfStore
|
||||
data NtfSTMStore = NtfSTMStore
|
||||
{ tokens :: TMap NtfTokenId NtfTknData,
|
||||
-- multiple registrations exist to protect from malicious registrations if token is compromised
|
||||
tokenRegistrations :: TMap DeviceToken (TMap ByteString NtfTokenId),
|
||||
@@ -39,15 +40,15 @@ data NtfStore = NtfStore
|
||||
tokenLastNtfs :: TMap NtfTokenId (TVar (NonEmpty PNMessageData))
|
||||
}
|
||||
|
||||
newNtfStore :: IO NtfStore
|
||||
newNtfStore = do
|
||||
newNtfSTMStore :: IO NtfSTMStore
|
||||
newNtfSTMStore = do
|
||||
tokens <- TM.emptyIO
|
||||
tokenRegistrations <- TM.emptyIO
|
||||
subscriptions <- TM.emptyIO
|
||||
tokenSubscriptions <- TM.emptyIO
|
||||
subscriptionLookup <- TM.emptyIO
|
||||
tokenLastNtfs <- TM.emptyIO
|
||||
pure NtfStore {tokens, tokenRegistrations, subscriptions, tokenSubscriptions, subscriptionLookup, tokenLastNtfs}
|
||||
pure NtfSTMStore {tokens, tokenRegistrations, subscriptions, tokenSubscriptions, subscriptionLookup, tokenLastNtfs}
|
||||
|
||||
data NtfTknData = NtfTknData
|
||||
{ ntfTknId :: NtfTokenId,
|
||||
@@ -57,14 +58,16 @@ data NtfTknData = NtfTknData
|
||||
tknDhKeys :: C.KeyPair 'C.X25519,
|
||||
tknDhSecret :: C.DhSecretX25519,
|
||||
tknRegCode :: NtfRegCode,
|
||||
tknCronInterval :: TVar Word16
|
||||
tknCronInterval :: TVar Word16,
|
||||
tknUpdatedAt :: TVar (Maybe RoundedSystemTime)
|
||||
}
|
||||
|
||||
mkNtfTknData :: NtfTokenId -> NewNtfEntity 'Token -> C.KeyPair 'C.X25519 -> C.DhSecretX25519 -> NtfRegCode -> STM NtfTknData
|
||||
mkNtfTknData ntfTknId (NewNtfTkn token tknVerifyKey _) tknDhKeys tknDhSecret tknRegCode = do
|
||||
tknStatus <- newTVar NTRegistered
|
||||
tknCronInterval <- newTVar 0
|
||||
pure NtfTknData {ntfTknId, token, tknStatus, tknVerifyKey, tknDhKeys, tknDhSecret, tknRegCode, tknCronInterval}
|
||||
mkNtfTknData :: NtfTokenId -> NewNtfEntity 'Token -> C.KeyPair 'C.X25519 -> C.DhSecretX25519 -> NtfRegCode -> RoundedSystemTime -> IO NtfTknData
|
||||
mkNtfTknData ntfTknId (NewNtfTkn token tknVerifyKey _) tknDhKeys tknDhSecret tknRegCode ts = do
|
||||
tknStatus <- newTVarIO NTRegistered
|
||||
tknCronInterval <- newTVarIO 0
|
||||
tknUpdatedAt <- newTVarIO $ Just ts
|
||||
pure NtfTknData {ntfTknId, token, tknStatus, tknVerifyKey, tknDhKeys, tknDhSecret, tknRegCode, tknCronInterval, tknUpdatedAt}
|
||||
|
||||
data NtfSubData = NtfSubData
|
||||
{ ntfSubId :: NtfSubscriptionId,
|
||||
@@ -77,18 +80,11 @@ data NtfSubData = NtfSubData
|
||||
ntfSubServer :: NtfSubData -> SMPServer
|
||||
ntfSubServer NtfSubData {smpQueue = SMPQueueNtf {smpServer}} = smpServer
|
||||
|
||||
data NtfEntityRec (e :: NtfEntity) where
|
||||
NtfTkn :: NtfTknData -> NtfEntityRec 'Token
|
||||
NtfSub :: NtfSubData -> NtfEntityRec 'Subscription
|
||||
stmGetNtfTokenIO :: NtfSTMStore -> NtfTokenId -> IO (Maybe NtfTknData)
|
||||
stmGetNtfTokenIO st tknId = TM.lookupIO tknId (tokens st)
|
||||
|
||||
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
|
||||
stmAddNtfToken :: NtfSTMStore -> NtfTokenId -> NtfTknData -> STM ()
|
||||
stmAddNtfToken st tknId tkn@NtfTknData {token, tknVerifyKey} = do
|
||||
TM.insert tknId tkn $ tokens st
|
||||
TM.lookup token regs >>= \case
|
||||
Just tIds -> TM.insert regKey tknId tIds
|
||||
@@ -99,16 +95,8 @@ addNtfToken st tknId tkn@NtfTknData {token, tknVerifyKey} = do
|
||||
regs = tokenRegistrations st
|
||||
regKey = C.toPubKey C.pubKeyBytes tknVerifyKey
|
||||
|
||||
getNtfTokenRegistration :: NtfStore -> NewNtfEntity 'Token -> STM (Maybe NtfTknData)
|
||||
getNtfTokenRegistration st (NewNtfTkn token tknVerifyKey _) =
|
||||
TM.lookup token (tokenRegistrations st)
|
||||
$>>= TM.lookup regKey
|
||||
$>>= (`TM.lookup` tokens st)
|
||||
where
|
||||
regKey = C.toPubKey C.pubKeyBytes tknVerifyKey
|
||||
|
||||
removeInactiveTokenRegistrations :: NtfStore -> NtfTknData -> STM [NtfTokenId]
|
||||
removeInactiveTokenRegistrations st NtfTknData {ntfTknId = tId, token} =
|
||||
stmRemoveInactiveTokenRegistrations :: NtfSTMStore -> NtfTknData -> STM [NtfTokenId]
|
||||
stmRemoveInactiveTokenRegistrations st NtfTknData {ntfTknId = tId, token} =
|
||||
TM.lookup token (tokenRegistrations st)
|
||||
>>= maybe (pure []) removeRegs
|
||||
where
|
||||
@@ -122,8 +110,8 @@ removeInactiveTokenRegistrations st NtfTknData {ntfTknId = tId, token} =
|
||||
void $ deleteTokenSubs st tId'
|
||||
pure $ map snd tIds
|
||||
|
||||
removeTokenRegistration :: NtfStore -> NtfTknData -> STM ()
|
||||
removeTokenRegistration st NtfTknData {ntfTknId = tId, token, tknVerifyKey} =
|
||||
stmRemoveTokenRegistration :: NtfSTMStore -> NtfTknData -> STM ()
|
||||
stmRemoveTokenRegistration st NtfTknData {ntfTknId = tId, token, tknVerifyKey} =
|
||||
TM.lookup token (tokenRegistrations st) >>= mapM_ removeReg
|
||||
where
|
||||
removeReg regs =
|
||||
@@ -131,8 +119,8 @@ removeTokenRegistration st NtfTknData {ntfTknId = tId, token, tknVerifyKey} =
|
||||
>>= mapM_ (\tId' -> when (tId == tId') $ TM.delete k regs)
|
||||
k = C.toPubKey C.pubKeyBytes tknVerifyKey
|
||||
|
||||
deleteNtfToken :: NtfStore -> NtfTokenId -> STM [SMPQueueNtf]
|
||||
deleteNtfToken st tknId = do
|
||||
stmDeleteNtfToken :: NtfSTMStore -> NtfTokenId -> STM [SMPQueueNtf]
|
||||
stmDeleteNtfToken st tknId = do
|
||||
void $
|
||||
TM.lookupDelete tknId (tokens st) $>>= \NtfTknData {token, tknVerifyKey} ->
|
||||
TM.lookup token regs $>>= \tIds ->
|
||||
@@ -144,7 +132,7 @@ deleteNtfToken st tknId = do
|
||||
regs = tokenRegistrations st
|
||||
regKey = C.toPubKey C.pubKeyBytes
|
||||
|
||||
deleteTokenSubs :: NtfStore -> NtfTokenId -> STM [SMPQueueNtf]
|
||||
deleteTokenSubs :: NtfSTMStore -> NtfTokenId -> STM [SMPQueueNtf]
|
||||
deleteTokenSubs st tknId = do
|
||||
qs <-
|
||||
TM.lookupDelete tknId (tokenSubscriptions st)
|
||||
@@ -156,33 +144,11 @@ deleteTokenSubs st tknId = do
|
||||
$>>= \NtfSubData {smpQueue} ->
|
||||
TM.delete smpQueue (subscriptionLookup st) $> Just smpQueue
|
||||
|
||||
getNtfSubscription :: NtfStore -> NtfSubscriptionId -> STM (Maybe NtfSubData)
|
||||
getNtfSubscription st subId =
|
||||
TM.lookup subId (subscriptions st)
|
||||
stmGetNtfSubscriptionIO :: NtfSTMStore -> NtfSubscriptionId -> IO (Maybe NtfSubData)
|
||||
stmGetNtfSubscriptionIO st subId = TM.lookupIO subId (subscriptions st)
|
||||
|
||||
findNtfSubscription :: NtfStore -> SMPQueueNtf -> STM (Maybe NtfSubData)
|
||||
findNtfSubscription st smpQueue = do
|
||||
TM.lookup smpQueue (subscriptionLookup st)
|
||||
$>>= \subId -> TM.lookup subId (subscriptions st)
|
||||
|
||||
findNtfSubscriptionToken :: NtfStore -> SMPQueueNtf -> STM (Maybe NtfTknData)
|
||||
findNtfSubscriptionToken st smpQueue = do
|
||||
findNtfSubscription st smpQueue
|
||||
$>>= \NtfSubData {tokenId} -> getActiveNtfToken st tokenId
|
||||
|
||||
getActiveNtfToken :: NtfStore -> NtfTokenId -> STM (Maybe NtfTknData)
|
||||
getActiveNtfToken st tknId =
|
||||
getNtfToken st tknId $>>= \tkn@NtfTknData {tknStatus} -> do
|
||||
tStatus <- readTVar tknStatus
|
||||
pure $ if tStatus == NTActive then Just tkn else Nothing
|
||||
|
||||
mkNtfSubData :: NtfSubscriptionId -> NewNtfEntity 'Subscription -> STM NtfSubData
|
||||
mkNtfSubData ntfSubId (NewNtfSub tokenId smpQueue notifierKey) = do
|
||||
subStatus <- newTVar NSNew
|
||||
pure NtfSubData {ntfSubId, smpQueue, tokenId, subStatus, notifierKey}
|
||||
|
||||
addNtfSubscription :: NtfStore -> NtfSubscriptionId -> NtfSubData -> STM (Maybe ())
|
||||
addNtfSubscription st subId sub@NtfSubData {smpQueue, tokenId} =
|
||||
stmAddNtfSubscription :: NtfSTMStore -> NtfSubscriptionId -> NtfSubData -> STM (Maybe ())
|
||||
stmAddNtfSubscription st subId sub@NtfSubData {smpQueue, tokenId} =
|
||||
TM.lookup tokenId (tokenSubscriptions st) >>= maybe newTokenSub pure >>= insertSub
|
||||
where
|
||||
newTokenSub = do
|
||||
@@ -196,8 +162,8 @@ addNtfSubscription st subId sub@NtfSubData {smpQueue, tokenId} =
|
||||
-- return Nothing if subscription existed before
|
||||
pure $ Just ()
|
||||
|
||||
deleteNtfSubscription :: NtfStore -> NtfSubscriptionId -> STM ()
|
||||
deleteNtfSubscription st subId = do
|
||||
stmDeleteNtfSubscription :: NtfSTMStore -> NtfSubscriptionId -> STM ()
|
||||
stmDeleteNtfSubscription st subId = do
|
||||
TM.lookupDelete subId (subscriptions st)
|
||||
>>= mapM_
|
||||
( \NtfSubData {smpQueue, tokenId} -> do
|
||||
@@ -206,32 +172,10 @@ deleteNtfSubscription st subId = do
|
||||
forM_ ts_ $ \ts -> modifyTVar' ts $ S.delete subId
|
||||
)
|
||||
|
||||
addTokenLastNtf :: NtfStore -> NtfTokenId -> PNMessageData -> IO (NonEmpty PNMessageData)
|
||||
addTokenLastNtf st tknId newNtf =
|
||||
TM.lookupIO tknId (tokenLastNtfs st) >>= maybe (atomically maybeNewTokenLastNtfs) (atomically . addNtf)
|
||||
where
|
||||
maybeNewTokenLastNtfs =
|
||||
TM.lookup tknId (tokenLastNtfs st) >>= maybe newTokenLastNtfs addNtf
|
||||
newTokenLastNtfs = do
|
||||
v <- newTVar [newNtf]
|
||||
TM.insert tknId v $ tokenLastNtfs st
|
||||
pure [newNtf]
|
||||
addNtf v =
|
||||
stateTVar v $ \ntfs -> let !ntfs' = rebuildList ntfs in (ntfs', ntfs')
|
||||
where
|
||||
rebuildList :: NonEmpty PNMessageData -> NonEmpty PNMessageData
|
||||
rebuildList = foldr keepPrevNtf [newNtf]
|
||||
where
|
||||
PNMessageData {smpQueue = newNtfQ} = newNtf
|
||||
keepPrevNtf ntf@PNMessageData {smpQueue} ntfs
|
||||
| smpQueue /= newNtfQ && length ntfs < maxNtfs = ntf <| ntfs
|
||||
| otherwise = ntfs
|
||||
maxNtfs = 6
|
||||
|
||||
-- This function is expected to be called after store log is read,
|
||||
-- as it checks for token existence when adding last notification.
|
||||
storeTokenLastNtf :: NtfStore -> NtfTokenId -> PNMessageData -> IO ()
|
||||
storeTokenLastNtf (NtfStore {tokens, tokenLastNtfs}) tknId ntf = do
|
||||
stmStoreTokenLastNtf :: NtfSTMStore -> NtfTokenId -> PNMessageData -> IO ()
|
||||
stmStoreTokenLastNtf (NtfSTMStore {tokens, tokenLastNtfs}) tknId ntf = do
|
||||
TM.lookupIO tknId tokenLastNtfs >>= atomically . maybe newTokenLastNtfs (`modifyTVar'` (ntf <|))
|
||||
where
|
||||
newTokenLastNtfs = TM.lookup tknId tokenLastNtfs >>= maybe insertForExistingToken (`modifyTVar'` (ntf <|))
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Messaging.Notifications.Server.Store.Migrations where
|
||||
|
||||
import Data.List (sortOn)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Simplex.Messaging.Agent.Store.Shared
|
||||
import Text.RawString.QQ (r)
|
||||
|
||||
ntfServerSchemaMigrations :: [(String, Text, Maybe Text)]
|
||||
ntfServerSchemaMigrations =
|
||||
[ ("20250417_initial", m20250417_initial, Nothing)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
ntfServerMigrations :: [Migration]
|
||||
ntfServerMigrations = sortOn name $ map migration ntfServerSchemaMigrations
|
||||
where
|
||||
migration (name, up, down) = Migration {name, up, down = down}
|
||||
|
||||
m20250417_initial :: Text
|
||||
m20250417_initial =
|
||||
T.pack
|
||||
[r|
|
||||
CREATE TABLE tokens(
|
||||
token_id BYTEA NOT NULL,
|
||||
push_provider TEXT NOT NULL,
|
||||
push_provider_token BYTEA NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
verify_key BYTEA NOT NULL,
|
||||
dh_priv_key BYTEA NOT NULL,
|
||||
dh_secret BYTEA NOT NULL,
|
||||
reg_code BYTEA NOT NULL,
|
||||
cron_interval BIGINT NOT NULL, -- minutes
|
||||
cron_sent_at BIGINT, -- seconds
|
||||
updated_at BIGINT,
|
||||
PRIMARY KEY (token_id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_tokens_push_provider_token ON tokens(push_provider, push_provider_token, verify_key);
|
||||
CREATE INDEX idx_tokens_status_cron_interval_sent_at ON tokens(status, cron_interval, (cron_sent_at + cron_interval * 60));
|
||||
|
||||
CREATE TABLE smp_servers(
|
||||
smp_server_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
smp_host TEXT NOT NULL,
|
||||
smp_port TEXT NOT NULL,
|
||||
smp_keyhash BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_smp_servers ON smp_servers(smp_host, smp_port, smp_keyhash);
|
||||
|
||||
CREATE TABLE subscriptions(
|
||||
subscription_id BYTEA NOT NULL,
|
||||
token_id BYTEA NOT NULL REFERENCES tokens ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||
smp_server_id BIGINT REFERENCES smp_servers ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
smp_notifier_id BYTEA NOT NULL,
|
||||
smp_notifier_key BYTEA NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
PRIMARY KEY (subscription_id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_subscriptions_smp_server_id_notifier_id ON subscriptions(smp_server_id, smp_notifier_id);
|
||||
CREATE INDEX idx_subscriptions_smp_server_id_status ON subscriptions(smp_server_id, status);
|
||||
CREATE INDEX idx_subscriptions_token_id ON subscriptions(token_id);
|
||||
|
||||
CREATE TABLE last_notifications(
|
||||
token_ntf_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
token_id BYTEA NOT NULL REFERENCES tokens ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||
subscription_id BYTEA NOT NULL REFERENCES subscriptions ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||
sent_at TIMESTAMPTZ NOT NULL,
|
||||
nmsg_nonce BYTEA NOT NULL,
|
||||
nmsg_data BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_last_notifications_token_id_sent_at ON last_notifications(token_id, sent_at);
|
||||
CREATE INDEX idx_last_notifications_subscription_id ON last_notifications(subscription_id);
|
||||
|
||||
CREATE UNIQUE INDEX idx_last_notifications_token_subscription ON last_notifications(token_id, subscription_id);
|
||||
|]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user