Compare commits

...
294 Commits
Author SHA1 Message Date
shum 4e19a18aed expr: disable pragma 2025-05-12 11:03:37 +00:00
Evgeny Poberezkin ec5a60430d 6.4.0.1 2025-04-22 22:01:16 +01:00
Evgeny e4d4b51c59 smp server: fix/test database import (#1521) 2025-04-22 21:59:27 +01:00
Evgeny Poberezkin 6b60f8bab6 Revert "ntf server: optimize in-memory storage (#1516)"
This reverts commit afb338a41a.
2025-04-21 21:01:24 +01:00
shum aa9b93eee5 ci: retry only in stable or release 2025-04-21 19:06:04 +01:00
Evgeny afb338a41a ntf server: optimize in-memory storage (#1516)
* ntf server: optimize in-memory storage

* test

* ntf server: fix store log parser for token status
2025-04-21 17:12:16 +01:00
Evgenyandshum 1e29f7c811 ci: adjust output, retry tests on failure, include hashes in release notes (#1520)
* scripts/reproduce-builds: rename with prefix and adjust output

* ci: rerun tests and include hashes in release

* ci: fix variables and really skip 8.10.7

* ci: fix prepare step

* ci: setup swap for 8.10.7

* ci: fix upload and formatting

* ci: properly format hashes

* ci: simplify

---------

Co-authored-by: shum <shum@liber.li>
2025-04-21 17:05:31 +01:00
Evgeny Poberezkin d11be15295 ntf server: fix store log parser for token status 2025-04-16 13:01:14 +01:00
Evgeny Poberezkin 305f79d2a6 6.4.0.0 2025-04-14 10:52:05 +01:00
Evgeny Poberezkin af988f774e Merge branch 'stable' 2025-04-14 10:51:34 +01:00
Evgeny Poberezkin 82e389298a docs: CHANGELOG 2025-04-14 10:51:23 +01:00
Evgeny c784d5ce0c Merge pull request #1495 from simplex-chat/short-links
smp protocol: short 1-time invitations and contact address links
2025-04-14 09:58:09 +01:00
Evgeny c2c4730953 smp server: serve .well-known folder via server pages (#1514) 2025-04-13 22:02:48 +01:00
Evgeny Poberezkin ccdd8e1775 agent: additional encodings for short links 2025-04-13 13:43:27 +01:00
Evgeny Poberezkin 307a784174 Merge branch 'master' into short-links 2025-04-13 10:51:32 +01:00
Evgeny Poberezkin 9abc0fa88d 6.3.2.0 2025-04-11 23:22:48 +01:00
Evgeny 76aad61f00 smp server: update server page for short links to correctly change URI for apps (#1511)
* smp server: update server page for short links to correctly change URI for apps

* change page titles
2025-04-11 23:18:23 +01:00
Evgeny 37ce109009 smp server: .well-known folder for server pages for SimpleX apps to handle preset server links (#1510)
* smp server: .well-known folder for server pages for SimpleX apps to handle preset server links

* add short link group path

* test
2025-04-11 18:30:43 +01:00
Evgeny a66163dc46 agent: additional short link types and encodings (#1509) 2025-04-11 12:29:44 +01:00
Evgeny Poberezkin bdf8bf093c smp server: make symlinks relative 2025-04-10 20:58:33 +01:00
Evgenyandsh b7a9542213 smp server: short links and owners for channels (#1506)
* smp server: short links and owners for channels

* types

* support mutliple rcv keys

* fix down migration, test/create server schema dump

* reduce schema dump

* parameterize type for link data by connection type

* return full connection link data

* test version

* change short link encoding

* test: print pg_dump output

* server pages, link encoding

* fix connection request when queue data and sender ID are created for old servers

* test, change pattern

* ci: install postgresql tools in runner (#1507)

* ci: install postgresql tools in runner

* ci: docker shell abort on error

* fix pattern for ghc 8.10.7

* patch ConnReqUriData SMP encoding to preserve queue mode after decoding

* test for RKEY

* fix/test store log with RKEY

---------

Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com>
2025-04-10 19:09:47 +01:00
Evgeny Poberezkin 3a3f9fd51e Merge branch 'master' into short-links 2025-04-10 11:51:37 +01:00
Evgeny 6dc9d76ed3 smp agent: handle client/agent version downgrades after connection was established (#1508) 2025-04-10 11:50:19 +01:00
Evgeny a2a4b80af4 agent: padded encryption for link data, shorten/restore short links, tests (#1505)
* agent: padded encryption for link data, tests

* lambda

* test short links via proxy

* tests: server persistence with short links

* rfc: group links

* shorten, restore, test short links encoding

* rfc
2025-04-05 07:45:39 +01:00
Evgeny 7ec0ae3bb5 agent: types and encodings to use from the app (#1504)
* agent: types and encodings to use from the app

* use action forks
2025-04-03 16:31:22 +01:00
Evgeny 2c5530c9f0 agent: binary protocol encoding for connection request (#1503)
* agent: binary protocol encoding for connection request

* enable tests

* test
2025-04-01 22:32:49 +01:00
Evgeny 94ee3ceced agent: replace sndSecure with queueMode in queue URIs and records (#1502)
* agent: replace sndSecure with queueMode in queue URIs and records

* fix test
2025-03-31 16:54:29 +01:00
Evgeny Poberezkin dd67de4d71 Merge branch 'master' into short-links 2025-03-31 16:54:07 +01:00
Evgeny 285fd93c32 smp server: notices about server pages and port 443, enable store log by default (#1501)
* smp server: notices about server pages and port 443, enable store log by default

* enable store log by default for ntf and xftp servers, fix tests
2025-03-31 16:41:11 +01:00
Evgeny 56bec06856 smp protocol: remove creating notifications from NEW command, new ideas for notifications (#1500) 2025-03-30 19:56:16 +01:00
Evgeny 04cbed90fb agent: set/update and delete contact short link data (#1499)
* agent: set/update and delete contact short link data

* delete contact link data, tests

* comments

* type signature for GHC 8.10.7
2025-03-30 11:17:25 +01:00
Evgeny c1a6647f19 agent: join connection when 1-time invitation short link is already secured (#1496)
* agent: join connection when 1-time invitation short link is already secured

* do not pass short link to join

* delete short link record after connection
2025-03-29 09:42:56 +00:00
Evgeny Poberezkin 1dd677eec2 Merge branch 'master' into short-links 2025-03-28 19:50:18 +00:00
Evgeny 7636bc7491 smp server: remove locks for deleted queues, additional statistics for objects in memory (#1498)
* smp server: remove locks for deleted queues, additional statistics for objects in memory

* version

* reduce queue cache usage

* less caching, refactor

* comments

* revert version
2025-03-28 18:51:54 +00:00
sh 79adb83782 ci: add reproduce builds workflow (#1497)
* ci: add reproduce builds workflow

* ci: trigger webhook and build every day

* ci: change secrets
2025-03-27 16:06:32 +00:00
Evgeny b83d897650 smp protocol: short links and other changes from RFC (#1489)
* smp protocol: short links types and other changes from RFC

* add fields for queue link ID and data

* create queue and ntf credentials with NEW command

* all tests

* simplfiy types, update rfc

* update rfc

* include SenderId in NEW request in case queue data is sent

* store queue data and generate link ID if needed

* update rfc

* agent API and types

* SMP commands and persistence for short links

* SMP client functions for short links

* agent client functions for short links

* create rcv queue with short link (TODO secret_box)

* encryption and encoding for link data, postgres client migration

* test creating short link

* get link and data, tests

* comments

* type signature
2025-03-26 17:26:27 +00:00
Evgeny Poberezkin 0c3b25706a Merge branch 'stable' 2025-03-23 18:18:07 +00:00
sh c3f57beafd scripts: fix script for reproducible builds (#1492)
* scripts: fix script for reproducible builds

* scripts: add automatic hash comparasion + compute file with checksums

* scripts: silence cleanup

* ci/scripts: enable parallel build (#1493)
2025-03-23 18:17:27 +00:00
Evgeny Poberezkin aace3fd2fb 6.3.1.0 2025-03-22 13:51:55 +00:00
sh 2e67ed9c4c ci/scripts: refactor reproducible builds (#1490)
* ci: build cache, remove 20.04, minor refactor

* scripts: update build reproducibility script

* script: add postgresql reproducibility

* script: no cache when building image

* scripts: force symlinks

* scripts: also download prebuilt postgresql binary

* scripts: add missing app var
2025-03-22 13:27:11 +00:00
Evgeny 614fa2b163 smp server: reduce queue expiration/idle intervals, skip expiring very old queues (#1488)
* smp server: reduce idle queue interval and queue expiration interval

* only expire recent queues (not very old)

* fix

* version
2025-03-20 08:57:47 +00:00
Evgeny 903e96bdfa rfc: smp queue protocol changes (#1485)
* rfc: smp queue protocol changes

* questions

* update

* update

* correction
2025-03-19 10:47:57 +00:00
Evgeny 5c0adcbbff Merge pull request #1467 from simplex-chat/postgres
SMP server PostgreSQL storage - feature branch (merge, dont squash)
2025-03-18 16:48:25 +00:00
sh b2f16eeff4 ci: seperate builds (pg/standard) (#1486) 2025-03-18 09:42:53 +00:00
Evgeny 6db79808aa smp server: use COPY to import store log to postgres db, improve concurrency and error handling (#1487)
* smp server: use COPY to import store log to postgres db

* compact queues when importing to postgres

* mempty

* version

* handle errors while expiring, mask async exceptions while getting queue

* whitespace

* version
2025-03-18 09:40:22 +00:00
Evgeny f4b55bfc0c smp server: CLI option to compact store log with PostgreSQL database (#1484)
* smp server: CLI option to compact store log with PostgreSQL database

* version

* fix test
2025-03-16 18:47:02 +00:00
Evgeny fe64d42db1 smp server: split postgres support to a separate executable, to not require postgres library in the main binary (#1482)
* smp server: split postgres support to a separate executable, to not require postgres library in the main binary

* comments

* enable server_postgres flag by default, add CPP option to test

* refactor

* change default for server_postgres to False

* diff
2025-03-16 11:37:25 +00:00
Evgeny 1b5a9f3b0c smp server: do not cache all queues from database while processing expirations (#1483)
* smp server: expire only active queues

* version

* do not cache all queues while processing expirations

* refactor

* foldWithOptions_

* version

* use shared lock when expiring all queues

* use TMVar

* comment

* rename

* remove fold options

* do not create locks in the Map for temporarily loaded queues

* fix

* revert version
2025-03-15 20:11:30 +00:00
Evgeny fdf8bd7ee2 smp server: mask database store operations from async exception for state consistency (#1481)
* smp server: mask database store operations from async exception for state consistency

* remove unused, comments
2025-03-13 13:57:46 +00:00
Evgeny 019a32a623 smp server: optionally append store log with postgres storage (without loading and compacting, for debugging during migration) (#1480)
* smp server: optionally maintain store log with postgres storage (without loading and compacting, for debugging during migration)

* refactor

* remove comment
2025-03-13 10:25:56 +00:00
Evgeny d44f09d111 tests: pass AStoreType to agent test as a parameter (#1479)
* tests: run agent tests with PostgreSQL SMP servers

* agent tests with postgres database

* enable tests

* fix store log tests

* fix test
2025-03-12 21:47:44 +00:00
Evgeny 6b5de2c51b smp server: exception on invalid migration, check queue storage settings on server start (#1478)
* smp server: check queue storage settings on server start

* fix incorrect postgres migration not throwing exception
2025-03-10 17:28:26 +00:00
shum 9410fb6f16 docker: add libpq dep 2025-03-10 10:03:44 +00:00
Evgeny 7b42aaa132 smp server: expire messages in postgres database, mark queues as deleted, combine tables (#1471)
* smp server: expire messages in postgres database

* tty

* fail if nothing updated in db

* remove old deleted queues

* index

* fix tests
2025-03-10 09:31:50 +00:00
Evgeny Poberezkin e4b9aa9746 Merge branch 'master' into postgres 2025-03-09 21:58:39 +00:00
Evgeny Poberezkin aa26a55df4 Merge branch 'master' into stable 2025-03-09 21:53:03 +00:00
sh 6e505f5c0b scripts: add script to reproduce binaries locally (#1477) 2025-03-08 20:51:20 +00:00
Evgeny Poberezkin a491a1d878 6.3.0.8 2025-03-07 14:30:00 +00:00
shandEvgeny Poberezkin 36f5539b9a ci: introduce reproducible builds (#1476)
* ci: introduce reproducible builds

* ci: return 20.04

* smp server: increase timing test threshold

* ci: test outside docker

* ci: fix test step

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2025-03-07 14:15:18 +00:00
Evgeny 1a2afe8bfd agent: fix JSON encoding for protocol errors to be compatible with iOS (#1475) 2025-03-06 08:02:27 +00:00
Evgeny Poberezkin 9fece9ce3d 6.3.0.7 2025-03-02 22:45:07 +00:00
Evgeny Poberezkin 2e2ede5968 Merge branch 'master' into postgres 2025-03-02 22:35:28 +00:00
Evgeny 205d4ead1c smp server: remove store log backups when server starts (#1472) 2025-02-27 07:39:05 +00:00
Evgenyandspaced4ndy 80a070a8ea smp server: update STM cache only after PostgreSQL update (#1470)
* refactor journal store

* update postgres store methods to update STM cache only after DB update

* todos

* drop

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>

* fix GHC 8.10.7

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2025-02-26 11:43:08 +00:00
Evgeny 172540984c agent, smp server: PostgreSQL connection pool (#1468)
* agent, smp server: PostgreSQL connection pool

* simplify, create all connections before start

* rename

* remove imports
2025-02-25 15:33:54 +00:00
Evgenyandspaced4ndy 4dc40bd795 smp server: PostgreSQL queue store (#1448)
* smp server: queue store typeclass

* parameterize JournalMsgStore

* typeclass for queue store

* postgres WIP

* compiles, passes tests

* remove StoreType

* split migrations

* progress

* addQueueRec

* reduce type spaghetti

* remove addQueue from typeclass definition

* getQueue

* test postgres storage in SMP server

* fix schema

* comment

* import queues to postgresql

* import queues to postgresql

* log

* fix test

* counts

* ci: test smp server with postgres backend (#1463)

* ci: test smp server with postgres backend

* postgres service

* attempt

* attempt

* empty

* empty

* PGHOST attempt

* PGHOST + softlink attempt

* only softlink attempt

* working attempt (PGHOST)

* remove env var

* empty

* do not start server without DB schema, do not import when schema exists

* export database

* enable all tests, disable two tests

* option for migration confirmation

* comments

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2025-02-24 15:47:27 +00:00
Evgeny Poberezkin f9d7b1eebc 6.3.0.6 2025-02-23 19:42:16 +00:00
Evgeny ffbc733d58 smp server: remove duplicate progress log (#1466) 2025-02-23 19:40:49 +00:00
Evgeny 2286726d72 smp server: start options maintenance and skip-warnings (#1465)
* smp server: start options `maintenance` and `skip-warnings`

* ignore invalid parsing of the last lines

* parsingErr

* fix
2025-02-22 19:26:03 +00:00
spaced4ndy 1b8110a332 xftp server: restore file status from log (#1461)
* xftp server: restore file blocking info from log

* fix parse

* rework

* update

* rename
2025-02-21 22:43:56 +00:00
sh dad7e1b60c simplex-servers-update: download scripts from tag (#1457)
* simplex-servers-update: download scripts from tag

* set remote version when re-executing the update script

* safeguard variables

* additional checks
2025-02-20 09:26:03 +00:00
Evgeny 72c2ddcf57 agent: move migrations (#1459) 2025-02-18 23:39:29 +00:00
Evgeny a75e138965 smp server: remove empty queues journals when expiring messages of idle queues (#1458)
* smp server: remove empty queues journals when expiring messages of idle queues

* remove unnecessary update

* ci: update action

* rename
2025-02-18 20:04:58 +00:00
Evgeny fa319d798a smp server: remove empty journals when opening message queue (#1456)
* smp server: remove empty journals when opening message queue

* update, do not backup state

* test

* version

* do not close queue state when queue is opened for writing

* comment

* quota = 4

* refactor openMsgQueue to prevent extra state backups

* use interval in config

* version, expire backups after 5 min

* refactor

* test
2025-02-17 23:11:34 +00:00
Evgeny Poberezkin c192339af9 6.3.0.5 2025-02-15 15:36:44 +00:00
Evgeny fa67d128d1 agent: fix deleting messages after delivery to avoid deleting shared message bodies (#1455)
* agent: fix deleting messages after delivery to avoid deleting shared message bodies

* fix, comments

* rename

* comment
2025-02-15 12:40:10 +00:00
Evgeny 55ff581655 SMP client: dont block on writing to sending queues (#1454)
* SMP client: dont block on writing to sending queues

* only fork if full
2025-02-15 11:31:05 +00:00
sh 560b257af7 scripts: simplex-servers-update menu to update only server binaries etc. (#1445)
* scripts/simplex-servers-update: refactor

* simplex-servers-update: don't check empty variable

* simplex-servers-update: minor decoration change

* simplex-servers-update: add new line when reexecuting script

* simplex-servers-update: refactor menus

* simplex-servers-update: fix re-executing

* simplex-servers-update: fix menus

* simplex-servers-update: do not rerended menu every time

* simplex-servers-update: update menus logic

* simplex-servers-update: ask permission to update only if versions differ

* simplex-servers-update: skip empty variable

* simplex-servers-update: include while loop

* simplex-servers-update: update re-executing skip logic

* simplex-servers-update: ignore empty

* simplex-servers-update: s/while/if

* simplex-servers-update: ignore empty variable

* simplex-servers-update: update skipping scripts

* simplex-servers-update: minor fixes

* simplex-servers-update: add spaces

* simplex-servers-update: safeguard files and add cleanup logic

* simplex-servers-update: update menus

* simplex-servers-update: fix binary check

* simplex-servers-update: adjust erasing lines

* simplex-servers-update: simplify bin update logic

* simplex-servers-update: fix binary path

* simplex-servers-update: format errors

* scripts/main/simplex-servers-update: initial refactor2

* simplex-servers-update: incr update

* simplex-servers-update: initial dynamic menu

* simplex-servers-update: misc fixes

* simplex-servers-update: fix VER

* simplex-servers-update: add VER check

* simplex-servers-update: abort early if there's no options available

* simplex-servers-update: add new helper function and update_misc

* simplex-servers-update: pass selection

* simplex-servers-update: non-interactive usage

* simplex-servers-update: add help menu

* simplex-servers-update: misc fixes and new binary update logic

* simplex-servers-update: local path check

* simplex-servers-update: fix change check

* simplex-servers-update: OLF -> OLD

* simplex-servers-update: always return code from functions

* simplex-servers-update: new menu flow

* simplex-servers-update: fixes

* simplex-servers-update: download function + fixes

* simplex-servers-update: s/bins/binaries

* simplex-servers-update: download message

* simplex-servers-update: cleanup
2025-02-15 10:31:19 +00:00
spaced4ndyandEvgeny Poberezkin 7ac80bffcb agent: store shared message body only once (when it is the same across messages when batching) (#1453)
* agent: store shared message body only once (when it is the same across messages when batching)

* rename

* refactor

* refactor

* save bodies and messages in single transaction

* comment

* comment

* comment

* box

* mapME

* box

* ValueOrRef

* remove instances

* refactor

* comments

* test

* refactor

* mapAccumLM compatibility with ghc 8.10.7

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2025-02-14 18:01:40 +00:00
spaced4ndyandEvgeny Poberezkin 0d8a1a2879 agent: encrypt messages on delivery (#1446)
* agent: save message body once (plan, schema)

* split

* new type

* bs

* encrypt on delivery

* schema

* fix test

* check pad size

* rename

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2025-02-14 12:35:18 +00:00
Evgeny Poberezkin bd97cb0449 6.3.0.4 2025-02-10 15:12:54 +00:00
Evgeny a58d3540ad agent: refactor migrations (#1452)
* agent: refactor migrations

* fix postgres
2025-02-07 17:17:53 +00:00
Evgenyandspaced4ndy 5dbe6337ea ntf server: additional statistics, new invalid token reasons (#1451)
* agent: check ntf token status on registration

* remove check

* update on check

* refactor

* version

* fix

* ntf server: additional statistics

* swap

* version

* more stats

* test, verify invalid

* rename

* exclude test token from stats

* increase delay

* handle invalid token in retry, more reasons

* focus tests

* disable new tests in CI

* fix

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2025-02-07 12:19:11 +00:00
spaced4ndyandEvgeny Poberezkin b633f89c1a agent: check ntf token status on registration (#1450)
* agent: check ntf token status on registration

* remove check

* update on check

* refactor

* version

* fix

* test, verify invalid

* rename

* increase delay

* disable new tests in CI

* fix

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2025-02-07 11:36:29 +00:00
Evgeny 944a22a2fb ntf server: record token invalidation reason, add date of the last token activity (#1449)
* ntf server: record token invalidation reason, add date of the last token activity

* update time

* rename

* optional

* include token ID in delivery error

* version

* protocol version

* fix, log error
2025-02-06 17:11:35 +00:00
Evgeny ce24f83b64 refactor STM queues (#1447) 2025-02-05 12:04:27 +00:00
Evgeny Poberezkin 45373e7f1f 6.3.0.3 2025-01-31 13:01:03 +00:00
sh efe71cd598 docker: refactor (#1438)
* docker: refactor

* github/docker: bump actions and adjust smp ports
2025-01-30 09:22:13 +00:00
Evgeny ae41717b9b smp server: use origin client version when processing proxied command, fixes old client sending to new server (#1443)
* smp server: pass origin client version when processing proxied command, fixes old client sending to new server

* version

* version
2025-01-28 22:04:46 +00:00
Evgeny e78ab60c97 build: fix postgres (#1444) 2025-01-28 22:02:41 +00:00
Evgeny b3c8358a43 agent: combine connection deletion events (#1442) 2025-01-25 08:52:40 +00:00
sh be81fe1f74 scripts/systemd: update services (#1440) 2025-01-24 13:37:56 +00:00
sh 817f5e1737 scripts/systemd: update services (#1440) 2025-01-24 13:35:33 +00:00
Evgeny eda9e36c82 agent: track queries (#1439) 2025-01-24 10:31:50 +00:00
Evgeny Poberezkin 2318975375 6.3.0.2 2025-01-20 16:38:14 +00:00
Evgeny 298666380f servers: handle accept error ECONNABORTED (#1437) 2025-01-20 16:32:42 +00:00
Evgeny 6a9075141f xftp server: use recipient ID in control port to delete and block files, smp server: fix version negotiation (#1434)
* xftp server: use recipient ID in control port to delete and block files

* cap smp proxy agent version at 10

* version

* fix prometheus

* fix

* remove old version support

* log connection parameter on error

* tests

* log sent command tag

* log error and client version

* cap proxy version for previous destination server

* comment, test

* remove logging tag

* remove logs

* version

* SMP version 14

* version

* remove comments

* version
2025-01-20 13:45:49 +00:00
spaced4ndy 488c7082f3 agent: store interface (#1436) 2025-01-20 17:02:39 +04:00
spaced4ndy fdde9863cd agent: add reopenStore function for postgres; run notification tests with postgres (#1435) 2025-01-17 16:27:37 +04:00
Evgeny Poberezkin 9404a3ab63 xftp server: block stats 2025-01-14 10:46:27 +00:00
Evgeny Poberezkin dadf6ec5b6 6.3.0.1 2025-01-13 08:39:41 +00:00
Evgeny c528ea4f31 smp server: update versions (#1433) 2025-01-12 22:11:18 +00:00
Evgeny 3d4e0b06c0 servers: blocking records for content moderation (#1430)
* servers: blocking records for content moderation

* update

* encode BLOCKED as AUTH in old versions

* update

* unblock queue command

* test, status command
2025-01-12 19:34:00 +00:00
spaced4ndy 9d9ec8cd0b agent: remove unused functions (#1432)
* agent: remove unused functions

* cleanup imports
2025-01-10 07:59:17 +00:00
Evgeny 992b42e922 agent: option to enable/disable vacuum after SQLite migration (#1429) 2024-12-28 21:06:42 +00:00
Evgeny 3cf9dacbc0 Merge pull request #1428 from simplex-chat/ep/postgres-compat
agent: restore methods for backwards compatibility with simplex-chat
2024-12-28 11:39:18 +00:00
Evgeny Poberezkin cfde5932fd agent: restore methods for backwards compatibility with simplex-chat 2024-12-28 10:33:22 +00:00
Evgeny 69fb9a9e0e Merge pull request #1417 from simplex-chat/postgres
agent: support postgres backend
2024-12-27 19:52:18 +00:00
Evgeny Poberezkin 0a118f5631 Merge branch 'master' into postgres 2024-12-27 11:58:19 +00:00
spaced4ndy 0b986c1158 postgres: export fromOnlyBI 2024-12-26 20:13:27 +04:00
Evgeny Poberezkin 184a95cd2a 6.3.0.0 2024-12-26 13:54:38 +00:00
spaced4ndy b7a8df40c4 Merge branch 'master' into postgres 2024-12-25 12:15:41 +04:00
spaced4ndyandEvgeny Poberezkin bf28902327 agent: reuse ratchet on repeat join (#1426)
* agent: reuse ratchet on repeat join

* check status

* update

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-12-25 00:07:45 +00:00
Evgeny Poberezkin 426bf68763 6.2.2.0 2024-12-24 21:04:58 +00:00
spaced4ndy e557dc86e7 Merge branch 'master' into postgres 2024-12-20 17:18:54 +04:00
Evgeny Poberezkin f5cef2556b build: move type to client_library module 2024-12-20 13:12:11 +00:00
spaced4ndyandEvgeny Poberezkin cf66aadc20 postgres: store implementation, conditional compilation (#1421)
* postgres: implementation wip

* to from field

* agent store compiles

* methods

* create store

* tests wip

* migration tests pass

* tests compile

* fix tests

* tests wip

* bool int

* tests wip

* tests wip

* more boolint

* more fixes

* more fields pass

* more fixes

* binary

* instances, binary

* test passes

* remove todos, more tests pass

* fix conflict

* fix bool

* fix sequence breaking

* fix insertedRowId

* skip ratchet re-synchronization tests

* after test

* file tests

* after test

* rename

* remove comment

* format

* remove unused

* suppress notices

* fixes

* move

* fix

* instance

* instance2

* fix

* instances

* comment

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-12-20 15:54:58 +04:00
sh 77a5ed2ec6 6.2.1.0 (#1424) 2024-12-19 20:45:17 +00:00
Evgenyandsh 4d640c16aa smp server: log prometheus metrics (#1411)
* smp server: log prometheus metrics

* save metrics

* diff

* lines

* version

* do not include Prometheus into client

* corrections

Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com>

* corrections

Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com>

* corrections

Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com>

* add timestamp to metrics

* remove type

* remove version

---------

Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com>
2024-12-18 11:19:13 +00:00
Evgeny 0a827307d6 build: remove some modules from the client build (#1418)
* build: remove some modules from the client build

* remove websockets from client_library
2024-12-14 15:22:06 +00:00
spaced4ndy 64149599de postgres: db interfaces wip (sqlite passes) (#1419) 2024-12-12 17:42:58 +04:00
spaced4ndy 07be469f49 postgres: schema (#1416) 2024-12-10 18:53:01 +04:00
Evgeny Poberezkin 79e9447b73 6.2.0.7 2024-12-07 14:36:50 +00:00
spaced4ndy 9893935e7c agent: fix acceptContact inconsistent state after app restart (#1412) 2024-12-05 19:27:21 +04:00
Evgeny Poberezkin 966b9990e0 6.2.0.6 2024-12-04 19:57:11 +00:00
spaced4ndy bef1e38295 xftp: add debug info to "too many recipients" error (#1410) 2024-12-04 19:56:33 +00:00
Evgeny Poberezkin 4b43cb8054 6.2.0.5 2024-12-03 12:33:32 +00:00
Evgeny 38ad3c046e agent: function to check if contact address is the same (#1409) 2024-12-02 11:41:06 +00:00
Evgeny b2afe6f40c rfc: moving queue records to journal storage and queue blobs, expiring messages in journal storage (#1407)
* rfc: moving queue records to journal storage and queue blobs

* rfc: expiring messages in journal storage

* add comment
2024-11-28 12:22:52 +00:00
Evgeny Poberezkin 601620bdde 6.2.0.4 2024-11-22 19:50:04 +00:00
Evgeny 97104988a3 server: support server roles and operators (#1343)
* server: support server roles and operators

* make server operator optional

* allRoles

* fix test

* different server host in tests

* remove ServerCfg fields used only in UI

* comments

* choose different server for invitation when connecting via address

* fix test in ghc8107

* simplify
2024-11-22 16:43:37 +00:00
Evgeny 45333bd340 smp server: do not open/read journal message queues that are known to be empty (#1406)
* smp server: do not open/read journal message queues that are known to be empty

* cleanup

* version

* close empty queues on first subscription

* revert version
2024-11-22 12:29:31 +00:00
Evgeny bbcb1abfda agent: treat blocked STM and other critical errors that offer restart as temporary (#1405) 2024-11-21 22:01:19 +00:00
Evgeny Poberezkin a64c1aa2c4 6.2.0.3 2024-11-17 09:45:17 +00:00
Evgeny 21fbbf9106 smp server: update message counts during message expiration, increase idle interval (#1404)
* smp server: update message counts during message expiration, increase idle interval

* version

* fix

* flip results

* version
2024-11-17 09:44:40 +00:00
Evgeny 17a0be10fa smp server: expire messages in idle message queues (including not opened) (#1403)
* smp server: expire messages in idle message queues (including not opened)

* use message expiration interval

* simpler

* version

* remove version
2024-11-15 10:26:24 +00:00
Evgeny Poberezkin 3017d14392 6.2.0.2 2024-11-07 08:13:45 +00:00
Evgeny d3275cef48 smp server: combine messages and queue storage to optimise performance, prevent race condition when deleting queue and to avoid "orphan" messages. (#1395)
* smp server: combine queue and message store into one class (WIP)

* keep deleted queue tombstones to prevent race conditions and errors when restoring

* move store log from server to store implementations

* STMQueueStore type class

* fix store closed when messages expired, handle store writing errors

* types

* version

* fix recovery from missing write journal, tests

* version
2024-11-07 08:09:11 +00:00
Evgeny Poberezkin 0fd4aa1e23 6.2.0.1 2024-11-03 21:10:45 +00:00
Evgeny 7c21945569 smp server: option to skip expiring messages on start, read queue state file end only (#1400) 2024-11-03 21:08:07 +00:00
ffecf200d4 build: client_library flag to build only used modules, remove package yaml (#1394)
* move dependencies to library

* remove package.yaml

* remove auto-generated comment

* cleanup target definitions

* add executable options

* add test rtsopts

* revert common options

* don't hide unused packages warning

* Add client_only cabal flag (#1396)

---------

Co-authored-by: IC Rainbow <aenor.realm@gmail.com>
Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com>
2024-11-02 15:34:33 +00:00
Evgeny 58d3c160ef smp server: fix logging progress when importing journal (#1393) 2024-10-28 14:22:18 +00:00
Evgeny Poberezkin 326cf08efa 6.2.0.0 2024-10-28 10:28:54 +00:00
Evgeny Poberezkin d49dc201c3 smp server: log file name on hClose error 2024-10-28 10:26:24 +00:00
Evgeny 6ff4c3f1ce Merge pull request #1375 from simplex-chat/journal-store
smp server: journal message store
2024-10-26 17:03:19 +01:00
Evgeny 5940514f40 smp server: remove queue from map when closing, test (#1392)
* smp server: remove queue from map when closing, test

* remove print

* refactor
2024-10-26 16:14:55 +01:00
Evgeny Poberezkin 916150742e Merge branch 'master' into journal-store 2024-10-26 12:58:32 +01:00
Evgeny Poberezkin 1735b619e8 6.1.3.0 2024-10-26 12:57:43 +01:00
Evgeny 6b61d101dc smp server: fix restoring notifications (#1391) 2024-10-26 12:56:29 +01:00
Evgeny 56ca7bf30e smp server: do not log progress when server starts (#1390)
* smp server: do not log progress when server starts

* test
2024-10-26 10:33:18 +01:00
Evgeny Poberezkin 2a218675b2 Merge branch 'master' into journal-store 2024-10-26 08:12:07 +01:00
Evgeny 8b9eb49b57 smp server: log operation of restore error (#1389)
* smp server: log operation of restore error

* space

* rename

* revert version
2024-10-26 08:11:07 +01:00
Evgeny 3860341584 rfc: iOS notifications 2 (#1334) 2024-10-25 21:42:13 +04:00
Evgeny Poberezkin 9078118a6d Merge branch 'master' into journal-store 2024-10-25 15:43:04 +01:00
Evgeny Poberezkin a7f5297ad1 6.1.2.0 2024-10-25 15:41:30 +01:00
Evgeny 870a60fc5d smp server: reduce memory usage when starting, all servers: prevent losing state on termination while compacting state (#1385)
* smp server: reduce memory usage when starting

* servers: prevent losing state when compacting is incomplete (terminated abnormally)

* fix test

* fix/refactor

* message

* remove .bak

* simpler
2024-10-25 15:39:59 +01:00
spaced4ndyandEvgeny Poberezkin a8471eed5b ntf: get messages for multiple last notifications (#1378)
* ntf: get messages for multiple last notifications (#1352)

* ntf: separate get ntf conns api (#1379)

* ntf: separate get ntf conns api

* nonempty

* update

* update

* remove single get api

* fix test

* refactor

* refactor

* ntf: batch get connections (#1387)

* ntf: batch get apis

* works

* fix

* fix

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-25 15:39:41 +04:00
Evgeny a3351ad060 smp server: fix export of journal storage (#1384)
* smp: fix export of journal storage

* catch errors when exporting messages
2024-10-25 09:11:47 +01:00
Evgeny 2322f5bf59 smp server: logging format, mask/handle exceptions during journal store operations (#1381)
* smp server: fixed logging format for journal store errors

* version

* colon

* logs

* refactor

* space

* remove comment

* log file name in fixFileSize

* logError

* stricter

* process all queues more efficiently

* use monoid for queue processing

* expire messages concurrently

* concurrently 2

* Revert "concurrently 2"

This reverts commit c1aee1f22c.

* Revert "expire messages concurrently"

This reverts commit fc53137cdb.

* show queue directory or ID in errors

* foldM

* mask_

* try

* mask more

* refactor

* command to delete journal

* uninterruptibleMask_ when writing to state file

* fix ghc8.10.7

* version

* revert version
2024-10-24 19:44:47 +01:00
Evgeny c9c075fd49 smp server: merge quota messages and set queue to "over quota" state after restoring, server tests with journal and memory store (#1380)
* smp: run server tests with journal and memory store

* merge quota messages, set queue to "over quota" state after restoring

* fix test
2024-10-23 09:17:23 +01:00
Evgeny 1484523164 smp server, journal store: check file size on append, validate queue state (#1377)
* smp server: journal store queue state validation

* validate state on update, refactor

* handle message parsing errors

* typo

* fix test, throw exception when file is smaller than expected

* fix

* fix test in ghc 8.10.7
2024-10-22 12:09:51 +01:00
Evgeny dc17402896 smp server: updated journal store API (#1374)
* core: updated journal store API

* parameterize JournalState by read/write type

* collect stats when importing/expiring

* compare stored stats and store for messages and notifications

* simplify
2024-10-21 16:50:07 +01:00
Evgeny 0414ea59f0 smp server: journal message store (#1370)
* smp server: remove STM function from MsgStore

* polymorphic MsgStore

* jourmal storage for messages (WIP)

* more journal, test setup

* writeMsg

* test

* tryDelMsg

* delMsgQueue

* remove MsgStoreClass instance of existential wrapper for Msg stores

* store config

* extract common logic out of store instances

* add store type to config

* open journals, cache last message, tests pass

* CLI commands

* refactor import/export messages

* cli commands to import/export journal message store

* export journal without draining, import/export tests

* journal command

* import/export progress

* better progress info

* only log queue state once when importing

* logs

* handle IO errors in journal store, return as STORE error

* recover from state file errors

* fix message files after crash

* fix messages folder
2024-10-21 11:50:30 +01:00
Evgeny Poberezkin 967afaf802 6.1.1.0 2024-10-18 12:47:27 +01:00
Evgeny 77ee9bea38 smp protocol: add STORE error (#1372) 2024-10-18 10:13:01 +01:00
Evgeny e0cd28a65a smp server: stop server faster (#1371)
* smp server: stop server faster

* .702

* asks

* fix leak

* kill client when server stopped

* remove version

* fix version
2024-10-18 09:12:59 +01:00
Evgeny Poberezkin 7b6c86c6c1 docs: correction 2024-10-14 08:32:19 +01:00
Evgeny Poberezkin 1d0a418011 rfc: rename file 2024-10-12 17:12:45 +01:00
Evgeny Poberezkin 9253f5647a Merge branch 'stable' 2024-10-12 16:30:54 +01:00
Evgeny Poberezkin c41bfe831d 6.1.0.7: changelog for v6.1.0 2024-10-11 13:01:12 +01:00
Alexander BondarenkoandEvgeny Poberezkin c9af7e327e smp server: graceful shutdown on SIGINT (#1360)
* smp-server: graceful shutdown on SIGINT

* .501

* clean up

* remove version

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-11 12:49:15 +01:00
Evgeny 8870442830 smp server: reject SKEY with different key earlier (at verification step), add tests (#1366) 2024-10-11 12:13:52 +01:00
spaced4ndyandEvgeny Poberezkin 9ad26c97a1 agent: prepareConnectionToAccept api (#1365)
* agent: prepare conn (plan)

* implement

* refactor

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-11 11:05:14 +01:00
spaced4ndy e1201f0967 agent: test db indexes (#1364) 2024-10-09 15:16:08 +04:00
Evgeny Poberezkin ca2202e3c8 6.1.0.6 2024-10-07 22:41:53 +01:00
Evgeny f24ee70138 clients: do not send SNI (#1363) 2024-10-07 22:40:30 +01:00
Evgeny Poberezkin 8981cae3c2 ci: always generate change log 2024-10-07 22:22:02 +01:00
Evgeny Poberezkin b8971a31bc 6.1.0.5 2024-10-07 10:24:19 +01:00
Evgeny e01398a31a agent: send MSGNTF on NO_MSG error (#1362)
* agent: send MSGNTF on NO_MSG error

* simplify
2024-10-07 10:22:58 +01:00
Evgeny f871f20172 smp server: fix notification delivery (#1350)
* .401

* stats for undelivered notifications

* logs, stats

* control port show ntf client IDs

* check that Ntf client is still current and that queue is not full, drop notifications otherwise

* prevent losing notifications when client is not current or queue full

* add log when no notifications, remove some logs

* reduce STM transaction

* revert version change
2024-10-07 09:01:28 +01:00
Evgeny 80d3518d55 tests: store log (#1348)
* 6.1.0.4

* tests: store log

* random

* more tests
2024-10-06 21:21:08 +01:00
spaced4ndyandEvgeny Poberezkin 49935473ae ntf: agent to notfiy MSGNTF on successful ack (#1355)
* ntf: agent to notfiy MSGNTF on successful ack

* move msgntf

* release lock earlier

* fix tests

* do not release get lock

* fix

* diff

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-10-06 19:48:22 +01:00
Evgeny 0705d05078 smp: transport block encryption (#1317) 2024-10-06 18:05:06 +01:00
Evgeny 97d45fb2a6 smp server: pass server information via CLI during server initialization (#1356)
* smp server: pass server information via CLI during server initialization

* more info

* enable client expiration by default, disable port 8000

* update
2024-10-06 12:34:44 +01:00
Evgeny 50e4abf836 ntf server: control port (#1354)
* ntf server: control port

* version .405

* control

* use own_server_domains from INI file

* fix subs by server in control port

* bigger queues

* ntf server: only print subscriptions per own server when they are > 0

* fix tests

* revert version change

* dont import listThreads in ghc 8.10.7
2024-10-06 12:33:14 +01:00
spaced4ndy b130ea439a server: fix msgGetNoMsg stat (#1358) 2024-10-04 17:19:05 +04:00
sh 0e921d3258 scripts: preparations for new deployments (#1357)
* scripts/install: allow specifying version

* scripts/update: allow specifying version

* scripts/services: allow 443 for smp, allow infinite startup
2024-10-04 09:57:07 +01:00
Evgeny d0f65329fb ntf server: enable pings on ntf subscriptions (#1353) 2024-10-03 12:12:55 +01:00
Evgeny Poberezkin 61b2b9df1a Re-commit: smp server: persist notifications to avoid losing them when ntf server is offline (#1336)
This reverts commit 0ba3e69872.
2024-10-02 12:23:46 +01:00
Evgeny Poberezkin 04e4a37d85 6.1.0.4 2024-10-02 09:51:29 +01:00
Evgeny Poberezkin 0ba3e69872 Revert "smp server: persist notifications to avoid losing them when ntf server is offline (#1336)"
This reverts commit 0739f7b702.
2024-10-02 09:49:43 +01:00
Evgeny Poberezkin 7488923fc3 6.0.6.0 2024-10-02 09:24:29 +01:00
Evgeny cf559ee43f smp server: fix notifiers lost when compacting store log (#1347) 2024-10-02 09:24:04 +01:00
Evgeny 2ab0c2a7c6 smp server: fix notifiers lost when compacting store log (#1347) 2024-10-01 22:28:01 +01:00
spaced4ndy 243548631a ntf: repeat failed token deletions (#1346) 2024-10-01 16:30:14 +04:00
Evgeny Poberezkin da79d544cf 6.1.0.3 2024-09-30 18:27:14 +01:00
spaced4ndy e808825c95 ntf: fix endless worker loop (#1345) 2024-09-30 15:29:14 +01:00
Evgeny Poberezkin a9576935cf 6.1.0.2 2024-09-29 18:54:04 +01:00
spaced4ndyandEvgeny Poberezkin 9cf6c97137 ntf: batch ntf worker command processing (#1337)
* ntf: batch ntf worker command processing

* remove comment

* change batch size

* wip

* catch

* refactor

* refactor

* batch check

* refactor

* reschedule

* increase ntfSubCheckInterval

* first check interval

* check more statuses

* refactor, remove foldr'

* refactor 2

* refactor client

* refactor 3

* ntf server: improve support for batched commands (#1340)

* re-create ntf subscriptions on NTF AUTH errors

* name

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-29 18:50:38 +01:00
Evgeny 0739f7b702 smp server: persist notifications to avoid losing them when ntf server is offline (#1336)
* ntf server: types for storing notifications

* fix tests

* remove comments

* batch NMSGs test

* fix test

* thread to expire notifications

* persist notifications on restart

* optimize

* refactor

* remove reverse

* control port stats
2024-09-29 18:50:16 +01:00
Evgeny e12710fa55 smp server: show version on server page (#1341) 2024-09-29 18:46:38 +01:00
EvgenyandIC Rainbow 2a120dfe57 smp server: Allow serving HTTPS and transport on the same port (v2) (#1327)
* smp-server: Allow serving HTTPS and transport on the same port

* update rfc

* servers: refactor TLS credentials

* provide server credentials in SNI hook

* determine TLS server params dynamically, when starting the server

* remove alpn from TransportServerConfig to decide it dynamically where server is started

* diff

* combine HTTP and SMP on the shared port

* Update to SockAddr

* Fix params and web.https parser

* Switch fork urls

* WIP: add smpServerTestStatic test

* Update warp-tls repo

* shared connection tests

* cleanup

* Add protocol tests

* rename cert file, enable both ports and web by default

* terminate with message on missing credentials

* test cert file

* client option to use port 443 as default SMP port

* use SNI in non-SMP clients

* supported

* remove TODO

* advice

* fix test build

* Add RSA-4096 check for web creds, fix test

* Remove directory listing from static app

* message

* messages

* update log tests

---------

Co-authored-by: IC Rainbow <aenor.realm@gmail.com>
2024-09-28 23:15:17 +01:00
Evgeny 3c18c4b66a servers: refactor TLS credentials (#1326)
* servers: refactor TLS credentials

* provide server credentials in SNI hook

* determine TLS server params dynamically, when starting the server

* remove alpn from TransportServerConfig to decide it dynamically where server is started
2024-09-28 22:21:08 +01:00
Evgeny 21eee2b548 smp servers: separately account for subscribed clients, to disconnect those without subscriptions more effectively (and to be able to deliver notifications to subscribed ntf servers) (#1339) 2024-09-28 19:59:53 +01:00
Evgeny Poberezkin 03168b9fbf 6.1.0.1 2024-09-26 12:23:31 +01:00
spaced4ndy 41e1b2bd39 ntf: use common function for differentiating temporary errors (#1333) 2024-09-25 18:40:59 +01:00
Evgeny 1afa0206a5 smp: increase server and default agent queue sizes (#1335) 2024-09-25 18:33:45 +01:00
spaced4ndyandEvgeny Poberezkin 81fcdf8ac9 ntf: batch smp worker command processing (#1331)
* ntf: refactor smp worker (separate command processing into steps)

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-25 15:00:06 +04:00
Evgeny 1f67b403de agent: function to get multiple work items (#1330) 2024-09-25 10:55:45 +01:00
Evgeny 8e7f3f7b27 agent: return full argument with batched results (#1332) 2024-09-24 16:16:52 +01:00
Evgeny Poberezkin e6e9f1d5a5 Merge branch 'stable' 2024-09-24 12:36:05 +01:00
Evgeny Poberezkin 4268b90763 6.0.5.0 2024-09-24 12:34:46 +01:00
Evgeny 856137b763 rfс: efficient message storage (#1295)
* rfs: efficient message storage

* typos

* update
2024-09-24 12:29:44 +01:00
Evgeny d0ee505c3a rfc: protocol for SMP queue data blobs and short links (#1309)
* rfc: protocol for SMP queue data blobs and short links

* update

* private rendezvous
2024-09-24 12:29:11 +01:00
Evgeny 7dcac19a67 xrcp: session encryption with forward secrecy (#1328)
* xrcp: session encryption with forward secrecy

* update doc

* accept keys as parameters

* docs: counter

* spec corrections
2024-09-24 09:22:26 +01:00
Evgeny 22260cd719 agent: transport isolation mode "Session" (default) to use new SOCKS credentials when client restarts or SOCKS proxy configuration changes (#1321)
* agent: transport isolation mode "Session" (default) to use new SOCKS credentials when client restarts or SOCKS proxy configuration changes

* fix test
2024-09-22 22:22:05 +01:00
spaced4ndyandEvgeny Poberezkin bef11e4cbe agent: ntf errs (#1316)
* agent: ntf errs

* refactor

* errs

* refactor

* simplify

* inline

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-20 20:08:42 +04:00
Evgeny Poberezkin d5a65656f5 test: socks proxy parameter encoding 2024-09-16 13:15:30 +01:00
Evgeny Poberezkin 309ef3766c 6.1.0.0 2024-09-16 07:28:50 +01:00
Evgeny Poberezkin bde7b97f6a Merge branch 'stable' 2024-09-15 21:38:22 +01:00
Evgeny 398dbeedea servers: allow multiple server ports for SMP and Ntf servers (#1319)
* servers: allow multiple server ports for SMP and Ntf servers

* fix
2024-09-15 21:37:09 +01:00
Evgeny fa772af6c6 agent: support socks proxy without isolate-by-auth, with and without credentials (#1320)
* agent: support socks proxy without isolate-by-auth, with and without credentials

* add unit tests

* make xftp use correct SOCKS credentials

* rename

* support ipv6 in brackets, test parsing

* constant

* textToHostMode

* space
2024-09-15 21:36:31 +01:00
Evgeny Poberezkin 9ebdd2b0ea Merge branch 'stable' 2024-09-14 19:00:36 +01:00
Evgeny bec4e5e038 smp: reduce max message sizes (#1318) 2024-09-14 17:34:29 +01:00
spaced4ndy ea67b34ba7 ntf: batch supervisor commands (#1313) 2024-09-13 15:26:57 +04:00
spaced4ndyandEvgeny Poberezkin e247f691cb agent: do not reuse notifier credentials when re-creating notification subscription; on deleting ntf sub delete directly on smp server skipping ntf server (#1311)
* agent: change notifier credentials when re-creating notification subscription

* skip ndel

* enable tests

* fix race

* Revert "fix race"

This reverts commit ed9b18e8a7.

* delete record

* rename

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-09-12 14:02:51 +04:00
Evgeny Poberezkin 896b2425a4 xrcp: use SHA3-256 in hybrid key agreement (#1302)
This reverts commit 62133ceb24.
2024-09-11 19:32:40 +01:00
spaced4ndy 30e15b881f agent: don't prohibit deletion of notifications token if different is passed (fixes turning notifications off after importing database) (#1308)
* agent: don't prohibit deletion of notifications token if different is passed (fixes turning notifications off after importing database)

* ignore error

* notify
2024-09-11 19:30:59 +01:00
Evgeny Poberezkin 24ded9e5a0 Merge branch 'master' into stable 2024-09-11 19:03:29 +01:00
Evgeny Poberezkin f5e666ae4f 6.0.4.0 2024-09-11 18:51:26 +01:00
Evgeny Poberezkin 62133ceb24 Revert "xrcp: use SHA3-256 in hybrid key agreement (#1302)"
This reverts commit 67d38090ed.
2024-09-11 18:45:44 +01:00
Evgeny 3b50e1fb7d ntf server: only use SOCKS proxy for servers without public address (#1314) 2024-09-11 18:41:40 +01:00
Evgeny 7c25b3b1e0 smp protocol: send DELD when subscribed queue is deleted (#1312)
* smp protocol: send DELD when subscribed queue is deleted

* fix, test

* refactor

* send DELD event only if the client supports it (version 10); send END otherwise

* fix test

* notify on notifier rotation

* increase test delays
2024-09-11 13:16:51 +01:00
Evgeny a70bd02c67 xftp server: round down file creation time to 1 hour (#1310) 2024-09-10 08:14:05 +01:00
Evgeny 990dcec348 smp server: add created/updated/used date to queues to manage expiration (#1306)
* smp server: add created/updated/used date to queues to manage expiration, all: make Map updates strict in value

* remove strict

* remove time precision

* diff

* style

* only update when time changed
2024-09-09 14:53:11 +01:00
Evgeny 75712641ee rfc: stabilize iOS notifications (#1221) 2024-09-09 17:07:32 +04:00
spaced4ndy 946e16339e agent: process last notification from list (#1307) 2024-09-09 16:42:14 +04:00
spaced4ndy 092ed088ca ntf: support for multiple messages encoding (#1305) 2024-09-09 16:03:17 +04:00
sh 7000431249 scripts/servers: update stopscript (#1286)
* scripts/servers: update stopscript

* Major refactoring

* archive -> backups

* simplify logic

* sort files by timestamp
2024-09-09 10:19:17 +01:00
Evgeny dab1980d79 xftp: report receive file error with redirected file ID, when redirect is present (#1304)
* xftp: report receive file error with redirected file ID, when redirect is present

* fix test
2024-09-09 08:08:16 +01:00
Evgeny 344a295845 agent: error when user record is not in database (#1303) 2024-09-08 16:50:22 +01:00
Evgeny 67d38090ed xrcp: use SHA3-256 in hybrid key agreement (#1302) 2024-09-08 15:45:45 +01:00
Evgeny a9e8d02593 server: bind control port server only to 127.0.0.1 for better security (in case of firewall misconfuguration) (#1280) 2024-09-05 13:48:09 +01:00
Evgeny d859f27999 ntf server: remove debug logging (#1284) 2024-09-05 13:26:34 +01:00
Evgeny e86338d555 smp server: fewer map updates on re-subscriptions (#1297)
* smp server: fewer map updates on re-subscriptions

* temp version

* replace Client with ClientId in queues

* version

* version

* comments

* reduce threads when sending ENDs

* revert version
2024-09-05 13:25:41 +01:00
Evgeny 137ebc1cad servers: reduce memory used for period stats (#1298) 2024-09-02 23:12:08 +01:00
Evgeny d84a49b85a smp server: split and reduce STM transactions (#1294) 2024-09-02 17:06:31 +01:00
Evgeny d5efe3406a agent: fix race when sending a message to the deleted connection (#1296) 2024-09-02 15:07:16 +01:00
Evgeny Poberezkin d559a66145 6.0.3.0 2024-08-30 12:55:17 +01:00
Evgeny ce6777b68d newtype for server entity IDs, fix TRcvQueues (#1290)
* put DRG state to IORef, split STM transaction of sending notification (#1288)

* put DRG state to IORef, split STM transaction of sending notification

* remove comment

* remove comment

* add comment

* revert version

* newtype for server entity IDs, fix TRcvQueues

* Revert "put DRG state to IORef, split STM transaction of sending notification (#1288)"

This reverts commit 517933d189.

* logServer
2024-08-30 12:50:02 +01:00
Evgeny 655e7ad7d5 smp server: get message queue faster, avoiding STM contention if queue exists, split transaction for notification delivery (#1289)
* put DRG state to IORef, split STM transaction of sending notification (#1288)

* put DRG state to IORef, split STM transaction of sending notification

* remove comment

* remove comment

* add comment

* revert version

* smp server: get message queue faster, avoiding STM contention if queue exists

* IORef for counter

* Revert "put DRG state to IORef, split STM transaction of sending notification (#1288)"

This reverts commit 517933d189.

* version

* remove IORef

* split notification delivery transations

* revert version
2024-08-30 11:53:22 +01:00
Evgeny 9596a03139 servers: reduce STM transactions (#1287)
* servers: reduce STM transactions

* switch stats and pending ENDs to IORef

* more IORef, split pending ENDs to use in one thread
2024-08-29 13:18:12 +01:00
Evgeny 2e7e476f81 smp server: remove "expensive" stats (#1285) 2024-08-28 18:20:44 +01:00
Evgeny 2c9ad74599 smp server: batch END responses when subscribed client switches (version 2) (#1283)
* fix incorrect entity ID for notification subscription END when queue is deleted

* throttle sending ENDs

* fix stats

* clean up
2024-08-28 10:14:41 +01:00
Evgeny Poberezkin aa60bd6770 use strict Maps, fix stats for sent END batches 2024-08-26 19:35:57 +01:00
Evgeny 16cf5c8628 smp server: stats for END events and for SUB/DEL event batches (#1281)
* smp server: count queued and sent END events

* fix

* shadowing

* stats for batches

* fix
2024-08-26 14:58:18 +01:00
Evgeny Poberezkin 56986f82c8 6.0.2.0 2024-08-24 14:54:32 +01:00
Evgeny 53e829a21c agent: store query errors, reduce slow query threshold to 1ms (#1277)
* agent: collect query errors stats

* simplify

* test

* use microseconds

* parens

* revert change to track all queries, reduce threshold to 1 ms
2024-08-24 14:51:26 +01:00
Evgeny ab4acadcc1 agent: fix stuck connection commands when server is not responding (#1276)
* agent: fix stuck connection commands when server is not responding

* fix

* rename

* test SKEY retry
2024-08-24 13:54:20 +01:00
Evgeny 325e679bc4 ntf server: reduce PING interval to 1 minute, only mark subscriptions with END if it is received by the currently active client (#1275) 2024-08-24 11:34:59 +01:00
Evgeny Poberezkin d81cae712e Merge branch 'master' into stable 2024-08-23 14:41:15 +01:00
Evgeny Poberezkin 1d22608f86 6.0.1.0 2024-08-20 18:46:42 +01:00
Evgeny 39f3cf3082 agent: only create notification subscriptions to connections without errors or with temporary errors (#1270) 2024-08-20 16:05:50 +01:00
sh 5be81c562f scripts: update install/update (#1271) 2024-08-20 15:57:29 +01:00
Evgeny 2bdda1ff1f servers: safe compacting of store logs (#1269)
* servers: safe compacting of store logs

* fix
2024-08-20 13:36:37 +01:00
Evgeny ac930dff30 smp server: possible race when creating client that might lead to memory leak (#1260) 2024-08-20 12:25:58 +01:00
DiogoandEvgeny Poberezkin 1cbf8c0015 agent: support changing user of the new connection (#1267)
* agent: support new connection user id update

* another way for assertion

* add more tests to setConnUserId

* remove fdescribes

* allow rcv connection to change user id

* add functional test to api

* remove fdescribe

* refactor

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-08-20 10:57:09 +01:00
Evgeny 571d148bdf agent: do not start delivery workers when there are no messages to deliver (#1263)
* agent: use weak ThreadId and forkIO in workers instead of async (reduce memory)

* agent: do not start and exit delivery workers when there are no messages to deliver (#1264)

* agent: exit delivery workers when no messages to deliver

* only start delivery workers when there are pending messages

* fix

* focus test

* enable all tests

* lift

* do not exit workers when there is no work
2024-08-18 21:30:06 +01:00
Evgeny 32a150eb40 agent: enable notifications for all connections (#1262) 2024-08-18 14:07:09 +01:00
EvgenyandAlexander Bondarenko 5ad6e5f2f3 deps: upgrade tls to 1.9 (#1265)
* deps: use tls-2.0

* roll back RCP "cleanup"

* use tls 1.9

---------

Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com>
2024-08-18 13:55:12 +01:00
Evgeny f229e135e3 agent: reduce subscription transactions (#1259)
* agent: reduce subscription transactions

* nub

* remove commented
2024-08-16 10:06:22 +01:00
Evgeny Poberezkin d15d87c111 Merge branch 'master' into stable 2024-08-11 18:06:11 +01:00
Evgeny Poberezkin 0dd52dc69f v6.0.0 (6.0.0.8) 2024-08-09 23:09:09 +01:00
Evgeny 47dd0c4c64 agent: do not send worker crash errors when agent is not active (stopped) (#1256) 2024-08-09 21:10:27 +01:00
Evgeny 7762b4d364 reduce the number of STM transactions (#1255)
* reduce the number of STM transactions

* reduce STM more

* refactor

* remove more
2024-08-09 16:38:46 +01:00
Evgeny Poberezkin 7d8457263b 6.0.0.7 2024-08-08 13:15:01 +01:00
Evgeny fc6b9c0e1b agent: stop statistics and cleanup when chat is stopped (#1251) 2024-08-07 13:14:09 +01:00
Evgeny Poberezkin a76e15fd77 6.0.0.6 2024-08-06 12:39:11 +01:00
Evgeny a017047c52 smp server: fix race when client is marked as subscribed after it is disconnected, preventing its GC (#1250)
* smp server: fix race when client is marked as subscribed after it is disconnected, preventing its GC

* refactor
2024-08-06 08:17:38 +01:00
Evgeny f4d1a33ed8 agent: retry loop that resumes subscriptions as soon as agent is moved to foreground, suspend retry loops while agent is suspended (#1249)
* agent: retry loop that resumes subscriptions as soon as agent is moved to foreground, suspend retry loops while agent is suspended

* reset retry enterval when moving to foreground

* account for network state too

* simplify

* typo

* simplify
2024-08-05 15:09:46 +01:00
Evgeny Poberezkin 03ea151be5 agent: support additional messages for connection in message batches (#1247)
* agent: support additional messages for connection in message batches

* fix, test

* test 2 connections

* use TVar instead of TMVar for GHC 8.10.7
2024-07-30 17:12:29 +01:00
Evgeny Poberezkin 83f8622b23 6.0.0.5 2024-07-29 21:02:22 +01:00
Evgeny Poberezkin 3753379ae4 agent: support dababase access with high priority for more responsive user actions (#1248)
* agent: support dababase access with high priority for more responsive user actions

* rename, export

* fix
2024-07-29 18:57:16 +01:00
Evgeny Poberezkin 2de16cfae8 6.0.0.4 2024-07-28 15:03:32 +01:00
Evgeny Poberezkinandspaced4ndy 5fa3c149e9 smp server: fix server-info, additional stats, allow expiring inactive clients which have prohibit subscriptions only (iOS NSE clients) (#1237)
* smp server: fix server-info

* fix

* faster saving messages

* remove comment

* move ProhibitSub out of TVar

* subscription stats

* stabilize test

* order

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>

* more notification stats

* count ntf stats

* update server-info

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-07-25 16:06:40 +01:00
Evgeny Poberezkin 9093c7b120 agent, ntf server: only mark subscriptions as pending that were created by the disconnected client (#1242)
* ntf server: only mark subscriptions as pending if the disconnected client is current

* add sessionId to subscribed queue

* add sessionId to subscriptions in ntf server agent

* fix
2024-07-25 13:07:28 +01:00
Evgeny Poberezkin 47ae921b86 use strict Map (#1241) 2024-07-23 08:27:54 +01:00
spaced4ndyandEvgeny Poberezkin c605156302 agent: getAgentSubsTotal api (#1238)
* agent: getAgentSubsTotal api

* move

* export

* count sessions

* simplify

* simplify 2

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-07-22 15:43:38 +01:00
Evgeny Poberezkin 051bf38bc7 agent: remove delays in notification processing, add notification stats (#1235)
* agent: remove delays in notification processing, add notification stats

* do not replace token after failed check

* more stats

* refactor

* fix

* backwards compatible JSON parsing

* retry deleting subscription on temporary error

* remove attempts to get multiple notification messages

* fix JSON decoding to be backwards compatible
2024-07-22 15:42:34 +01:00
Evgeny Poberezkin 8423c636a8 rename queueId to entId (can be message queue, proxied session and data blob ID to be added) (#1240) 2024-07-22 12:01:38 +01:00
Evgeny Poberezkin cc5732f41f smp server: stats for commands retrieving messages when notifications are received (#1236)
* more stats

* fix stats
2024-07-20 14:10:10 +01:00
Evgeny Poberezkin 1ac0068d22 Merge commit 'f392ce0a9355cd3883400906ae6c361b77ca46ea' into stable 2024-07-03 14:09:54 +01:00
Evgeny Poberezkin e371a4a62d Merge branch 'master' into stable 2024-06-16 07:58:05 -07:00
sh 369efc24c2 scripts: update install/update scripts (#1192) 2024-06-08 08:12:16 +01:00
210 changed files with 24481 additions and 9127 deletions
+44
View File
@@ -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
View File
@@ -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') && matrix.os == 'ubuntu-22.04'
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
+6 -6
View File
@@ -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: |
+45
View File
@@ -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
+203
View File
@@ -1,3 +1,206 @@
# 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.
# 6.1.2
Servers: more reliable restoring of state.
SMP server: reduced memory usage and faster start.
Notifications: compensate for iOS notifications being droppted by Apple while device is offline (#1378):
- Ntf server: send multiple SMP notifications in one iOS notification.
- Agent: get multiple messages for one iOS notification.
# 6.1.1
SMP:
- stop server faster (#1371)
- add STORE error (#1372)
# 6.1.0
Version 6.1.0.7
SMP server and client:
- transport block encryption (#1317).
Agent:
- batch and optimize iOS notifications processing (#1308, #1311, #1313, #1316, #1330, #1331, #1333, #1337, #1346).
- allow receiving multiple messages from single iOS notification (#1355, #1362).
- prepare connection to accept to avoid race condition with events (#1365).
- transport isolation mode "Session" (default) to use new SOCKS credentials when client restarts or SOCKS proxy configuration changes (#1321).
Ntf server:
- control port (#1354).
- enable pings on ntf subscriptions, to resubscribe on reconnection (#1353).
SMP server:
- support multiple server ports (#1319).
- support serving HTTPS and SMP transport on the same port (#1326, #1327).
- persist iOS notifications to avoid losing them when Ntf server is offline (#1336, #1339, #1350).
- fix lost notification subscriptions (#1347).
- reject SKEY with different key earlier, at verification step (#1366).
- pass server information via CLI during server initialization (#1356).
- show version on server page (#1341).
- explicit graceful shutdown on SIGINT (#1360).
XRCP (remote access protocol):
- use SHA3-256 in hybrid key agreement (#1302).
- session encryption with forward secrecy (#1328).
# 6.0.5
SMP agent:
- support generic SOCKS proxy (without isolate-by-auth).
- reduce max message sizes
# 6.0.4
SMP server:
- better performance/memory: fewer map updates on re-subscriptions (#1297), split and reduce STM transactions (#1294)
- send DELD when subscribed queue is deleted (#1312)
- add created/updated/used date to queues to manage expiration (#1306)
XFTP server: truncate file creation time to 1 hour (#1310)
Servers:
- bind control port only to 127.0.0.1 for better security in case of firewall misconfiguration (#1280)
- reduce memory used for period stats (#1298)
Agent: process last notification from list (#1307)
- report receive file error with redirected file ID, when redirect is present (#1304)
- special error when deleted user record is not in database (#1303)
- fix race when sending a message to the deleted connection (#1296)
- support for multiple messages in a single notification
Ntf server:
- only use SOCKS proxy for servers without public address (#1314)
# 6.0.3
Agent:
- fix possible stuck queue rotation (#1290).
SMP server:
- batch END responses when subscribed client switches to reduce server and client traffic.
- reduce STM transactions for better performance.
- add stats for END events and for SUB/DEL event batches.
- remove "expensive" stats to save memory.
# 6.0.2
SMP agent:
- fix stuck connection commands when a server is not responding.
- store query errors, reduce slow query threshold to 1ms.
Notification server:
- reduce PING interval to 1 minute.
- fix subscriptions disabled on race condition (only mark subscriptions with END status when received via the active connection).
# 6.0.1
SMP agent:
- support changing user of the new connection.
- do not start delivery workers when there are no messages to deliver.
- enable notifications for all connections.
- combine database transactions when subscribing.
SMP server:
- safe compacting of store log.
- fix possible race when creating client that might lead to memory leak.
Dependencies: upgrade tls to 1.9
# 6.0.0
Version 6.0.0.8
Agent:
- enabled fast handshake support.
- batch-send multiple messages in each connection.
- resume subscriptions as soon as agent moves to foreground or as network connection resumes.
- "known" servers to determine whether to use SMP proxy.
- retry on SMP proxy NO_SESSION error.
- fixes to notification subscriptions.
- persistent server statistics.
- better concurrency.
SMP server:
- reduce threads usage.
- additional statistics.
- improve disabling inactive clients.
- additional control port commands for monitoring.
Notification server:
- support onion-only SMP servers.
# 5.8.2
Agent:
+31 -8
View File
@@ -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
+31
View File
@@ -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
+9 -2
View File
@@ -149,8 +149,15 @@ On Linux, you can deploy smp and xftp server using Docker. This will download im
You can install and setup servers automatically using our script:
```sh
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/install.sh -o simplex-server-install.sh \
&& if echo 'b8cf2be103f21f9461d9a500bcd3db06ab7d01d68871b07f4bd245195cbead1d simplex-server-install.sh' | sha256sum -c; then chmod +x ./simplex-server-install.sh && ./simplex-server-install.sh; rm ./simplex-server-install.sh; else echo "SHA-256 checksum is incorrect!" && rm ./simplex-server-install.sh; fi
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/install.sh -o simplex-server-install.sh &&\
if echo '53fcdb4ceab324316e2c4cda7e84dbbb344f32550a65975a7895425e5a1be757 simplex-server-install.sh' | sha256sum -c; then
chmod +x ./simplex-server-install.sh
./simplex-server-install.sh
rm ./simplex-server-install.sh
else
echo "SHA-256 checksum is incorrect!"
rm ./simplex-server-install.sh
fi
```
### Build from source
+1 -1
View File
@@ -15,7 +15,7 @@ logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
main :: IO ()
main = do
setLogLevel LogDebug -- change to LogError in production
setLogLevel LogInfo
cfgPath <- getEnvPath "NTF_SERVER_CFG_PATH" defaultCfgPath
logPath <- getEnvPath "NTF_SERVER_LOG_PATH" defaultLogPath
withGlobalLogging logCfg $ ntfServerCLI cfgPath logPath
+1 -1
View File
@@ -19,4 +19,4 @@ main = do
setLogLevel LogDebug
cfgPath <- getEnvPath "SMP_SERVER_CFG_PATH" defaultCfgPath
logPath <- getEnvPath "SMP_SERVER_LOG_PATH" defaultLogPath
withGlobalLogging logCfg $ smpServerCLI_ Static.generateSite Static.serveStaticFiles cfgPath logPath
withGlobalLogging logCfg $ smpServerCLI_ Static.generateSite Static.serveStaticFiles Static.attachStaticFiles 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
View File
@@ -0,0 +1 @@
../link.html
+1
View File
@@ -0,0 +1 @@
../link.html
+1
View File
@@ -0,0 +1 @@
../link.html
+10
View File
@@ -221,6 +221,10 @@
Public information
</h2>
<table id="public-info">
<tr class="text-grey-black dark:text-white text-base">
<td>Server version:</td>
<td>${version}</td>
</tr>
<tr class="text-grey-black dark:text-white text-base">
<td>Source code:</td>
<td><a href="${sourceCode}" target="_blank">${sourceCode}</a></td>
@@ -291,6 +295,12 @@
<td>${hostingEntity} (${hostingCountry})</td>
</tr>
</x-hosting>
<x-hostingType>
<tr class="text-grey-black dark:text-white text-base">
<td>Hosting type:</td>
<td>${hostingType}</td>
</tr>
</x-hostingType>
<x-serverCountry>
<tr class="text-grey-black dark:text-white text-base">
<td>Server country:</td>
+12 -13
View File
@@ -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&nbsp;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&nbsp;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&nbsp;network'
} else if (url.includes('/a') || url.includes('/contact')) {
element.innerHTML = 'This is a public address of the SimpleX&nbsp;network user'
} else if (url.includes('/i') || url.includes('/invitation')) {
element.innerHTML = 'This is a one-time link of the SimpleX&nbsp;network user'
} else if (url.includes('/c')) {
element.innerHTML = 'This is a public channel address on SimpleX&nbsp;network'
}
}
</script>
+12 -1
View File
@@ -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 {
+82 -14
View File
@@ -7,45 +7,111 @@ module Static where
import Control.Logger.Simple
import Control.Monad
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as B
import Data.Char (toUpper)
import Data.IORef (readIORef)
import Data.Maybe (fromMaybe)
import Data.String (fromString)
import Data.Text.Encoding (encodeUtf8)
import Network.Wai.Application.Static as S
import Network.Wai.Handler.Warp as W
import qualified Network.Wai.Handler.WarpTLS as W
import Network.Socket (getPeerName)
import Network.Wai (Application, 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
import qualified Network.Wai.Handler.WarpTLS as WT
import Simplex.Messaging.Encoding.String (strEncode)
import Simplex.Messaging.Server (AttachHTTP)
import Simplex.Messaging.Server.Information
import Simplex.Messaging.Server.Main (EmbeddedWebParams (..), WebHttpsParams (..))
import Simplex.Messaging.Transport (simplexMQVersion)
import Simplex.Messaging.Transport.Client (TransportHost (..))
import Simplex.Messaging.Util (tshow)
import Static.Embedded as E
import System.Directory (createDirectoryIfMissing)
import System.FilePath
import UnliftIO.Concurrent (forkFinally)
import UnliftIO.Exception (bracket, finally)
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)"
W.runTLS (W.tlsSettings cert key) (mkSettings port) (S.staticApp $ S.defaultFileServerSettings webStaticPath)
WT.runTLS (WT.tlsSettings cert key) (mkSettings port) app
where
mkSettings port = setPort port defaultSettings
app = staticFiles webStaticPath
mkSettings port = W.setPort port warpSettings
-- | Prepare context and prepare HTTP handler for TLS connections that already passed TLS.handshake and ALPN check.
attachStaticFiles :: FilePath -> (AttachHTTP -> IO ()) -> IO ()
attachStaticFiles path action =
-- Initialize global internal state for http server.
WI.withII warpSettings $ \ii -> do
action $ \socket cxt -> do
-- Initialize internal per-connection resources.
addr <- getPeerName socket
withConnection addr cxt $ \(conn, transport) ->
withTimeout ii conn $ \th ->
-- Run Warp connection handler to process HTTP requests for static files.
WI.serveConnection conn ii th addr transport warpSettings app
where
app = staticFiles path
-- from warp-tls
withConnection socket cxt = bracket (WT.attachConn socket cxt) (terminate . fst)
-- from warp
withTimeout ii conn =
bracket
(WI.registerKillThread (WI.timeoutManager ii) (WI.connClose conn))
WI.cancel
-- shared clean up
terminate conn = WI.connClose conn `finally` (readIORef (WI.connWriteBuffer conn) >>= WI.bufFree)
warpSettings :: W.Settings
warpSettings = W.setGracefulShutdownTimeout (Just 1) W.defaultSettings
staticFiles :: FilePath -> Application
staticFiles root = S.staticApp settings . changeWellKnownPath
where
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
@@ -78,6 +144,7 @@ serverInformation ServerInformation {config, information} onionHost = render E.i
where
basic =
[ ("sourceCode", Just . encodeUtf8 $ sourceCode spi),
("version", Just $ B.pack simplexMQVersion),
("website", encodeUtf8 <$> website spi)
]
conds ServerConditions {conditions, amendments} =
@@ -109,7 +176,8 @@ serverInformation ServerInformation {config, information} onionHost = render E.i
("hostingCountry", encodeUtf8 <$> country)
]
server =
[ ("serverCountry", fmap encodeUtf8 $ serverCountry =<< information)
[ ("serverCountry", encodeUtf8 <$> serverCountry spi),
("hostingType", (\s -> maybe s (\(c, rest) -> toUpper c `B.cons` rest) $ B.uncons s) . strEncode <$> hostingType spi)
]
-- Copy-pasted from simplex-chat Simplex.Chat.Types.Preferences
+3
View File
@@ -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/")
+23
View File
@@ -4,6 +4,15 @@ packages: .
-- packages: . ../http2
-- packages: . ../network-transport
-- uncomment two sections below to run tests with coverage
-- package *
-- coverage: True
-- library-coverage: True
-- package attoparsec
-- coverage: False
-- library-coverage: False
index-state: 2023-12-12T00:00:00Z
package cryptostore
@@ -28,3 +37,17 @@ source-repository-package
type: git
location: https://github.com/simplex-chat/sqlcipher-simple.git
tag: a46bd361a19376c5211f1058908fc0ae6bf42446
-- waiting for published warp-tls-3.4.7
source-repository-package
type: git
location: https://github.com/yesodweb/wai.git
tag: ec5e017d896a78e787a5acea62b37a4e677dec2e
subdir: warp-tls
-- backported fork due http-5.0
source-repository-package
type: git
location: https://github.com/simplex-chat/wai.git
tag: 2f6e5aa5f05ba9140ac99e195ee647b4f7d926b0
subdir: warp
+105 -21
View File
@@ -2,10 +2,6 @@
set -eu
# Links to scripts/configs
bin="https://github.com/simplex-chat/simplexmq/releases/latest/download"
bin_smp="$bin/smp-server-ubuntu-20_04-x86-64"
bin_xftp="$bin/xftp-server-ubuntu-20_04-x86-64"
scripts="https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/scripts/main"
scripts_systemd_smp="$scripts/smp-server.service"
scripts_systemd_xftp="$scripts/xftp-server.service"
@@ -26,6 +22,8 @@ path_conf_var="/var/opt"
path_conf_smp="$path_conf_etc/simplex $path_conf_var/simplex"
path_conf_xftp="$path_conf_etc/simplex-xftp $path_conf_var/simplex-xftp /srv/xftp"
path_conf_info="$path_conf_etc/simplex-info"
path_systemd="/etc/systemd/system"
path_systemd_smp="$path_systemd/smp-server.service"
path_systemd_xftp="$path_systemd/xftp-server.service"
@@ -56,7 +54,7 @@ ${GRN}1.${NC} Install latest binaries from GitHub releases:
${GRN}2.${NC} Create server directories:
- smp: ${YLW}${path_conf_smp}${NC}
- xftp: ${YLW}${path_conf_xftp}${NC}
${GRN}3.${NC} Setup user for each server:
${GRN}3.${NC} Setup user for server:
- xmp: ${YLW}${user_smp}${NC}
- xftp: ${YLW}${user_xftp}${NC}
${GRN}4.${NC} Create systemd services:
@@ -65,7 +63,12 @@ ${GRN}4.${NC} Create systemd services:
${GRN}5.${NC} Install stopscript (systemd), update and uninstallation script:
- all: ${YLW}${path_bin_update}${NC}, ${YLW}${path_bin_uninstall}${NC}, ${YLW}${path_bin_stopscript}${NC}
Press ${GRN}ENTER${NC} to continue or ${RED}Ctrl+C${NC} to cancel installation"
Press:
- ${GRN}1${NC} to install smp server
- ${GRN}2${NC} to install xftp server
- ${RED}Ctrl+C${NC} to cancel installation
Selection: "
end="Installtion is complete!
@@ -76,27 +79,79 @@ Please checkout our server guides:
To uninstall with full clean-up, simply run: ${YLW}sudo /usr/local/bin/simplex-servers-uninstall${NC}
"
set_version() {
ver="${VER:-latest}"
case "$ver" in
latest)
bin="https://github.com/simplex-chat/simplexmq/releases/latest/download"
remote_version="$(curl --proto '=https' --tlsv1.2 -sSf -L https://api.github.com/repos/simplex-chat/simplexmq/releases/latest | grep -i "tag_name" | awk -F \" '{print $4}')"
;;
*)
bin="https://github.com/simplex-chat/simplexmq/releases/download/${ver}"
remote_version="${ver}"
;;
esac
}
os_test() {
. /etc/os-release
case "$VERSION_ID" in
20.04|22.04) : ;;
24.04) VERSION_ID='22.04' ;;
*) printf "${RED}Unsupported Ubuntu version!${NC}\nPlease file Github issue with request to support Ubuntu %s: https://github.com/simplex-chat/simplexmq/issues/new\n" "$VERSION_ID" && exit 1 ;;
esac
version="$(printf '%s' "$VERSION_ID" | tr '.' '_')"
arch="$(uname -p)"
case "$arch" in
x86_64) arch="$(printf '%s' "$arch" | tr '_' '-')" ;;
*) printf "${RED}Unsupported architecture!${NC}\nPlease file Github issue with request to support %s architecture: https://github.com/simplex-chat/simplexmq/issues/new" "$arch" && exit 1 ;;
esac
bin_smp="$bin/smp-server-ubuntu-${version}-${arch}"
bin_xftp="$bin/xftp-server-ubuntu-${version}-${arch}"
}
setup_bins() {
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_smp" -o "$path_bin_smp" && chmod +x "$path_bin_smp"
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_xftp" -o "$path_bin_xftp" && chmod +x "$path_bin_xftp"
eval "bin=\$bin_${1}"
eval "path=\$path_bin_${1}"
curl --proto '=https' --tlsv1.2 -sSf -L "$bin" -o "$path" && chmod +x "$path"
unset bin path
}
setup_users() {
useradd -M "$user_smp" 2> /dev/null || true
useradd -M "$user_xftp" 2> /dev/null || true
eval "user=\$user_${1}"
useradd -M "$user" 2> /dev/null || true
unset user
}
setup_dirs() {
# Unquoted varibles, so field splitting can occur
mkdir -p $path_conf_smp
chown "$user_smp":"$user_smp" $path_conf_smp
mkdir -p $path_conf_xftp
chown "$user_xftp":"$user_xftp" $path_conf_xftp
eval "path_conf=\$path_conf_${1}"
eval "user=\$user_${1}"
mkdir -p $path_conf
mkdir -p $path_conf_info
printf "local_version_%s='%s'\n" "$1" "$remote_version" >> "$path_conf_info/release"
chown -R "$user":"$user" $path_conf
unset path_conf user
}
setup_systemd() {
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd_smp" -o "$path_systemd_smp"
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd_xftp" -o "$path_systemd_xftp"
eval "scripts_systemd=\$scripts_systemd_${1}"
eval "path_systemd=\$path_systemd_${1}"
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd" -o "$path_systemd"
unset scripts_systemd path_systemd
}
setup_scripts() {
@@ -110,32 +165,61 @@ checks() {
printf "This script is intended to be run with root privileges. Please re-run script using sudo."
exit 1
fi
set_version
os_test
mkdir -p $path_conf_info
}
main() {
checks
printf "%b\n%b\n" "${BLU}$logo${NC}" "$welcome"
printf "%b\n%b" "${BLU}$logo${NC}" "$welcome"
read ans
case "$ans" in
1) setup='smp' ;;
2) setup='xftp' ;;
*) printf 'Installation aborted.\n' && exit 0 ;;
esac
printf "Installing binaries..."
setup_bins
for i in $setup; do
setup_bins "$i"
done
printf "${GRN} Done!${NC}\n"
printf "Creating users..."
setup_users
for i in $setup; do
setup_users "$i"
done
printf "${GRN} Done!${NC}\n"
printf "Creating directories..."
setup_dirs
for i in $setup; do
setup_dirs "$i"
done
printf "${GRN} Done!${NC}\n"
printf "Creating systemd services..."
setup_systemd
for i in $setup; do
setup_systemd "$i"
done
printf "${GRN} Done!${NC}\n"
printf "Installing stopscript, update and uninstallation script..."
setup_scripts
printf "${GRN} Done!${NC}\n"
printf "%b" "$end"
-211
View File
@@ -1,211 +0,0 @@
name: simplexmq
version: 6.0.0.3
synopsis: SimpleXMQ message broker
description: |
This package includes <./docs/Simplex-Messaging-Server.html server>,
<./docs/Simplex-Messaging-Client.html client> and
<./docs/Simplex-Messaging-Agent.html agent> for SMP protocols:
.
* <https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md SMP protocol>
* <https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md SMP agent protocol>
.
See <https://github.com/simplex-chat/simplex-chat terminal chat prototype> built with SimpleXMQ broker.
homepage: https://github.com/simplex-chat/simplexmq#readme
license: AGPL-3
author: simplex.chat
maintainer: chat@simplex.chat
copyright: 2020-2022 simplex.chat
category: Chat, Network, Web, System, Cryptography
extra-source-files:
- README.md
- CHANGELOG.md
- cbits/sha512.h
- cbits/sntrup761.h
- apps/smp-server/static/*.html
- apps/smp-server/static/media/*
dependencies:
- aeson == 2.2.*
- ansi-terminal >= 0.10 && < 0.12
- asn1-encoding == 0.9.*
- asn1-types == 0.3.*
- async == 2.2.*
- attoparsec == 0.14.*
- base >= 4.14 && < 5
- base64-bytestring >= 1.0 && < 1.3
- case-insensitive == 1.2.*
- composition == 1.0.*
- constraints >= 0.12 && < 0.14
- containers == 0.6.*
- crypton == 0.34.*
- crypton-x509 == 1.7.*
- crypton-x509-store == 1.6.*
- crypton-x509-validation == 1.6.*
- cryptostore == 0.3.*
- data-default == 0.7.*
- direct-sqlcipher == 2.3.*
- directory == 1.3.*
- filepath == 1.4.*
- hourglass == 0.2.*
- http-types == 0.12.*
- http2 >= 4.2.2 && < 4.3
- ini == 0.4.1
- iproute == 1.7.*
- iso8601-time == 0.1.*
- memory == 0.18.*
- mtl >= 2.3.1 && < 3.0
- network >= 3.1.2.7 && < 3.2
- network-info >= 0.2 && < 0.3
- network-transport == 0.5.6
- network-udp >= 0.0 && < 0.1
- optparse-applicative >= 0.15 && < 0.17
- 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.7.0 && < 1.8
- transformers == 0.6.*
- unliftio == 0.2.*
- unliftio-core == 0.2.*
- websockets == 0.12.*
- yaml == 0.11.*
- zstd == 0.1.3.*
flags:
swift:
description: Enable swift JSON format
manual: True
default: False
use_crypton:
description: Use crypton etc. in cryptostore
manual: True
default: True
# cpp-options:
# - -Dslow_servers
when:
- condition: flag(swift)
cpp-options:
- -DswiftJSON
- condition: impl(ghc >= 9.6.2)
dependencies:
- bytestring == 0.11.*
- template-haskell == 2.20.*
- text >= 2.0.1 && < 2.2
- condition: impl(ghc < 9.6.2)
dependencies:
- bytestring == 0.10.*
- template-haskell == 2.16.*
- text >= 1.2.3.0 && < 1.3
library:
source-dirs: src
c-sources:
- cbits/sha512.c
- cbits/sntrup761.c
include-dirs: cbits
extra-libraries: crypto
executables:
smp-server:
source-dirs:
- apps/smp-server
- apps/smp-server/web
main: Main.hs
dependencies:
- file-embed
- simplexmq
- wai-app-static
- warp
- warp-tls
ghc-options:
- -threaded
- -rtsopts
ntf-server:
source-dirs: apps/ntf-server
main: Main.hs
dependencies:
- simplexmq
ghc-options:
- -threaded
- -rtsopts
xftp-server:
source-dirs: apps/xftp-server
main: Main.hs
dependencies:
- simplexmq
ghc-options:
- -threaded
- -rtsopts
xftp:
source-dirs: apps/xftp
main: Main.hs
dependencies:
- simplexmq
ghc-options:
- -threaded
- -rtsopts
tests:
simplexmq-test:
source-dirs: tests
main: Test.hs
dependencies:
- simplexmq
- deepseq == 1.4.*
- generic-random == 1.5.*
- hspec == 2.11.*
- hspec-core == 2.11.*
- HUnit == 1.6.*
- QuickCheck == 2.14.*
- silently == 1.2.*
- main-tester == 0.2.*
- timeit == 2.0.*
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-A64M
- -with-rtsopts=-N1
ghc-options:
# - -haddock
- -Weverything
- -Wno-missing-exported-signatures
- -Wno-missing-import-lists
- -Wno-missed-specialisations
- -Wno-all-missed-specialisations
- -Wno-unsafe
- -Wno-safe
- -Wno-missing-local-signatures
- -Wno-missing-kind-signatures
- -Wno-missing-deriving-strategies
- -Wno-monomorphism-restriction
- -Wno-prepositive-qualified-module
- -Wno-unused-packages
- -Wno-implicit-prelude
- -Wno-missing-safe-haskell-mode
- -Wno-missing-export-lists
- -Wno-partial-fields
- -Wcompat
- -Werror=incomplete-record-updates
- -Werror=incomplete-patterns
- -Werror=incomplete-uni-patterns
- -Werror=missing-methods
- -Werror=tabs
- -Wredundant-constraints
- -Wincomplete-record-updates
- -Wunused-type-patterns
- -O2
default-extensions:
- StrictData
+5 -3
View File
@@ -559,11 +559,13 @@ In current implementation of XFTP protocol in SimpleX Chat clients don't use FAC
- perform traffic correlation attacks against senders and recipients and correlate senders and recipients within the monitored set, frustrated by the number of users on the servers.
- observe how much traffic is being sent, and make guesses as to its purpose
- observe how much traffic is being sent, and make guesses as to its purpose.
*cannot, even in case of a compromised transport protocol:*
- in case of a compromised transport protocol, correlate file senders and receivers.
- perform traffic correlation attacks with any increase in efficiency over a non-compromised transport protocol
*cannot, in case of a non-compromised transport protocol:*
- perform traffic correlation attacks.
#### XFTP server
+29 -9
View File
@@ -67,7 +67,7 @@ The session invitation contains this data:
- CA TLS certificate fingerprint of the controller - this is part of long term identity of the controller established during the first session, and repeated in the subsequent session announcements.
- Session Ed25519 public key used to verify the announcement and commands - this mitigates the compromise of the long term signature key, as the controller will have to sign each command with this key first.
- Long-term Ed25519 public key used to verify the announcement and commands - this is part of the long term controller identity.
- Session X25519 DH key and SNTRUP761 KEM encapsulation key to agree session encryption (both for multicast announcement and for commands and responses in TLS), as described in https://datatracker.ietf.org/doc/draft-josefsson-ntruprime-hybrid/. The new keys are used for each session, and if client key is already available (from the previous session), the computed shared secret will be used to encrypt the announcement multicast packet. The out-of-band invitation is unencrypted. DH public key and KEM encapsulation key are sent unencrypted. NaCL crypto_box is used for encryption.
- Session X25519 DH key to agree session encryption (both for multicast announcement and for commands and responses in TLS), as described in https://datatracker.ietf.org/doc/draft-josefsson-ntruprime-hybrid/. The new keys are used for each session, and if client key is already available (from the previous session), the computed shared secret will be used to encrypt the announcement multicast packet. The out-of-band invitation is unencrypted. DH public key and KEM encapsulation key are sent unencrypted. NaCL crypto_box is used for encryption.
Host application decrypts (except the first session) and validates the invitation:
- Session signature is valid.
@@ -184,7 +184,7 @@ The controller decrypts (including the first session) and validates the received
The controller should reply with with `ctrlHello` or `ctrlError` response:
```abnf
ctrlHello = %s"HELLO " kemCiphertext nonce encrypted(unpaddedSize ctrlHelloJSON helloPad) pad
ctrlHello = %s"HELLO " kemCiphertext encrypted(unpaddedSize ctrlHelloJSON helloPad) pad
; ctrlHelloJSON is encrypted with the hybrid secret,
; including both previously agreed DH secret and KEM secret from kemCiphertext
unpaddedSize = largeLength
@@ -206,6 +206,8 @@ JTD schema for the encrypted part of controller HELLO block `ctrlHelloJSON`:
}
```
Controller `hello` block and all subsequent protocol messages are encrypted with the chain keys derived from the hybrid key (see key exchange below) - that is why conntroller hello block does not include nonce. That provides forward secrecy within the XRCP session. Receiving this `hello` block allows host to compute the same hybrid keys and to derive the same chain keys.
Once the controller replies HELLO to the valid host HELLO block, it should stop accepting new TCP connections.
### Controller/host session operation
@@ -223,10 +225,12 @@ tlsunique channel binding from TLS session MUST be included in commands (include
The syntax for encrypted command and response body encoding:
```abnf
commandBody = encBody sessSignature idSignature [attachment]
responseBody = encBody [attachment] ; counter must match command
encBody = nonce encLength32 encrypted(tlsunique counter body)
attachment = %x01 nonce encLength32 encrypted(attachment)
commandBody = counter encBody sessSignature idSignature [attachment]
responseBody = counter encBody [attachment] ; counter must match command
; counter is placed outside of encrypted body to allow correlating encryption keys
; with the chain keys (each command and response are encrypted by different keys)
encBody = encLength32 encrypted(tlsunique body)
attachment = %x01 encLength32 encrypted(attachment)
noAttachment = %x00
tlsunique = length 1*OCTET
counter = 8*8 OCTET ; int64
@@ -239,7 +243,7 @@ If the command or response includes attachment, its hash must be included in com
Initial announcement is shared out-of-band (URI with xrcp scheme), and it is not encrypted.
This announcement contains only DH keys, as KEM key is too large to include in QR code, which are used to agree encryption key for host HELLO block. The host HELLO block will contain DH key in plaintext part and KEM encapsulation (public) key in encrypted part, that will be used to determine the shared secret (using SHA256 over concatenated DH shared secret and KEM encapsulated secret) both for controller HELLO response (that contains KEM ciphertext in plaintext part) and subsequent session commands and responses.
This announcement contains only DH keys, as KEM key is too large to include in QR code, which are used to agree encryption key for host HELLO block. The host HELLO block will contain DH key in plaintext part and KEM encapsulation (public) key in encrypted part, that will be used to determine the shared secret (using SHA3-256 over concatenated DH shared secret and KEM encapsulated secret) to derive keys for controller HELLO response (that contains KEM ciphertext in plaintext part) and subsequent session commands and responses.
During the next session the announcement is sent via encrypted multicast block. The shared key for this announcement and for host HELLO block is determined using the KEM shared secret from the previous session and DH shared secret computed using the host DH key from the previous session and the new controller DH key from the announcement.
@@ -250,7 +254,7 @@ In pseudo-code:
```
// session 1
hostHelloSecret(1) = dhSecret(1)
sessionSecret(1) = sha256(dhSecret(1) || kemSecret(1)) // to encrypt session 1 data, incl. controller hello
sessionSecret(1) = sha3-256(dhSecret(1) || kemSecret(1)) // to encrypt session 1 data, incl. controller hello
dhSecret(1) = dh(hostHelloDhKey(1), controllerInvitationDhKey(1))
kemCiphertext(1) = enc(kemSecret(1), kemEncKey(1))
// kemEncKey is included in host HELLO, kemCiphertext - in controller HELLO
@@ -262,7 +266,7 @@ dhSecret(n') = dh(hostHelloDhKey(n - 1), controllerDhKey(n))
// session n
hostHelloSecret(n) = dhSecret(n)
sessionSecret(n) = sha256(dhSecret(n) || kemSecret(n)) // to encrypt session n data, incl. controller hello
sessionSecret(n) = sha3-256(dhSecret(n) || kemSecret(n)) // to encrypt session n data, incl. controller hello
dhSecret(n) = dh(hostHelloDhKey(n), controllerDhKey(n))
// controllerDhKey(n) is either from invitation or from multicast announcement
kemCiphertext(n) = enc(kemSecret(n), kemEncKey(n))
@@ -273,6 +277,22 @@ If controller fails to store the new host DH key after receiving HELLO block, th
To decrypt a multicast announcement, the host should try to decrypt it using the keys of all known (paired) remote controllers.
Once kemSecret is agreed for the session, it is used to derive two chain keys, to receive and to send messages:
```
host: sndKey, rcvKey = HKDF(kemSecret, "SimpleXSbChainInit", 64)
controller: rcvKey, sndKey = HKDF(kemSecret, "SimpleXSbChainInit", 64)
```
where HKDF is based on SHA512, with empty salt.
Actual keys and nonces to encrypt and decrypt messages are derived from these chain keys:
```
to send: (sndKey', sk, nonce) = HKDF(sndKey, "SimpleXSbChain", 88)
to receive: (rcvKey', sk, nonce) = HKDF(rcvKey, "SimpleXSbChain", 88)
```
## Threat model
#### A passive network adversary able to monitor the site-local traffic:
+124
View File
@@ -0,0 +1,124 @@
# Short invitation links
## Problem
Long links look scary and unsafe for many users. While this is a perceived problem, rather than a real one, it hurts adoption.
What is worse, long links do not fit in profile descriptions of other social networks where people might want to advertize their contact addresses.
The current link size limitation is also the reason for not including PQ KEM keys into invitation links and addresses, postponing the moment when PQ-resistant encryption kicks in - if we include PQ KEM key into the link, the QR code will not be scannable.
Additionally, if we store short links, they can also include chat preferences and public profile data.
## Solution
MITM-resistant link shortening.
Instead of generating the random address that would resolve into the link - doing so would create the possibility of MITM by the server hosting this link - we can use private key as the link ID that will be passed to the accepting party, and the hash of the public key as ID for the server - the accepting party would present this key itself as ID and it will also be used for server to client encryption (see Protocol below). HKDF will be used to derive symmetric key from private key and used in secret_box together with random nonce (to allow replacing data with the same key but with a different nonce - nonce will be sent to the server too). secret_box construction is authenticated encryption, so it would protect from MITM.
The proposed syntax:
```abnf
shortConnectionRequest = connectionScheme "/" connReqType "#/" smpServer "/" linkHash
connReqType = %s"invitation" / %s"contact"
connectionScheme = (%s"https://" clientAppServer) / %s"simplex:"
clientAppServer = hostname [ ":" port ]
; client app server, e.g. simplex.chat
smpServer = serverIdentity "@" srvHosts [":" port] ; no smp:// prefix, no escaping
srvHosts = <hostname> ["," srvHosts] ; RFC1123, RFC5891
linkHash = <base64url encoded SHA256 or SHA512 hash of the original link>
```
If SMP server supports pages, its name can be used as clientAppServer, without repeating it after #, for a shorter link.
Example link:
```
https://simplex.chat/contact/#0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im/abcdefghij0123456789abcdefghij0123456789abc=
```
This link has the length of ~136 characters (256 bits), which is shorter than the full contact address (~310 characters) and much shorter than invitation links (~528 characters) even without post-quantum keys added to them.
This size can be further reduced by
- use server domain in the link.
- do not include onion address, as the connection happens via proxy anyway, if it's untrusted server.
- not pinning server TLS certificate - the downside here is that while the attack that compromises TLS will not be able to substitute the link (because it's hash will not match), it will be able to intercept and to block it.
- using shorter hash, e.g. SHA128 - reducing the collision resistance.
If the server is known, the client could use it's hash and onion address, otherwise it could trust the proxy to use any existing session with the same hostname or to accept the risk of interception - given that there is no risk of substitution.
With the first two of these "improvements" the link could be ~122 characters:
```
https://smp8.simplex.im/contact/#0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU@/abcdefghij0123456789abcdefghij0123456789abc
```
If onion address is preserved the link will be ~184 characters (won't fit in Twitter 160 characters bio):
```
https://smp8.simplex.im/contact/#0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU@beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion/abcdefghij0123456789abcdefghij0123456789abc
```
If we implement it, the request to resolve the link would be made via proxied SMP command (to avoid the direct connection between the client and the recipient's server).
Pros:
- a bit shorter link.
- possibility to include post-quantum keys into the full link keeping the same shortened link size.
- possibility to include chat profile of contact or group, and preferences, for a much better connection experience, and to show this information when the link sent in the conversation (clients can resolve them automatically, without connecting - it can be resolved by the sending clients).
- server will not have access to the link.
Cons:
- protocol complexity.
- observers can access the link content, so for 1-time invitation we should only include permissions and not profile.
Pros are a huge improvement of UX of connecting both within and from outside of the app (e.g., link can be resolved even before creating chat profile, as part of the onboarding).
## Protocol
To support short links, the SMP servers would provide a simple key-value store enabled by three additional commands: `WRT`, `CLR` and `READ`
`WRT` command is used to store and to update values in the store. The size of the value is limited by the same size as sent messages (or, possibly, smaller - as connection information size used in confirmation messages) - the clients would use this fixed size irrespective of the content. `WRT` command will be sent with the data blob ID in the transaction entityId field, public authorization key used to authorize `WRT` and `CLR` commands (subsequent WRT commands to the existing key must use the same key), and the data blob.
`CLR` command must use with the same entity ID and must be authorized by the same key.
`READ` command must use the ID which hash would be equal of the ID used to create the data blob, and this ID would also be used as public authorization
## Algorithm to store and to retrieve data blob.
**Store data blob**
- the data blob owner generates X25519 key pair: `(k, pk)`.
- private key `pk` will be included in the short link shared with the other party (only base64url encoded key bytes, not X509 encoding).
- `HKDF(pk)` will be used to encrypt the link data with secret_box before storing it on the server.
- the hash of public key `sha256(k)` will be used as ID by the owner to store and to remove the data blob (`WRT` and `CLR` commands).
**Retrieve data blob**
- the sender uses the public key `k` derived from the private key `pk` included in the link as entity ID to retrieve data blob (the server will compute the ID used by the owner as `sha256(k)` and will be able to look it up). This provides the quality that the traffic of the parties has no shared IDs inside TLS. It also means that unlike message queue creation, the ID to retrieve the blob was never sent to the blob creator, and also is not known to the server in advance (the second part is only an observation, in itself it does not increase security, as server has access to an encrypted blob anyway).
- note that the sender does not authorize the request to retrieve the blob, as it would not increase security unless a different key is used to authorize, and adding a key would increase link size.
- server session keys with the sender will be `(sk, spk)`, where `sk` is public key shared with the sender during session handshake, and `spk` is the private key known only to the server.
- this public key `k` will also be combined with server session key `spk` using `dh(k, spk)` to encrypt the response, so that there is no ciphertext in common in sent and received traffic for these blobs. Correlation ID will be used as a nonce for this encryption.
- having received the blob, the client can now decrypt it using secret_box with `HKDF(pk)`.
Using the same key as ID for the request, and also to additionally encrypt the response allows to use a single key in the link, without increasing the link size.
## Threat model
**Compromised SMP server**
can:
- delete link data.
- hide link selectively from some requests.
cannot:
- undetectably replace link data.
- access unencrypted link data, whether it was or was not accessed by the accepting party.
- observe IP addresses of the users accessing link data.
**Passive observer who observed short link**:
can:
- access original unencrypted link data
cannot:
- replace or delete the link data
+18
View File
@@ -0,0 +1,18 @@
# iOS notifications stability
## Problem
iOS notifications may fail to deliver for several reasons, but there are two important reasons that we could address:
- when notification server is not subscribed to SMP server(s), the notifications can be dropped - it can happen because either notification server restarts or becuase SMP server restarted and some messages are received before notification server resubscribed. We lose approximately 3% of notifications because of this reason.
- when user device is offline or has low power condition, Apple does not deliver notification, but puts them to storage. If while the notification is in storage a new one arrives it would overwrite the previous notification. If it was the message to the same message queue, the client will download messages anyway, up to a limit, but if the message was to another queue, it will not be delivered until the app is opened. Apple delivers about 88% of notifications that should be delivered (not accounting for uninstalled apps), the rest is replaced with the newer notifications.
## Solution
The first problem can be solved by preserving notifications for a limited time (say 1 hour) in case there is no subscription to notification from notification server. At the very least, they can be preserved in SMP server memory but can also be stored to a file on restart, similar to messages, and be delivered when notification server resubscribes. It is sufficient to store one notification per messaging queue.
The second problem is both more damaging and more complex to solve. The solution could be to always deliver several last notifications to different queues in one packet (Apple allows up to ~4-5kb notification size, and we are sending packets of fixed size 512 bytes, so we could fit up to 8-10 of them in each notification).
Every time a client receives such batch of notifications if can:
- check if that notification was already received in the previous batch.
- if it was received, it would be ignored, otherwise it would be processed.
- process them one by one, started from the most recent one while the time allows.
+179
View File
@@ -0,0 +1,179 @@
# SMP server message storage
## Problem
Currently SMP servers store all queues in server memory. As the traffic grows, so does the number of undelivered messages. What is worse, Haskell is not avoiding heap fragmentation when messages are allocated and then de-allocated - undelivered messages use ByteString and GC cannot move them around, as they use pinned memory.
## Possible solutions
### Solution 1: solve only GC fragmentation problem
Move from ByteString to some other primitive to store messages in memory long term, e.g. ShortByteString, or manage allocation/de-allocation of stored messages manually in some other way.
Pros: the simplest solution that avoids substantial re-engineering of the server.
Cons:
- not a long term solution, as memory growth still has limits.
- may be ineffective, as it introduces additional copying of bytes.
### Solution 2: move message storage to hard drive
Use files or RocksDB to store messages.
Pros:
- much lower memory usage.
- no message loss in case of abnormal server termination (important until clients have delivery redundancy).
- this is a long term solution, and at some point it might need to be done anyway.
Cons:
- substantial re-engineering costs and risks.
- metadata privacy. Currently we only save undelivered messages when server is restarted, with this approach all messages will be stored for some time. this argument is limited, as hosting providers of VMs can make memory snapshots too, on the other hand they are harder to analyze than files. On another hand, with this approach messages will be stored for a shorter time.
#### RocksDB and other key-value stores
The downside of any key-value stores is that they don't seem to have efficient primitives for sequential delivery. While sequential delivery can be modelled with linked lists, they would require 1 key insert (on send), 3 key updates (1 update to update queue data on send, 1 update of the last message to point to the next, 1 update on delivery or message expiration) and 1 key deletion (on delivery or message expiration) for each delivered message.
This might result in substantial write amplification and compacting costs.
In general, tree structures that are efficient for quick lookups and updates, given approximately fixed value size, are inefficient for modelling queues.
#### Files
The upside of files is that they are well suited for sequential delivery and don't result in the same churn, with careful design, as trees do.
The downside of filesystem is that it does not scale well with the large number of files in a folder, so queues will have to be spread across multiple folders, following tree-like structure.
I could not find an available library that efficiently models sequential delivery in highly concurrent environment.
A possible design could be the following.
##### Queue folder and files
Each message queue is stored in its own folder (see below on folder locations). Folder would contain these files:
- `messages.abcd.log` - the file that is used to read messages from, sequentially
- `messages.efgh.log` - the optional file that is used to write messages, in case it is different from read file.
- `queue.log` - append-only file where the last line represents the current queue state
- `queue.timestamp.log` - previous states of queue.log file
Each line in "queue.log" file has this syntax
```abnf
queueLogLine =
%s"read_file=" base64
%s"read_msg=" digits
%s"read_byte=" digits
%s"write_file=" base64
%s"write_msg=" digits
```
When queue is first requested by the server:
```c
if queue folder exists:
read queue state from last line of queue.log
if queue.log contained more than one line: // compaction
copy queue.log to queue.timestamp.log
write one line queue state to queue.log
else:
create queue folder
create messages.abcd.log (abcd is some random string)
read_msg = 0
read_byte = 0
create queue.log with one line: "read_file=abcd read_msg=0 read_byte=0 write_files=abcd write_msg=0"
open read_file in ReadMode and seek to read_byte position
nextReadByte = read_byte
nextReadMsg = read_msg
open write_file in AppendMode
```
When message is added to the queue (assumes that queue state is loaded to server memory, if not the previous section will be done first):
```c
if write_msg > max_queue_messages:
return quota error
else if write_msg = max_queue_messages:
add quota_exceeded message to write_file
update queue state: write_msg += 1
append updated queue state to queue.log
else
// It is required that `max_queue_messages < max_file_messages`,
// so that we never need more than one additional write file.
if write_msg >= max_file_messages: // queue file rotation
create messages.efgh.log // efgh is some random string
update queue state: write_file=efgh write_msg=0 // read file remains the same as it was
append updated queue state to queue.log
copy queue.log to queue.timestamp.log
// `old` needs to be defined to limit the number and storage duration,
// preserving not more than N files, and not more than M days files, "and then some"
// (that is if the queue has high churn, we have file from M days before in any case, for any debugging).
delete `old` `queue.timestamp.log` files
write one line queue state to queue.log // compaction
add message to write_file
update queue state: write_msg += 1
append updated queue state to queue.log
```
The above algorithm assumes `max_queue_messages < than max_file_messages`, so that we never need more than one write file.
When message is delivered, it is simply read from the read queue, queue state does not change yet:
```c
if nextReadMsg > read_msg:
deliver cached message, no need to read it again
else
read message from read_file handle
nextReadMsg = read_msg + 1
nextReadByte = current position in file
```
When message delivery is acknowledged, the read queue needs to be advanced, and possibly switched to read from the current write_queue:
```c
if nextReadByte == read_byte:
return error // nothing was delivered
else if nextReadByte = EOF:
// end of file is reached, possibly some other condition,
// but it should allow changing max_file_messages on server restart
currReadFile = read_file
read_file = write_file
read_msg = 0
read_byte = 0
append updated queue state to queue.log
delete currReadFile
else
read_msg += 1
read_byte = nextReadByte
// `seek` should not be necessary, as the handle is already at nextReadByte position here
// seek to read_byte
append updated queue state to queue.log
```
The above algorithm delegates the problem of compaction and fragmentation management to file system, that is very optimized for such scenarios.
Also, read and write files will grow to almost a constant size, so the space they used may be re-used.
An important consideration is that writes to queue.log and message.log files and queue state modifications have to be sequential, without concurrency - it can be managed with the usual locks.
##### Queue folders structure
Most Linux systems use EXT4 filesystem where the file lookup time scales linearly to the number of files. While alternatives with logarithmic lookup time exist (XFS), they may be very complex to configure on the existing systems.
So storing all queue folders in one folder won't scale.
To solve this problem we could use recipient queue ID in base64url format not as a folder name, but as a folder path, splitting it to path fragments of some length. The number of fragments can be configurable and migration to a different fragment size can be supported as the number of queues on a given server grows.
Currently, queue ID is 24 bytes random number, thus allowing 2^192 possible queue IDs. If we assume that a server must hold 1b queues, it means that we have ~2^162 possible addresses for each existing queue. 24 bytes in base64 is 32 characters that can be split into say 8 fragments with 4 characters each, so that queue folder path for queue with ID `abcdefghijklmnopqrstuvwxyz012345` would be:
`/var/opt/simplex/messages/abcd/efgh/ijkl/mnop/qrst/uvwx/yz01/2345`
The maximum theoretic number of the folders on the 1st level is 64^4, or 2^24 ~ 16m - this is probably still a large number of subfolders for EXT4. Given that addresses are random, all the possible combinations in the first folder can be used with a large number of queues.
So we could use an unequal split of path, two letters each and the last being long:
`/var/opt/simplex/messages/ab/cd/ef/ghijklmnopqrstuvwxyz012345`
The first three levels in this case can have 4096 subfolders each, and it gives 68b possible subfolders (64^2^3), so the last level will be sparse in case of 1b queues on the server. So we could make it 4 levels with 2 letters to never think about it, accounting for a large variance of the random numbers distribution:
`/var/opt/simplex/messages/ab/cd/ef/gh/ijklmnopqrstuvwxyz012345`
+81
View File
@@ -0,0 +1,81 @@
# Storage considerations for SMP queues
See [Short invitation links](./2024-06-21-short-links.md).
## Problem
1) queue records are created permanently, until the clients delete them.
2) clients only delete queue records based on some user action, pending connections do not expire.
While part 2 should be improved in the client, indefinite storage of queue records becomes a much bigger issue if each of them would result in a permanent storage of 4-16kb blob in server memory, without server-side expiration for short invitation links.
## Possible solutions
1) Add some queue timestamp, e.g. queue creation date, to expire unsecured queues after say 3 weeks.
The problem with this approach is that contact addresses are also unsecured queues, and they should not be expired.
We could set really large expiration time, and require that clients "update" the unsecured queues they need at least every 1-2 years, but it would not solve the problem of storing a large number of blobs in the server memory for unused/abandoned 1-time invitations.
2) Do not store blobs in memory / append-only log, and instead use something like RocksDB. While it may be a correct long term solution, it may be not expedient enough at the current POC stage for this feature. Also, the lack of expiration is wrong in any case and would indefinitely grow server storage.
3) Add flag allowing the server to differentiate permanent queues used as contact addresses, also using different blob sizes for them. In this case, messaging queues will be expired if not secured after 3 weeks, and contact address queues would be expired if not "updated" by the owner within 2 years.
Probably all three solutions need to be used, to avoid creating a non-expiring blob storage in memory, as in case too many of such blobs are created it would not be possible to differentiate between real users and resource exhaustion attacks, and unlike with messages, they won't be expiring too.
Servers already can differentiate messaging queues and contact address queues, if they want to:
- with the old 4-message handshake, the confirmation message on a normal queue was different, and also KEY command was eventually used.
- with the fast 2-message handshake, while the confirmation message has the same syntax, and the differences are inside encrypted envelope, the client still uses SKEY command.
- in both cases, the usual messaging queues are secured, and contact addresses are not, so this difference is visible in the storage as well (although it is not easy to differentiate between abandoned 1-time invitations and contact addresses).
Differentiating these queues can also allow different message retention times - e.g., the queues for contact addresses could have bigger size, but have lower message retention time.
## Proposed solution
1. Add queue updated_at date into queue records. While it adds some metadata, it seems necessary to manage retention and quality of service. It will not include exact time, only date, and the time of creation will be replaced by the time of any update - queue secured, a message is sent, or queue owner subscribes to the queue. To avoid the need to update store log on every message this information can be appended to store log on server termination. Or given that only one update per day is needed it may be ok to make these updates as they happen (temporarily making the sequence and time of these events available in storage).
2. Add flag to indicate the queue usage - messaging queue or queue for contact address connection requests. This would result in different queue size and different retention policy for queue and its messages. We already have "sender can secure flag" which is, effectively, this flag - contact address queues are never secured. So this does not increase stored metadata in any way.
## Possible changes to short links
This is a design considerations and a concept, not a design yet.
Instead of implementing a generic blob storage that can be used as an attack vector, and adds additional failure point (another server storing blob that is necessary to connect to the queue on the current server), but instead adds an extended queue information blobs, most of which could be dropped without the loss of connectivity, so that the attack can be mitigated by deleting these blobs without users losing the ability to connect, as long as the queue and minimal extended information is retained.
So, to make the connection there need to be these elements:
- queue server and queue ID - mandatory part, that can be included in short link
- SMP key - mandatory part for all queues. We are considering initializing ratchets earlier for contact addresses, and include ratchet keys and pre-keys into queue data as well, but it is out of scope here.
- Ratchet keys - mandatory part for 1-time invitation that won't fit in short link.
- PQ key - optional part that can be stored with addresses if ratchet keys are added and with 1-time invitations.
- App blobs - chat preferences for 1-time invitation links and profile information for contact addresses.
So rather that storing one blob with a large address inside it, not associated with the queue, increasing probability of failure and reducing our ability to mitigate resource exhaustion, we could store extended blobs associated with the queues.
Also, we need the address shared with the sender (party accepting the connection) to be short. We could use a similar approach that was proposed for data blobs, using a single random seed per queues to derive multiple keys and IDs from it. For example:
1. The queue owner:
- generates Ed25529 key pair `(sk, spk)` and X25519 key pair `(dhk, dhpk)` to use with the server, same as now sent in NEW command.
- generates queue recipient ID (this ID can still be server-generated).
- generates X25519 key pair `(k, pk)` to use with the accepting party.
- derives from `k`:
- sender ID.
- symmetric key for authenticated encryption of blobs.
- `k` will be used as short link.
2. All other data from the invitation can be included in queue creation request and be associated with the queue as 1-3 blobs with different priority:
- ratchet keys - it will have a small size, so only this blob cannot be removed, while other blobs can be removed in case of resource exhaustion.
- PQ keys - optional blob.
- conversation preferences and profile - can be removed depending on creation time, e.g. all new blobs can be removed.
The algorithm used to derive key and ID from `k` needs to be cryptographically secure, e.g. it could be some KDF or ChaCha DRG initialized with `k` as seed, TBC.
So, coupling blob storage with messaging queues has these pros/cons:
Cons:
- no additional layer of privacy - the server used for connection is visible in the link, even after the blobs are removed from the server.
Pros:
- no additional point of failure in the connection process - the same server will be used to retrieve necessary blobs as for connection.
- queue blobs of messaging blobs will be automatically removed once the queue is secured or expired, without additional request from the recipient - reducing the storage and the time these blobs are available.
- queue blobs for contact addresses will be structured and some of the large blobs can be removed in case of resource exhaustion attack (and recreated by the client if needed), with the only downside that PQ handshake will be postponed (which is the case now) and profile will not be available at a point of connection.
+80
View File
@@ -0,0 +1,80 @@
# Blob extensions for SMP queues
Evolution of the design for short links, see [here](./2024-06-21-short-links.md) and [here](./2024-09-05-queue-storage.md).
## Problems
Allow storing extended information with SMP queues to improve UX and security of making connections:
- short invitation links and contact addresses.
- PQ encryption from the first message.
- present user profile with chat preferences and welcome message when the public address link is scanned.
## Design
1. Queue creation/update date is already added to server persistence, allowing to expire queues and blobs, depending on their usage.
2. Add "queue type" metadata to NEW command to indicate whether messaging queue is used as public address or as messaging queue (see previous docs on why it doesn't change threat model). While at the moment it would match sndSecure flag there may be future scenarios when they diverge. Initially only "invitation" and "contact" types will be supported.
3. Prohibit sndSecure flag for "contact" queues, prohibit securing contact queues.
4. Add "queue blobs" to NEW command:
- blob0: ratchetKeys up to N0 bytes - priority 0, can't be removed by the server, only in "invitation"
- blob1: PQ key up to N1 bytes - priority 1, can be removed by the server, only used in "invitation"
- blob2: Application data up to N2 bytes - priority 2, can be removed by the server.
5. Add linkId to NEW command
6. linkId and blobs will be removed when queue is secured.
7. Add recipient command to remove/upsert blob2 for contact queues.
8. Add sender command to retrieve blobs.
## Protocol
### Creating a queue:
The queue owner:
- generates Ed25529 key pair `(sk, spk)` and X25519 key pair `(dhk, dhpk)` to use with the server, same as now. `sk` and `dhk` will be sent in NEW command.
- generates X25519 key pair `(k, pk)` to use with the accepting party to encrypt queue messages.
- derives from `k` using HKDF:
- symmetric key `bk` for authenticated encryption of blobs.
- `linkId`, will be sent in NEW command.
- `k` will be used as short link.
- sends NEW command.
NEW command syntax:
```abnf
create = %s"NEW " linkId queueType recipientAuthPublicKey recipientDhPublicKey
basicAuth subscribe sndSecure [ "0" blob0 ] [ "1" blob1 ] [ "2" blob2 ]
queueType = %s"I" / %s "C" ; new parameter
linkId = length *OCTET ; new parameter,
; can be empty in which case blobs won't be allowed
blob0 = word16 *OCTET ; new parameter, encrypted ratchet keys,
; including nonce and auth tag
blob1 = word16 *OCTET ; new parameter, encrypted PQ key
blob2 = word16 *OCTET ; new parameter, encrypted application data
```
SET - command to update queue blobs (recipientId is used as entity ID):
```abnf
set = %s"SET " linkId [ "2" blob2 ] ; passing empty blob removes it
linkId ; updated (or the same) linkId, can be empty to remove blobs
; allows to change the address without removing the queue / changing blobs
; (e.g., to avoid losing the messages).
```
### Sending messages to the queue
GET - command to get queue blobs (linkId is used as entity ID):
```abnf
get = %s"GET"
```
Response to GET:
```abnf
blobs = %s"BLOB" senderId [ "0" blob0 ] [ "1" blob1 ] [ "2" blob2 ]
```
As blobs are retrieved using a separate linkId, once blobs are removed it will be impossible to find senderId from short link - it is a threat model improvement. Once server storage is compacted, it will be impossible to find queue related to the link even with the access to server data (unless server preserves the data).
### Possible privacy improvement
We could only allow unauthorized GET and authorized SET commands for long-term "contact" queues, and return BLOB in response to SKEY (or require that GET is authorized) - so that only the person who secures the queue will get access to data blobs. This way it ensures that the parties transmitting the invitation links cannot retrieve their content without the sender noticing it.
+26
View File
@@ -0,0 +1,26 @@
# Private rendezvous protocol
## Problem
Our current handshake protocol is open to this attack: whoever observes the link exchange, knows on which server connection is being made, and if the traffic on this server is observed, then it can confirm communication between parties. Further, even with the [last proposal](./2024-09-09-smp-blobs.md#possible-privacy-improvement), having real-time access to the server data allows to establish the exact messaging queue that is used to send messages.
## Solution
We could make the initial link exchange more private by making it harder for any observer to discover which server will be used for messaging by hiding this information from the server that hosts the initial link.
Preliminary, the protocol could be the following:
1. Connection initiator stores 224-256 bytes of encrypted connection link on a rendezvous server (link contains server host and linkId on another messaging server, not a rendezvous one).
2. Rendezvous server adds these links to buckets, up to 64 links per bucket. Bucket ID is the timestamp when the bucket was created + a sequential bucket number, in case more than one bucket is created per second.
3. The server responds to the link creator with a bucket ID where this link was added. That bucket ID is its timestamp + a number prevents server "fingerprinting" clients and using say one bucket for each client. If timestamp is different or a bucket number within this timestamp is too large, the client can refuse to use it, depending on the client settings.
4. The initiating party will pass to the accepting party the rendezvous server host, the hash of this bucket ID (bucket link) and the passphrase to derive the key from. The initiating party has an option to pass a link and passphrase via two channels - in which case the link will only contain the bucket ID.
5. The accepting party would then request the bucket via its ID hash (the server would store hashes to be able to look up - hash is used to prevent showing time in the link) and attempt to decrypt all contained links using the provided key.
The accepting party then will continue the connection via the decrypted link.
This obviously does not protect accepting party from the initiating party, if it can choose rendezvous server it controls. It also does not protect from the malicious rendezvous server that would collaborate with link observers. I think reunion doesnt protect from it too.
But it does protect connection from whoever observes the link, particularly if this link only contains the bucket and the key is passed separately, via some other channel.
+163
View File
@@ -0,0 +1,163 @@
# Sharing protocol ports with HTTPS
Some networks block all ports other than web ports, including port 5223 used for SMP protocol by default. Running SMP servers on a common web port 443 would allow them to work on more networks. The servers would need to provide an HTTPS page for browsers (and probes).
## Problem
Browsers and tools rely on system CA bundles instead of certificate pinning.
The crypto parameters used by HTTPS are different from what the protocols use.
Public certificate providers like LetsEncrypt can only sign specific types of keys and Ed25519 isn't one of them.
This means a server should distinguish browser and protocol clients and adjust its behavior to match.
## Solution
`tls` package has a server hook that allows producing a different set of `TLS.Credentials` according to a client-provided "Server Name Indication" extension.
Since LE certificates are only handed out to domain names, TLS client will be sending the SNI.
However client transports are constructed over connected sockets and the SNI wouldn't be present unless explicitly requested.
When a client sends SNI, then it's a browser and a web credentials should be used.
Otherwise it's a protocol client to be offered the self-signed ca, cert and key.
When a transport colocated with a HTTPS, its ALPN list should be extended with `h2 http/1.1`.
The browsers will send it, and it should be checked before running transport client.
If HTTP ALPN is detected, then the client connection is served with HTTP `Application` instead (the same "server information" page).
If some client connects to server IP, doesn't send SNI and doesn't send ALPN, it will look like a pre-handshake client.
In that case a server will send its handshake first.
This can be mitigated by delaying its handshake and letting the probe to issue its HTTP request.
## Implementation plan
An unmodified client should be able to use protocols on port 443 right away.
The switchover happens inside `runTransportServerState` before `runClient`:
```haskell
runServer (tcpPort, ATransport t) = do
-- ...
runTransportServerState_ ss started tcpPort serverParams tCfg $ \socket h -> do -- expose raw socket for warp-tls internals to attach
negotiated <- getSessionALPN
if allowHTTP t && isHTTP negotiated -- only attempt the switch for the TLS transport
then runHTTP socket (tlsContext h)-- ... collect data and produce values needed to run WAI Application
else runClient serverSignKey t h `runReaderT` env -- performs serverHandshake etc as usual
```
The web app and server live outside, so `runHttp` has to be provided by the `runSMPServer` caller.
Additonally, Warp is using its `InternalInfo` object that's scoped to `withII` bracket.
```haskell
runServer ini = do
-- ...
runWebServer ini ServerInformation {config, information} $ if sharedHttps then Nothing else webHttpsParams -- suppress serving https
if sharedHttps
then withRunHTTP staticFilesPath \attachStatic -> runSMPServer cfg (Just attachStatic) -- provide wrapped application runner
else runSMPServer cfg Nothing
```
### Upstream
The implementation relies on a few modification to upstream code:
- `warp-tls`: The library provides `httpOverTls`, but it wants to do handshake itself.
Since we have to do the handshake to switch on ALPN, the setup function has to be split.
This is a resonable change that may be upstreamed and nothing blocks us from using the recent version.
- `warp`: Only the re-export of `serveConnection` is needed.
Unfortunately the most recent `warp` version can't be used right away due to dependency cascade around `http-5` and `auto-update-2`.
So a fork containing the backported re-export has to be used until the dependencies are refreshed.
### TLS.ServerParams
When a server has port sharing enabled, a new set of TLS params is loaded and combined with transport params:
```haskell
newEnv config = do
-- ...
tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig)
sharedServerParams <- forM ((,) <$> sharedHttpsCredentials config <*> alpn transportConfig) $ \((chain, key), alpn) ->
let ca = Nothing -- It is possible to provide CA certificate, but it is typical for web server to use combined certificate chains
loadHTTPSServerParams tlsServerParams ca chain key alpn
```
`loadHTTPSServerParams` extends params with:
1. `onALPNClientSuggest` hook gets `["h2", "http/1.1"]` added to the ALPN list which is now required.
2. `onServerNameIndication` hook added, which upon detecting client SNI prepends the web credentials.
3. `sharedCredentials = T.Credentials []` should be done to prevent transport credentials confusing browsers.
But that aborts key exchange somewhere in tls internals, so disabled for now.
As a workaround, another set of dummy credentials can be provided in the hope that any sane browser would reject them.
Like, RC4 ciphers, "impossible" digest combination, etc.
### supportedParameters
TLS certificate chains provided by LetsEncrypt use ECDSA/P256 and that requires extending `supportedParameters` with things disabled in transports:
```haskell
browserCiphers =
[ TE.cipher_TLS13_AES128CCM8_SHA256
, TE.cipher_ECDHE_ECDSA_AES128CCM8_SHA256
, TE.cipher_ECDHE_ECDSA_AES256CCM8_SHA256
]
browserGroups =
[ T.P256
]
browserSigs =
[ (T.HashSHA256, T.SignatureECDSA),
(T.HashSHA384, T.SignatureECDSA)
]
```
This may not be enough for other certificate providers.
## Configuration
> XXX: This is for the current implementation and should be updated.
Web certificate chain is picked up from the WEB section:
```ini
[TRANSPORT]
port: 443
[WEB]
https: 443
cert: /etc/opt/simplex/web.cert
key: /etc/opt/simplex/web.key
# Alternatively, with a proper access configuration, the paths can point to the LE creds directly:
# cert: /etc/letsencrypt/live/smp.hostname.tld/fullchain.pem
# key: /etc/letsencrypt/live/smp.hostname.tld/privkey.pem
```
When `TRANSPORT.port` matches `WEB.https` the transport server becomes shared.
Perhaps a more desirable option would be explicit configuration resulting in additional transported to run:
```ini
[TRANSPORT]
port: 5223 ; pure protocol transport
# control_port: 5224
shared_port: 443 ; variant 1: register in TRANSPORT
[WEB]
https: 443
cert: /etc/opt/simplex/web.cert
key: /etc/opt/simplex/web.key
# transport: on ; variant 2:
```
## Caveats
Serving static files and the protocols togother may pose a problem for those who currently use dedicated web servers as they should switch to embedded http handlers.
As before, using embedded HTTP server is increasing attack surface.
Users who want to run everything on a single host will have to add and extra IP address and bind servers to specific IPs instead of 0.0.0.0.
An amalgamated server binary can be provided that would contain both SMP and XFTP servers, where transport will dispatch connections by handshake ALPN.
## Alternative: Use transports routable with reverse-proxies
An "industrial" reverse proxy may do the ALPN routing, serving HTTP by itself and delegating `smp` and `xftp` to protocol servers.
Same with the `websockets`.
Since this in effect does TLS termination, the protocol servers will have to rely on credentials from protocol handshakes.
+49
View File
@@ -0,0 +1,49 @@
# iOS notifications delivery
## Problem
For iOS notifications to be delivered the client has to create credentials for notification subscription on SMP server using NKEY command and after that create a subscription on notification server using SNEW command. These two commands are sent in sequence, after the connections are created, and for it to happen the client needs to be online and in foreground.
iOS users tend to close the app when it is not used, and iOS has very limited permissions for background activities, so these notification subscriptions are created with a substantial delay, and notifications do not work.
This problem is distinct from and probably more common than other problems affecting notifications delivery described [here](./2024-07-06-ios-notifications.md).
## Solution
1. When the new connection is created, the client already knows if it needs to create notification subscription or not, based on the conversation setting (e.g., if the group is muted, the client will not create notification subscription as well.). We should extend NEW command to avoid the need to send additional NKEY command with an option to create notification subscription at the point where connection is created. NDEL would still be used to disable this notification, and NKEY will be used to re-enable it.
2. In the same way we stopped using SDEL command (NDEL sends notification DELD to subscribed notification server) to delete notificaiton subscriptions from notification server, we should delegate creating notification subscription on notification server to SMP servers. Clients could use keys agreed with ntf server for e2e encryption and for command authorization to encrypt and sign instruction to create notification subscription that will be forwarded to notification server using protocol similar to SMP proxies. This will avoid the need for clients to separately contact notification servers that won't happen until they are online.
3. Instead of making Ntf server trust DELD notifications, we could send deletion instructions signed by the client, which will only fail to send in case notification server is down (and they won't be sent later after server restart).
Cons:
- If SMP servers were to retain in the storage the information about which notification server is used for which queue, it would reduce metadata privacy. While currently it is not an issue, as all notification servers are known and operated by us, once there are other client apps, this can be used for app users fingerprinting, which would act as a deterrence from using new apps but only if app users use servers of operators who are different from the app provider. To mitigate it, we could only store it in server memory and include notification instruction in subscription commands (SUB) and include notification subscription status in SUB responses. We don't need to mitigate the problem of server being able to store this information, as messaging servers can observe which notification servers connect to them anyway.
- If SMP server is restarted before the subscription request is forwared to the notification server, then it will have to be forwarded again, once the client subscribes. The problem here is that if the client is offline, it will neither subscribe to the queue to send notification subscription request, nor receive notifications from this queue. Storing notification server and subscription request would mitigate that, as in this case we could send all pending requests on server start, without depending on client subscriptions.
- "Small" agent will need to support connections to ntf servers and manage workers that retry sending pending subscription requests.
- Until the client learns the public keys of notification server, it will not be able to decrypt notifications. It potentially can be mitigated by using the public key of the server returned when token is created, in this way different client keys (per-queue) will be combined with the same ntf server key (per-token).
## Implementation details
1. NEW and NKEY commands will need to be extended to include notification subscription request. As the notifier ID needs to be sent to notification server, this notifier ID will have to be client-generated and supplied as part of NEW command.
now:
```haskell
NEW :: RcvPublicAuthKey -> RcvPublicDhKey -> Maybe BasicAuth -> SubscriptionMode -> SenderCanSecure -> Command Recipient
NKEY :: NtfPublicAuthKey -> RcvNtfPublicDhKey -> Command Recipient
```
extended:
```haskell
NEW :: RcvPublicAuthKey -> RcvPublicDhKey -> Maybe BasicAuth -> SubscriptionMode -> SenderCanSecure -> Maybe NtfRequest -> Command Recipient
data NtfRequest = NtfRequest NotifierId NtfPublicAuthKey RcvNtfPublicDhKey NtfServerRequest
data NtfServerRequest = NtfServerRequest NtfServer EncSingedNtfCmd
NKEY :: NtfPublicAuthKey -> RcvNtfPublicDhKey -> Maybe NtfServerRequest -> Command Recipient
-- NotifierID is passed in entity ID field of the transmission
```
2. Notification server will need to support an additional command to receive "proxied" subscription commands, `SFWD`, that would include `NtfServerRequest`. This command can include both `SNEW` and `SDEL` commands.
+15
View File
@@ -0,0 +1,15 @@
# Expiring messages in journal storage
## Problem
The journal storage servers recently migrated to do not delete delivered or expired messages, they only update pointers to journal file lines. The messages are actually deleted when the whole journal file is deleted (when fully deleted or fully expired).
The problem is that in case the queue stops receiving the new messages then writing of messages won't switch to the new journal file, and the current journal file containing delivered or expired messages would never be deleted.
## Solution
Remove current journal file and update queue_state.log during message expiration of "idle" queue (that is, without any new messages received or delivered within 3 hours) in case when:
- the queue is "empty" after the expiration
- the queue contains only quota marker(s), in which case move them to a new journal file and update the queue_state accordingly. Quota markers can be kept indefinitely to prevent writing the new messages to the dormant queues that reached capacity, so it's important to handle this case.
Also remove current journal file when the queue is opened in case it is empty (as it would not be ever expired in case it remains empty), and also update queue_state.log
+58
View File
@@ -0,0 +1,58 @@
# Blob extensions for SMP queues 2 and queue storage
This document evolves the design proposed [here](./2024-09-09-smp-blobs.md).
## Problems
In addition to problems in the first doc, we have these issues with in-memory queue record storage:
- many queues are idle or rarely used, but they are loaded to memory, and currently just loading all queues uses 20gb RAM on each server, and takes 10 min to process, increasing downtimes during restarts.
- adding blobs to memory would make this problem much worse.
## Proposed solution
Move queues to the same journalling approach as [used for messages](./2024-09-01-smp-message-storage.md) now, with independent file names in the same folders.
Each queue change would be logged to its own file, and every time the queue is opened the whole file will be read and compacted to a single line - replacing one store log for all queues, with individual log files for each queue.
Queue deletion would not be making a record in the file, instead it would be deleting the entire folder - it would reduce retention period for any metadata of deleted queues.
We could additionally record deletions to the central log, for debugging, and reset it on every start. But in this case we should not remove folders at the point of deletion, but rather mark them as deleted and delete on restart. TBC
It would also allow simplifying blob storage by having only one blob per queue - for example, limied to 16kb (a bit smaller to fit in block) for contact address queues and 4-8kb for invitations (to fit PQ keys and conversation preferences).
We would also need to be able to lookup recipient ID via sender/notifier/link IDs.
One possible solution is to use and load to memory a central index file. But it is likely to also consume a lot of memory and result in slow starts.
Another solution that is probably better is to use the same folder structure and put notifier/sender/link files with the ID of the recipient queue inside the files. So to locate recipient queue the sender would have to locate folder containing the reference file pointing to the recipient queue and then to locate the actual queue data.
## Implementation details
Each queue folder would these files:
- queue_state.log (and timestamped backups) - to store pointers to message journals (already implemented)
- messages.randomBase64.log - message journals (already implemented)
- queue_rec.log (and timestamped backups) - to log complete queue record every time it is changed (so only the last line needs to be read following the same logic as with queue_state.log, to prevent file corruption).
- blob.data, blob.data.bak, blob.timestamp.data - files for data blobs (to make sure some copy of this file is readable/correct in case of write corruption) - the same two step overwrite process will be used as currently with store log compacting:
- on write: 1. if file exists, move it to .bak, 2. store new blob to .data, 3. move .bak to .timestamp.data
- on read: 1. if .bak exists, move it to .data 2. use .data
Additional suggestion to reduce probability of queue_state.log and queue_rec.log file corruption is to do one of the following:
- log end of lines in the beginning of the output, not in the end, to prevent the last line from being corrupted in case the previous line was not fully stored. The downside is that the file will not be EOL terminated, and there will be no confirmation that the output was fully made.
- log EOL both in the beginning and at the end of output, and ignore empty lines in between - this would both confirm that the last line is fully logged and prevent corruption of the next line in case it was not.
- check the last byte of the file and log EOL if it is not EOL. Probably cleanest approach, but with a small performance cost.
If queue folder is a reference to the queue, it may have one of these files:
- notifier.id
- sender.id
- link.id
These files would contain a one line with the recipient ID of the queue. These files would never change, they can only be deleted when queue is deleted or when notifier/link is deleted.
There is logic in code preventing using the same ID in different contexts, and the ID size is large enough to make any collisions unlikely (192 bits), so with correctly working code the queue folder would either have one of reference files, and nothing else, or the queue and message files from the beginning of this section. But even if the same ID is re-used in different context, it should not cause any problems as file names don't overlap.
While we could store different types of references in different types of folders, it would have additional costs of maintaining 4 folder hierarchies. Instead we could use the fact that it is one hierarchy to prevent using the same ID in different contexts.
## Protocol
The only change in protocol is that there will be only one blob per queue, without markers (see the previous doc). Otherwise the protocol and proposed privacy improvement seem reasonable.
+304
View File
@@ -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.
+93
View File
@@ -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.
+159
View File
@@ -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.
+1 -1
View File
@@ -53,7 +53,7 @@ The session invitation contains this data:
- CA TLS certificate fingerprint of the controller - this is part of long term identity of the controller established during the first session, and repeated in the subsequent session announcements.
- Session Ed25519 public key used to verify the announcement and commands - this mitigates the compromise of the long term signature key, as the controller will have to sign each command with this key first.
- Long-term Ed25519 public key used to verify the announcement and commands - this is part of the long term controller identity.
- Session X25519 DH key and sntrup761 KEM encapsulation key to agree session encryption (both for multicast announcement and for commands and responses in TLS), as described in https://datatracker.ietf.org/doc/draft-josefsson-ntruprime-hybrid/. The new keys are used for each session, and if client key is already available (from the previous session), the computed shared secret will be used to encrypt the announcement multicast packet. The out-of-band invitation is unencrypted. These DH public key and KEM encapsulation key are always sent unencrypted. NaCL Cryptobox is used for encryption.
- Session X25519 DH key and sntrup761 KEM encapsulation key to agree session encryption (both for multicast announcement and for commands and responses in TLS), as described in https://datatracker.ietf.org/doc/draft-josefsson-ntruprime-hybrid/. The new keys are used for each session, and if client key is already available (from the previous session), the computed shared secret will be used to encrypt the announcement multicast packet. The out-of-band invitation is unencrypted. This DH public key is always sent unencrypted. NaCL Cryptobox is used for encryption.
Host device decrypts (except the first session) and validates the invitation:
- Session signature is valid.
@@ -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
+11
View File
@@ -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}'
+9
View File
@@ -0,0 +1,9 @@
# Mandatory
ADDR=your_ip_or_addr
QUOTA=120gb
# Optional
#PASS='123123'
# Debug
#SIMPLEX_XFTP_IMAGE=xftp-server-dev
+16
View File
@@ -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
+62 -23
View File
@@ -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
+66 -26
View File
@@ -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
+167 -21
View File
@@ -1,30 +1,176 @@
#!/usr/bin/env sh
set -eu
path_conf_var="/var/opt"
path_conf_smp="$path_conf_var/simplex"
path_conf_xftp="$path_conf_var/simplex-xftp"
path_conf_storelog_smp="$path_conf_smp/smp-server-store.log"
path_conf_storelog_xftp="$path_conf_xftp/file-server-store.log"
# Common
# ------
date="$(date -u '+%Y-%m-%dT%H:%M:%S')"
backup_smp() {
if [ -e "$path_conf_storelog_smp" ]; then
cp "$path_conf_storelog_smp" "${path_conf_storelog_smp}.${date:-date-failed}"
fi
GRN='\033[0;32m'
YLW='\033[1;33m'
BLU='\033[1;34m'
RED='\033[0;31m'
NC='\033[0m'
path_conf_var="/var/opt"
smp_variables() {
path_conf_smp="$path_conf_var/simplex"
path_conf_smp_archive="$path_conf_smp/backups"
path_conf_smp_archive_storelog="$path_conf_smp_archive/queues"
path_conf_smp_archive_stats="$path_conf_smp_archive/stats"
path_conf_smp_archive_messages="$path_conf_smp_archive/messages"
path_conf_storelog_smp="$path_conf_smp/smp-server-store.log"
path_conf_storelog_smp_out="$path_conf_smp_archive_storelog/smp-server-store.log.${date:-date-failed}"
path_conf_stats_smp="$path_conf_smp/smp-server-stats.log"
path_conf_stats_smp_out="$path_conf_smp_archive_stats/smp-server-stats.log.${date:-date-failed}"
path_conf_messages_smp="$path_conf_smp/smp-server-messages.log"
path_conf_messages_smp_out="$path_conf_smp_archive_messages/smp-server-messages.log.${date:-date-failed}"
}
backup_xftp() {
if [ -e "$path_conf_storelog_xftp" ]; then
cp "$path_conf_storelog_xftp" "${path_conf_storelog_xftp}.${date:-date-failed}"
fi
xftp_variables() {
path_conf_xftp="$path_conf_var/simplex-xftp"
path_conf_xftp_archive="$path_conf_xftp/backups"
path_conf_xftp_archive_storelog="$path_conf_xftp_archive/queues"
path_conf_xftp_archive_stats="$path_conf_xftp_archive/stats"
path_conf_storelog_xftp="$path_conf_xftp/file-server-store.log"
path_conf_storelog_xftp_out="$path_conf_xftp_archive_storelog/file-server-store.log.${date:-date-failed}"
path_conf_stats_xftp="$path_conf_xftp/file-server-stats.log"
path_conf_stats_xftp_out="$path_conf_xftp_archive_stats/file-server-stats.log.${date:-date-failed}"
}
if [ "$1" = 'smp-server' ]; then
backup_smp
elif [ "$1" = 'xftp-server' ]; then
backup_xftp
else
backup_smp
backup_xftp
fi
checks() {
result=${SERVICE_RESULT:-exit-code}
status=${EXIT_STATUS:-TERM}
case "$result" in
success)
case "$status" in
TERM)
printf "${RED}Refusing to backup files with failed service state${NC}\n"
exit 1
;;
*)
:
;;
esac
;;
*)
printf "${RED}Refusing to backup files with failed service state${NC}\n"
exit 1
;;
esac
}
smp_check() {
if [ ! -d "$path_conf_smp_archive_storelog" ]; then
mkdir -p "$path_conf_smp_archive_storelog"
fi
if [ ! -d "$path_conf_smp_archive_messages" ]; then
mkdir -p "$path_conf_smp_archive_messages"
fi
if [ ! -d "$path_conf_smp_archive_stats" ]; then
mkdir -p "$path_conf_smp_archive_stats"
fi
}
xftp_check() {
if [ ! -d "$path_conf_xftp_archive_storelog" ]; then
mkdir -p "$path_conf_xftp_archive_storelog"
fi
if [ ! -d "$path_conf_xftp_archive_stats" ]; then
mkdir -p "$path_conf_xftp_archive_stats"
fi
}
backup() {
file="$1"
out="$2"
file_type="$3"
if [ -e "$file" ]; then
if cp "$file" "$out"; then
printf "${YLW}${file_type}${NC} ${GRN}backup successful:${NC} ${BLU}%s${NC}\n" "${out}"
else
printf "${YLW}${file_type}${NC} ${RED}backup failed!${NC}\n"
fi
fi
unset file out file_type
}
cleanup() {
directory="$1"
file_type="$2"
files_date=$(find "$directory" -type f -exec stat --format="%y" {} + | awk '{print $1}' | sort -nr | uniq | awk 'NR==2')
if [ -n "$files_date" ]; then
files=$(find "$directory" -type f -not -newermt "$files_date" -printf "%T@ %Tc %p\n" | sort -n | awk '{print $NF}')
if [ -n "$files" ]; then
printf '%s' "$files" | xargs rm -f
printf "${YLW}Old ${file_type} files${NC}${GRN} has been deleted:${NC}\n"
files_colored=$(printf '%s' "$files" | awk '{print "\033[1;34m"$0"\033[0m"}')
printf "${files_colored}\n"
fi
fi
unset directory file_type files_date files
}
smp_backup() {
backup "$path_conf_storelog_smp" "$path_conf_storelog_smp_out" 'Storelog'
backup "$path_conf_messages_smp" "$path_conf_messages_smp_out" 'Messages'
backup "$path_conf_stats_smp" "$path_conf_stats_smp_out" 'Stats'
}
smp_cleanup() {
cleanup "$path_conf_smp_archive_storelog" 'storelog'
cleanup "$path_conf_smp_archive_stats" 'stats'
cleanup "$path_conf_smp_archive_messages" 'messages'
}
xftp_backup() {
backup "$path_conf_storelog_xftp" "$path_conf_storelog_xftp_out" 'Storelog'
backup "$path_conf_stats_xftp" "$path_conf_stats_xftp_out" 'Stats'
}
xftp_cleanup() {
cleanup "$path_conf_xftp_archive_storelog" 'storelog'
cleanup "$path_conf_xftp_archive_stats" 'stats'
}
main() {
type="${1:-}"
if [ -z "${DOCKER+x}" ]; then
checks
fi
case "$type" in
smp-server)
smp_variables
smp_check
smp_backup
smp_cleanup
;;
xftp-server)
xftp_variables
xftp_check
xftp_backup
xftp_cleanup
;;
*)
printf "${YLW}Unknown server type.${NC}\n"
exit 1
;;
esac
}
main "$@"
+8 -4
View File
@@ -13,11 +13,15 @@ fi
printf "${RED}This action will permanently remove all configs, directories, binaries from Installation Script. Please backup any relevant configs if they are needed.${NC}\n\nPress ${GRN}ENTER${NC} to continue or ${RED}Ctrl+C${NC} to cancel installation"
read ans
systemctl stop smp-server
systemctl stop xftp-server
systemctl disable --now smp-server 2>/dev/null || true
systemctl revert smp-server 2>/dev/null || true
systemctl disable --now xftp-server 2>/dev/null || true
systemctl revert xftp-server 2>/dev/null || true
systemctl daemon-reload 2>/dev/null || true
rm -rf /var/opt/simplex /etc/opt/simplex /var/opt/simplex-xftp /etc/opt/simplex-xftp /srv/xftp /etc/systemd/system/smp-server.service /etc/systemd/system/xftp-server.service /usr/local/bin/smp-server /usr/local/bin/xftp-server /usr/local/bin/simplex-servers-update /usr/local/bin/simplex-servers-uninstall
rm -rf /var/opt/simplex /etc/opt/simplex /etc/opt/simplex-info /var/opt/simplex-xftp /etc/opt/simplex-xftp /srv/xftp /etc/systemd/system/smp-server.service /etc/systemd/system/xftp-server.service /usr/local/bin/smp-server /usr/local/bin/xftp-server /usr/local/bin/simplex-servers-update /usr/local/bin/simplex-servers-uninstall /usr/local/bin/simplex-servers-stopscript
userdel smp && userdel xftp
userdel smp 2>/dev/null || true
userdel xftp 2>/dev/null || true
printf "Uninstallation is complete! Thanks for trying out SimpleX!\n"
+529 -121
View File
@@ -1,17 +1,8 @@
#!/usr/bin/env sh
set -eu
# Links to scripts/configs
bin="https://github.com/simplex-chat/simplexmq/releases/latest/download"
bin_smp="$bin/smp-server-ubuntu-20_04-x86-64"
bin_xftp="$bin/xftp-server-ubuntu-20_04-x86-64"
scripts="https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/scripts/main"
scripts_systemd_smp="$scripts/smp-server.service"
scripts_systemd_xftp="$scripts/xftp-server.service"
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"
@@ -26,149 +17,566 @@ 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"
path_tmp_systemd_smp="$path_tmp_bin/smp-server.service"
path_tmp_systemd_xftp="$path_tmp_bin/xftp-server.service"
path_conf_etc='/etc/opt'
path_conf_info='/etc/opt/simplex-info'
GRN='\033[0;32m'
BLU='\033[1;36m'
YLW='\033[1;33m'
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
local_version="$($path_bin_smp -v | awk '{print $3}')"
remote_version="$(curl --proto '=https' --tlsv1.2 -sSf -L https://api.github.com/repos/simplex-chat/simplexmq/releases/latest | grep -i "tag_name" | awk -F \" '{print $4}')"
update_scripts() {
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_update" -o "$path_tmp_bin_update" && chmod +x "$path_tmp_bin_update"
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"
######################
### Misc functions ###
######################
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"
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
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
}
# Checks "sanity" of downloaded thing, e.g. if it's really a script or binary
check_sanity() {
path="$1"
criteria="$2"
update_systemd() {
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd_smp" -o "$path_tmp_systemd_smp"
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd_xftp" -o "$path_tmp_systemd_xftp"
if diff -q "$path_systemd_smp" "$path_tmp_systemd_smp" > /dev/null 2>&1; then
printf -- "- ${YLW}smp-server service is up-to-date${NC}.\n"
rm "$path_tmp_systemd_smp"
else
printf -- "- Updating smp-server service..."
mv "$path_tmp_systemd_smp" "$path_systemd_smp"
systemctl daemon-reload
printf "${GRN}Done!${NC}\n"
fi
if diff -q "$path_systemd_xftp" "$path_tmp_systemd_xftp" > /dev/null 2>&1; then
printf -- "- ${YLW}xftp-server service is up-to-date${NC}.\n"
rm "$path_tmp_systemd_xftp"
else
printf -- "- Updating xftp-server service..."
mv "$path_tmp_systemd_xftp" "$path_systemd_xftp"
systemctl daemon-reload
printf "${GRN}Done!${NC}\n"
fi
}
update_bins() {
if [ "$local_version" != "$remote_version" ]; then
if systemctl is-active --quiet smp-server; then
printf -- "- Stopping smp-server service..."
systemctl stop smp-server
printf "${GRN}Done!${NC}\n"
printf -- "- Updating smp-server bin to %s..." "$remote_version"
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_smp" -o "$path_bin_smp" && chmod +x "$path_bin_smp"
printf "${GRN}Done!${NC}\n"
case "$criteria" in
string:*)
pattern="$(printf '%s' "$criteria" | awk '{print $2}')"
printf -- "- Starting smp-server service..."
systemctl start smp-server
printf "${GRN}Done!${NC}\n"
else
printf -- "- Updating smp-server bin..."
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_smp" -o "$path_bin_smp" && chmod +x "$path_bin_smp"
printf "${GRN}Done!${NC}\n"
fi
if grep -q "$pattern" "$path"; then
sane=0
else
sane=1
fi
;;
file:*)
pattern="$(printf '%s' "$criteria" | awk '{print $2}')"
if systemctl is-active --quiet xftp-server; then
printf -- "- Stopping xftp-server service..."
systemctl stop xftp-server
printf "${GRN}Done!${NC}\n"
if file "$path" | grep -q "$pattern"; then
sane=0
else
sane=1
fi
;;
*) printf 'Unknown criteria.\n'; sane=1 ;;
esac
printf -- "- Updating xftp-server bin to %s..." "$remote_version"
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_xftp" -o "$path_bin_xftp" && chmod +x "$path_bin_xftp"
printf "${GRN}Done!${NC}\n"
printf -- "- Starting xftp-server service..."
systemctl start xftp-server
printf "${GRN}Done!${NC}\n"
else
printf -- "- Updating xftp-server bin..."
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_smp" -o "$path_bin_xftp" && chmod +x "$path_bin_xftp"
printf "${GRN}Done!${NC}\n"
fi
else
printf -- "- ${YLW}smp-server and xftp-server binaries is up-to-date${NC}.\n"
fi
unset path string
return "$sane"
}
# 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 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
if [ -z "$apps" ]; then
printf "${RED}No simplex servers installed! Aborting.${NC}\n"
exit 1
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
# 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
# If there isn't info file, populate it
printf "local_version_${server}=unset\n" >> "$path_conf_info/release"
fi
done
# 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
check_versions
check_distro
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"
update_systemd
printf "Updating simplex server binaries...\n"
update_bins
# 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 "$@"
+11
View File
@@ -5,11 +5,22 @@ 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]
WantedBy=multi-user.target
+10
View File
@@ -5,11 +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]
+139
View File
@@ -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
+257 -406
View File
@@ -1,11 +1,7 @@
cabal-version: 1.12
-- This file has been generated from package.yaml by hpack version 0.35.0.
--
-- see: https://github.com/sol/hpack
name: simplexmq
version: 6.0.0.3
version: 6.4.0.1
synopsis: SimpleXMQ message broker
description: This package includes <./docs/Simplex-Messaging-Server.html server>,
<./docs/Simplex-Messaging-Client.html client> and
@@ -66,24 +62,31 @@ flag use_crypton
manual: True
default: True
flag client_library
description: Don't build server-related code.
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
Simplex.FileTransfer.Protocol
Simplex.FileTransfer.Server
Simplex.FileTransfer.Server.Control
Simplex.FileTransfer.Server.Env
Simplex.FileTransfer.Server.Main
Simplex.FileTransfer.Server.Stats
Simplex.FileTransfer.Server.Store
Simplex.FileTransfer.Server.StoreLog
Simplex.FileTransfer.Transport
Simplex.FileTransfer.Types
Simplex.FileTransfer.Util
@@ -97,44 +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.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
@@ -148,36 +121,18 @@ 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
Simplex.Messaging.Notifications.Protocol
Simplex.Messaging.Notifications.Server
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.Notifications.Transport
Simplex.Messaging.Notifications.Types
Simplex.Messaging.Parsers
Simplex.Messaging.Protocol
Simplex.Messaging.Server
Simplex.Messaging.Server.CLI
Simplex.Messaging.Server.Control
Simplex.Messaging.Server.Env.STM
Simplex.Messaging.Server.Expiration
Simplex.Messaging.Server.Information
Simplex.Messaging.Server.Main
Simplex.Messaging.Server.MsgStore
Simplex.Messaging.Server.MsgStore.STM
Simplex.Messaging.Server.QueueStore
Simplex.Messaging.Server.QueueStore.Postgres.Config
Simplex.Messaging.Server.QueueStore.QueueInfo
Simplex.Messaging.Server.QueueStore.STM
Simplex.Messaging.Server.Stats
Simplex.Messaging.Server.StoreLog
Simplex.Messaging.ServiceScheme
Simplex.Messaging.Session
Simplex.Messaging.TMap
@@ -191,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
@@ -200,13 +154,118 @@ 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
Simplex.FileTransfer.Server.Main
Simplex.FileTransfer.Server.Stats
Simplex.FileTransfer.Server.Store
Simplex.FileTransfer.Server.StoreLog
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.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.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.Server.QueueStore.Postgres
Simplex.Messaging.Server.QueueStore.Postgres.Migrations
other-modules:
Paths_simplexmq
hs-source-dirs:
src
default-extensions:
StrictData
ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2
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-home-modules -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2
include-dirs:
cbits
c-sources:
@@ -216,14 +275,12 @@ library
crypto
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
, asn1-encoding ==0.9.*
, asn1-types ==0.3.*
, async ==2.2.*
, attoparsec ==0.14.*
, base >=4.14 && <5
, base64-bytestring >=1.0 && <1.3
, case-insensitive ==1.2.*
, composition ==1.0.*
, constraints >=0.12 && <0.14
, containers ==0.6.*
@@ -233,13 +290,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.*
@@ -248,38 +303,55 @@ 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.7.0 && <1.8
, 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
if flag(swift)
cpp-options: -DswiftJSON
if !flag(client_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
if impl(ghc >= 9.6.2)
build-depends:
bytestring ==0.11.*
, template-haskell ==2.20.*
, text >=2.0.1 && <2.2
if impl(ghc < 9.6.2)
build-depends:
bytestring ==0.10.*
, template-haskell ==2.16.*
, text >=1.2.3.0 && <1.3
executable ntf-server
if flag(client_library)
buildable: False
main-is: Main.hs
other-modules:
Paths_simplexmq
@@ -287,74 +359,18 @@ executable ntf-server
apps/ntf-server
default-extensions:
StrictData
ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2 -threaded -rtsopts
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
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
, asn1-encoding ==0.9.*
, asn1-types ==0.3.*
, async ==2.2.*
, attoparsec ==0.14.*
, base >=4.14 && <5
, base64-bytestring >=1.0 && <1.3
, case-insensitive ==1.2.*
, composition ==1.0.*
, constraints >=0.12 && <0.14
, containers ==0.6.*
, crypton ==0.34.*
, crypton-x509 ==1.7.*
, crypton-x509-store ==1.6.*
, crypton-x509-validation ==1.6.*
, cryptostore ==0.3.*
, data-default ==0.7.*
, direct-sqlcipher ==2.3.*
, directory ==1.3.*
, filepath ==1.4.*
, hourglass ==0.2.*
, http-types ==0.12.*
, http2 >=4.2.2 && <4.3
, ini ==0.4.1
, iproute ==1.7.*
, iso8601-time ==0.1.*
, memory ==0.18.*
, mtl >=2.3.1 && <3.0
, network >=3.1.2.7 && <3.2
, network-info ==0.2.*
, network-transport ==0.5.6
, network-udp ==0.0.*
, optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3
, simple-logger ==0.1.*
base
, simple-logger
, simplexmq
, socks ==0.6.*
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
, temporary ==1.3.*
, time ==1.12.*
, time-manager ==0.0.*
, tls >=1.7.0 && <1.8
, transformers ==0.6.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, websockets ==0.12.*
, yaml ==0.11.*
, zstd ==0.1.3.*
default-language: Haskell2010
if flag(swift)
cpp-options: -DswiftJSON
if impl(ghc >= 9.6.2)
build-depends:
bytestring ==0.11.*
, template-haskell ==2.20.*
, text >=2.0.1 && <2.2
if impl(ghc < 9.6.2)
build-depends:
bytestring ==0.10.*
, template-haskell ==2.16.*
, text >=1.2.3.0 && <1.3
executable smp-server
if flag(client_library)
buildable: False
if flag(server_postgres)
cpp-options: -DdbServerPostgres
main-is: Main.hs
other-modules:
Static
@@ -365,78 +381,27 @@ executable smp-server
apps/smp-server/web
default-extensions:
StrictData
ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2 -threaded -rtsopts
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
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
, asn1-encoding ==0.9.*
, asn1-types ==0.3.*
, async ==2.2.*
, attoparsec ==0.14.*
, base >=4.14 && <5
, base64-bytestring >=1.0 && <1.3
, case-insensitive ==1.2.*
, composition ==1.0.*
, constraints >=0.12 && <0.14
, containers ==0.6.*
, crypton ==0.34.*
, crypton-x509 ==1.7.*
, crypton-x509-store ==1.6.*
, crypton-x509-validation ==1.6.*
, cryptostore ==0.3.*
, data-default ==0.7.*
, direct-sqlcipher ==2.3.*
, directory ==1.3.*
base
, bytestring
, directory
, file-embed
, 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.*
, mtl >=2.3.1 && <3.0
, network >=3.1.2.7 && <3.2
, network-info ==0.2.*
, network-transport ==0.5.6
, network-udp ==0.0.*
, optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3
, simple-logger ==0.1.*
, filepath
, network
, simple-logger
, simplexmq
, socks ==0.6.*
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
, temporary ==1.3.*
, time ==1.12.*
, time-manager ==0.0.*
, tls >=1.7.0 && <1.8
, transformers ==0.6.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, text
, unliftio
, wai
, wai-app-static
, warp
, warp-tls
, websockets ==0.12.*
, yaml ==0.11.*
, zstd ==0.1.3.*
, warp ==3.3.30
, warp-tls ==3.4.7
default-language: Haskell2010
if flag(swift)
cpp-options: -DswiftJSON
if impl(ghc >= 9.6.2)
build-depends:
bytestring ==0.11.*
, template-haskell ==2.20.*
, text >=2.0.1 && <2.2
if impl(ghc < 9.6.2)
build-depends:
bytestring ==0.10.*
, template-haskell ==2.16.*
, text >=1.2.3.0 && <1.3
executable xftp
if flag(client_library)
buildable: False
main-is: Main.hs
other-modules:
Paths_simplexmq
@@ -444,74 +409,15 @@ executable xftp
apps/xftp
default-extensions:
StrictData
ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2 -threaded -rtsopts
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
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
, asn1-encoding ==0.9.*
, asn1-types ==0.3.*
, async ==2.2.*
, attoparsec ==0.14.*
, base >=4.14 && <5
, base64-bytestring >=1.0 && <1.3
, case-insensitive ==1.2.*
, composition ==1.0.*
, constraints >=0.12 && <0.14
, containers ==0.6.*
, crypton ==0.34.*
, crypton-x509 ==1.7.*
, crypton-x509-store ==1.6.*
, crypton-x509-validation ==1.6.*
, cryptostore ==0.3.*
, data-default ==0.7.*
, direct-sqlcipher ==2.3.*
, directory ==1.3.*
, filepath ==1.4.*
, hourglass ==0.2.*
, http-types ==0.12.*
, http2 >=4.2.2 && <4.3
, ini ==0.4.1
, iproute ==1.7.*
, iso8601-time ==0.1.*
, memory ==0.18.*
, mtl >=2.3.1 && <3.0
, network >=3.1.2.7 && <3.2
, network-info ==0.2.*
, network-transport ==0.5.6
, network-udp ==0.0.*
, optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3
, simple-logger ==0.1.*
base
, simplexmq
, socks ==0.6.*
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
, temporary ==1.3.*
, time ==1.12.*
, time-manager ==0.0.*
, tls >=1.7.0 && <1.8
, transformers ==0.6.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, websockets ==0.12.*
, yaml ==0.11.*
, zstd ==0.1.3.*
default-language: Haskell2010
if flag(swift)
cpp-options: -DswiftJSON
if impl(ghc >= 9.6.2)
build-depends:
bytestring ==0.11.*
, template-haskell ==2.20.*
, text >=2.0.1 && <2.2
if impl(ghc < 9.6.2)
build-depends:
bytestring ==0.10.*
, template-haskell ==2.16.*
, text >=1.2.3.0 && <1.3
executable xftp-server
if flag(client_library)
buildable: False
main-is: Main.hs
other-modules:
Paths_simplexmq
@@ -519,74 +425,16 @@ executable xftp-server
apps/xftp-server
default-extensions:
StrictData
ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2 -threaded -rtsopts
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
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
, asn1-encoding ==0.9.*
, asn1-types ==0.3.*
, async ==2.2.*
, attoparsec ==0.14.*
, base >=4.14 && <5
, base64-bytestring >=1.0 && <1.3
, case-insensitive ==1.2.*
, composition ==1.0.*
, constraints >=0.12 && <0.14
, containers ==0.6.*
, crypton ==0.34.*
, crypton-x509 ==1.7.*
, crypton-x509-store ==1.6.*
, crypton-x509-validation ==1.6.*
, cryptostore ==0.3.*
, data-default ==0.7.*
, direct-sqlcipher ==2.3.*
, directory ==1.3.*
, filepath ==1.4.*
, hourglass ==0.2.*
, http-types ==0.12.*
, http2 >=4.2.2 && <4.3
, ini ==0.4.1
, iproute ==1.7.*
, iso8601-time ==0.1.*
, memory ==0.18.*
, mtl >=2.3.1 && <3.0
, network >=3.1.2.7 && <3.2
, network-info ==0.2.*
, network-transport ==0.5.6
, network-udp ==0.0.*
, optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3
, simple-logger ==0.1.*
base
, simple-logger
, simplexmq
, socks ==0.6.*
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
, temporary ==1.3.*
, time ==1.12.*
, time-manager ==0.0.*
, tls >=1.7.0 && <1.8
, transformers ==0.6.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, websockets ==0.12.*
, yaml ==0.11.*
, zstd ==0.1.3.*
default-language: Haskell2010
if flag(swift)
cpp-options: -DswiftJSON
if impl(ghc >= 9.6.2)
build-depends:
bytestring ==0.11.*
, template-haskell ==2.20.*
, text >=2.0.1 && <2.2
if impl(ghc < 9.6.2)
build-depends:
bytestring ==0.10.*
, template-haskell ==2.16.*
, text >=1.2.3.0 && <1.3
test-suite simplexmq-test
if flag(client_library)
buildable: False
type: exitcode-stdio-1.0
main-is: Test.hs
other-modules:
@@ -597,14 +445,17 @@ test-suite simplexmq-test
AgentTests.FunctionalAPITests
AgentTests.MigrationTests
AgentTests.NotificationTests
AgentTests.SchemaDump
AgentTests.SQLiteTests
AgentTests.ServerChoice
AgentTests.ShortLinkTests
CLITests
CoreTests.BatchingTests
CoreTests.CryptoFileTests
CoreTests.CryptoTests
CoreTests.EncodingTests
CoreTests.MsgStoreTests
CoreTests.RetryIntervalTests
CoreTests.SOCKSSettings
CoreTests.StoreLogTests
CoreTests.TRcvQueuesTests
CoreTests.UtilTests
CoreTests.VersionRangeTests
@@ -621,83 +472,83 @@ test-suite simplexmq-test
XFTPCLI
XFTPClient
XFTPServerTests
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:
ServerTests.SchemaDump
hs-source-dirs:
tests
apps/smp-server/web
default-extensions:
StrictData
ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=incomplete-uni-patterns -Werror=missing-methods -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -O2 -threaded -rtsopts -with-rtsopts=-A64M -with-rtsopts=-N1
-- 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:
HUnit ==1.6.*
, QuickCheck ==2.14.*
, aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
, asn1-encoding ==0.9.*
, asn1-types ==0.3.*
, async ==2.2.*
, attoparsec ==0.14.*
, base >=4.14 && <5
, base64-bytestring >=1.0 && <1.3
, case-insensitive ==1.2.*
, composition ==1.0.*
, constraints >=0.12 && <0.14
, containers ==0.6.*
, crypton ==0.34.*
, crypton-x509 ==1.7.*
, crypton-x509-store ==1.6.*
, crypton-x509-validation ==1.6.*
, cryptostore ==0.3.*
, data-default ==0.7.*
, deepseq ==1.4.*
, direct-sqlcipher ==2.3.*
, directory ==1.3.*
, filepath ==1.4.*
base
, aeson
, async
, base64-bytestring
, bytestring
, containers
, crypton
, crypton-x509
, crypton-x509-store
, crypton-x509-validation
, directory
, file-embed
, filepath
, generic-random ==1.5.*
, hourglass ==0.2.*
, hashable
, hspec ==2.11.*
, hspec-core ==2.11.*
, http-types ==0.12.*
, http2 >=4.2.2 && <4.3
, ini ==0.4.1
, iproute ==1.7.*
, iso8601-time ==0.1.*
, http-client
, http-types
, http2
, HUnit ==1.6.*
, ini
, iso8601-time
, main-tester ==0.2.*
, memory ==0.18.*
, mtl >=2.3.1 && <3.0
, network >=3.1.2.7 && <3.2
, network-info ==0.2.*
, network-transport ==0.5.6
, network-udp ==0.0.*
, optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3
, mtl
, network
, QuickCheck ==2.14.*
, random
, silently ==1.2.*
, simple-logger ==0.1.*
, simple-logger
, simplexmq
, socks ==0.6.*
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
, temporary ==1.3.*
, time ==1.12.*
, time-manager ==0.0.*
, stm
, text
, time
, timeit ==2.0.*
, tls >=1.7.0 && <1.8
, transformers ==0.6.*
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, websockets ==0.12.*
, yaml ==0.11.*
, zstd ==0.1.3.*
, transformers
, unliftio
, unliftio-core
, unordered-containers
, wai
, wai-app-static
, warp
, warp-tls
, yaml
default-language: Haskell2010
if flag(swift)
cpp-options: -DswiftJSON
if impl(ghc >= 9.6.2)
if flag(client_postgres)
cpp-options: -DdbPostgres
else
build-depends:
bytestring ==0.11.*
, template-haskell ==2.20.*
, text >=2.0.1 && <2.2
if impl(ghc < 9.6.2)
memory
, sqlcipher-simple
if !flag(client_postgres) || flag(server_postgres)
build-depends:
bytestring ==0.10.*
, template-haskell ==2.16.*
, text >=1.2.3.0 && <1.3
deepseq ==1.4.*
, process
if flag(client_postgres) || flag(server_postgres)
build-depends:
postgresql-simple ==0.7.*
if flag(server_postgres)
cpp-options: -DdbServerPostgres
+45 -41
View File
@@ -43,16 +43,15 @@ import Data.Either (partitionEithers, rights)
import Data.Int (Int64)
import Data.List (foldl', partition, sortOn)
import qualified Data.List.NonEmpty as L
import Data.Map (Map)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (mapMaybe)
import Data.Maybe (fromMaybe, mapMaybe)
import qualified Data.Set as S
import Data.Text (Text)
import Data.Time.Clock (getCurrentTime)
import Data.Time.Format (defaultTimeLocale, formatTime)
import Simplex.FileTransfer.Chunks (toKB)
import Simplex.FileTransfer.Client (XFTPChunkSpec (..))
import Simplex.FileTransfer.Client.Main
import Simplex.FileTransfer.Client (XFTPChunkSpec (..), getChunkDigest, prepareChunkSizes, prepareChunkSpecs, singleChunkSize)
import Simplex.FileTransfer.Crypto
import Simplex.FileTransfer.Description
import Simplex.FileTransfer.Protocol (FileParty (..), SFileParty (..))
@@ -66,15 +65,15 @@ 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
import qualified Simplex.Messaging.Crypto.Lazy as LC
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String (strDecode, strEncode)
import Simplex.Messaging.Protocol (EntityId, ProtocolServer, ProtocolType (..), XFTPServer)
import Simplex.Messaging.Protocol (ProtocolServer, ProtocolType (..), XFTPServer)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Util (catchAll_, liftError, tshow, unlessM, whenM)
import System.FilePath (takeFileName, (</>))
@@ -184,16 +183,18 @@ runXFTPRcvWorker c srv Worker {doWork} = do
cfg <- asks config
forever $ do
lift $ waitForWork doWork
atomically $ assertAgentForeground c
liftIO $ assertAgentForeground c
runXFTPOperation cfg
where
runXFTPOperation :: AgentConfig -> AM ()
runXFTPOperation AgentConfig {rcvFilesTTL, reconnectInterval = ri, xftpConsecutiveRetries} =
withWork c doWork (\db -> getNextRcvChunkToDownload db srv rcvFilesTTL) $ \case
(RcvFileChunk {rcvFileId, rcvFileEntityId, fileTmpPath, replicas = []}, _) -> rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) (INTERNAL "chunk has no replicas")
(fc@RcvFileChunk {userId, rcvFileId, rcvFileEntityId, digest, fileTmpPath, replicas = replica@RcvFileChunkReplica {rcvChunkReplicaId, server, delay} : _}, approvedRelays) -> do
(RcvFileChunk {rcvFileId, rcvFileEntityId, fileTmpPath, replicas = []}, _, redirectEntityId_) ->
rcvWorkerInternalError c rcvFileId rcvFileEntityId redirectEntityId_ (Just fileTmpPath) (INTERNAL "chunk has no replicas")
(fc@RcvFileChunk {userId, rcvFileId, rcvFileEntityId, digest, fileTmpPath, replicas = replica@RcvFileChunkReplica {rcvChunkReplicaId, server, delay} : _}, approvedRelays, redirectEntityId_) -> do
let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay
withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do
liftIO $ waitWhileSuspended c
liftIO $ waitForUserNetwork c
atomically $ incXFTPServerStat c userId srv downloadAttempts
downloadFileChunk fc replica approvedRelays
@@ -201,16 +202,16 @@ runXFTPRcvWorker c srv Worker {doWork} = do
where
retryLoop loop e replicaDelay = do
flip catchAgentError (\_ -> pure ()) $ do
when (serverHostError e) $ notify c rcvFileEntityId $ RFWARN e
when (serverHostError e) $ notify c (fromMaybe rcvFileEntityId redirectEntityId_) (RFWARN e)
liftIO $ closeXFTPServerClient c userId server digest
withStore' c $ \db -> updateRcvChunkReplicaDelay db rcvChunkReplicaId replicaDelay
atomically $ assertAgentForeground c
liftIO $ assertAgentForeground c
loop
retryDone e = do
atomically . incXFTPServerStat c userId srv $ case e of
XFTP _ XFTP.AUTH -> downloadAuthErrs
_ -> downloadErrs
rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) e
rcvWorkerInternalError c rcvFileId rcvFileEntityId redirectEntityId_ (Just fileTmpPath) e
downloadFileChunk :: RcvFileChunk -> RcvFileChunkReplica -> Bool -> AM ()
downloadFileChunk RcvFileChunk {userId, rcvFileId, rcvFileEntityId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath} replica approvedRelays = do
unlessM ((approvedRelays ||) <$> ipAddressProtected') $ throwE $ FILE NOT_APPROVED
@@ -220,7 +221,7 @@ runXFTPRcvWorker c srv Worker {doWork} = do
chunkSpec = XFTPRcvChunkSpec chunkPath chSize (unFileDigest digest)
relChunkPath = fileTmpPath </> takeFileName chunkPath
agentXFTPDownloadChunk c userId digest replica chunkSpec
atomically $ waitUntilForeground c
liftIO $ waitUntilForeground c
(entityId, complete, progress) <- withStore c $ \db -> runExceptT $ do
liftIO $ updateRcvFileChunkReceived db (rcvChunkReplicaId replica) rcvChunkId relChunkPath
RcvFile {size = FileSize currentSize, chunks, redirect} <- ExceptT $ getRcvFile db rcvFileId
@@ -239,7 +240,7 @@ runXFTPRcvWorker c srv Worker {doWork} = do
where
ipAddressProtected' :: AM Bool
ipAddressProtected' = do
cfg <- liftIO $ getNetworkConfig' c
cfg <- liftIO $ getFastNetworkConfig c
pure $ ipAddressProtected cfg srv
receivedSize :: [RcvFileChunk] -> Int64
receivedSize = foldl' (\sz ch -> sz + receivedChunkSize ch) 0
@@ -261,25 +262,25 @@ retryOnError name loop done e = do
then loop
else done
rcvWorkerInternalError :: AgentClient -> DBRcvFileId -> RcvFileId -> Maybe FilePath -> AgentErrorType -> AM ()
rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath err = do
rcvWorkerInternalError :: AgentClient -> DBRcvFileId -> RcvFileId -> Maybe RcvFileId -> Maybe FilePath -> AgentErrorType -> AM ()
rcvWorkerInternalError c rcvFileId rcvFileEntityId redirectEntityId_ tmpPath err = do
lift $ forM_ tmpPath (removePath <=< toFSFilePath)
withStore' c $ \db -> updateRcvFileError db rcvFileId (show err)
notify c rcvFileEntityId $ RFERR err
notify c (fromMaybe rcvFileEntityId redirectEntityId_) (RFERR err)
runXFTPRcvLocalWorker :: AgentClient -> Worker -> AM ()
runXFTPRcvLocalWorker c Worker {doWork} = do
cfg <- asks config
forever $ do
lift $ waitForWork doWork
atomically $ assertAgentForeground c
liftIO $ assertAgentForeground c
runXFTPOperation cfg
where
runXFTPOperation :: AgentConfig -> AM ()
runXFTPOperation AgentConfig {rcvFilesTTL} =
withWork c doWork (`getNextRcvFileToDecrypt` rcvFilesTTL) $
\f@RcvFile {rcvFileId, rcvFileEntityId, tmpPath} ->
decryptFile f `catchAgentError` rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath
\f@RcvFile {rcvFileId, rcvFileEntityId, tmpPath, redirect} ->
decryptFile f `catchAgentError` rcvWorkerInternalError c rcvFileId rcvFileEntityId (redirectEntityId <$> redirect) tmpPath
decryptFile :: RcvFile -> AM ()
decryptFile RcvFile {rcvFileId, rcvFileEntityId, size, digest, key, nonce, tmpPath, saveFile, status, chunks, redirect} = do
let CryptoFile savePath cfArgs = saveFile
@@ -298,12 +299,12 @@ runXFTPRcvLocalWorker c Worker {doWork} = do
Nothing -> do
notify c rcvFileEntityId $ RFDONE fsSavePath
lift $ forM_ tmpPath (removePath <=< toFSFilePath)
atomically $ waitUntilForeground c
liftIO $ waitUntilForeground c
withStore' c (`updateRcvFileComplete` rcvFileId)
Just RcvFileRedirect {redirectFileInfo, redirectDbId} -> do
let RedirectFileInfo {size = redirectSize, digest = redirectDigest} = redirectFileInfo
lift $ forM_ tmpPath (removePath <=< toFSFilePath)
atomically $ waitUntilForeground c
liftIO $ waitUntilForeground c
withStore' c (`updateRcvFileComplete` rcvFileId)
-- proceed with redirect
yaml <- liftError (FILE . FILE_IO . show) (CF.readFile $ CryptoFile fsSavePath cfArgs) `agentFinally` (lift $ toFSFilePath fsSavePath >>= removePath)
@@ -345,7 +346,7 @@ xftpDeleteRcvFiles' c rcvFileEntityIds = do
batchFiles :: (DB.Connection -> DBRcvFileId -> IO a) -> [RcvFile] -> AM' [Either AgentErrorType a]
batchFiles f rcvFiles = withStoreBatch' c $ \db -> map (\RcvFile {rcvFileId} -> f db rcvFileId) rcvFiles
notify :: forall m e. (MonadIO m, AEntityI e) => AgentClient -> EntityId -> AEvent e -> m ()
notify :: forall m e. (MonadIO m, AEntityI e) => AgentClient -> AEntityId -> AEvent e -> m ()
notify c entId cmd = atomically $ writeTBQueue (subQ c) ("", entId, AEvt (sAEntity @e) cmd)
xftpSendFile' :: AgentClient -> UserId -> CryptoFile -> Int -> AM SndFileId
@@ -391,7 +392,7 @@ runXFTPSndPrepareWorker c Worker {doWork} = do
cfg <- asks config
forever $ do
lift $ waitForWork doWork
atomically $ assertAgentForeground c
liftIO $ assertAgentForeground c
runXFTPOperation cfg
where
runXFTPOperation :: AgentConfig -> AM ()
@@ -453,31 +454,32 @@ runXFTPSndPrepareWorker c Worker {doWork} = do
SndFileChunkReplica {server} : _ -> Right server
createChunk :: Int -> SndFileChunk -> AM (ProtocolServer 'PXFTP)
createChunk numRecipients' ch = do
atomically $ assertAgentForeground c
liftIO $ assertAgentForeground c
(replica, ProtoServerWithAuth srv _) <- tryCreate
withStore' c $ \db -> createSndFileReplica db ch replica
pure srv
where
tryCreate = do
usedSrvs <- newTVarIO ([] :: [XFTPServer])
triedHosts <- newTVarIO S.empty
let AgentClient {xftpServers} = c
userSrvCount <- length <$> atomically (TM.lookup userId xftpServers)
userSrvCount <- liftIO $ length <$> TM.lookupIO userId xftpServers
withRetryIntervalCount (riFast ri) $ \n _ loop -> do
liftIO $ waitWhileSuspended c
liftIO $ waitForUserNetwork c
let triedAllSrvs = n > userSrvCount
createWithNextSrv usedSrvs
createWithNextSrv triedHosts
`catchAgentError` \e -> retryOnError "XFTP prepare worker" (retryLoop loop triedAllSrvs e) (throwE e) e
where
-- we don't do closeXFTPServerClient here to not risk closing connection for concurrent chunk upload
retryLoop loop triedAllSrvs e = do
flip catchAgentError (\_ -> pure ()) $ do
when (triedAllSrvs && serverHostError e) $ notify c sndFileEntityId $ SFWARN e
atomically $ assertAgentForeground c
liftIO $ assertAgentForeground c
loop
createWithNextSrv usedSrvs = do
createWithNextSrv triedHosts = do
deleted <- withStore' c $ \db -> getSndFileDeleted db sndFileId
when deleted $ throwE $ FILE NO_FILE
withNextSrv c userId usedSrvs [] $ \srvAuth -> do
withNextSrv c userId storageSrvs triedHosts [] $ \srvAuth -> do
replica <- agentXFTPNewChunk c ch numRecipients' srvAuth
pure (replica, srvAuth)
@@ -492,7 +494,7 @@ runXFTPSndWorker c srv Worker {doWork} = do
cfg <- asks config
forever $ do
lift $ waitForWork doWork
atomically $ assertAgentForeground c
liftIO $ assertAgentForeground c
runXFTPOperation cfg
where
runXFTPOperation :: AgentConfig -> AM ()
@@ -502,6 +504,7 @@ runXFTPSndWorker c srv Worker {doWork} = do
fc@SndFileChunk {userId, sndFileId, sndFileEntityId, filePrefixPath, digest, replicas = replica@SndFileChunkReplica {sndChunkReplicaId, server, delay} : _} -> do
let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay
withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do
liftIO $ waitWhileSuspended c
liftIO $ waitForUserNetwork c
atomically $ incXFTPServerStat c userId srv uploadAttempts
uploadFileChunk cfg fc replica
@@ -512,7 +515,7 @@ runXFTPSndWorker c srv Worker {doWork} = do
when (serverHostError e) $ notify c sndFileEntityId $ SFWARN e
liftIO $ closeXFTPServerClient c userId server digest
withStore' c $ \db -> updateSndChunkReplicaDelay db sndChunkReplicaId replicaDelay
atomically $ assertAgentForeground c
liftIO $ assertAgentForeground c
loop
retryDone e = do
atomically $ incXFTPServerStat c userId srv uploadErrs
@@ -523,9 +526,9 @@ runXFTPSndWorker c srv Worker {doWork} = do
fsFilePath <- lift $ toFSFilePath filePath
unlessM (doesFileExist fsFilePath) $ throwE $ FILE NO_FILE
let chunkSpec' = chunkSpec {filePath = fsFilePath} :: XFTPChunkSpec
atomically $ assertAgentForeground c
liftIO $ assertAgentForeground c
agentXFTPUploadChunk c userId chunkDigest replica' chunkSpec'
atomically $ waitUntilForeground c
liftIO $ waitUntilForeground c
sf@SndFile {sndFileEntityId, prefixPath, chunks} <- withStore c $ \db -> do
updateSndChunkReplicaStatus db sndChunkReplicaId SFRSUploaded
getSndFile db sndFileId
@@ -542,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
@@ -663,7 +666,7 @@ runXFTPDelWorker c srv Worker {doWork} = do
cfg <- asks config
forever $ do
lift $ waitForWork doWork
atomically $ assertAgentForeground c
liftIO $ assertAgentForeground c
runXFTPOperation cfg
where
runXFTPOperation :: AgentConfig -> AM ()
@@ -674,6 +677,7 @@ runXFTPDelWorker c srv Worker {doWork} = do
processDeletedReplica replica@DeletedSndChunkReplica {deletedSndChunkReplicaId, userId, server, chunkDigest, delay} = do
let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay
withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do
liftIO $ waitWhileSuspended c
liftIO $ waitForUserNetwork c
atomically $ incXFTPServerStat c userId srv deleteAttempts
deleteChunkReplica
@@ -684,7 +688,7 @@ runXFTPDelWorker c srv Worker {doWork} = do
when (serverHostError e) $ notify c "" $ SFWARN e
liftIO $ closeXFTPServerClient c userId server chunkDigest
withStore' c $ \db -> updateDeletedSndChunkReplicaDelay db deletedSndChunkReplicaId replicaDelay
atomically $ assertAgentForeground c
liftIO $ assertAgentForeground c
loop
retryDone e = do
atomically $ incXFTPServerStat c userId srv deleteErrs
@@ -699,7 +703,7 @@ delWorkerInternalError c deletedSndChunkReplicaId e = do
withStore' c $ \db -> deleteDeletedSndChunkReplica db deletedSndChunkReplicaId
notify c "" $ SFERR e
assertAgentForeground :: AgentClient -> STM ()
assertAgentForeground :: AgentClient -> IO ()
assertAgentForeground c = do
throwWhenInactive c
waitUntilForeground c
+55 -12
View File
@@ -20,15 +20,19 @@ 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.Server.Env (supportedXFTPhandshakes)
import Simplex.FileTransfer.Transport
import Simplex.Messaging.Client
( NetworkConfig (..),
@@ -36,8 +40,8 @@ import Simplex.Messaging.Client
TransportSession,
chooseTransportHost,
defaultNetworkConfig,
proxyUsername,
transportClientConfig,
clientSocksCredentials,
unexpectedResponse,
)
import qualified Simplex.Messaging.Crypto as C
@@ -50,8 +54,9 @@ import Simplex.Messaging.Protocol
ProtocolServer (..),
RecipientId,
SenderId,
pattern NoEntity,
)
import Simplex.Messaging.Transport (ALPN, HandshakeError (..), THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), supportedParameters)
import Simplex.Messaging.Transport (ALPN, HandshakeError (..), THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), defaultSupportedParams)
import Simplex.Messaging.Transport.Client (TransportClientConfig, TransportHost, alpn)
import Simplex.Messaging.Transport.HTTP2
import Simplex.Messaging.Transport.HTTP2.Client
@@ -97,21 +102,21 @@ defaultXFTPClientConfig =
clientALPN = Just supportedXFTPhandshakes
}
getXFTPClient :: TransportSession FileResponse -> XFTPClientConfig -> (XFTPClient -> IO ()) -> IO (Either XFTPClientError XFTPClient)
getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, xftpNetworkConfig, serverVRange} disconnected = runExceptT $ do
let username = proxyUsername transportSession
getXFTPClient :: TransportSession FileResponse -> XFTPClientConfig -> UTCTime -> (XFTPClient -> IO ()) -> IO (Either XFTPClientError XFTPClient)
getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, xftpNetworkConfig, serverVRange} proxySessTs disconnected = runExceptT $ do
let socksCreds = clientSocksCredentials xftpNetworkConfig proxySessTs transportSession
ProtocolServer _ host port keyHash = srv
useHost <- liftEither $ chooseTransportHost xftpNetworkConfig host
let tcConfig = (transportClientConfig xftpNetworkConfig useHost) {alpn = clientALPN}
let tcConfig = (transportClientConfig xftpNetworkConfig useHost False) {alpn = clientALPN}
http2Config = xftpHTTP2Config tcConfig config
clientVar <- newTVarIO Nothing
let usePort = if null port then "443" else port
clientDisconnected = readTVarIO clientVar >>= mapM_ disconnected
http2Client <- liftError' xftpClientError $ getVerifiedHTTP2Client (Just username) useHost usePort (Just keyHash) Nothing http2Config clientDisconnected
http2Client <- liftError' xftpClientError $ getVerifiedHTTP2Client socksCreds useHost usePort (Just keyHash) Nothing http2Config clientDisconnected
let HTTP2Client {sessionId, sessionALPN} = http2Client
v = VersionXFTP 1
thServerVRange = versionToRange v
thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = v, thServerVRange, thAuth = Nothing, implySessId = False, batch = True}
thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = v, thServerVRange, thAuth = Nothing, implySessId = False, encryptBlock = Nothing, batch = True}
logDebug $ "Client negotiated handshake protocol: " <> tshow sessionALPN
thParams@THandleParams {thVersion} <- case sessionALPN of
Just "xftp/1" -> xftpClientHandshakeV1 serverVRange keyHash http2Client thParams0
@@ -171,7 +176,7 @@ xftpHTTP2Config :: TransportClientConfig -> XFTPClientConfig -> HTTP2ClientConfi
xftpHTTP2Config transportConfig XFTPClientConfig {xftpNetworkConfig = NetworkConfig {tcpConnectTimeout}} =
defaultHTTP2ClientConfig
{ bodyHeadSize = xftpBlockSize,
suportedTLSParams = supportedParameters,
suportedTLSParams = defaultSupportedParams,
connTimeout = tcpConnectTimeout,
transportConfig
}
@@ -222,7 +227,7 @@ createXFTPChunk ::
Maybe BasicAuth ->
ExceptT XFTPClientError IO (SenderId, NonEmpty RecipientId)
createXFTPChunk c spKey file rcps auth_ =
sendXFTPCommand c spKey "" (FNEW file rcps auth_) Nothing >>= \case
sendXFTPCommand c spKey NoEntity (FNEW file rcps auth_) Nothing >>= \case
(FRSndIds sId rIds, body) -> noFile body (sId, rIds)
(r, _) -> throwE $ unexpectedResponse r
@@ -278,7 +283,7 @@ pingXFTP :: XFTPClient -> ExceptT XFTPClientError IO ()
pingXFTP c@XFTPClient {thParams} = do
t <-
liftEither . first PCETransportError $
xftpEncodeTransmission thParams ("", "", FileCmd SFRecipient PING)
xftpEncodeTransmission thParams ("", NoEntity, FileCmd SFRecipient PING)
(r, _) <- sendXFTPTransmission c t Nothing
case r of
FRPong -> pure ()
@@ -297,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
+8 -5
View File
@@ -16,6 +16,7 @@ import Data.Bifunctor (first)
import qualified Data.ByteString.Char8 as B
import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8)
import Data.Time.Clock (UTCTime, getCurrentTime)
import Simplex.FileTransfer.Client
import Simplex.Messaging.Agent.RetryInterval
import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientError (..), temporaryClientError)
@@ -30,6 +31,7 @@ type XFTPClientVar = TMVar (Either XFTPClientAgentError XFTPClient)
data XFTPClientAgent = XFTPClientAgent
{ xftpClients :: TMap XFTPServer XFTPClientVar,
startedAt :: UTCTime,
config :: XFTPClientAgentConfig
}
@@ -53,22 +55,23 @@ defaultXFTPClientAgentConfig =
data XFTPClientAgentError = XFTPClientAgentError XFTPServer XFTPClientError
deriving (Show, Exception)
newXFTPAgent :: XFTPClientAgentConfig -> STM XFTPClientAgent
newXFTPAgent :: XFTPClientAgentConfig -> IO XFTPClientAgent
newXFTPAgent config = do
xftpClients <- TM.empty
pure XFTPClientAgent {xftpClients, config}
xftpClients <- TM.emptyIO
startedAt <- getCurrentTime
pure XFTPClientAgent {xftpClients, startedAt, config}
type ME a = ExceptT XFTPClientAgentError IO a
getXFTPServerClient :: XFTPClientAgent -> XFTPServer -> ME XFTPClient
getXFTPServerClient XFTPClientAgent {xftpClients, config} srv = do
getXFTPServerClient XFTPClientAgent {xftpClients, startedAt, config} srv = do
atomically getClientVar >>= either newXFTPClient waitForXFTPClient
where
connectClient :: ME XFTPClient
connectClient =
ExceptT $
first (XFTPClientAgentError srv)
<$> getXFTPClient (1, srv, Nothing) (xftpConfig config) clientDisconnected
<$> getXFTPClient (1, srv, Nothing) (xftpConfig config) startedAt clientDisconnected
clientDisconnected :: XFTPClient -> IO ()
clientDisconnected _ = do
+6 -73
View File
@@ -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)
@@ -43,9 +38,9 @@ import Data.Int (Int64)
import Data.List (foldl', sortOn)
import Data.List.NonEmpty (NonEmpty (..), nonEmpty)
import qualified Data.List.NonEmpty as L
import Data.Map (Map)
import qualified Data.Map as M
import Data.Maybe (fromMaybe, listToMaybe)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
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}
@@ -313,7 +284,7 @@ cliSendFileOpts SendOptions {filePath, outputDir, numRecipients, xftpServers, re
pure (encPath, fdRcv, fdSnd, chunkSpecs, encSize)
uploadFile :: TVar ChaChaDRG -> [XFTPChunkSpec] -> TVar [Int64] -> Int64 -> ExceptT CLIError IO [SentFileChunk]
uploadFile g chunks uploadedChunks encSize = do
a <- atomically $ newXFTPAgent defaultXFTPClientAgentConfig
a <- liftIO $ newXFTPAgent defaultXFTPClientAgentConfig
gen <- newTVarIO =<< liftIO newStdGen
let xftpSrvs = fromMaybe defaultXFTPServers (nonEmpty xftpServers)
srvs <- liftIO $ replicateM (length chunks) $ getXFTPServer gen xftpSrvs
@@ -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
@@ -429,7 +393,7 @@ cliReceiveFile ReceiveOptions {fileDescription, filePath, retryCount, tempPath,
receive (ValidFileDescription FileDescription {size, digest, key, nonce, chunks}) = do
encPath <- getEncPath tempPath "xftp"
createDirectory encPath
a <- atomically $ newXFTPAgent defaultXFTPClientAgentConfig
a <- liftIO $ newXFTPAgent defaultXFTPClientAgentConfig
liftIO $ printNoNewLine "Downloading file..."
downloadedChunks <- newTVarIO []
let srv FileChunk {replicas} = case replicas of
@@ -494,7 +458,7 @@ cliDeleteFile DeleteOptions {fileDescription, retryCount, yes} = do
where
deleteFile :: ValidFileDescription 'FSender -> ExceptT CLIError IO ()
deleteFile (ValidFileDescription FileDescription {chunks}) = do
a <- atomically $ newXFTPAgent defaultXFTPClientAgentConfig
a <- liftIO $ newXFTPAgent defaultXFTPClientAgentConfig
forM_ chunks $ deleteFileChunk a
liftIO $ do
printNoNewLine "File deleted!"
@@ -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
+32 -19
View File
@@ -1,12 +1,15 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
@@ -35,6 +38,10 @@ module Simplex.FileTransfer.Description
FileClientData,
fileDescriptionURI,
qrSizeLimit,
maxFileSize,
maxFileSizeStr,
maxFileSizeHard,
fileSizeLen,
)
where
@@ -52,19 +59,18 @@ import Data.Int (Int64)
import Data.List (foldl', sortOn)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as L
import Data.Map (Map)
import qualified Data.Map as M
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe)
import Data.String
import Data.Text (Text)
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)
@@ -107,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
@@ -120,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,
@@ -139,12 +144,9 @@ data FileChunkReplica = FileChunkReplica
}
deriving (Eq, Show)
newtype ChunkReplicaId = ChunkReplicaId {unChunkReplicaId :: ByteString}
newtype ChunkReplicaId = ChunkReplicaId {unChunkReplicaId :: XFTPFileId}
deriving (Eq, Show)
instance StrEncoding ChunkReplicaId where
strEncode (ChunkReplicaId fid) = strEncode fid
strP = ChunkReplicaId <$> strP
deriving newtype (StrEncoding)
instance FromJSON ChunkReplicaId where
parseJSON = strParseJSON "ChunkReplicaId"
@@ -153,10 +155,6 @@ instance ToJSON ChunkReplicaId where
toJSON = strToJSON
toEncoding = strToJEncoding
instance FromField ChunkReplicaId where fromField f = ChunkReplicaId <$> fromField f
instance ToField ChunkReplicaId where toField (ChunkReplicaId s) = toField s
data YAMLFileDescription = YAMLFileDescription
{ party :: FileParty,
size :: String,
@@ -271,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
@@ -293,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 =
+9 -6
View File
@@ -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
@@ -41,6 +41,7 @@ import Simplex.Messaging.Protocol
ProtocolType (..),
RcvPublicAuthKey,
RcvPublicDhKey,
EntityId (..),
RecipientId,
SenderId,
SentRawTransmission,
@@ -170,7 +171,7 @@ data FileInfo = FileInfo
}
deriving (Show)
type XFTPFileId = ByteString
type XFTPFileId = EntityId
instance FilePartyI p => ProtocolEncoding XFTPVersion XFTPErrorType (FileCommand p) where
type Tag (FileCommand p) = FileCommandTag p
@@ -191,7 +192,7 @@ instance FilePartyI p => ProtocolEncoding XFTPVersion XFTPErrorType (FileCommand
fromProtocolError = fromProtocolError @XFTPVersion @XFTPErrorType @FileResponse
{-# INLINE fromProtocolError #-}
checkCredentials (auth, _, fileId, _) cmd = case cmd of
checkCredentials (auth, _, EntityId fileId, _) cmd = case cmd of
-- FNEW must not have signature and chunk ID
FNEW {}
| isNothing auth -> Left $ CMD NO_AUTH
@@ -275,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
@@ -301,7 +304,7 @@ instance ProtocolEncoding XFTPVersion XFTPErrorType FileResponse where
PEBlock -> BLOCK
{-# INLINE fromProtocolError #-}
checkCredentials (_, _, entId, _) cmd = case cmd of
checkCredentials (_, _, EntityId entId, _) cmd = case cmd of
FRSndIds {} -> noEntity
-- ERR response does not always have entity ID
FRErr _ -> Right cmd
+94 -73
View File
@@ -9,6 +9,7 @@
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
@@ -32,11 +33,11 @@ import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe, isJust)
import qualified Data.Text as T
import Data.Time.Clock (UTCTime (..), diffTimeToPicoseconds, getCurrentTime)
import Data.Time.Clock.System (SystemTime (..), getSystemTime)
import Data.Time.Format.ISO8601 (iso8601Show)
import Data.Word (Word32)
import qualified Data.X509 as X
import GHC.IO.Handle (hSetNewlineMode)
import GHC.IORef (atomicSwapIORef)
import GHC.Stats (getRTSStats)
import qualified Network.HTTP.Types as N
import qualified Network.HTTP2.Server as H
@@ -52,18 +53,20 @@ import qualified Simplex.Messaging.Crypto as C
import qualified Simplex.Messaging.Crypto.Lazy as LC
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (CorrId (..), RcvPublicAuthKey, RcvPublicDhKey, RecipientId, TransmissionAuth)
import Simplex.Messaging.Protocol (CorrId (..), 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, ServerEntityStatus (..), getRoundedSystemTime)
import Simplex.Messaging.Server.Stats
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport (SessionId, THandleAuth (..), THandleParams (..), TransportPeer (..))
import Simplex.Messaging.Transport (ALPN, SessionId, THandleAuth (..), THandleParams (..), TransportPeer (..), defaultSupportedParams)
import Simplex.Messaging.Transport.Buffer (trimCR)
import Simplex.Messaging.Transport.HTTP2
import Simplex.Messaging.Transport.HTTP2.File (fileBlockSize)
import Simplex.Messaging.Transport.HTTP2.Server
import Simplex.Messaging.Transport.Server (runTCPServer, tlsServerCredentials)
import Simplex.Messaging.Transport.Server (runLocalTCPServer)
import Simplex.Messaging.Util
import Simplex.Messaging.Version
import System.Exit (exitFailure)
@@ -89,36 +92,35 @@ data XFTPTransportRequest = XFTPTransportRequest
runXFTPServer :: XFTPServerConfig -> IO ()
runXFTPServer cfg = do
started <- newEmptyTMVarIO
runXFTPServerBlocking started cfg
runXFTPServerBlocking started cfg $ Just supportedXFTPhandshakes
runXFTPServerBlocking :: TMVar Bool -> XFTPServerConfig -> IO ()
runXFTPServerBlocking started cfg = newXFTPServerEnv cfg >>= runReaderT (xftpServer cfg started)
runXFTPServerBlocking :: TMVar Bool -> XFTPServerConfig -> Maybe [ALPN] -> IO ()
runXFTPServerBlocking started cfg alpn_ = newXFTPServerEnv cfg >>= runReaderT (xftpServer cfg started alpn_)
data Handshake
= HandshakeSent C.PrivateKeyX25519
| HandshakeAccepted (THandleParams XFTPVersion 'TServer)
xftpServer :: XFTPServerConfig -> TMVar Bool -> M ()
xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration, fileExpiration, xftpServerVRange} started = do
xftpServer :: XFTPServerConfig -> TMVar Bool -> Maybe [ALPN] -> M ()
xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration, fileExpiration, xftpServerVRange} started alpn_ = do
mapM_ (expireServerFiles Nothing) fileExpiration
restoreServerStats
raceAny_ (runServer : expireFilesThread_ cfg <> serverStatsThread_ cfg <> controlPortThread_ cfg) `finally` stopServer
where
runServer :: M ()
runServer = do
serverParams <- asks tlsServerParams
let (chain, pk) = tlsServerCredentials serverParams
srvCreds@(chain, pk) <- asks tlsServerCreds
signKey <- liftIO $ case C.x509ToPrivate (pk, []) >>= C.privKey of
Right pk' -> pure pk'
Left e -> putStrLn ("servers has no valid key: " <> show e) >> exitFailure
env <- ask
sessions <- atomically TM.empty
sessions <- liftIO TM.emptyIO
let cleanup sessionId = atomically $ TM.delete sessionId sessions
liftIO . runHTTP2Server started xftpPort defaultHTTP2BufferSize serverParams transportConfig inactiveClientExpiration cleanup $ \sessionId sessionALPN r sendResponse -> do
liftIO . runHTTP2Server started xftpPort defaultHTTP2BufferSize defaultSupportedParams srvCreds alpn_ transportConfig inactiveClientExpiration cleanup $ \sessionId sessionALPN r sendResponse -> do
reqBody <- getHTTP2Body r xftpBlockSize
let v = VersionXFTP 1
thServerVRange = versionToRange v
thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = v, thServerVRange, thAuth = Nothing, implySessId = False, batch = True}
thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = v, thServerVRange, thAuth = Nothing, implySessId = False, encryptBlock = Nothing, batch = True}
req0 = XFTPTransportRequest {thParams = thParams0, request = r, reqBody, sendResponse}
flip runReaderT env $ case sessionALPN of
Nothing -> processRequest req0
@@ -179,6 +181,7 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
stopServer = do
withFileLog closeStoreLog
saveServerStats
logInfo "Server stopped"
expireFilesThread_ :: XFTPServerConfig -> [M ()]
expireFilesThread_ XFTPServerConfig {fileExpiration = Just fileExp} = [expireFiles fileExp]
@@ -207,17 +210,17 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
withFile statsFilePath AppendMode $ \h -> liftIO $ do
hSetBuffering h LineBuffering
ts <- getCurrentTime
fromTime' <- atomically $ swapTVar fromTime ts
filesCreated' <- atomically $ swapTVar filesCreated 0
fileRecipients' <- atomically $ swapTVar fileRecipients 0
filesUploaded' <- atomically $ swapTVar filesUploaded 0
filesExpired' <- atomically $ swapTVar filesExpired 0
filesDeleted' <- atomically $ swapTVar filesDeleted 0
files <- atomically $ periodStatCounts filesDownloaded ts
fileDownloads' <- atomically $ swapTVar fileDownloads 0
fileDownloadAcks' <- atomically $ swapTVar fileDownloadAcks 0
filesCount' <- readTVarIO filesCount
filesSize' <- readTVarIO filesSize
fromTime' <- atomicSwapIORef fromTime ts
filesCreated' <- atomicSwapIORef filesCreated 0
fileRecipients' <- atomicSwapIORef fileRecipients 0
filesUploaded' <- atomicSwapIORef filesUploaded 0
filesExpired' <- atomicSwapIORef filesExpired 0
filesDeleted' <- atomicSwapIORef filesDeleted 0
files <- liftIO $ periodStatCounts filesDownloaded ts
fileDownloads' <- atomicSwapIORef fileDownloads 0
fileDownloadAcks' <- atomicSwapIORef fileDownloadAcks 0
filesCount' <- readIORef filesCount
filesSize' <- readIORef filesSize
hPutStrLn h $
intercalate
","
@@ -247,7 +250,7 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
u <- askUnliftIO
liftIO $ do
labelMyThread "control port server"
runTCPServer cpStarted port $ runCPClient u
runLocalTCPServer cpStarted port $ runCPClient u
where
runCPClient :: UnliftIO (ReaderT XFTPEnv IO) -> Socket -> IO ()
runCPClient u sock = do
@@ -284,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 ()
@@ -309,7 +316,7 @@ data ServerFile = ServerFile
processRequest :: XFTPTransportRequest -> M ()
processRequest XFTPTransportRequest {thParams, reqBody = body@HTTP2Body {bodyHead}, sendResponse}
| B.length bodyHead /= xftpBlockSize = sendXFTPResponse ("", "", FRErr BLOCK) Nothing
| B.length bodyHead /= xftpBlockSize = sendXFTPResponse ("", NoEntity, FRErr BLOCK) Nothing
| otherwise = do
case xftpDecodeTransmission thParams bodyHead of
Right (sig_, signed, (corrId, fId, cmdOrErr)) ->
@@ -318,11 +325,11 @@ 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)
Left e -> sendXFTPResponse ("", "", FRErr e) Nothing
Left e -> sendXFTPResponse ("", NoEntity, FRErr e) Nothing
where
sendXFTPResponse (corrId, fId, resp) serverFile_ = do
let t_ = xftpEncodeTransmission thParams (corrId, fId, resp)
@@ -352,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 =
@@ -364,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
@@ -387,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)
@@ -397,23 +410,23 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
r <- runExceptT $ do
sizes <- asks $ allowedChunkSizes . config
unless (size file `elem` sizes) $ throwE SIZE
ts <- liftIO getSystemTime
ts <- liftIO getFileTime
-- TODO validate body empty
sId <- ExceptT $ addFileRetry st file 3 ts
rcps <- mapM (ExceptT . addRecipientRetry st 3 sId) rks
lift $ withFileLog $ \sl -> do
logAddFile sl sId file ts
logAddFile sl sId file ts EntityActive
logAddRecipients sl sId rcps
stats <- asks serverStats
atomically $ modifyTVar' (filesCreated stats) (+ 1)
atomically $ modifyTVar' (fileRecipients stats) (+ length rks)
lift $ incFileStat filesCreated
liftIO $ atomicModifyIORef'_ (fileRecipients stats) (+ length rks)
let rIds = L.map (\(FileRecipient rId _) -> rId) rcps
pure $ FRSndIds sId rIds
pure $ either FRErr id r
addFileRetry :: FileStore -> FileInfo -> Int -> SystemTime -> M (Either XFTPErrorType XFTPFileId)
addFileRetry :: FileStore -> FileInfo -> Int -> RoundedSystemTime -> M (Either XFTPErrorType XFTPFileId)
addFileRetry st file n ts =
retryAdd n $ \sId -> runExceptT $ do
ExceptT $ addFile st sId file ts
ExceptT $ addFile st sId file ts EntityActive
pure sId
addRecipientRetry :: FileStore -> Int -> XFTPFileId -> RcvPublicAuthKey -> M (Either XFTPErrorType FileRecipient)
addRecipientRetry st n sId rpk =
@@ -435,7 +448,7 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
rcps <- mapM (ExceptT . addRecipientRetry st 3 sId) rks
lift $ withFileLog $ \sl -> logAddRecipients sl sId rcps
stats <- asks serverStats
atomically $ modifyTVar' (fileRecipients stats) (+ length rks)
liftIO $ atomicModifyIORef'_ (fileRecipients stats) (+ length rks)
let rIds = L.map (\(FileRecipient rId _) -> rId) rcps
pure $ FRRcvIds rIds
pure $ either FRErr id r
@@ -463,19 +476,19 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
\used -> let used' = used + fromIntegral size in if used' <= quota then (True, used') else (False, used)
receive = do
path <- asks $ filesPath . config
let fPath = path </> B.unpack (B64.encode senderId)
let fPath = path </> B.unpack (B64.encode $ unEntityId senderId)
receiveChunk (XFTPRcvChunkSpec fPath size digest) >>= \case
Right () -> do
stats <- asks serverStats
withFileLog $ \sl -> logPutFile sl senderId fPath
atomically $ writeTVar filePath (Just fPath)
atomically $ modifyTVar' (filesUploaded stats) (+ 1)
atomically $ modifyTVar' (filesCount stats) (+ 1)
atomically $ modifyTVar' (filesSize stats) (+ fromIntegral size)
incFileStat filesUploaded
incFileStat filesCount
liftIO $ atomicModifyIORef'_ (filesSize stats) (+ fromIntegral size)
pure FROk
Left e -> do
us <- asks $ usedStorage . store
atomically . modifyTVar' us $ subtract (fromIntegral size)
atomically $ modifyTVar' us $ subtract (fromIntegral size)
liftIO $ whenM (doesFileExist fPath) (removeFile fPath) `catch` logFileError
pure $ FRErr e
receiveChunk spec = do
@@ -494,8 +507,8 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
case LC.cbInit dhSecret cbNonce of
Right sbState -> do
stats <- asks serverStats
atomically $ modifyTVar' (fileDownloads stats) (+ 1)
atomically $ updatePeriodStats (filesDownloaded stats) senderId
incFileStat fileDownloads
liftIO $ updatePeriodStats (filesDownloaded stats) senderId
pure (FRFile sDhKey cbNonce, Just ServerFile {filePath = path, fileSize = size, sbState})
_ -> pure (FRErr INTERNAL, Nothing)
_ -> pure (FRErr NO_FILE, Nothing)
@@ -511,24 +524,35 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
withFileLog (`logAckFile` rId)
st <- asks store
atomically $ deleteRecipient st rId fr
stats <- asks serverStats
atomically $ modifyTVar' (fileDownloadAcks stats) (+ 1)
incFileStat fileDownloadAcks
pure FROk
deleteServerFile_ :: FileRec -> M (Either XFTPErrorType ())
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
atomically $ modifyTVar' (filesDeleted stats) (+ 1)
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
atomically $ modifyTVar' (filesCount stats) (subtract 1)
atomically $ modifyTVar' (filesSize stats) (subtract $ fromIntegral $ size fileInfo)
liftIO $ atomicModifyIORef'_ (filesCount stats) (subtract 1)
liftIO $ atomicModifyIORef'_ (filesSize stats) (subtract $ fromIntegral $ size fileInfo)
getFileTime :: IO RoundedSystemTime
getFileTime = getRoundedSystemTime fileTimePrecision
expireServerFiles :: Maybe Int -> ExpirationConfig -> M ()
expireServerFiles itemDelay expCfg = do
@@ -554,29 +578,26 @@ expireServerFiles itemDelay expCfg = do
delete st sId = do
withFileLog (`logDeleteFile` sId)
void . atomically $ deleteFile st sId -- will not update usedStorage if sId isn't in store
FileServerStats {filesExpired} <- asks serverStats
atomically $ modifyTVar' filesExpired (+ 1)
incFileStat filesExpired
randomId :: Int -> M ByteString
randomId n = atomically . C.randomBytes n =<< asks random
getFileId :: M XFTPFileId
getFileId = do
size <- asks (fileIdSize . config)
atomically . C.randomBytes size =<< asks random
getFileId = fmap EntityId . randomId =<< asks (fileIdSize . config)
withFileLog :: (StoreLog 'WriteMode -> IO a) -> M ()
withFileLog action = liftIO . mapM_ action =<< asks storeLog
incFileStat :: (FileServerStats -> TVar Int) -> M ()
incFileStat :: (FileServerStats -> IORef Int) -> M ()
incFileStat statSel = do
stats <- asks serverStats
atomically $ modifyTVar (statSel stats) (+ 1)
liftIO $ atomicModifyIORef'_ (statSel stats) (+ 1)
saveServerStats :: M ()
saveServerStats =
asks (serverStatsBackupFile . config)
>>= mapM_ (\f -> asks serverStats >>= atomically . getFileServerStatsData >>= liftIO . saveStats f)
>>= mapM_ (\f -> asks serverStats >>= liftIO . getFileServerStatsData >>= liftIO . saveStats f)
where
saveStats f stats = do
logInfo $ "saving server stats to file " <> T.pack f
@@ -594,7 +615,7 @@ restoreServerStats = asks (serverStatsBackupFile . config) >>= mapM_ restoreStat
FileStore {files, usedStorage} <- asks store
_filesCount <- M.size <$> readTVarIO files
_filesSize <- readTVarIO usedStorage
atomically $ setFileServerStats s d {_filesCount, _filesSize}
liftIO $ setFileServerStats s d {_filesCount, _filesSize}
renameFile f $ f <> ".bak"
logInfo "server stats restored"
when (statsFilesCount /= _filesCount) $ logWarn $ "Files count differs: stats: " <> tshow statsFilesCount <> ", store: " <> tshow _filesCount
+6 -5
View File
@@ -4,16 +4,15 @@
module Simplex.FileTransfer.Server.Control where
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.ByteString (ByteString)
import Simplex.FileTransfer.Protocol (XFTPFileId)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (BasicAuth)
data CPClientRole = CPRNone | CPRUser | CPRAdmin
import Simplex.Messaging.Protocol (BasicAuth, BlockingInfo)
data ControlProtocol
= CPAuth BasicAuth
| CPStatsRTS
| CPDelete ByteString
| CPDelete XFTPFileId
| CPBlock XFTPFileId BlockingInfo
| CPHelp
| CPQuit
| CPSkip
@@ -23,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 -> ""
@@ -31,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
+12 -20
View File
@@ -11,7 +11,6 @@ module Simplex.FileTransfer.Server.Env where
import Control.Logger.Simple
import Control.Monad
import Control.Monad.IO.Unlift
import Crypto.Random
import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty)
@@ -29,8 +28,7 @@ import Simplex.FileTransfer.Transport (VersionRangeXFTP)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Protocol (BasicAuth, RcvPublicAuthKey)
import Simplex.Messaging.Server.Expiration
import Simplex.Messaging.Transport (ALPN)
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), loadFingerprint, loadTLSServerParams)
import Simplex.Messaging.Transport.Server (ServerCredentials (..), TransportServerConfig (..), loadFingerprint, loadServerCredential)
import Simplex.Messaging.Util (tshow)
import System.IO (IOMode (..))
import UnliftIO.STM
@@ -58,10 +56,7 @@ data XFTPServerConfig = XFTPServerConfig
fileTimeout :: Int,
-- | time after which inactive clients can be disconnected and check interval, seconds
inactiveClientExpiration :: Maybe ExpirationConfig,
-- CA certificate private key is not needed for initialization
caCertificateFile :: FilePath,
privateKeyFile :: FilePath,
certificateFile :: FilePath,
xftpCredentials :: ServerCredentials,
-- | XFTP client-server protocol version range
xftpServerVRange :: VersionRangeXFTP,
-- stats config - see SMP server config
@@ -76,7 +71,7 @@ data XFTPServerConfig = XFTPServerConfig
defaultInactiveClientExpiration :: ExpirationConfig
defaultInactiveClientExpiration =
ExpirationConfig
{ ttl = 43200, -- seconds, 12 hours
{ ttl = 21600, -- seconds, 6 hours
checkInterval = 3600 -- seconds, 1 hours
}
@@ -86,7 +81,7 @@ data XFTPEnv = XFTPEnv
storeLog :: Maybe (StoreLog 'WriteMode),
random :: TVar ChaChaDRG,
serverIdentity :: C.KeyHash,
tlsServerParams :: T.ServerParams,
tlsServerCreds :: T.Credential,
serverStats :: FileServerStats
}
@@ -100,23 +95,20 @@ defaultFileExpiration =
checkInterval = 2 * 3600 -- seconds, 2 hours
}
supportedXFTPhandshakes :: [ALPN]
supportedXFTPhandshakes = ["xftp/1"]
newXFTPServerEnv :: XFTPServerConfig -> IO XFTPEnv
newXFTPServerEnv config@XFTPServerConfig {storeLogFile, fileSizeQuota, caCertificateFile, certificateFile, privateKeyFile, transportConfig} = do
random <- liftIO C.newRandom
store <- atomically newFileStore
storeLog <- liftIO $ mapM (`readWriteFileStore` store) storeLogFile
newXFTPServerEnv config@XFTPServerConfig {storeLogFile, fileSizeQuota, xftpCredentials} = do
random <- C.newRandom
store <- newFileStore
storeLog <- mapM (`readWriteFileStore` store) storeLogFile
used <- countUsedStorage <$> readTVarIO (files store)
atomically $ writeTVar (usedStorage store) used
forM_ fileSizeQuota $ \quota -> do
logInfo $ "Total / available storage: " <> tshow quota <> " / " <> tshow (quota - used)
when (quota < used) $ logInfo "WARNING: storage quota is less than used storage, no files can be uploaded!"
tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig)
Fingerprint fp <- liftIO $ loadFingerprint caCertificateFile
serverStats <- atomically . newFileServerStats =<< liftIO getCurrentTime
pure XFTPEnv {config, store, storeLog, random, tlsServerParams, serverIdentity = C.KeyHash fp, serverStats}
tlsServerCreds <- loadServerCredential xftpCredentials
Fingerprint fp <- loadFingerprint xftpCredentials
serverStats <- newFileServerStats =<< getCurrentTime
pure XFTPEnv {config, store, storeLog, random, tlsServerCreds, serverIdentity = C.KeyHash fp, serverStats}
countUsedStorage :: M.Map k FileRec -> Int64
countUsedStorage = M.foldl' (\acc FileRec {fileInfo = FileInfo {size}} -> acc + fromIntegral size) 0
+35 -19
View File
@@ -1,3 +1,4 @@
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
@@ -19,7 +20,7 @@ import Options.Applicative
import Simplex.FileTransfer.Chunks
import Simplex.FileTransfer.Description (FileSize (..))
import Simplex.FileTransfer.Server (runXFTPServer)
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defFileExpirationHours, defaultFileExpiration, defaultInactiveClientExpiration, supportedXFTPhandshakes)
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defFileExpirationHours, defaultFileExpiration, defaultInactiveClientExpiration)
import Simplex.FileTransfer.Transport (supportedFileServerVRange)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
@@ -28,7 +29,7 @@ import Simplex.Messaging.Server.CLI
import Simplex.Messaging.Server.Expiration
import Simplex.Messaging.Transport (simplexMQVersion)
import Simplex.Messaging.Transport.Client (TransportHost (..))
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig)
import Simplex.Messaging.Transport.Server (ServerCredentials (..), TransportServerConfig (..), defaultTransportServerConfig)
import Simplex.Messaging.Util (safeDecodeUtf8, tshow)
import System.Directory (createDirectoryIfMissing, doesFileExist)
import System.FilePath (combine)
@@ -51,7 +52,9 @@ xftpServerCLI cfgPath logPath = do
True -> readIniFile iniFile >>= either exitError runServer
_ -> exitError $ "Error: server is not initialized (" <> iniFile <> " does not exist).\nRun `" <> executableName <> " init`."
Delete -> do
confirmOrExit "WARNING: deleting the server will make all queues inaccessible, because the server identity (certificate fingerprint) will change.\nTHIS CANNOT BE UNDONE!"
confirmOrExit
"WARNING: deleting the server will make all queues inaccessible, because the server identity (certificate fingerprint) will change.\nTHIS CANNOT BE UNDONE!"
"Server NOT deleted"
deleteDirIfExists cfgPath
deleteDirIfExists logPath
putStrLn "Deleted configuration and log files"
@@ -101,6 +104,7 @@ xftpServerCLI cfgPath logPath = do
\\n\
\# control_port_admin_password:\n\
\# control_port_user_password:\n\
\\n\
\[TRANSPORT]\n\
\# host is only used to print server address on start\n"
<> ("host: " <> T.pack host <> "\n")
@@ -173,9 +177,12 @@ xftpServerCLI cfgPath logPath = do
{ ttl = readStrictIni "INACTIVE_CLIENTS" "ttl" ini,
checkInterval = readStrictIni "INACTIVE_CLIENTS" "check_interval" ini
},
caCertificateFile = c caCrtFile,
privateKeyFile = c serverKeyFile,
certificateFile = c serverCrtFile,
xftpCredentials =
ServerCredentials
{ caCertificateFile = Just $ c caCrtFile,
privateKeyFile = c serverKeyFile,
certificateFile = c serverCrtFile
},
xftpServerVRange = supportedFileServerVRange,
logStatsInterval = logStats $> 86400, -- seconds
logStatsStartTime = 0, -- seconds from 00:00 UTC
@@ -183,8 +190,7 @@ xftpServerCLI cfgPath logPath = do
serverStatsBackupFile = logStats $> combine logPath "file-server-stats.log",
transportConfig =
defaultTransportServerConfig
{ logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini,
alpn = Just supportedXFTPhandshakes
{ logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini
},
responseDelay = 0
}
@@ -215,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'
@@ -231,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"
@@ -239,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}
+60 -51
View File
@@ -7,25 +7,25 @@ module Simplex.FileTransfer.Server.Stats where
import Control.Applicative ((<|>))
import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Char8 as B
import Data.IORef
import Data.Int (Int64)
import Data.Time.Clock (UTCTime)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (SenderId)
import Simplex.Messaging.Server.Stats (PeriodStats, PeriodStatsData, getPeriodStatsData, newPeriodStats, setPeriodStats)
import UnliftIO.STM
data FileServerStats = FileServerStats
{ fromTime :: TVar UTCTime,
filesCreated :: TVar Int,
fileRecipients :: TVar Int,
filesUploaded :: TVar Int,
filesExpired :: TVar Int,
filesDeleted :: TVar Int,
filesDownloaded :: PeriodStats SenderId,
fileDownloads :: TVar Int,
fileDownloadAcks :: TVar Int,
filesCount :: TVar Int,
filesSize :: TVar Int64
{ fromTime :: IORef UTCTime,
filesCreated :: IORef Int,
fileRecipients :: IORef Int,
filesUploaded :: IORef Int,
filesExpired :: IORef Int,
filesDeleted :: IORef Int,
filesBlocked :: IORef Int,
filesDownloaded :: PeriodStats,
fileDownloads :: IORef Int,
fileDownloadAcks :: IORef Int,
filesCount :: IORef Int,
filesSize :: IORef Int64
}
data FileServerStatsData = FileServerStatsData
@@ -35,7 +35,8 @@ data FileServerStatsData = FileServerStatsData
_filesUploaded :: Int,
_filesExpired :: Int,
_filesDeleted :: Int,
_filesDownloaded :: PeriodStatsData SenderId,
_filesBlocked :: Int,
_filesDownloaded :: PeriodStatsData,
_fileDownloads :: Int,
_fileDownloadAcks :: Int,
_filesCount :: Int,
@@ -43,52 +44,56 @@ data FileServerStatsData = FileServerStatsData
}
deriving (Show)
newFileServerStats :: UTCTime -> STM FileServerStats
newFileServerStats :: UTCTime -> IO FileServerStats
newFileServerStats ts = do
fromTime <- newTVar ts
filesCreated <- newTVar 0
fileRecipients <- newTVar 0
filesUploaded <- newTVar 0
filesExpired <- newTVar 0
filesDeleted <- newTVar 0
fromTime <- newIORef ts
filesCreated <- newIORef 0
fileRecipients <- newIORef 0
filesUploaded <- newIORef 0
filesExpired <- newIORef 0
filesDeleted <- newIORef 0
filesBlocked <- newIORef 0
filesDownloaded <- newPeriodStats
fileDownloads <- newTVar 0
fileDownloadAcks <- newTVar 0
filesCount <- newTVar 0
filesSize <- newTVar 0
pure FileServerStats {fromTime, filesCreated, fileRecipients, filesUploaded, filesExpired, filesDeleted, filesDownloaded, fileDownloads, fileDownloadAcks, filesCount, filesSize}
fileDownloads <- newIORef 0
fileDownloadAcks <- newIORef 0
filesCount <- newIORef 0
filesSize <- newIORef 0
pure FileServerStats {fromTime, filesCreated, fileRecipients, filesUploaded, filesExpired, filesDeleted, filesBlocked, filesDownloaded, fileDownloads, fileDownloadAcks, filesCount, filesSize}
getFileServerStatsData :: FileServerStats -> STM FileServerStatsData
getFileServerStatsData :: FileServerStats -> IO FileServerStatsData
getFileServerStatsData s = do
_fromTime <- readTVar $ fromTime (s :: FileServerStats)
_filesCreated <- readTVar $ filesCreated s
_fileRecipients <- readTVar $ fileRecipients s
_filesUploaded <- readTVar $ filesUploaded s
_filesExpired <- readTVar $ filesExpired s
_filesDeleted <- readTVar $ filesDeleted s
_fromTime <- readIORef $ fromTime (s :: FileServerStats)
_filesCreated <- readIORef $ filesCreated s
_fileRecipients <- readIORef $ fileRecipients s
_filesUploaded <- readIORef $ filesUploaded s
_filesExpired <- readIORef $ filesExpired s
_filesDeleted <- readIORef $ filesDeleted s
_filesBlocked <- readIORef $ filesBlocked s
_filesDownloaded <- getPeriodStatsData $ filesDownloaded s
_fileDownloads <- readTVar $ fileDownloads s
_fileDownloadAcks <- readTVar $ fileDownloadAcks s
_filesCount <- readTVar $ filesCount s
_filesSize <- readTVar $ filesSize s
pure FileServerStatsData {_fromTime, _filesCreated, _fileRecipients, _filesUploaded, _filesExpired, _filesDeleted, _filesDownloaded, _fileDownloads, _fileDownloadAcks, _filesCount, _filesSize}
_fileDownloads <- readIORef $ fileDownloads s
_fileDownloadAcks <- readIORef $ fileDownloadAcks s
_filesCount <- readIORef $ filesCount s
_filesSize <- readIORef $ filesSize s
pure FileServerStatsData {_fromTime, _filesCreated, _fileRecipients, _filesUploaded, _filesExpired, _filesDeleted, _filesBlocked, _filesDownloaded, _fileDownloads, _fileDownloadAcks, _filesCount, _filesSize}
setFileServerStats :: FileServerStats -> FileServerStatsData -> STM ()
-- this function is not thread safe, it is used on server start only
setFileServerStats :: FileServerStats -> FileServerStatsData -> IO ()
setFileServerStats s d = do
writeTVar (fromTime (s :: FileServerStats)) $! _fromTime (d :: FileServerStatsData)
writeTVar (filesCreated s) $! _filesCreated d
writeTVar (fileRecipients s) $! _fileRecipients d
writeTVar (filesUploaded s) $! _filesUploaded d
writeTVar (filesExpired s) $! _filesExpired d
writeTVar (filesDeleted s) $! _filesDeleted d
writeIORef (fromTime (s :: FileServerStats)) $! _fromTime (d :: FileServerStatsData)
writeIORef (filesCreated s) $! _filesCreated d
writeIORef (fileRecipients s) $! _fileRecipients d
writeIORef (filesUploaded s) $! _filesUploaded d
writeIORef (filesExpired s) $! _filesExpired d
writeIORef (filesDeleted s) $! _filesDeleted d
writeIORef (filesBlocked s) $! _filesBlocked d
setPeriodStats (filesDownloaded s) $! _filesDownloaded d
writeTVar (fileDownloads s) $! _fileDownloads d
writeTVar (fileDownloadAcks s) $! _fileDownloadAcks d
writeTVar (filesCount s) $! _filesCount d
writeTVar (filesSize s) $! _filesSize d
writeIORef (fileDownloads s) $! _fileDownloads d
writeIORef (fileDownloadAcks s) $! _fileDownloadAcks d
writeIORef (filesCount s) $! _filesCount d
writeIORef (filesSize s) $! _filesSize d
instance StrEncoding FileServerStatsData where
strEncode FileServerStatsData {_fromTime, _filesCreated, _fileRecipients, _filesUploaded, _filesExpired, _filesDeleted, _filesDownloaded, _fileDownloads, _fileDownloadAcks, _filesCount, _filesSize} =
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
+32 -15
View File
@@ -13,24 +13,27 @@ module Simplex.FileTransfer.Server.Store
setFilePath,
addRecipient,
deleteFile,
blockFile,
deleteRecipient,
expiredFilePath,
getFile,
ackFile,
fileTimePrecision,
)
where
import Control.Concurrent.STM
import Control.Monad
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Int (Int64)
import Data.Set (Set)
import qualified Data.Set as S
import Data.Time.Clock.System (SystemTime (..))
import Simplex.FileTransfer.Protocol (FileInfo (..), SFileParty (..), XFTPFileId)
import Simplex.FileTransfer.Transport (XFTPErrorType (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (RcvPublicAuthKey, RecipientId, SenderId)
import Simplex.Messaging.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, ($>>=))
@@ -46,34 +49,40 @@ data FileRec = FileRec
fileInfo :: FileInfo,
filePath :: TVar (Maybe FilePath),
recipientIds :: TVar (Set RecipientId),
createdAt :: SystemTime
createdAt :: RoundedSystemTime,
fileStatus :: TVar ServerEntityStatus
}
fileTimePrecision :: Int64
fileTimePrecision = 3600 -- truncate creation time to 1 hour
data FileRecipient = FileRecipient RecipientId RcvPublicAuthKey
deriving (Show)
instance StrEncoding FileRecipient where
strEncode (FileRecipient rId rKey) = strEncode rId <> ":" <> strEncode rKey
strP = FileRecipient <$> strP <* A.char ':' <*> strP
newFileStore :: STM FileStore
newFileStore :: IO FileStore
newFileStore = do
files <- TM.empty
recipients <- TM.empty
usedStorage <- newTVar 0
files <- TM.emptyIO
recipients <- TM.emptyIO
usedStorage <- newTVarIO 0
pure FileStore {files, recipients, usedStorage}
addFile :: FileStore -> SenderId -> FileInfo -> SystemTime -> STM (Either XFTPErrorType ())
addFile FileStore {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 -> SystemTime -> 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 =
@@ -104,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
@@ -120,8 +137,8 @@ getFile st party fId = case party of
expiredFilePath :: FileStore -> XFTPFileId -> Int64 -> STM (Maybe (Maybe FilePath))
expiredFilePath FileStore {files} sId old =
TM.lookup sId files
$>>= \FileRec {filePath, createdAt} ->
if systemSeconds createdAt < old
$>>= \FileRec {filePath, createdAt = RoundedSystemTime createdAt} ->
if createdAt + fileTimePrecision < old
then Just <$> readTVar filePath
else pure Nothing
+25 -21
View File
@@ -14,58 +14,63 @@ 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)
import qualified Data.Map.Strict as M
import Data.Time.Clock.System (SystemTime)
import Simplex.FileTransfer.Protocol (FileInfo (..))
import Simplex.FileTransfer.Server.Store
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (RcvPublicAuthKey, RecipientId, SenderId)
import Simplex.Messaging.Protocol (BlockingInfo, RcvPublicAuthKey, RecipientId, SenderId)
import Simplex.Messaging.Server.QueueStore (RoundedSystemTime, ServerEntityStatus (..))
import Simplex.Messaging.Server.StoreLog
import Simplex.Messaging.Util (bshow, whenM)
import System.Directory (doesFileExist, renameFile)
import Simplex.Messaging.Util (bshow)
import System.IO
data FileStoreLogRecord
= AddFile SenderId FileInfo SystemTime
= 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 -> SystemTime -> 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,17 +81,14 @@ 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
readWriteFileStore :: FilePath -> FileStore -> IO (StoreLog 'WriteMode)
readWriteFileStore f st = do
whenM (doesFileExist f) $ do
readFileStore f st
renameFile f $ f <> ".bak"
s <- openWriteStoreLog f
writeFileStore s st
pure s
readWriteFileStore = readWriteStoreLog readFileStore writeFileStore
readFileStore :: FilePath -> FileStore -> IO ()
readFileStore f st = mapM_ (addFileLogRecord . LB.toStrict) . LB.lines =<< LB.readFile f
@@ -98,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
@@ -111,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
+52 -10
View File
@@ -11,7 +11,9 @@
module Simplex.FileTransfer.Transport
( supportedFileServerVRange,
authCmdsXFTPVersion,
blockedFilesXFTPVersion,
xftpClientHandshakeStub,
supportedXFTPhandshakes,
XFTPClientHandshake (..),
-- xftpClientHandshake,
XFTPServerHandshake (..),
@@ -32,7 +34,6 @@ module Simplex.FileTransfer.Transport
)
where
import Control.Applicative ((<|>))
import qualified Control.Exception as E
import Control.Logger.Simple
import Control.Monad
@@ -56,8 +57,8 @@ 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.Transport (SessionId, THandle (..), THandleParams (..), TransportError (..), TransportPeer (..))
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)
import Simplex.Messaging.Version
@@ -91,15 +92,21 @@ 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"]
data XFTPServerHandshake = XFTPServerHandshake
{ xftpVersionRange :: VersionRangeXFTP,
@@ -207,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
@@ -227,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
@@ -244,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"
@@ -262,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 -4
View File
@@ -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,10 +22,11 @@ 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
type RcvFileId = ByteString -- Agent entity ID
type SndFileId = ByteString
type SndFileId = ByteString -- Agent entity ID
authTagSize :: Int64
authTagSize = fromIntegral C.authTagSize
@@ -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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+80 -55
View File
@@ -17,11 +17,14 @@ module Simplex.Messaging.Agent.Env.SQLite
AgentConfig (..),
InitialAgentServers (..),
ServerCfg (..),
ServerRoles (..),
OperatorId,
UserServers (..),
NetworkConfig (..),
presetServerCfg,
enabledServerCfg,
allRoles,
mkUserServers,
serverHosts,
defaultAgentConfig,
defaultReconnectInterval,
tryAgentError,
@@ -41,18 +44,21 @@ module Simplex.Messaging.Agent.Env.SQLite
)
where
import Control.Concurrent (ThreadId)
import Control.Exception (BlockedIndefinitelyOnSTM (..), SomeException, fromException)
import Control.Monad.Except
import Control.Monad.IO.Unlift
import Control.Monad.Reader
import Crypto.Random
import Data.Aeson (FromJSON (..), ToJSON (..))
import qualified Data.Aeson.TH as JQ
import Data.ByteArray (ScrubbedBytes)
import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as L
import Data.Map (Map)
import Data.Map.Strict (Map)
import Data.Maybe (fromMaybe)
import Data.Set (Set)
import qualified Data.Set as S
import Data.Time.Clock (NominalDiffTime, nominalDay)
import Data.Time.Clock.System (SystemTime (..))
import Data.Word (Word16)
@@ -61,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)
@@ -70,14 +78,14 @@ import Simplex.Messaging.Notifications.Client (defaultNTFClientConfig)
import Simplex.Messaging.Notifications.Transport (NTFVersion)
import Simplex.Messaging.Notifications.Types
import Simplex.Messaging.Parsers (defaultJSON)
import Simplex.Messaging.Protocol (NtfServer, ProtoServerWithAuth, ProtocolServer, ProtocolType (..), ProtocolTypeI, VersionRangeSMPC, XFTPServer, supportedSMPClientVRange)
import Simplex.Messaging.Protocol (NtfServer, ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI, VersionRangeSMPC, XFTPServer, supportedSMPClientVRange)
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport (SMPVersion, TLS, Transport (..))
import Simplex.Messaging.Transport.Client (defaultSMPPort)
import Simplex.Messaging.Transport (SMPVersion)
import Simplex.Messaging.Transport.Client (TransportHost)
import Simplex.Messaging.Util (allFinally, catchAllErrors, catchAllErrors', tryAllErrors, tryAllErrors')
import System.Mem.Weak (Weak)
import System.Random (StdGen, newStdGen)
import UnliftIO (Async, SomeException)
import UnliftIO.STM
type AM' a = ReaderT Env IO a
@@ -93,29 +101,42 @@ data InitialAgentServers = InitialAgentServers
data ServerCfg p = ServerCfg
{ server :: ProtoServerWithAuth p,
preset :: Bool,
tested :: Maybe Bool,
enabled :: Bool
operator :: Maybe OperatorId,
enabled :: Bool,
roles :: ServerRoles
}
deriving (Show)
enabledServerCfg :: ProtoServerWithAuth p -> ServerCfg p
enabledServerCfg server = ServerCfg {server, preset = False, tested = Nothing, enabled = True}
data ServerRoles = ServerRoles
{ storage :: Bool,
proxy :: Bool
}
deriving (Show)
presetServerCfg :: Bool -> ProtoServerWithAuth p -> ServerCfg p
presetServerCfg enabled server = ServerCfg {server, preset = True, tested = Nothing, enabled}
allRoles :: ServerRoles
allRoles = ServerRoles True True
presetServerCfg :: Bool -> ServerRoles -> Maybe OperatorId -> ProtoServerWithAuth p -> ServerCfg p
presetServerCfg enabled roles operator server =
ServerCfg {server, operator, enabled, roles}
data UserServers p = UserServers
{ enabledSrvs :: NonEmpty (ProtoServerWithAuth p),
knownSrvs :: NonEmpty (ProtocolServer p)
{ storageSrvs :: NonEmpty (Maybe OperatorId, ProtoServerWithAuth p),
proxySrvs :: NonEmpty (Maybe OperatorId, ProtoServerWithAuth p),
knownHosts :: Set TransportHost
}
type OperatorId = Int64
-- This function sets all servers as enabled in case all passed servers are disabled.
mkUserServers :: NonEmpty (ServerCfg p) -> UserServers p
mkUserServers srvs = UserServers {enabledSrvs, knownSrvs}
mkUserServers srvs = UserServers {storageSrvs = filterSrvs storage, proxySrvs = filterSrvs proxy, knownHosts}
where
enabledSrvs = L.map (\ServerCfg {server} -> server) $ fromMaybe srvs $ L.nonEmpty $ L.filter (\ServerCfg {enabled} -> enabled) srvs
knownSrvs = L.map (\ServerCfg {server = ProtoServerWithAuth srv _} -> srv) srvs
filterSrvs role = L.map (\ServerCfg {operator, server} -> (operator, server)) $ fromMaybe srvs $ L.nonEmpty $ L.filter (\ServerCfg {enabled, roles} -> enabled && role roles) srvs
knownHosts = S.unions $ L.map (\ServerCfg {server = ProtoServerWithAuth srv _} -> serverHosts srv) srvs
serverHosts :: ProtocolServer p -> Set TransportHost
serverHosts ProtocolServer {host} = S.fromList $ L.toList host
data AgentConfig = AgentConfig
{ tcpPort :: Maybe ServiceName,
@@ -148,10 +169,9 @@ data AgentConfig = AgentConfig
xftpMaxRecipientsPerRequest :: Int,
deleteErrorCount :: Int,
ntfCron :: Word16,
ntfWorkerDelay :: Int,
ntfSMPWorkerDelay :: Int,
ntfBatchSize :: Int,
ntfSubFirstCheckInterval :: NominalDiffTime,
ntfSubCheckInterval :: NominalDiffTime,
ntfMaxMessages :: Int,
caCertificateFile :: FilePath,
privateKeyFile :: FilePath,
certificateFile :: FilePath,
@@ -165,7 +185,7 @@ defaultReconnectInterval =
RetryInterval
{ initialInterval = 2_000000,
increaseAfter = 10_000000,
maxInterval = 60_000000
maxInterval = 180_000000
}
defaultMessageRetryInterval :: RetryInterval2
@@ -175,7 +195,7 @@ defaultMessageRetryInterval =
RetryInterval
{ initialInterval = 2_000000,
increaseAfter = 10_000000,
maxInterval = 60_000000
maxInterval = 120_000000
},
riSlow =
RetryInterval
@@ -194,9 +214,9 @@ defaultAgentConfig =
rcvAuthAlg = C.AuthAlg C.SEd25519, -- this will stay as Ed25519
sndAuthAlg = C.AuthAlg C.SEd25519, -- TODO replace with X25519 when switching to v7
connIdBytes = 12,
tbqSize = 64,
smpCfg = defaultSMPClientConfig {defaultTransport = (show defaultSMPPort, transport @TLS)},
ntfCfg = defaultNTFClientConfig {defaultTransport = ("443", transport @TLS)},
tbqSize = 128,
smpCfg = defaultSMPClientConfig,
ntfCfg = defaultNTFClientConfig,
xftpCfg = defaultXFTPClientConfig,
reconnectInterval = defaultReconnectInterval,
messageRetryInterval = defaultMessageRetryInterval,
@@ -220,10 +240,9 @@ defaultAgentConfig =
xftpMaxRecipientsPerRequest = 200,
deleteErrorCount = 10,
ntfCron = 20, -- minutes
ntfWorkerDelay = 100000, -- microseconds
ntfSMPWorkerDelay = 500000, -- microseconds
ntfSubCheckInterval = nominalDay,
ntfMaxMessages = 3,
ntfBatchSize = 150,
ntfSubFirstCheckInterval = nominalDay,
ntfSubCheckInterval = 3 * nominalDay,
-- CA certificate private key is not needed for initialization
-- ! we do not generate these
caCertificateFile = "/etc/opt/simplex-agent/ca.crt",
@@ -236,7 +255,7 @@ defaultAgentConfig =
data Env = Env
{ config :: AgentConfig,
store :: SQLiteStore,
store :: DBStore,
random :: TVar ChaChaDRG,
randomServer :: TVar StdGen,
ntfSupervisor :: NtfSupervisor,
@@ -244,35 +263,37 @@ 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
ntfSupervisor <- atomically . newNtfSubSupervisor $ tbqSize config
xftpAgent <- atomically newXFTPAgent
ntfSupervisor <- newNtfSubSupervisor $ tbqSize config
xftpAgent <- newXFTPAgent
multicastSubscribers <- newTMVarIO 0
pure Env {config, store, random, randomServer, ntfSupervisor, xftpAgent, multicastSubscribers}
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),
ntfSubQ :: TBQueue (ConnId, NtfSupervisorCommand),
ntfSubQ :: TBQueue (NtfSupervisorCommand, NonEmpty ConnId),
ntfWorkers :: TMap NtfServer Worker,
ntfSMPWorkers :: TMap SMPServer Worker
ntfSMPWorkers :: TMap SMPServer Worker,
ntfTknDelWorkers :: TMap NtfServer Worker
}
data NtfSupervisorCommand = NSCCreate | NSCDelete | NSCSmpDelete | NSCNtfWorker NtfServer | NSCNtfSMPWorker SMPServer
data NtfSupervisorCommand = NSCCreate | NSCSmpDelete | NSCDeleteSub
deriving (Show)
newNtfSubSupervisor :: Natural -> STM NtfSupervisor
newNtfSubSupervisor :: Natural -> IO NtfSupervisor
newNtfSubSupervisor qSize = do
ntfTkn <- newTVar Nothing
ntfSubQ <- newTBQueue qSize
ntfWorkers <- TM.empty
ntfSMPWorkers <- TM.empty
pure NtfSupervisor {ntfTkn, ntfSubQ, ntfWorkers, ntfSMPWorkers}
ntfTkn <- newTVarIO Nothing
ntfSubQ <- newTBQueueIO qSize
ntfWorkers <- TM.emptyIO
ntfSMPWorkers <- TM.emptyIO
ntfTknDelWorkers <- TM.emptyIO
pure NtfSupervisor {ntfTkn, ntfSubQ, ntfWorkers, ntfSMPWorkers, ntfTknDelWorkers}
data XFTPAgent = XFTPAgent
{ -- if set, XFTP file paths will be considered as relative to this directory
@@ -282,12 +303,12 @@ data XFTPAgent = XFTPAgent
xftpDelWorkers :: TMap XFTPServer Worker
}
newXFTPAgent :: STM XFTPAgent
newXFTPAgent :: IO XFTPAgent
newXFTPAgent = do
xftpWorkDir <- newTVar Nothing
xftpRcvWorkers <- TM.empty
xftpSndWorkers <- TM.empty
xftpDelWorkers <- TM.empty
xftpWorkDir <- newTVarIO Nothing
xftpRcvWorkers <- TM.emptyIO
xftpSndWorkers <- TM.emptyIO
xftpDelWorkers <- TM.emptyIO
pure XFTPAgent {xftpWorkDir, xftpRcvWorkers, xftpSndWorkers, xftpDelWorkers}
tryAgentError :: AM a -> AM (Either AgentErrorType a)
@@ -312,13 +333,15 @@ agentFinally = allFinally mkInternal
{-# INLINE agentFinally #-}
mkInternal :: SomeException -> AgentErrorType
mkInternal = INTERNAL . show
mkInternal e = case fromException e of
Just BlockedIndefinitelyOnSTM -> CRITICAL True "Thread blocked indefinitely in STM transaction"
_ -> INTERNAL $ show e
{-# INLINE mkInternal #-}
data Worker = Worker
{ workerId :: Int,
doWork :: TMVar (),
action :: TMVar (Maybe (Async ())),
action :: TMVar (Maybe (Weak ThreadId)),
restarts :: TVar RestartCount
}
@@ -334,6 +357,8 @@ updateRestartCount t (RestartCount minute count) = do
$(pure [])
$(JQ.deriveJSON defaultJSON ''ServerRoles)
instance ProtocolTypeI p => ToJSON (ServerCfg p) where
toEncoding = $(JQ.mkToEncoding defaultJSON ''ServerCfg)
toJSON = $(JQ.mkToJSON defaultJSON ''ServerCfg)
+6
View File
@@ -1,10 +1,12 @@
module Simplex.Messaging.Agent.Lock
( Lock,
createLock,
createLockIO,
withLock,
withLock',
withGetLock,
withGetLocks,
getPutLock,
)
where
@@ -24,6 +26,10 @@ createLock :: STM Lock
createLock = newEmptyTMVar
{-# INLINE createLock #-}
createLockIO :: IO Lock
createLockIO = newEmptyTMVarIO
{-# INLINE createLockIO #-}
withLock :: MonadUnliftIO m => Lock -> String -> ExceptT e m a -> ExceptT e m a
withLock lock name = ExceptT . withLock' lock name . runExceptT
{-# INLINE withLock #-}
+470 -224
View File
@@ -1,7 +1,9 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
@@ -13,6 +15,7 @@ module Simplex.Messaging.Agent.NtfSubSupervisor
nsRemoveNtfToken,
sendNtfSubCommand,
instantNotifications,
deleteToken,
closeNtfSupervisor,
getNtfServer,
)
@@ -20,117 +23,170 @@ where
import Control.Logger.Simple (logError, logInfo)
import Control.Monad
import Control.Monad.Except
import Control.Monad.Reader
import Control.Monad.Trans.Except
import Crypto.Random (ChaChaDRG)
import Data.Bifunctor (first)
import Data.Either (fromRight, partitionEithers)
import Data.Functor (($>))
import Data.List (foldl')
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as L
import qualified Data.Map.Strict as M
import Data.Text (Text)
import Data.Maybe (catMaybes)
import qualified Data.Set as S
import Data.Time (UTCTime, addUTCTime, getCurrentTime)
import Data.Time.Clock (diffUTCTime)
import Simplex.Messaging.Agent.Client
import Simplex.Messaging.Agent.Env.SQLite
import Simplex.Messaging.Agent.Protocol (AEvent (..), AEvt (..), AgentErrorType (..), BrokerErrorType (..), ConnId, NotificationsMode (..), SAEntity (..))
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.RetryInterval
import Simplex.Messaging.Agent.Stats
import Simplex.Messaging.Agent.Store
import Simplex.Messaging.Agent.Store.SQLite
import 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 (NtfSubStatus (..), NtfTknStatus (..), SMPQueueNtf (..))
import Simplex.Messaging.Notifications.Protocol
import Simplex.Messaging.Notifications.Types
import Simplex.Messaging.Protocol (NtfServer, SMPServer, sameSrvAddr)
import Simplex.Messaging.Util (diffToMicroseconds, threadDelay', tshow, unlessM)
import Simplex.Messaging.Protocol (NtfServer, sameSrvAddr)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.Util (diffToMicroseconds, threadDelay', tshow)
import System.Random (randomR)
import UnliftIO
import UnliftIO.Concurrent (forkIO, threadDelay)
import UnliftIO.Concurrent (forkIO)
import qualified UnliftIO.Exception as E
runNtfSupervisor :: AgentClient -> AM' ()
runNtfSupervisor c = do
ns <- asks ntfSupervisor
runExceptT startTknDelete >>= \case
Left e -> notifyErr e
Right _ -> pure ()
forever $ do
cmd@(connId, _) <- atomically . readTBQueue $ ntfSubQ ns
handleErr connId . agentOperationBracket c AONtfNetwork waitUntilActive $
runExceptT (processNtfSub c cmd) >>= \case
Left e -> notifyErr connId e
cmd <- atomically . readTBQueue $ ntfSubQ ns
handleErr . agentOperationBracket c AONtfNetwork waitUntilActive $
runExceptT (processNtfCmd c cmd) >>= \case
Left e -> notifyErr e
Right _ -> return ()
where
handleErr :: ConnId -> AM' () -> AM' ()
handleErr connId = E.handle $ \(e :: E.SomeException) -> do
startTknDelete :: AM ()
startTknDelete = do
pendingDelServers <- withStore' c getPendingDelTknServers
lift . forM_ pendingDelServers $ getNtfTknDelWorker True c
handleErr :: AM' () -> AM' ()
handleErr = E.handle $ \(e :: E.SomeException) -> do
logError $ "runNtfSupervisor error " <> tshow e
notifyErr connId e
notifyErr connId e = notifyInternalError c connId $ "runNtfSupervisor error " <> show e
notifyErr e
notifyErr e = notifyInternalError' c $ "runNtfSupervisor error " <> show e
processNtfSub :: AgentClient -> (ConnId, NtfSupervisorCommand) -> AM ()
processNtfSub c (connId, cmd) = do
logInfo $ "processNtfSub - connId = " <> tshow connId <> " - cmd = " <> tshow cmd
partitionErrs :: (a -> ConnId) -> [a] -> [Either AgentErrorType b] -> ([(ConnId, AgentErrorType)], [b])
partitionErrs f xs = partitionEithers . zipWith (\x -> first (f x,)) xs
{-# INLINE partitionErrs #-}
ntfSubConnId :: NtfSubscription -> ConnId
ntfSubConnId NtfSubscription {connId} = connId
processNtfCmd :: AgentClient -> (NtfSupervisorCommand, NonEmpty ConnId) -> AM ()
processNtfCmd c (cmd, connIds) = do
logInfo $ "processNtfCmd - cmd = " <> tshow cmd
let connIds' = L.toList connIds
case cmd of
NSCCreate -> do
(a, RcvQueue {server = smpServer, clientNtfCreds}) <- withStore c $ \db -> runExceptT $ do
a <- liftIO $ getNtfSubscription db connId
q <- ExceptT $ getPrimaryRcvQueue db connId
pure (a, q)
logInfo $ "processNtfSub, NSCCreate - a = " <> tshow a
case a of
Nothing -> do
(cErrs, rqSubActions) <- lift $ partitionErrs id connIds' <$> withStoreBatch c (\db -> map (getQueueSub db) connIds')
notifyErrs c cErrs
logInfo $ "processNtfCmd, NSCCreate - length rqSubs = " <> tshow (length rqSubActions)
let (ns, rs, css, cns) = partitionQueueSubActions rqSubActions
createNewSubs ns
resetSubs rs
lift $ do
mapM_ (getNtfSMPWorker True c) (S.fromList css)
mapM_ (getNtfNTFWorker True c) (S.fromList cns)
where
getQueueSub ::
DB.Connection ->
ConnId ->
IO (Either AgentErrorType (RcvQueue, Maybe NtfSupervisorSub))
getQueueSub db connId = fmap (first storeError) $ runExceptT $ do
rq <- ExceptT $ getPrimaryRcvQueue db connId
sub <- liftIO $ getNtfSubscription db connId
pure (rq, sub)
createNewSubs :: [RcvQueue] -> AM ()
createNewSubs rqs = do
withTokenServer $ \ntfServer -> do
case clientNtfCreds of
Just ClientNtfCreds {notifierId} -> do
let newSub = newNtfSubscription connId smpServer (Just notifierId) ntfServer NASKey
withStore c $ \db -> createNtfSubscription db newSub $ NtfSubNTFAction NSACreate
lift . void $ getNtfNTFWorker True c ntfServer
Nothing -> do
let newSub = newNtfSubscription connId smpServer Nothing ntfServer NASNew
withStore c $ \db -> createNtfSubscription db newSub $ NtfSubSMPAction NSASmpKey
lift . void $ getNtfSMPWorker True c smpServer
(Just (sub@NtfSubscription {ntfSubStatus, ntfServer = subNtfServer, smpServer = smpServer', ntfQueueId}, action_)) -> do
case (clientNtfCreds, ntfQueueId) of
(Just ClientNtfCreds {notifierId}, Just ntfQueueId')
| sameSrvAddr smpServer smpServer' && notifierId == ntfQueueId' -> create
| otherwise -> rotate
(Nothing, Nothing) -> create
_ -> rotate
let newSubs = map (rqToNewSub ntfServer) rqs
(cErrs, _) <- lift $ partitionErrs ntfSubConnId newSubs <$> withStoreBatch c (\db -> map (storeNewSub db) newSubs)
notifyErrs c cErrs
kickSMPWorkers rqs
where
create :: AM ()
create = case action_ of
-- action was set to NULL after worker internal error
Nothing -> resetSubscription
Just (action, _)
-- subscription was marked for deletion / is being deleted
| isDeleteNtfSubAction action -> do
if ntfSubStatus == NASNew || ntfSubStatus == NASOff || ntfSubStatus == NASDeleted
then resetSubscription
else withTokenServer $ \ntfServer -> do
withStore' c $ \db -> supervisorUpdateNtfSub db sub {ntfServer} (NtfSubNTFAction NSACreate)
lift . void $ getNtfNTFWorker True c ntfServer
| otherwise -> case action of
NtfSubNTFAction _ -> lift . void $ getNtfNTFWorker True c subNtfServer
NtfSubSMPAction _ -> lift . void $ getNtfSMPWorker True c smpServer
rotate :: AM ()
rotate = do
withStore' c $ \db -> supervisorUpdateNtfSub db sub (NtfSubNTFAction NSARotate)
lift . void $ getNtfNTFWorker True c subNtfServer
resetSubscription :: AM ()
resetSubscription =
withTokenServer $ \ntfServer -> do
let sub' = sub {ntfQueueId = Nothing, ntfServer, ntfSubId = Nothing, ntfSubStatus = NASNew}
withStore' c $ \db -> supervisorUpdateNtfSub db sub' (NtfSubSMPAction NSASmpKey)
lift . void $ getNtfSMPWorker True c smpServer
NSCDelete -> do
sub_ <- withStore' c $ \db -> do
supervisorUpdateNtfAction db connId (NtfSubNTFAction NSADelete)
getNtfSubscription db connId
logInfo $ "processNtfSub, NSCDelete - sub_ = " <> tshow sub_
case sub_ of
(Just (NtfSubscription {ntfServer}, _)) -> lift . void $ getNtfNTFWorker True c ntfServer
_ -> pure () -- err "NSCDelete - no subscription"
rqToNewSub :: NtfServer -> RcvQueue -> NtfSubscription
rqToNewSub ntfServer RcvQueue {userId, connId, server} = newNtfSubscription userId connId server Nothing ntfServer NASNew
storeNewSub :: DB.Connection -> NtfSubscription -> IO (Either AgentErrorType ())
storeNewSub db sub = first storeError <$> createNtfSubscription db sub (NSASMP NSASmpKey)
resetSubs :: [(RcvQueue, NtfSubscription)] -> AM ()
resetSubs rqSubs = do
withTokenServer $ \ntfServer -> do
let subsToReset = map (toResetSub ntfServer) rqSubs
(cErrs, _) <- lift $ partitionErrs ntfSubConnId subsToReset <$> withStoreBatch' c (\db -> map (storeResetSub db) subsToReset)
notifyErrs c cErrs
let rqs = map fst rqSubs
kickSMPWorkers rqs
where
toResetSub :: NtfServer -> (RcvQueue, NtfSubscription) -> NtfSubscription
toResetSub ntfServer (rq, sub) =
let RcvQueue {server = smpServer} = rq
in sub {smpServer, ntfQueueId = Nothing, ntfServer, ntfSubId = Nothing, ntfSubStatus = NASNew}
storeResetSub :: DB.Connection -> NtfSubscription -> IO ()
storeResetSub db sub = supervisorUpdateNtfSub db sub (NSASMP NSASmpKey)
partitionQueueSubActions ::
[(RcvQueue, Maybe NtfSupervisorSub)] ->
( [RcvQueue], -- new subs
[(RcvQueue, NtfSubscription)], -- reset subs
[SMPServer], -- continue work (SMP)
[NtfServer] -- continue work (Ntf)
)
partitionQueueSubActions = foldr decideSubWork ([], [], [], [])
where
-- sub = Nothing, needs to be created
decideSubWork (rq, Nothing) (ns, rs, css, cns) = (rq : ns, rs, css, cns)
decideSubWork (rq, Just (sub, subAction_)) (ns, rs, css, cns) =
case (clientNtfCreds rq, ntfQueueId sub) of
-- notifier ID created on SMP server (on ntf server subscription can be registered or not yet),
-- need to clarify action
(Just ClientNtfCreds {notifierId}, Just ntfQueueId')
| sameSrvAddr (qServer rq) subSMPServer && notifierId == ntfQueueId' -> contOrReset
| otherwise -> reset
(Nothing, Nothing) -> contOrReset
_ -> reset
where
NtfSubscription {ntfServer = subNtfServer, smpServer = subSMPServer} = sub
contOrReset = case subAction_ of
-- action was set to NULL after worker internal error
Nothing -> reset
Just (action, _)
-- subscription was marked for deletion / is being deleted
| isDeleteNtfSubAction action -> reset
-- continue work on subscription (e.g. supervisor was repeatedly tasked with creating a subscription)
| otherwise -> case action of
NSASMP _ -> (ns, rs, qServer rq : css, cns)
NSANtf _ -> (ns, rs, css, subNtfServer : cns)
reset = (ns, (rq, sub) : rs, css, cns)
NSCSmpDelete -> do
withStore' c (`getPrimaryRcvQueue` connId) >>= \case
Right rq@RcvQueue {server = smpServer} -> do
logInfo $ "processNtfSub, NSCSmpDelete - rq = " <> tshow rq
withStore' c $ \db -> supervisorUpdateNtfAction db connId (NtfSubSMPAction NSASmpDelete)
lift . void $ getNtfSMPWorker True c smpServer
_ -> notifyInternalError c connId "NSCSmpDelete - no rcv queue"
NSCNtfWorker ntfServer -> lift . void $ getNtfNTFWorker True c ntfServer
NSCNtfSMPWorker smpServer -> lift . void $ getNtfSMPWorker True c smpServer
(cErrs, rqs) <- lift $ partitionErrs id connIds' <$> withStoreBatch c (\db -> map (getQueue db) connIds')
logInfo $ "processNtfCmd, NSCSmpDelete - length rqs = " <> tshow (length rqs)
(cErrs', _) <- lift $ partitionErrs qConnId rqs <$> withStoreBatch' c (\db -> map (updateAction db) rqs)
notifyErrs c (cErrs <> cErrs')
kickSMPWorkers rqs
where
getQueue :: DB.Connection -> ConnId -> IO (Either AgentErrorType RcvQueue)
getQueue db connId = first storeError <$> getPrimaryRcvQueue db connId
updateAction :: DB.Connection -> RcvQueue -> IO ()
updateAction db rq = supervisorUpdateNtfAction db (qConnId rq) (NSASMP NSASmpDelete)
NSCDeleteSub -> void $ lift $ withStoreBatch' c $ \db -> map (deleteNtfSubscription' db) connIds'
where
kickSMPWorkers :: [RcvQueue] -> AM ()
kickSMPWorkers rqs = do
let smpServers = S.fromList $ map qServer rqs
lift $ mapM_ (getNtfSMPWorker True c) smpServers
getNtfNTFWorker :: Bool -> AgentClient -> NtfServer -> AM' Worker
getNtfNTFWorker hasWork c server = do
@@ -142,162 +198,303 @@ getNtfSMPWorker hasWork c server = do
ws <- asks $ ntfSMPWorkers . ntfSupervisor
getAgentWorker "ntf_smp" hasWork c server ws $ runNtfSMPWorker c server
getNtfTknDelWorker :: Bool -> AgentClient -> NtfServer -> AM' Worker
getNtfTknDelWorker hasWork c server = do
ws <- asks $ ntfTknDelWorkers . ntfSupervisor
getAgentWorker "ntf_tkn_del" hasWork c server ws $ runNtfTknDelWorker c server
withTokenServer :: (NtfServer -> AM ()) -> AM ()
withTokenServer action = lift getNtfToken >>= mapM_ (\NtfToken {ntfServer} -> action ntfServer)
runNtfWorker :: AgentClient -> NtfServer -> Worker -> AM ()
runNtfWorker c srv Worker {doWork} = do
delay <- asks $ ntfWorkerDelay . config
runNtfWorker c srv Worker {doWork} =
forever $ do
waitForWork doWork
ExceptT $ agentOperationBracket c AONtfNetwork throwWhenInactive $ runExceptT runNtfOperation
threadDelay delay
where
runNtfOperation :: AM ()
runNtfOperation =
withWork c doWork (`getNextNtfSubNTFAction` srv) $
\nextSub@(NtfSubscription {connId}, _, _) -> do
logInfo $ "runNtfWorker, nextSub " <> tshow nextSub
ri <- asks $ reconnectInterval . config
withRetryInterval ri $ \_ loop -> do
liftIO $ waitForUserNetwork c
processSub nextSub
`catchAgentError` retryOnError c "NtfWorker" loop (workerInternalError c connId . show)
processSub :: (NtfSubscription, NtfSubNTFAction, NtfActionTs) -> AM ()
processSub (sub@NtfSubscription {connId, smpServer, ntfSubId}, action, actionTs) = do
ts <- liftIO getCurrentTime
unlessM (lift $ rescheduleAction doWork ts actionTs) $
case action of
NSACreate ->
lift getNtfToken >>= \case
Just tkn@NtfToken {ntfTokenId = Just tknId, ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
RcvQueue {clientNtfCreds} <- withStore c (`getPrimaryRcvQueue` connId)
case clientNtfCreds of
Just ClientNtfCreds {ntfPrivateKey, notifierId} -> do
nSubId <- agentNtfCreateSubscription c tknId tkn (SMPQueueNtf smpServer notifierId) ntfPrivateKey
-- possible improvement: smaller retry until Active, less frequently (daily?) once Active
let actionTs' = addUTCTime 30 ts
withStore' c $ \db ->
updateNtfSubscription db sub {ntfSubId = Just nSubId, ntfSubStatus = NASCreated NSNew} (NtfSubNTFAction NSACheck) actionTs'
_ -> workerInternalError c connId "NSACreate - no notifier queue credentials"
_ -> workerInternalError c connId "NSACreate - no active token"
NSACheck ->
lift getNtfToken >>= \case
Just tkn ->
case ntfSubId of
Just nSubId ->
agentNtfCheckSubscription c nSubId tkn >>= \case
NSAuth -> do
lift (getNtfServer c) >>= \case
Just ntfServer -> do
withStore' c $ \db ->
updateNtfSubscription db sub {ntfServer, ntfQueueId = Nothing, ntfSubId = Nothing, ntfSubStatus = NASNew} (NtfSubSMPAction NSASmpKey) ts
ns <- asks ntfSupervisor
atomically $ writeTBQueue (ntfSubQ ns) (connId, NSCNtfSMPWorker smpServer)
_ -> workerInternalError c connId "NSACheck - failed to reset subscription, notification server not configured"
status -> updateSubNextCheck ts status
Nothing -> workerInternalError c connId "NSACheck - no subscription ID"
_ -> workerInternalError c connId "NSACheck - no active token"
NSADelete -> case ntfSubId of
Just nSubId ->
(lift getNtfToken >>= mapM_ (agentNtfDeleteSubscription c nSubId))
`agentFinally` continueDeletion
_ -> continueDeletion
where
continueDeletion = do
let sub' = sub {ntfSubId = Nothing, ntfSubStatus = NASOff}
withStore' c $ \db -> updateNtfSubscription db sub' (NtfSubSMPAction NSASmpDelete) ts
ns <- asks ntfSupervisor
atomically $ writeTBQueue (ntfSubQ ns) (connId, NSCNtfSMPWorker smpServer)
NSARotate -> case ntfSubId of
Just nSubId ->
(lift getNtfToken >>= mapM_ (agentNtfDeleteSubscription c nSubId))
`agentFinally` deleteCreate
_ -> deleteCreate
where
deleteCreate = do
withStore' c $ \db -> deleteNtfSubscription db connId
ns <- asks ntfSupervisor
atomically $ writeTBQueue (ntfSubQ ns) (connId, NSCCreate)
runNtfOperation = do
ntfBatchSize <- asks $ ntfBatchSize . config
withWorkItems c doWork (\db -> getNextNtfSubNTFActions db srv ntfBatchSize) $ \nextSubs -> do
logInfo $ "runNtfWorker - length nextSubs = " <> tshow (length nextSubs)
currTs <- liftIO getCurrentTime
let (creates, checks, deletes, rotates) = splitActions currTs nextSubs
if null creates && null checks && null deletes && null rotates
then
let (_, _, firstActionTs) = L.head nextSubs
in lift $ rescheduleWork doWork currTs firstActionTs
else do
retrySubActions c creates createSubs
retrySubActions c checks checkSubs
retrySubActions c deletes deleteSubs
retrySubActions c rotates rotateSubs
splitActions :: UTCTime -> NonEmpty (NtfSubNTFAction, NtfSubscription, NtfActionTs) -> ([NtfSubscription], [NtfSubscription], [NtfSubscription], [NtfSubscription])
splitActions currTs = foldr addAction ([], [], [], [])
where
updateSubNextCheck ts toStatus = do
checkInterval <- asks $ ntfSubCheckInterval . config
let nextCheckTs = addUTCTime checkInterval ts
updateSub (NASCreated toStatus) (NtfSubNTFAction NSACheck) nextCheckTs
updateSub toStatus toAction actionTs' =
withStore' c $ \db ->
updateNtfSubscription db sub {ntfSubStatus = toStatus} toAction actionTs'
addAction (cmd, sub, ts) acc@(creates, checks, deletes, rotates) = case cmd of
NSACreate -> (sub : creates, checks, deletes, rotates)
NSACheck
| ts <= currTs -> (creates, sub : checks, deletes, rotates)
| otherwise -> acc
NSADelete -> (creates, checks, sub : deletes, rotates)
NSARotate -> (creates, checks, deletes, sub : rotates)
createSubs :: [NtfSubscription] -> AM' [NtfSubscription]
createSubs ntfSubs =
getNtfToken >>= \case
Just tkn@NtfToken {ntfServer, ntfTokenId = Just tknId, ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
subsRqs_ <- zip ntfSubs <$> withStoreBatch c (\db -> map (getQueue db) ntfSubs)
let (errs1, subs_, newSubs_) = splitSubs tknId subsRqs_
incStatByUserId ntfServer ntfCreateAttempts subs_
case (L.nonEmpty subs_, L.nonEmpty newSubs_) of
(Just subs, Just newSubs) -> do
rs <- L.zip subs <$> agentNtfCreateSubscriptions c tkn newSubs
let (ntfSubs', errs2, nSubIds) = splitResults $ L.toList rs
subs' = map fst nSubIds
errs2' = map (first ntfSubConnId) errs2
incStatByUserId ntfServer ntfCreated subs'
ts <- liftIO getCurrentTime
int <- asks $ ntfSubFirstCheckInterval . config
let checkTs = addUTCTime int ts
(errs3, _) <- partitionErrs ntfSubConnId subs' <$> withStoreBatch' c (\db -> map (updateSubNSACheck db checkTs) nSubIds)
workerErrors c $ errs1 <> errs2' <> errs3
pure ntfSubs'
_ -> workerErrors c errs1 $> []
_ -> do
let errs = map (\sub -> (ntfSubConnId sub, INTERNAL "NSACreate - no active token")) ntfSubs
workerErrors c errs
pure []
where
getQueue :: DB.Connection -> NtfSubscription -> IO (Either AgentErrorType RcvQueue)
getQueue db NtfSubscription {connId} = first storeError <$> getPrimaryRcvQueue db connId
splitSubs :: NtfTokenId -> [(NtfSubscription, Either AgentErrorType RcvQueue)] -> ([(ConnId, AgentErrorType)], [NtfSubscription], [NewNtfEntity 'Subscription])
splitSubs tknId = foldr splitSub ([], [], [])
where
splitSub (sub, rq) (errs, subs, newSubs) = case rq of
Right RcvQueue {clientNtfCreds = Just creds} -> (errs, sub : subs, toNewSub sub creds : newSubs)
Right _ -> ((ntfSubConnId sub, INTERNAL "NSACreate - no notifier queue credentials") : errs, subs, newSubs)
Left e -> ((ntfSubConnId sub, e) : errs, subs, newSubs)
toNewSub NtfSubscription {smpServer} ClientNtfCreds {ntfPrivateKey, notifierId} =
NewNtfSub tknId (SMPQueueNtf smpServer notifierId) ntfPrivateKey
updateSubNSACheck :: DB.Connection -> UTCTime -> (NtfSubscription, NtfSubscriptionId) -> IO ()
updateSubNSACheck db checkTs (sub, nSubId) = updateNtfSubscription db sub {ntfSubId = Just nSubId, ntfSubStatus = NASCreated NSNew} (NSANtf NSACheck) checkTs
checkSubs :: [NtfSubscription] -> AM' [NtfSubscription]
checkSubs ntfSubs =
getNtfToken >>= \case
Just tkn@NtfToken {ntfServer, ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
let (errs1, subs_, subIds_) = splitSubs ntfSubs
incStatByUserId ntfServer ntfCheckAttempts subs_
case (L.nonEmpty subs_, L.nonEmpty subIds_) of
(Just subs, Just subIds) -> do
rs <- L.zip subs <$> agentNtfCheckSubscriptions c tkn subIds
let (ntfSubs', errs2, nSubStatuses) = splitResults $ L.toList rs
subs' = map fst nSubStatuses
(errs2', authSubs) = partitionEithers $ map (\case (sub, NTF _ SMP.AUTH) -> Right sub; e -> Left $ first ntfSubConnId e) errs2
incStatByUserId ntfServer ntfChecked subs'
ts <- liftIO getCurrentTime
int <- asks $ ntfSubCheckInterval . config
let nextCheckTs = addUTCTime int ts
(errs3, srvs) <- partitionErrs ntfSubConnId subs' <$> withStoreBatch' c (\db -> map (updateSub db ntfServer ts nextCheckTs) nSubStatuses)
(errs4, srvs') <- partitionErrs ntfSubConnId authSubs <$> withStoreBatch' c (\db -> map (recreateNtfSub db ntfServer ts) authSubs)
mapM_ (getNtfSMPWorker True c) $ S.fromList (catMaybes srvs <> srvs')
workerErrors c $ errs1 <> errs2' <> errs3 <> errs4
pure ntfSubs'
_ -> workerErrors c errs1 $> []
_ -> do
let errs = map (\sub -> (ntfSubConnId sub, INTERNAL "NSACheck - no active token")) ntfSubs
workerErrors c errs
pure []
where
splitSubs :: [NtfSubscription] -> ([(ConnId, AgentErrorType)], [NtfSubscription], [NtfSubscriptionId])
splitSubs = foldr splitSub ([], [], [])
where
splitSub sub (errs, subs, subIds) = case sub of
NtfSubscription {ntfSubId = Just subId} -> (errs, sub : subs, subId : subIds)
_ -> ((ntfSubConnId sub, INTERNAL "NSACheck - no subscription ID") : errs, subs, subIds)
updateSub :: DB.Connection -> NtfServer -> UTCTime -> UTCTime -> (NtfSubscription, NtfSubStatus) -> IO (Maybe SMPServer)
updateSub db ntfServer ts nextCheckTs (sub, status)
| ntfShouldSubscribe status =
let sub' = sub {ntfSubStatus = NASCreated status}
in Nothing <$ updateNtfSubscription db sub' (NSANtf NSACheck) nextCheckTs
-- ntf server stopped subscribing to this queue
| otherwise = Just <$> recreateNtfSub db ntfServer ts sub
recreateNtfSub :: DB.Connection -> NtfServer -> UTCTime -> NtfSubscription -> IO SMPServer
recreateNtfSub db ntfServer ts sub@NtfSubscription {smpServer} =
let sub' = sub {ntfServer, ntfQueueId = Nothing, ntfSubId = Nothing, ntfSubStatus = NASNew}
in smpServer <$ updateNtfSubscription db sub' (NSASMP NSASmpKey) ts
incStatByUserId :: NtfServer -> (AgentNtfServerStats -> TVar Int) -> [NtfSubscription] -> AM' ()
incStatByUserId ntfServer sel ss =
forM_ (M.assocs userIdsCounts) $ \(userId, count) ->
atomically $ incNtfServerStat' c userId ntfServer sel count
where
userIdsCounts = foldl' (\acc NtfSubscription {userId} -> M.insertWith (+) userId 1 acc) M.empty ss
-- NSADelete and NSARotate are deprecated, but their processing is kept for legacy db records;
-- These actions are not batched
deleteSubs :: [NtfSubscription] -> AM' [NtfSubscription]
deleteSubs ntfSubs = do
retrySubs_ <- mapM (runCatching deleteSub) ntfSubs
pure $ catMaybes retrySubs_
where
deleteSub :: NtfSubscription -> AM (Maybe NtfSubscription)
deleteSub sub@NtfSubscription {smpServer} =
deleteNtfSub sub $ do
let sub' = sub {ntfSubId = Nothing, ntfSubStatus = NASOff}
ts <- liftIO getCurrentTime
withStore' c $ \db -> updateNtfSubscription db sub' (NSASMP NSASmpDelete) ts
lift . void $ getNtfSMPWorker True c smpServer
rotateSubs :: [NtfSubscription] -> AM' [NtfSubscription]
rotateSubs ntfSubs = do
retrySubs_ <- mapM (runCatching rotateSub) ntfSubs
pure $ catMaybes retrySubs_
where
rotateSub :: NtfSubscription -> AM (Maybe NtfSubscription)
rotateSub sub@NtfSubscription {connId} =
deleteNtfSub sub $ do
withStore' c $ \db -> deleteNtfSubscription db connId
ns <- asks ntfSupervisor
atomically $ writeTBQueue (ntfSubQ ns) (NSCCreate, [connId])
runCatching :: (NtfSubscription -> AM (Maybe NtfSubscription)) -> NtfSubscription -> AM' (Maybe NtfSubscription)
runCatching action sub@NtfSubscription {connId} =
fromRight Nothing
<$> runExceptT (action sub `catchAgentError` \e -> workerInternalError c connId (show e) $> Nothing)
-- deleteNtfSub is only used in NSADelete and NSARotate, so also deprecated
deleteNtfSub :: NtfSubscription -> AM () -> AM (Maybe NtfSubscription)
deleteNtfSub sub@NtfSubscription {userId, ntfSubId} continue = case ntfSubId of
Just nSubId ->
lift getNtfToken >>= \case
Just tkn@NtfToken {ntfServer} -> do
atomically $ incNtfServerStat c userId ntfServer ntfDelAttempts
tryAgentError (agentNtfDeleteSubscription c nSubId tkn) >>= \case
Right _ -> do
atomically $ incNtfServerStat c userId ntfServer ntfDeleted
continue'
Left e
| temporaryOrHostError e -> pure $ Just sub -- don't continue, retry
| otherwise -> continue'
Nothing -> continue'
_ -> continue'
where
continue' = continue $> Nothing -- continue without retry
runNtfSMPWorker :: AgentClient -> SMPServer -> Worker -> AM ()
runNtfSMPWorker c srv Worker {doWork} = do
env <- ask
delay <- asks $ ntfSMPWorkerDelay . config
forever $ do
waitForWork doWork
ExceptT . liftIO . agentOperationBracket c AONtfNetwork throwWhenInactive $
runReaderT (runExceptT runNtfSMPOperation) env
threadDelay delay
runNtfSMPWorker c srv Worker {doWork} = forever $ do
waitForWork doWork
ExceptT $ agentOperationBracket c AONtfNetwork throwWhenInactive $ runExceptT runNtfSMPOperation
where
runNtfSMPOperation =
withWork c doWork (`getNextNtfSubSMPAction` srv) $
\nextSub@(NtfSubscription {connId}, _, _) -> do
logInfo $ "runNtfSMPWorker, nextSub " <> tshow nextSub
ri <- asks $ reconnectInterval . config
withRetryInterval ri $ \_ loop -> do
liftIO $ waitForUserNetwork c
processSub nextSub
`catchAgentError` retryOnError c "NtfSMPWorker" loop (workerInternalError c connId . show)
processSub :: (NtfSubscription, NtfSubSMPAction, NtfActionTs) -> AM ()
processSub (sub@NtfSubscription {connId, ntfServer}, smpAction, actionTs) = do
ts <- liftIO getCurrentTime
unlessM (lift $ rescheduleAction doWork ts actionTs) $
case smpAction of
NSASmpKey ->
lift getNtfToken >>= \case
Just NtfToken {ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
rq <- withStore c (`getPrimaryRcvQueue` connId)
C.AuthAlg a <- asks (rcvAuthAlg . config)
g <- asks random
(ntfPublicKey, ntfPrivateKey) <- atomically $ C.generateAuthKeyPair a g
(rcvNtfPubDhKey, rcvNtfPrivDhKey) <- atomically $ C.generateKeyPair g
(notifierId, rcvNtfSrvPubDhKey) <- enableQueueNotifications c rq ntfPublicKey rcvNtfPubDhKey
let rcvNtfDhSecret = C.dh' rcvNtfSrvPubDhKey rcvNtfPrivDhKey
withStore' c $ \db -> do
setRcvQueueNtfCreds db connId $ Just ClientNtfCreds {ntfPublicKey, ntfPrivateKey, notifierId, rcvNtfDhSecret}
updateNtfSubscription db sub {ntfQueueId = Just notifierId, ntfSubStatus = NASKey} (NtfSubNTFAction NSACreate) ts
ns <- asks ntfSupervisor
atomically $ sendNtfSubCommand ns (connId, NSCNtfWorker ntfServer)
_ -> workerInternalError c connId "NSASmpKey - no active token"
NSASmpDelete -> do
rq_ <- withStore' c $ \db -> do
setRcvQueueNtfCreds db connId Nothing
getPrimaryRcvQueue db connId
mapM_ (disableQueueNotifications c) rq_
withStore' c $ \db -> deleteNtfSubscription db connId
runNtfSMPOperation :: AM ()
runNtfSMPOperation = do
ntfBatchSize <- asks $ ntfBatchSize . config
withWorkItems c doWork (\db -> getNextNtfSubSMPActions db srv ntfBatchSize) $ \nextSubs -> do
logInfo $ "runNtfSMPWorker - length nextSubs = " <> tshow (length nextSubs)
let (creates, deletes) = splitActions nextSubs
retrySubActions c creates createNotifierKeys
retrySubActions c deletes deleteNotifierKeys
splitActions :: NonEmpty (NtfSubSMPAction, NtfSubscription) -> ([NtfSubscription], [NtfSubscription])
splitActions = foldr addAction ([], [])
where
addAction (cmd, sub) (creates, deletes) = case cmd of
NSASmpKey -> (sub : creates, deletes)
NSASmpDelete -> (creates, sub : deletes)
createNotifierKeys :: [NtfSubscription] -> AM' [NtfSubscription]
createNotifierKeys ntfSubs =
getNtfToken >>= \case
Just NtfToken {ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
(errs1, subRqKeys) <- prepareQueueSmpKey ntfSubs
rs <- enableQueuesNtfs c subRqKeys
let (subRqKeys', errs2, successes) = splitResults rs
ntfSubs' = map eqnrNtfSub subRqKeys'
errs2' = map (first (qConnId . eqnrRq)) errs2
ts <- liftIO getCurrentTime
(errs3, srvs) <- partitionErrs (qConnId . eqnrRq . fst) successes <$> withStoreBatch' c (\db -> map (storeNtfSubCreds db ts) successes)
mapM_ (getNtfNTFWorker True c) $ S.fromList srvs
workerErrors c $ errs1 <> errs2' <> errs3
pure ntfSubs'
_ -> do
let errs = map (\sub -> (ntfSubConnId sub, INTERNAL "NSASmpKey - no active token")) ntfSubs
workerErrors c errs
pure []
where
prepareQueueSmpKey :: [NtfSubscription] -> AM' ([(ConnId, AgentErrorType)], [EnableQueueNtfReq])
prepareQueueSmpKey subs = do
alg <- asks (rcvAuthAlg . config)
g <- asks random
partitionErrs ntfSubConnId subs <$> withStoreBatch c (\db -> map (getQueue db alg g) subs)
where
getQueue :: DB.Connection -> C.AuthAlg -> TVar ChaChaDRG -> NtfSubscription -> IO (Either AgentErrorType EnableQueueNtfReq)
getQueue db (C.AuthAlg a) g sub = fmap (first storeError) $ runExceptT $ do
rq <- ExceptT $ getPrimaryRcvQueue db (ntfSubConnId sub)
authKeyPair <- atomically $ C.generateAuthKeyPair a g
rcvNtfKeyPair <- atomically $ C.generateKeyPair g
pure (EnableQueueNtfReq sub rq authKeyPair rcvNtfKeyPair)
storeNtfSubCreds :: DB.Connection -> UTCTime -> (EnableQueueNtfReq, (SMP.NotifierId, SMP.RcvNtfPublicDhKey)) -> IO NtfServer
storeNtfSubCreds db ts (EnableQueueNtfReq {eqnrNtfSub, eqnrAuthKeyPair = (ntfPublicKey, ntfPrivateKey), eqnrRcvKeyPair = (_, pk)}, (notifierId, srvPubDhKey)) = do
let NtfSubscription {ntfServer} = eqnrNtfSub
rcvNtfDhSecret = C.dh' srvPubDhKey pk
setRcvQueueNtfCreds db (ntfSubConnId eqnrNtfSub) $ Just ClientNtfCreds {ntfPublicKey, ntfPrivateKey, notifierId, rcvNtfDhSecret}
updateNtfSubscription db eqnrNtfSub {ntfQueueId = Just notifierId, ntfSubStatus = NASKey} (NSANtf NSACreate) ts
pure ntfServer
deleteNotifierKeys :: [NtfSubscription] -> AM' [NtfSubscription]
deleteNotifierKeys ntfSubs = do
(errs1, subRqs) <- partitionErrs ntfSubConnId ntfSubs <$> withStoreBatch c (\db -> map (resetCredsGetQueue db) ntfSubs)
rs <- disableQueuesNtfs c subRqs
let (subRqs', errs2, successes) = splitResults rs
ntfSubs' = map fst subRqs'
errs2' = map (first (qConnId . snd)) errs2
disabledRqs = map (snd . fst) successes
(errs3, _) <- partitionErrs qConnId disabledRqs <$> withStoreBatch' c (\db -> map (deleteSub db) disabledRqs)
workerErrors c $ errs1 <> errs2' <> errs3
pure ntfSubs'
where
resetCredsGetQueue :: DB.Connection -> NtfSubscription -> IO (Either AgentErrorType DisableQueueNtfReq)
resetCredsGetQueue db sub@NtfSubscription {connId} = fmap (first storeError) $ runExceptT $ do
liftIO $ setRcvQueueNtfCreds db connId Nothing
rq <- ExceptT $ getPrimaryRcvQueue db connId
pure (sub, rq)
deleteSub :: DB.Connection -> RcvQueue -> IO ()
deleteSub db rq = deleteNtfSubscription db (qConnId rq)
rescheduleAction :: TMVar () -> UTCTime -> UTCTime -> AM' Bool
rescheduleAction doWork ts actionTs
| actionTs <= ts = pure False
| otherwise = do
void . atomically $ tryTakeTMVar doWork
void . forkIO $ do
liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime actionTs ts
atomically $ hasWorkToDo' doWork
pure True
retrySubActions :: AgentClient -> [NtfSubscription] -> ([NtfSubscription] -> AM' [NtfSubscription]) -> AM ()
retrySubActions _ [] _ = pure ()
retrySubActions c subs action = do
v <- newTVarIO subs
ri <- asks $ reconnectInterval . config
withRetryInterval ri $ \_ loop -> do
liftIO $ waitWhileSuspended c
liftIO $ waitForUserNetwork c
subs' <- readTVarIO v
retrySubs <- lift $ action subs'
unless (null retrySubs) $ do
atomically $ writeTVar v retrySubs
retryNetworkLoop c loop
retryOnError :: AgentClient -> Text -> AM () -> (AgentErrorType -> AM ()) -> AgentErrorType -> AM ()
retryOnError c name loop done e = do
logError $ name <> " error: " <> tshow e
case e of
BROKER _ NETWORK -> retryLoop
BROKER _ TIMEOUT -> retryLoop
_ -> done e
-- (temporary errs, other errs, successes)
splitResults :: [(a, Either AgentErrorType r)] -> ([a], [(a, AgentErrorType)], [(a, r)])
splitResults = foldr addRes ([], [], [])
where
retryLoop = do
atomically $ endAgentOperation c AONtfNetwork
atomically $ throwWhenInactive c
atomically $ beginAgentOperation c AONtfNetwork
loop
addRes (a, r_) (as, errs, rs) = case r_ of
Right r -> (as, errs, (a, r) : rs)
Left e
| temporaryOrHostError e -> (a : as, errs, rs)
| otherwise -> (as, (a, e) : errs, rs)
rescheduleWork :: TMVar () -> UTCTime -> UTCTime -> AM' ()
rescheduleWork doWork ts actionTs = do
void . atomically $ tryTakeTMVar doWork
void . forkIO $ do
liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime actionTs ts
atomically $ hasWorkToDo' doWork
retryNetworkLoop :: AgentClient -> AM () -> AM ()
retryNetworkLoop c loop = do
atomically $ endAgentOperation c AONtfNetwork
liftIO $ throwWhenInactive c
atomically $ beginAgentOperation c AONtfNetwork
loop
workerErrors :: AgentClient -> [(ConnId, AgentErrorType)] -> AM' ()
workerErrors c connErrs =
unless (null connErrs) $ do
void $ withStoreBatch' c (\db -> map (setNullNtfSubscriptionAction db . fst) connErrs)
notifyErrs c connErrs
workerInternalError :: AgentClient -> ConnId -> String -> AM ()
workerInternalError c connId internalErrStr = do
@@ -309,6 +506,14 @@ notifyInternalError :: MonadIO m => AgentClient -> ConnId -> String -> m ()
notifyInternalError AgentClient {subQ} connId internalErrStr = atomically $ writeTBQueue subQ ("", connId, AEvt SAEConn $ ERR $ INTERNAL internalErrStr)
{-# INLINE notifyInternalError #-}
notifyInternalError' :: MonadIO m => AgentClient -> String -> m ()
notifyInternalError' AgentClient {subQ} internalErrStr = atomically $ writeTBQueue subQ ("", "", AEvt SAEConn $ ERR $ INTERNAL internalErrStr)
{-# INLINE notifyInternalError' #-}
notifyErrs :: MonadIO m => AgentClient -> [(ConnId, AgentErrorType)] -> m ()
notifyErrs AgentClient {subQ} connErrs = unless (null connErrs) $ atomically $ writeTBQueue subQ ("", "", AEvt SAENone $ ERRS connErrs)
{-# INLINE notifyErrs #-}
getNtfToken :: AM' (Maybe NtfToken)
getNtfToken = do
tkn <- asks $ ntfTkn . ntfSupervisor
@@ -320,7 +525,7 @@ nsUpdateToken ns tkn = writeTVar (ntfTkn ns) $ Just tkn
nsRemoveNtfToken :: NtfSupervisor -> STM ()
nsRemoveNtfToken ns = writeTVar (ntfTkn ns) Nothing
sendNtfSubCommand :: NtfSupervisor -> (ConnId, NtfSupervisorCommand) -> STM ()
sendNtfSubCommand :: NtfSupervisor -> (NtfSupervisorCommand, NonEmpty ConnId) -> STM ()
sendNtfSubCommand ns cmd = do
tkn <- readTVar (ntfTkn ns)
when (instantNotifications tkn) $ writeTBQueue (ntfSubQ ns) cmd
@@ -330,10 +535,51 @@ instantNotifications = \case
Just NtfToken {ntfTknStatus = NTActive, ntfMode = NMInstant} -> True
_ -> False
deleteToken :: AgentClient -> NtfToken -> AM ()
deleteToken c tkn@NtfToken {ntfServer, ntfTokenId, ntfPrivKey} = do
setToDelete <- withStore' c $ \db -> do
removeNtfToken db tkn
case ntfTokenId of
Just tknId -> addNtfTokenToDelete db ntfServer ntfPrivKey tknId $> True
Nothing -> pure False
ns <- asks ntfSupervisor
atomically $ nsRemoveNtfToken ns
when setToDelete $ void $ lift $ getNtfTknDelWorker True c ntfServer
runNtfTknDelWorker :: AgentClient -> NtfServer -> Worker -> AM ()
runNtfTknDelWorker c srv Worker {doWork} =
forever $ do
waitForWork doWork
ExceptT $ agentOperationBracket c AONtfNetwork throwWhenInactive $ runExceptT runNtfOperation
where
runNtfOperation :: AM ()
runNtfOperation =
withWork c doWork (`getNextNtfTokenToDelete` srv) $
\nextTknToDelete -> do
logInfo $ "runNtfTknDelWorker, nextTknToDelete " <> tshow nextTknToDelete
ri <- asks $ reconnectInterval . config
withRetryInterval ri $ \_ loop -> do
liftIO $ waitWhileSuspended c
liftIO $ waitForUserNetwork c
processTknToDelete nextTknToDelete `catchAgentError` retryTmpError loop nextTknToDelete
retryTmpError :: AM () -> NtfTokenToDelete -> AgentErrorType -> AM ()
retryTmpError loop (tknDbId, _, _) e = do
logError $ "ntf tkn del error: " <> tshow e
if temporaryOrHostError e
then retryNetworkLoop c loop
else do
withStore' c $ \db -> deleteNtfTokenToDelete db tknDbId
notifyInternalError' c (show e)
processTknToDelete :: NtfTokenToDelete -> AM ()
processTknToDelete (tknDbId, ntfPrivKey, tknId) = do
agentNtfDeleteToken c srv ntfPrivKey tknId
withStore' c $ \db -> deleteNtfTokenToDelete db tknDbId
closeNtfSupervisor :: NtfSupervisor -> IO ()
closeNtfSupervisor ns = do
stopWorkers $ ntfWorkers ns
stopWorkers $ ntfSMPWorkers ns
stopWorkers $ ntfTknDelWorkers ns
where
stopWorkers workers = atomically (swapTVar workers M.empty) >>= mapM_ (liftIO . cancelWorker)
+503 -64
View File
@@ -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
@@ -51,6 +54,7 @@ module Simplex.Messaging.Agent.Protocol
-- * SMP agent protocol types
ConnInfo,
SndQueueSecured,
AEntityId,
ACommand (..),
AEvent (..),
AEvt (..),
@@ -99,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 (..),
@@ -138,35 +157,42 @@ 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)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as L
import Data.Map (Map)
import qualified Data.Map as M
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe, isJust)
import Data.Text (Text)
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
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)
@@ -190,15 +216,14 @@ import Simplex.Messaging.Parsers
import Simplex.Messaging.Protocol
( AProtocolType,
BrokerErrorType (..),
EntityId,
ErrorType,
MsgBody,
MsgFlags,
MsgId,
NMsgMeta,
ProtocolServer (..),
QueueMode (..),
SMPClientVersion,
SMPMsgMeta,
SMPServer,
SMPServerWithAuth,
SndPublicAuthKey,
@@ -213,6 +238,8 @@ import Simplex.Messaging.Protocol
sameSrvAddr,
sndAuthKeySMPClientVersion,
srvHostnamesSMPClientVersion,
shortLinksSMPClientVersion,
senderCanSecure,
pattern ProtoServerWithAuth,
pattern SMPServer,
)
@@ -232,6 +259,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
@@ -277,20 +305,22 @@ supportedSMPAgentVRange = mkVersionRange minSupportedSMPAgentVersion currentSMPA
e2eEncConnInfoLength :: VersionSMPA -> PQSupport -> Int
e2eEncConnInfoLength v = \case
-- reduced by 3726 (roughly the increase of message ratchet header size + key and ciphertext in reply link)
PQSupportOn | v >= pqdrSMPAgentVersion -> 11122
_ -> 14848
PQSupportOn | v >= pqdrSMPAgentVersion -> 11106
_ -> 14832
e2eEncAgentMsgLength :: VersionSMPA -> PQSupport -> Int
e2eEncAgentMsgLength v = \case
-- reduced by 2222 (the increase of message ratchet header size)
PQSupportOn | v >= pqdrSMPAgentVersion -> 13634
_ -> 15856
PQSupportOn | v >= pqdrSMPAgentVersion -> 13618
_ -> 15840
-- | SMP agent event
type ATransmission = (ACorrId, EntityId, AEvt)
type ATransmission = (ACorrId, AEntityId, AEvt)
type UserId = Int64
type AEntityId = ByteString
type ACorrId = ByteString
data AEntity = AEConn | AERcvFile | AESndFile | AENone
@@ -342,6 +372,7 @@ data AEvent (e :: AEntity) where
INFO :: PQSupport -> ConnInfo -> AEvent AEConn
CON :: PQEncryption -> AEvent AEConn -- notification that connection is established
END :: AEvent AEConn
DELD :: AEvent AEConn
CONNECT :: AProtocolType -> TransportHost -> AEvent AENone
DISCONNECT :: AProtocolType -> TransportHost -> AEvent AENone
DOWN :: SMPServer -> [ConnId] -> AEvent AENone
@@ -353,16 +384,17 @@ data AEvent (e :: AEntity) where
MERR :: AgentMsgId -> AgentErrorType -> AEvent AEConn
MERRS :: NonEmpty AgentMsgId -> AgentErrorType -> AEvent AEConn
MSG :: MsgMeta -> MsgFlags -> MsgBody -> AEvent AEConn
MSGNTF :: SMPMsgMeta -> AEvent AEConn
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
JOINED :: SndQueueSecured -> AEvent AEConn
ERR :: AgentErrorType -> AEvent AEConn
ERRS :: [(ConnId, AgentErrorType)] -> AEvent AENone
SUSPENDED :: AEvent AENone
RFPROG :: Int64 -> Int64 -> AEvent AERcvFile
RFDONE :: FilePath -> AEvent AERcvFile
@@ -411,6 +443,7 @@ data AEventTag (e :: AEntity) where
INFO_ :: AEventTag AEConn
CON_ :: AEventTag AEConn
END_ :: AEventTag AEConn
DELD_ :: AEventTag AEConn
CONNECT_ :: AEventTag AENone
DISCONNECT_ :: AEventTag AENone
DOWN_ :: AEventTag AENone
@@ -425,13 +458,14 @@ 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
JOINED_ :: AEventTag AEConn
ERR_ :: AEventTag AEConn
ERRS_ :: AEventTag AENone
SUSPENDED_ :: AEventTag AENone
-- XFTP commands and responses
RFDONE_ :: AEventTag AERcvFile
@@ -464,6 +498,7 @@ aEventTag = \case
INFO {} -> INFO_
CON _ -> CON_
END -> END_
DELD -> DELD_
CONNECT {} -> CONNECT_
DISCONNECT {} -> DISCONNECT_
DOWN {} -> DOWN_
@@ -478,13 +513,14 @@ 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_
JOINED _ -> JOINED_
ERR _ -> ERR_
ERRS _ -> ERRS_
SUSPENDED -> SUSPENDED_
RFPROG {} -> RFPROG_
RFDONE {} -> RFDONE_
@@ -668,21 +704,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
@@ -848,20 +880,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
@@ -939,6 +958,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
@@ -1003,6 +1038,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)
@@ -1027,6 +1066,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
@@ -1076,6 +1145,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 #-}
@@ -1090,6 +1166,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
@@ -1137,16 +1223,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
@@ -1173,7 +1263,7 @@ data SMPQueueAddress = SMPQueueAddress
{ smpServer :: SMPServer,
senderId :: SMP.SenderId,
dhPublicKey :: C.PublicKeyX25519,
sndSecure :: Bool
queueMode :: Maybe QueueMode
}
deriving (Eq, Show)
@@ -1200,42 +1290,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
@@ -1243,6 +1351,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)
@@ -1256,6 +1369,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,
@@ -1266,6 +1602,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
@@ -1336,6 +1752,8 @@ data AgentErrorType
CMD {cmdErr :: CommandErrorType, errContext :: String}
| -- | connection errors
CONN {connErr :: ConnectionErrorType}
| -- | user not found in database
NO_USER
| -- | SMP protocol errors forwarded to agent clients
SMP {serverAddress :: String, smpErr :: ErrorType}
| -- | NTF protocol errors forwarded to agent clients
@@ -1396,6 +1814,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
@@ -1508,3 +1928,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
+24 -1
View File
@@ -9,6 +9,7 @@ module Simplex.Messaging.Agent.RetryInterval
RI2State (..),
withRetryInterval,
withRetryIntervalCount,
withRetryForeground,
withRetryLock2,
updateRetryInterval2,
nextRetryDelay,
@@ -16,10 +17,11 @@ module Simplex.Messaging.Agent.RetryInterval
where
import Control.Concurrent (forkIO)
import Control.Concurrent.STM (retry)
import Control.Monad (void)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Int (Int64)
import Simplex.Messaging.Util (threadDelay', whenM)
import Simplex.Messaging.Util (threadDelay', unlessM, whenM)
import UnliftIO.STM
data RetryInterval = RetryInterval
@@ -63,6 +65,27 @@ withRetryIntervalCount ri action = callAction 0 0 $ initialInterval ri
let elapsed' = elapsed + delay
callAction (n + 1) elapsed' $ nextRetryDelay elapsed' delay ri
withRetryForeground :: forall m a. MonadIO m => RetryInterval -> STM Bool -> STM Bool -> (Int64 -> m a -> m a) -> m a
withRetryForeground ri isForeground isOnline action = callAction 0 $ initialInterval ri
where
callAction :: Int64 -> Int64 -> m a
callAction elapsed delay = action delay loop
where
loop = do
-- limit delay to max Int value (~36 minutes on for 32 bit architectures)
d <- registerDelay $ fromIntegral $ min delay (fromIntegral (maxBound :: Int))
(wasForeground, wasOnline) <- atomically $ (,) <$> isForeground <*> isOnline
reset <- atomically $ do
foreground <- isForeground
online <- isOnline
let reset = (not wasForeground && foreground) || (not wasOnline && online)
unlessM ((reset ||) <$> readTVar d) retry
pure reset
let (elapsed', delay')
| reset = (0, initialInterval ri)
| otherwise = (elapsed + delay, nextRetryDelay elapsed' delay ri)
callAction elapsed' delay'
-- This function allows action to toggle between slow and fast retry intervals.
withRetryLock2 :: forall m. MonadIO m => RetryInterval2 -> TMVar () -> (RI2State -> (RetryIntervalMode -> m ()) -> m ()) -> m ()
withRetryLock2 RetryInterval2 {riSlow, riFast} lock action =
+168 -13
View File
@@ -1,17 +1,19 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE TemplateHaskell #-}
module Simplex.Messaging.Agent.Stats where
import Data.Aeson (FromJSON (..), FromJSONKey, ToJSON (..))
import qualified Data.Aeson.TH as J
import Data.Int (Int64)
import Data.Map (Map)
import Database.SQLite.Simple.FromField (FromField (..))
import Database.SQLite.Simple.ToField (ToField (..))
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Simplex.Messaging.Agent.Protocol (UserId)
import Simplex.Messaging.Parsers (defaultJSON, fromTextField_)
import Simplex.Messaging.Protocol (SMPServer, XFTPServer)
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
@@ -44,7 +46,12 @@ data AgentSMPServerStats = AgentSMPServerStats
connSubscribed :: TVar Int, -- total successful subscription
connSubAttempts :: TVar Int, -- subscription attempts
connSubIgnored :: TVar Int, -- subscription results ignored (client switched to different session or it was not pending)
connSubErrs :: TVar Int -- permanent subscription errors (temporary accounted for in attempts)
connSubErrs :: TVar Int, -- permanent subscription errors (temporary accounted for in attempts)
-- notifications stats
ntfKey :: TVar Int,
ntfKeyAttempts :: TVar Int,
ntfKeyDeleted :: TVar Int,
ntfKeyDeleteAttempts :: TVar Int
}
data AgentSMPServerStatsData = AgentSMPServerStatsData
@@ -75,10 +82,17 @@ data AgentSMPServerStatsData = AgentSMPServerStatsData
_connSubscribed :: Int,
_connSubAttempts :: Int,
_connSubIgnored :: Int,
_connSubErrs :: Int
_connSubErrs :: Int,
_ntfKey :: OptionalInt,
_ntfKeyAttempts :: OptionalInt,
_ntfKeyDeleted :: OptionalInt,
_ntfKeyDeleteAttempts :: OptionalInt
}
deriving (Show)
newtype OptionalInt = OInt {toInt :: Int}
deriving (Num, Show, ToJSON)
newAgentSMPServerStats :: STM AgentSMPServerStats
newAgentSMPServerStats = do
sentDirect <- newTVar 0
@@ -109,6 +123,10 @@ newAgentSMPServerStats = do
connSubAttempts <- newTVar 0
connSubIgnored <- newTVar 0
connSubErrs <- newTVar 0
ntfKey <- newTVar 0
ntfKeyAttempts <- newTVar 0
ntfKeyDeleted <- newTVar 0
ntfKeyDeleteAttempts <- newTVar 0
pure
AgentSMPServerStats
{ sentDirect,
@@ -138,7 +156,11 @@ newAgentSMPServerStats = do
connSubscribed,
connSubAttempts,
connSubIgnored,
connSubErrs
connSubErrs,
ntfKey,
ntfKeyAttempts,
ntfKeyDeleted,
ntfKeyDeleteAttempts
}
newAgentSMPServerStatsData :: AgentSMPServerStatsData
@@ -171,7 +193,11 @@ newAgentSMPServerStatsData =
_connSubscribed = 0,
_connSubAttempts = 0,
_connSubIgnored = 0,
_connSubErrs = 0
_connSubErrs = 0,
_ntfKey = 0,
_ntfKeyAttempts = 0,
_ntfKeyDeleted = 0,
_ntfKeyDeleteAttempts = 0
}
newAgentSMPServerStats' :: AgentSMPServerStatsData -> STM AgentSMPServerStats
@@ -204,6 +230,10 @@ newAgentSMPServerStats' s = do
connSubAttempts <- newTVar $ _connSubAttempts s
connSubIgnored <- newTVar $ _connSubIgnored s
connSubErrs <- newTVar $ _connSubErrs s
ntfKey <- newTVar $ toInt $ _ntfKey s
ntfKeyAttempts <- newTVar $ toInt $ _ntfKeyAttempts s
ntfKeyDeleted <- newTVar $ toInt $ _ntfKeyDeleted s
ntfKeyDeleteAttempts <- newTVar $ toInt $ _ntfKeyDeleteAttempts s
pure
AgentSMPServerStats
{ sentDirect,
@@ -233,7 +263,11 @@ newAgentSMPServerStats' s = do
connSubscribed,
connSubAttempts,
connSubIgnored,
connSubErrs
connSubErrs,
ntfKey,
ntfKeyAttempts,
ntfKeyDeleted,
ntfKeyDeleteAttempts
}
-- as this is used to periodically update stats in db,
@@ -268,6 +302,10 @@ getAgentSMPServerStats s = do
_connSubAttempts <- readTVarIO $ connSubAttempts s
_connSubIgnored <- readTVarIO $ connSubIgnored s
_connSubErrs <- readTVarIO $ connSubErrs s
_ntfKey <- OInt <$> readTVarIO (ntfKey s)
_ntfKeyAttempts <- OInt <$> readTVarIO (ntfKeyAttempts s)
_ntfKeyDeleted <- OInt <$> readTVarIO (ntfKeyDeleted s)
_ntfKeyDeleteAttempts <- OInt <$> readTVarIO (ntfKeyDeleteAttempts s)
pure
AgentSMPServerStatsData
{ _sentDirect,
@@ -297,7 +335,11 @@ getAgentSMPServerStats s = do
_connSubscribed,
_connSubAttempts,
_connSubIgnored,
_connSubErrs
_connSubErrs,
_ntfKey,
_ntfKeyAttempts,
_ntfKeyDeleted,
_ntfKeyDeleteAttempts
}
addSMPStatsData :: AgentSMPServerStatsData -> AgentSMPServerStatsData -> AgentSMPServerStatsData
@@ -330,7 +372,11 @@ addSMPStatsData sd1 sd2 =
_connSubscribed = _connSubscribed sd1 + _connSubscribed sd2,
_connSubAttempts = _connSubAttempts sd1 + _connSubAttempts sd2,
_connSubIgnored = _connSubIgnored sd1 + _connSubIgnored sd2,
_connSubErrs = _connSubErrs sd1 + _connSubErrs sd2
_connSubErrs = _connSubErrs sd1 + _connSubErrs sd2,
_ntfKey = _ntfKey sd1 + _ntfKey sd2,
_ntfKeyAttempts = _ntfKeyAttempts sd1 + _ntfKeyAttempts sd2,
_ntfKeyDeleted = _ntfKeyDeleted sd1 + _ntfKeyDeleted sd2,
_ntfKeyDeleteAttempts = _ntfKeyDeleteAttempts sd1 + _ntfKeyDeleteAttempts sd2
}
data AgentXFTPServerStats = AgentXFTPServerStats
@@ -490,18 +536,127 @@ addXFTPStatsData sd1 sd2 =
_deleteErrs = _deleteErrs sd1 + _deleteErrs sd2
}
data AgentNtfServerStats = AgentNtfServerStats
{ ntfCreated :: TVar Int,
ntfCreateAttempts :: TVar Int,
ntfChecked :: TVar Int,
ntfCheckAttempts :: TVar Int,
ntfDeleted :: TVar Int,
ntfDelAttempts :: TVar Int
}
data AgentNtfServerStatsData = AgentNtfServerStatsData
{ _ntfCreated :: Int,
_ntfCreateAttempts :: Int,
_ntfChecked :: Int,
_ntfCheckAttempts :: Int,
_ntfDeleted :: Int,
_ntfDelAttempts :: Int
}
deriving (Show)
newAgentNtfServerStats :: STM AgentNtfServerStats
newAgentNtfServerStats = do
ntfCreated <- newTVar 0
ntfCreateAttempts <- newTVar 0
ntfChecked <- newTVar 0
ntfCheckAttempts <- newTVar 0
ntfDeleted <- newTVar 0
ntfDelAttempts <- newTVar 0
pure
AgentNtfServerStats
{ ntfCreated,
ntfCreateAttempts,
ntfChecked,
ntfCheckAttempts,
ntfDeleted,
ntfDelAttempts
}
newAgentNtfServerStatsData :: AgentNtfServerStatsData
newAgentNtfServerStatsData =
AgentNtfServerStatsData
{ _ntfCreated = 0,
_ntfCreateAttempts = 0,
_ntfChecked = 0,
_ntfCheckAttempts = 0,
_ntfDeleted = 0,
_ntfDelAttempts = 0
}
newAgentNtfServerStats' :: AgentNtfServerStatsData -> STM AgentNtfServerStats
newAgentNtfServerStats' s = do
ntfCreated <- newTVar $ _ntfCreated s
ntfCreateAttempts <- newTVar $ _ntfCreateAttempts s
ntfChecked <- newTVar $ _ntfChecked s
ntfCheckAttempts <- newTVar $ _ntfCheckAttempts s
ntfDeleted <- newTVar $ _ntfDeleted s
ntfDelAttempts <- newTVar $ _ntfDelAttempts s
pure
AgentNtfServerStats
{ ntfCreated,
ntfCreateAttempts,
ntfChecked,
ntfCheckAttempts,
ntfDeleted,
ntfDelAttempts
}
getAgentNtfServerStats :: AgentNtfServerStats -> IO AgentNtfServerStatsData
getAgentNtfServerStats s = do
_ntfCreated <- readTVarIO $ ntfCreated s
_ntfCreateAttempts <- readTVarIO $ ntfCreateAttempts s
_ntfChecked <- readTVarIO $ ntfChecked s
_ntfCheckAttempts <- readTVarIO $ ntfCheckAttempts s
_ntfDeleted <- readTVarIO $ ntfDeleted s
_ntfDelAttempts <- readTVarIO $ ntfDelAttempts s
pure
AgentNtfServerStatsData
{ _ntfCreated,
_ntfCreateAttempts,
_ntfChecked,
_ntfCheckAttempts,
_ntfDeleted,
_ntfDelAttempts
}
addNtfStatsData :: AgentNtfServerStatsData -> AgentNtfServerStatsData -> AgentNtfServerStatsData
addNtfStatsData sd1 sd2 =
AgentNtfServerStatsData
{ _ntfCreated = _ntfCreated sd1 + _ntfCreated sd2,
_ntfCreateAttempts = _ntfCreateAttempts sd1 + _ntfCreateAttempts sd2,
_ntfChecked = _ntfChecked sd1 + _ntfChecked sd2,
_ntfCheckAttempts = _ntfCheckAttempts sd1 + _ntfCheckAttempts sd2,
_ntfDeleted = _ntfDeleted sd1 + _ntfDeleted sd2,
_ntfDelAttempts = _ntfDelAttempts sd1 + _ntfDelAttempts sd2
}
-- Type for gathering both smp and xftp stats across all users and servers,
-- to then be persisted to db as a single json.
data AgentPersistedServerStats = AgentPersistedServerStats
{ smpServersStats :: Map (UserId, SMPServer) AgentSMPServerStatsData,
xftpServersStats :: Map (UserId, XFTPServer) AgentXFTPServerStatsData
xftpServersStats :: Map (UserId, XFTPServer) AgentXFTPServerStatsData,
ntfServersStats :: OptionalMap (UserId, NtfServer) AgentNtfServerStatsData
}
deriving (Show)
instance FromJSON OptionalInt where
parseJSON v = OInt <$> parseJSON v
omittedField = Just (OInt 0)
newtype OptionalMap k v = OptionalMap (Map k v)
deriving (Show, ToJSON)
instance (FromJSONKey k, Ord k, FromJSON v) => FromJSON (OptionalMap k v) where
parseJSON v = OptionalMap <$> parseJSON v
omittedField = Just (OptionalMap M.empty)
$(J.deriveJSON defaultJSON ''AgentSMPServerStatsData)
$(J.deriveJSON defaultJSON ''AgentXFTPServerStatsData)
$(J.deriveJSON defaultJSON ''AgentNtfServerStatsData)
$(J.deriveJSON defaultJSON ''AgentPersistedServerStats)
instance ToField AgentPersistedServerStats where
+55 -7
View File
@@ -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
+18
View File
@@ -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,128 @@
{-# 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)
import UnliftIO.MVar
-- | 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
dbSem <- newMVar ()
dbPool <- newTBQueueIO poolSize
dbClosed <- newTVarIO True
let st = DBStore {dbConnstr = connstr, dbSchema = schema, dbPoolSize = fromIntegral poolSize, dbPool, dbSem, dbNew = False, dbClosed}
dbNew <- connectPool 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).
connectPool :: DBStore -> Bool -> IO Bool
connectPool DBStore {dbConnstr, dbSchema, dbPoolSize, dbPool, dbClosed} createSchema = uninterruptibleMask_ $ do
(conn, dbNew) <- connectDB dbConnstr dbSchema createSchema -- TODO [postgres] analogue for dbBusyLoop?
conns <- replicateM (dbPoolSize - 1) $ fst <$> connectDB dbConnstr dbSchema False
mapM_ (atomically . writeTBQueue dbPool) (conn : conns)
atomically $ writeTVar dbClosed False
pure dbNew
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 {dbPool, dbPoolSize, dbClosed} =
ifM (readTVarIO dbClosed) (putStrLn "closeDBStore: already closed") $ uninterruptibleMask_ $ do
replicateM_ dbPoolSize $ atomically (readTBQueue dbPool) >>= DB.close
atomically $ writeTVar dbClosed True
reopenDBStore :: DBStore -> IO ()
reopenDBStore st =
ifM
(readTVarIO $ dbClosed st)
(void $ connectPool 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,64 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TupleSections #-}
module Simplex.Messaging.Agent.Store.Postgres.Common
( DBStore (..),
DBOpts (..),
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 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,
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
}
withConnectionPriority :: DBStore -> Bool -> (PSQL.Connection -> IO a) -> IO a
withConnectionPriority DBStore {dbPool, dbSem} _priority =
bracket
(withMVar dbSem $ \_ -> atomically $ readTBQueue dbPool)
(atomically . writeTBQueue dbPool)
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
@@ -1,27 +1,29 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Simplex.Messaging.Agent.Store.SQLite.Common
( SQLiteStore (..),
( DBStore (..),
DBOpts (..),
withConnection,
withConnection',
withTransaction,
withTransaction',
withTransactionCtx,
withTransactionPriority,
dbBusyLoop,
storeKey,
)
where
import Control.Concurrent (threadDelay)
import Control.Concurrent.STM (retry)
import Data.ByteArray (ScrubbedBytes)
import qualified Data.ByteArray as BA
import Data.Time.Clock (diffUTCTime, getCurrentTime)
import Database.SQLite.Simple (SQLError)
import qualified Database.SQLite.Simple as SQL
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import Simplex.Messaging.Util (diffToMilliseconds)
import Simplex.Messaging.Util (ifM, unlessM)
import qualified UnliftIO.Exception as E
import UnliftIO.MVar
import UnliftIO.STM
@@ -29,38 +31,51 @@ 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,
dbConnection :: MVar DB.Connection,
dbClosed :: TVar Bool,
dbNew :: Bool
}
withConnection :: SQLiteStore -> (DB.Connection -> IO a) -> IO a
withConnection SQLiteStore {dbConnection} = withMVar dbConnection
data DBOpts = DBOpts
{ dbFilePath :: FilePath,
dbKey :: ScrubbedBytes,
keepKey :: Bool,
vacuum :: Bool,
track :: DB.TrackQueries
}
withConnection' :: SQLiteStore -> (SQL.Connection -> IO a) -> IO a
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
lowPriority = wait >> withMVar dbConnection (\db -> ifM free (Just <$> action db) (pure Nothing)) >>= maybe lowPriority pure
signal = atomically $ modifyTVar' dbSem (+ 1)
release = atomically $ modifyTVar' dbSem $ \sem -> if sem > 0 then sem - 1 else 0
wait = unlessM free $ atomically $ unlessM ((0 ==) <$> readTVar dbSem) retry
free = (0 ==) <$> readTVarIO dbSem
withConnection :: DBStore -> (DB.Connection -> IO a) -> IO a
withConnection st = withConnectionPriority st False
withConnection' :: DBStore -> (SQL.Connection -> IO a) -> IO a
withConnection' st action = withConnection st $ action . DB.conn
withTransaction :: SQLiteStore -> (DB.Connection -> IO a) -> IO a
withTransaction = withTransactionCtx Nothing
withTransaction' :: SQLiteStore -> (SQL.Connection -> IO a) -> IO a
withTransaction' :: DBStore -> (SQL.Connection -> IO a) -> IO a
withTransaction' st action = withTransaction st $ action . DB.conn
withTransactionCtx :: Maybe String -> SQLiteStore -> (DB.Connection -> IO a) -> IO a
withTransactionCtx ctx_ st action = withConnection st $ dbBusyLoop . transactionWithCtx
withTransaction :: DBStore -> (DB.Connection -> IO a) -> IO a
withTransaction st = withTransactionPriority st False
{-# INLINE withTransaction #-}
withTransactionPriority :: DBStore -> Bool -> (DB.Connection -> IO a) -> IO a
withTransactionPriority st priority action = withConnectionPriority st priority $ dbBusyLoop . transaction
where
transactionWithCtx db@DB.Connection {conn} = case ctx_ of
Nothing -> SQL.withImmediateTransaction conn $ action db
Just ctx -> do
t1 <- getCurrentTime
r <- SQL.withImmediateTransaction conn $ action db
t2 <- getCurrentTime
putStrLn $ "withTransactionCtx start :: " <> show t1 <> " :: " <> ctx
putStrLn $ "withTransactionCtx end :: " <> show t2 <> " :: " <> ctx <> " :: duration=" <> show (diffToMilliseconds $ diffUTCTime t2 t1)
pure r
transaction db@DB.Connection {conn} = SQL.withImmediateTransaction conn $ action db
dbBusyLoop :: forall a. IO a -> IO a
dbBusyLoop action = loop 500 3000000
+86 -33
View File
@@ -1,101 +1,154 @@
{-# 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.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)
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,
timeAvg :: Int64
timeAvg :: Int64,
errs :: Map Text Int
}
deriving (Show)
timeIt :: TMap Query SlowQueryStats -> Query -> IO a -> IO a
timeIt slow sql a = do
t <- getCurrentTime
r <- a
t' <- getCurrentTime
let diff = diffToMilliseconds $ diffUTCTime t' t
atomically $ when (diff > 5) $ 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 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
updateQueryStats diff (Just SlowQueryStats {count, timeMax, timeAvg}) =
updateQueryStats diff Nothing = Just $ SlowQueryStats 1 diff diff M.empty
updateQueryStats diff (Just SlowQueryStats {count, timeMax, timeAvg, errs}) =
Just $
SlowQueryStats
{ count = count + 1,
timeMax = max timeMax diff,
timeAvg = (timeAvg * count + diff) `div` (count + 1)
timeAvg = (timeAvg * count + diff) `div` (count + 1),
errs
}
open :: String -> IO Connection
open f = do
open :: String -> TrackQueries -> IO Connection
open f track = do
conn <- SQL.open f
slow <- atomically $ TM.empty
pure Connection {conn, slow}
slow <- TM.emptyIO
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,138 +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 as M
import Data.Maybe (isNothing, mapMaybe)
import qualified Data.Map.Strict as M
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.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)
]
-- | 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
@@ -144,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 (
@@ -174,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)

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