Compare commits

...
Author SHA1 Message Date
Alexander Bondarenko 00d38f013d catch IOErrors in getRelay 2024-04-22 20:10:06 +03:00
Alexander Bondarenko d084a32f2b mark proxy timeouts as temporary 2024-04-22 20:09:08 +03:00
Alexander Bondarenko 0eee9eb65b revert String encoding, discard invalid errors in QC 2024-04-19 13:06:32 +03:00
Alexander Bondarenko 47c1b93dc9 fix encoding 2024-04-18 23:35:21 +01:00
Evgeny Poberezkin daece6b736 use substring in PROXY UNEXPECTED error 2024-04-18 23:35:21 +01:00
Alexander Bondarenko 4d7012a5bb put smp errors into proxy wrappers 2024-04-18 23:35:21 +01:00
Evgeny Poberezkin 7712070cb3 Merge branch 'master' into proxy 2024-04-18 23:33:06 +01:00
Evgeny Poberezkin c00c223f3b remove (or make optional) client key from handshakes (#1104)
* remove (or make optional) client key from handshakes

* remove comment
2024-04-18 22:43:49 +01:00
a3b229f668 SMP proxy: low level client and server implementation (#1096)
* SMP proxy: low level client and server implementation

* SMP proxy: server implementation (#1098)

* wip

* PRXY command

* progress

* SMP Proxy: client-level implementation (#1101)

* buildable

* encode messages

* update pkey

* fix queue types

* wrap SEND in proxy lookup

* WIP proxy client

* WIP

* post-rebase fixes

* encode something with something

* cleanup

* update

* fix nonce/corrId in batchingTests

* WIP: dig into createSMPProxySession

* agent

* test progress

* pass the test

* parameterize transport handle with transport peer to include server certificate (#1100)

* parameterize transport handle with transport peer to include server certificate

* include server certificate into THandle

* load server chain and sign key

* fix key type

* fix for 8.10

---------

Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com>
Co-authored-by: IC Rainbow <aenor.realm@gmail.com>

* cleanup

* add 2-server test

* remove subsumed test

* checkCredentials for BrokerMsg

* skip batching tests

* remove userId param

* remove agent changes

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>

---------

Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com>

* remove unused type

* icrease test timeout

* reduce transport block

* envelope sizes

* don't fork unless have proxied commands to process

---------

Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com>
Co-authored-by: IC Rainbow <aenor.realm@gmail.com>
2024-04-18 22:35:17 +01:00
Evgeny Poberezkin 8d7e0b1d09 Merge branch 'master' into proxy 2024-04-17 17:46:39 +01:00
2f43b43225 parameterize transport handle with transport peer to include server certificate (#1100)
* parameterize transport handle with transport peer to include server certificate

* include server certificate into THandle

* load server chain and sign key

* fix key type

* fix for 8.10

---------

Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com>
Co-authored-by: IC Rainbow <aenor.realm@gmail.com>
2024-04-17 17:46:22 +01:00
ad4b5b6b71 parameterize transport handle with transport peer to include server certificate (#1100)
* parameterize transport handle with transport peer to include server certificate

* include server certificate into THandle

* load server chain and sign key

* fix key type

* fix for 8.10

---------

Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com>
Co-authored-by: IC Rainbow <aenor.realm@gmail.com>
2024-04-15 13:47:48 +01:00
Evgeny Poberezkin 98eb2742bc Merge branch 'master' into proxy 2024-04-13 18:51:17 +01:00
Evgeny Poberezkin ebb75ced12 extract SessionVar from AgentClient to reuse (#1099) 2024-04-13 18:33:12 +01:00
Evgeny Poberezkin 5e783396e0 5.6.2.2: v5.6.2 2024-04-12 23:14:20 +01:00
Evgeny Poberezkin b741b65700 use online status in network information (#1094)
* use online status in network information

* test

* refactor

* flip condition

* fix
2024-04-12 22:22:24 +01:00
Alexander BondarenkoandEvgeny Poberezkin 875ddd80d6 SMP proxy: protocol (#954)
* WIP: proxy-related types

* test plan

* buildable with stubs

* add auth test

* update protocol

* fix

* update rfc

* update protocol/types

* disable test

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-04-12 18:04:29 +01:00
Alexander BondarenkoandEvgeny Poberezkin 036b7523a5 xftp: matrix test for new versions (#1093)
* xftp: matrix test for new versions

* manual matrix

* more, faster

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-04-12 13:28:16 +01:00
spaced4ndyandEvgeny Poberezkin 3b1dd6dff9 agent: notify about available quota (#1087)
* agent: notify about available quota

* rename

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-04-11 09:38:12 +01:00
Evgeny Poberezkin 2e44ec5083 fix message expiration test (#1092) 2024-04-11 08:52:15 +01:00
Evgeny Poberezkin fbc25b171e xftp: reply ERR AUTH when file is removed from server storage (#1091) 2024-04-10 23:02:48 +01:00
Evgeny Poberezkinandspaced4ndy 8fa9ed6317 wait for user network availability (#1085)
* ghc-options

* wait for user network availability

* test

* update

* comment

* refactor

* slow config

* line

* waitForUserNetwork in xftp and ntf workers

* refactor

* refactor with registerDelay

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-04-10 21:50:05 +01:00
Alexander Bondarenko 91cf6841e0 http2: fix client setup (#1090)
* http2: cancel client action on setup timeout

* ignore incompatible server keys
2024-04-10 21:00:39 +01:00
Evgeny PoberezkinandAlexander Bondarenko 1219446996 dispose agent instances, fix tests, (#1089)
* dispose agent instances in tests

* fix quota test

* tests: fix tests with -threaded (#1088)

* fix some tests

* match RTS opts with apps

* less verbose rts stats

* enable sqlite extended error codes

* clean up

* unfocus

* remove extendedErrorCode

It's actually setExtendedResultCodes, which isn't yet available.

* diff

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>

* fix switch test

* fix

---------

Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com>
2024-04-10 19:34:02 +01:00
Evgeny Poberezkin b994fd9f0f use sendRequest for HTTP2 2024-04-09 21:14:10 +01:00
Alexander BondarenkoandEvgeny Poberezkin 4c20ff6d00 xftp: negotiate protocol with ALPN (#1047)
* xftp: negotiate protocol with ALPN

* add RFC

* add handshake implementation

* implement extended handshake

* enable authentication

* update rfc

* Apply suggestions from code review

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>

* cleanup

* discard trailing data

* cleanup diff

* use find

* rename

* refactor

* add x509 tests

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-04-09 15:03:40 +01:00
spaced4ndyandEvgeny Poberezkin 791368c7be export lock function (#1073)
* export lock functions

* rename with tick

* simplify

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-04-04 15:37:31 +01:00
Alexander Bondarenko a4cfcfcc85 ntf-server: fix test token handling (#1083)
* use formatted logger

* fix DeviceToken parser for apns_null

* exclude PPNull from token stats
2024-04-04 14:46:40 +01:00
Evgeny Poberezkin 6bc4f6c94e 5.6.2.1 2024-04-03 10:04:41 +01:00
Evgeny Poberezkin 84b8c8417b revert base64 change (#1081) 2024-04-03 09:27:36 +01:00
Alexander Bondarenko be9a84905f servers: read stores by chunks (#1080) 2024-04-01 19:42:30 +01:00
Evgeny Poberezkin bfd532e833 5.6.2.0 2024-04-01 11:33:22 +01:00
Alexander BondarenkoandEvgeny Poberezkin a698f9a0c4 xftp-server: run expiration checks on start (#1042)
* xftp-server: run expiration checks on start

* rename

* refactor

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-04-01 11:29:19 +01:00
39bb804fab xftp: fix repeated replica creation if it was in uploaded status (#1079)
* test with failing files (in progress)

* print

* add replica uploading state

* Revert "add replica uploading state"

This reverts commit 7068213aa6.

* <=

* fix

* prints

* test no redundancy

* all tests no redundancy

* revert delay

* refactor

---------

Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-04-01 13:37:35 +04:00
Evgeny PoberezkinandAlexander Bondarenko 6ded721daa remove monad typeclasses to reduce overhead (#1077)
* remove monad typeclasses to reduce overhead

* remove unliftIO

* StrictData

* inline

* optional agent port

* avoid MonadUnliftIO instance (#1078)

* avoid MonadUnliftIO instance

* simpler liftError'

* rename

* narrow down instance

* revert

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>

* logServer

---------

Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com>
2024-03-31 20:50:35 +01:00
Evgeny Poberezkin 6c48092f5e 5.6.1.0 2024-03-28 18:23:19 +00:00
Evgeny Poberezkin 44410535fd do not pass key to control port of xftp server (#1074) 2024-03-28 18:16:36 +00:00
Alexander BondarenkoandEvgeny Poberezkin bbc9eccf4d xftp: prevent overwriting completed upload (#1063)
* xftp: prevent overwriting completed upload

* add size check for skipCommitted

* fix import

* fail on incorrect size

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-03-28 18:12:48 +00:00
Alexander BondarenkoandEvgeny Poberezkin ee90ea6a69 replace base64-bytestring with base64 (#1065)
* replace base64-bytestring with base64

* minify

* use bytestring-0.10 compatible fork

PR pending...

* bump base64 fork with text compat

* move compat details to modules

* switch repo

* add back module

* cleanup

* minify

* clean imports

* rename

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-03-27 23:35:09 +00:00
Alexander BondarenkoandEvgeny Poberezkin 5e0123313c log control port commands, authentication for control port (#1071)
* log control port commands

* auth

* add auth to xftp, config and commands

* log missing auth

* put smp save under auth

* corrections

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-03-26 18:04:28 +00:00
Evgeny Poberezkin 2712fbc711 add key to XFTP server control port command (#1070)
* add key to XFTP server control port command

* name
2024-03-26 09:38:28 +00:00
Evgeny Poberezkin dbaef5a0f8 fix SMP server msgCount stat (#1069) 2024-03-25 16:26:40 +00:00
Alexander BondarenkoandEvgeny Poberezkin ff1d7f2fac ntf-server: break processing transaction (#1067)
* ntf-server: break processing transaction

* make resubscribe sync

* diff

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-03-25 15:35:33 +00:00
Evgeny Poberezkin c5a5edadf5 rfc: relay metadata and network decentralization (#1056) 2024-03-24 08:51:35 +00:00
Evgeny Poberezkin 8496884b42 5.6.0.4 2024-03-21 15:18:11 +00:00
Evgeny Poberezkin b32259d048 notify about critical error on agent crash (#1062)
* notify about critical error on agent crash

* waitUntilActive

* disposeAgent

* fix
2024-03-21 14:54:57 +00:00
Alexander Bondarenko a48c22ea36 core: fix leak in serverDown handler (#1061) 2024-03-21 12:03:19 +00:00
Alexander Bondarenko 9eacae5492 xftp: remove duplicate check in receiveServerFile (#1057) 2024-03-20 15:50:38 +00:00
Evgeny Poberezkin 7b6b80cea2 v5.6.0-beta.3 2024-03-19 15:00:42 +00:00
Alexander Bondarenko d68ff7e22f xftp-server: set initial usedStorage from actual file records (#1045) 2024-03-19 13:53:02 +00:00
Evgeny Poberezkin 46e49fa823 xftp: return OK on duplicate upload attempt 2024-03-19 13:52:07 +00:00
Alexander BondarenkoandEvgeny Poberezkin db3bddecca xftp-server: fix receiveServerFile (#1048)
* xftp-server: fix receiveServerFile

* refactor

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-03-19 13:34:21 +00:00
Alexander Bondarenko 097cec1c35 utils: add stateless compress1 (#1053) 2024-03-19 12:13:42 +00:00
Alexander Bondarenko ca68eca86e agent: fix leak in getChunkDigest (#1051) 2024-03-15 12:18:15 +04:00
Alexander Bondarenkoandspaced4ndy ace09cc07d xftp: force single chunk for redirect descriptions (#1050)
* xftp: force single chunk for redirect descriptions

* Update src/Simplex/FileTransfer/Client/Main.hs

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

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-03-15 12:08:52 +04:00
spaced4ndy 293a2ca3f1 agent: remove withStoreCtx (#1044) 2024-03-13 13:33:59 +04:00
spaced4ndy e93ea6df71 xftp: fix sending large files (#1043) 2024-03-13 13:33:43 +04:00
Evgeny Poberezkin 0aa4ae7228 v5.6.0-beta.2 2024-03-12 14:31:30 +00:00
Alexander BondarenkoandEvgeny Poberezkin 2cad0cb201 core: check ACK handling with return type (#1041)
* core: check ACK handling with return type

* fix ratchet sync

* add SQL Locked to dbBusyLoop

* rename

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-03-11 19:06:53 +00:00
Evgeny Poberezkin 78eb4f764f v5.6.0-beta.1 2024-03-10 19:41:06 +00:00
Evgeny Poberezkin 819ae7040d Merge pull request #1024 from simplex-chat/pq
agent: quantum-resistant double ratchet encryption
2024-03-10 19:38:44 +00:00
Evgeny Poberezkin b4c90781bb pqdr: update envelope sizes 2024-03-10 17:53:57 +00:00
Evgeny Poberezkin 851ed2d02e pqdr: more tests, pass e2e version to rcEncrypt from config (#1040)
* pqdr: more tests, pass e2e version to rcEncrypt from config

* fix
2024-03-10 13:29:03 +00:00
Evgeny Poberezkin 054b6edb14 pqdr: clean up (#1039) 2024-03-10 11:27:31 +00:00
Evgeny Poberezkin dab55e0a9b pqdr: return agent version from connection request version & PQ support check api 2024-03-08 23:13:21 +00:00
Alexander Bondarenko 8cdd49b912 core: restore Eq instances (#1038)
* core: restore Eq instances

* remove duplicates from tests
2024-03-08 13:43:33 +00:00
Alexander Bondarenko b4e55146b8 core: fix VersionRange JSON instances (#1037) 2024-03-08 10:58:45 +00:00
Evgeny Poberezkin 03d73f442f JSON instance for VersionRange (#1036) 2024-03-08 10:01:58 +00:00
Evgeny Poberezkin 8ff4c628b5 pqdr: make envelope sizes dependent on version, test enabling PQ (#1035) 2024-03-08 08:28:15 +00:00
spaced4ndy 5e23fa6cfc agent pq: connRequestPQSupport api (#1034) 2024-03-07 19:44:48 +04:00
Alexander BondarenkoandEvgeny Poberezkin dd22b5c823 core: tweak compress api (#1029)
* convert compress to a wrapper with passthrough fallback

* add compress1 for non-batched compression

* use original size as upper bound for scratch

* refactor

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-03-07 14:17:32 +00:00
Evgeny Poberezkin 11288866f9 pqdr: refactor 2024-03-07 12:41:10 +00:00
Evgeny Poberezkin 07fa75ec49 pqdr: agent api to confirm PQ encryption support during connection handshake, fix incorrect PQ support (#1032)
* pqdr: agent api to confirm PQ encryption support during connection handshake

* fix CONF, tests

* fix REQ, tests

* remove unused
2024-03-07 08:35:40 +00:00
Evgeny Poberezkin 4ffb6a348a pqdr: use different newtypes for supporting and enabling PQ encryption in connections (#1031)
* pqdr: use different newtypes for supporting and enabling PQ encryption in connections

* rename field, fix test

* refactor
2024-03-06 21:28:03 +00:00
Evgeny Poberezkin b435a4dacb envelope sizes dependent on PQ encryption (#1028)
* envelope sizes dependent on PQ encryption (WIP)

* add "supported" flag to ratchets, update this flag on ratchet resync

* change connection PQ status on sendMessage

* comment, fix

* refactor
2024-03-06 16:38:30 +00:00
Alexander BondarenkoandEvgeny Poberezkin e04705d9c5 utils: add generic batching and compression (#1018)
* extract batchTransmissions_

* add Simplex.Messaging.Compression

* add combined compression/batching

* force NonEmpty for batching

* hide FFI and allocation related IO

* split packing

* remove batch compression, tweak API

* OCD over API

* remove Empty, extract passthrough const

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-03-06 09:10:49 +00:00
Evgeny Poberezkin b050cf5027 double ratchet versioning for post-quantum encryption (#1025)
* correctly parse new Ratchet fields when omitted

* rfc: migrating connection versions to pqdr

* update rfc

* WIP (dont commit)

* rename versions

* update ratchet version based on PQ encryption feature flag

* remove duplicate function

* synchronize ratchet, fix tests, refactor

* comments

* test

* pattern
2024-03-05 17:07:15 +00:00
Evgeny Poberezkin 52a67daea6 agent: pass PQ encryption flag separately for each message in batch APIs (#1027) 2024-03-05 11:09:07 +00:00
Evgeny Poberezkin ba1bfaa5aa Merge branch 'master' into pq 2024-03-04 20:13:01 +00:00
Evgeny Poberezkin dd2bd11584 parameterize version scopes with phantom types (#1026)
* parameterize version scopes with phantom types

* move Version to another module

* parens
2024-03-04 19:06:51 +00:00
Evgeny Poberezkinandspaced4ndy e06e22328f agent: quantum-resistant double ratchet encryption (#939)
* doc

* diff

* ratchet header

* types

* ratchet step with PQ KEM, message header with KEM

* comment

* update types, remove Eq instances, store KEM keys to database

* pqx3dh

* PQ double ratchet test

* pqdr tests pass

* fix most tests

* refactor

* allow KEM proposals from both sides

* test names

* agent API parameters to use PQ KEM

* initialize ratchet state for enabling KEM

* fix/test KEM state machine to support disabling/enabling via messages

* more tests

* diff

* diff2

* refactor

* refactor

* refactor

* refactor

* remove Maybe

* rename

* add PQ encryption status to CON, MID and MSG events and sendMessage API results

* different PQ parameter when creating connection

* rename/reorganize types for PQ encryption modes

* rename

* fix testWaitDeliveryTimeout

* rename

* rename2

* ghc8107

* rename

* increase timeouts for concurrent send/receive test

* enable all tests

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-03-03 19:40:49 +00:00
spaced4ndy 30fd4065d9 rename delete waiting delivery tests (#1022) 2024-03-03 12:56:54 +04:00
Alexander Bondarenko 246a0d10c2 xftp: raise internal upload limit to 5gb (#1020)
* xftp: raise internal upload limit to 5gb

* extract hard limit from agent
2024-03-02 18:46:05 +00:00
Evgeny Poberezkin ce78646c7f refactor creating connection record (#1021) 2024-03-02 18:27:51 +00:00
spaced4ndyandEvgeny Poberezkin 294d7ec8dd agent: delay connection deletion to finish delivery of pending messages (#1015)
* agent: delay connection deletion to finish delivery of pending messages (wip)

* fixes, test

* notify, test

* add tests

* comment

* add test

* timeout

* test timeout

* up

* more tests

* rename

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-02-29 18:08:58 +00:00
Evgeny Poberezkin c9ec7ea274 Merge branch 'stable' 2024-02-27 18:07:30 +00:00
Evgeny Poberezkin 5011ac2e7c Merge branch 'v5522' into stable 2024-02-27 17:58:08 +00:00
Evgeny Poberezkin 0987895926 5.5.2.2 2024-02-27 00:00:29 +00:00
2e3c961dab agent: optimize execution performance of slow memory operations during (re)subscriptions (#1013)
* agent: remove redundant set building

* tests: add tRcvQueuesTests (#1016)

* double accounting

* unit tests for invariants

* batch add queues

* ghc8107 compatibility

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-02-27 00:00:29 +00:00
spaced4ndyandEvgeny Poberezkin 050a921fbb agent: batch db operations for deleting connections and xftp files (#1009)
* agent: batch db operations for deleting connections

* batch delete rcv files

* snd files

* refactor

* refactor2

* lines

* refactor

* fix prefix path

* refactor

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-02-26 10:28:50 +00:00
Evgeny Poberezkin e2ec737c68 stop using public keys from table (#1014)
* stop using public keys from table

* comment
2024-02-25 16:19:04 +00:00
Alexander Bondarenko 95c0914809 agent: speed up SMP clientDisconnected (#1011) 2024-02-24 14:59:14 +00:00
Alexander BondarenkoandEvgeny Poberezkin 385463192b agent: extend worker summary (#1007)
* agent: extend worker summary with active/idle split

* simplify

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-02-24 13:29:36 +00:00
Evgeny Poberezkin d7394aa762 fix ntf server version range in the client 2024-02-22 19:33:14 +00:00
Evgeny Poberezkin 0d843ea4ce Merge branch 'stable' 2024-02-21 20:33:06 +00:00
Evgeny Poberezkin 04cd2aa4ad Merge pull request #1006 from simplex-chat/v5521
5.5.2.1
2024-02-21 20:07:34 +00:00
Evgeny Poberezkin a34da38966 Merge branch 'stable' into v5521 2024-02-21 19:28:30 +00:00
Evgeny Poberezkin 32c94df040 5.5.2.1 2024-02-21 14:13:07 +00:00
Evgeny Poberezkin 5c1519df7f optimize: use IntMap (#1005) 2024-02-21 11:52:24 +00:00
Evgeny PoberezkinandAlexander Bondarenko 03c24c3ada agent: fix performance leak, smp server: optimize and fix potential memory leak (#1004)
* fix agent performance leak when re-connecting clients, optimize SMP server

* let the clients remove themselves from their clientvars

* fix another test

* do not call removeSubs when AgentClient is not active

* revert some changes

* revert more, refactor

* comment

* rename

* refactor

* refactor

---------

Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com>
2024-02-21 11:45:34 +00:00
Evgeny Poberezkin 12dac60397 v5.6.0-beta.0 2024-02-18 16:31:48 +00:00
Evgeny Poberezkin 9b38f69e7e smp server: add stats for queues deleted before and after they were secured (#1002) 2024-02-18 16:25:32 +00:00
Evgeny Poberezkin 155831ae36 tests: test notification decryption 2024-02-18 09:30:21 +00:00
Evgeny Poberezkin 89140e0ff0 use package version for all servers (#1001) 2024-02-18 08:56:35 +00:00
Evgeny Poberezkin caeeb2df9c fix protocol in NTF server error 2024-02-17 15:32:44 +00:00
Evgeny Poberezkin 51be2fea20 agent: test NTF server (#1000) 2024-02-17 14:55:40 +00:00
Evgeny Poberezkin f6acc5c240 remove test chunk after XFTP server test (#999) 2024-02-16 23:24:57 +00:00
Evgeny Poberezkin abf956d425 fix tests for new protocol versions (#998)
* fix basic auth tests

* fix batching and syntax tests

* fix test matrix
2024-02-16 20:21:15 +00:00
Alexander Bondarenko 194a7bb58e agent: remove redirect entries when deleting rcv files (#995)
* agent: remove redirect entries when deleting rcv files

* update schema dump

* use rcv_file primary key for redirect lookup

* allow multiple redirects
2024-02-16 19:49:50 +04:00
Evgeny Poberezkin c179073260 remove support for old versions (#990)
* remove support for old versions (WIP)

* fix

* updates

* use version var
2024-02-16 13:28:50 +00:00
416f1b1721 smp: command authorization (#982)
* smp: command authorization

* fix encoding, most tests

* remove old tests

* authorize via crypto_box

* extract authenticator to Crypto module

* make TransmissionAuth Maybe

* rfc

* support authenticators in NTF protocol, test matrix (no backwards compatibility yet from new clients to old servers)

* fix/add tests, add version config to "small" agent

* separate client and server versions for SMP protocol

* test batching SMP v7

* do not send session ID in each transmission

* refactor auth verification in the server, split tests

* server "warm up" fixes timing test

* uncomment SUB timing test

* comments, disable two timing tests

* rename version

* increase auth timing test failure threshold

* use different algorithms to authorize snd/rcv commands, use random correlation ID

* transport: fetch and store server certificate (#985)

* THandleParams (WIP, does not compile)

* transport: fetch and store server certificate

* smp: add getOnlinePubKey example to smpClientHandshake

* add server certs and sign authPub

* cleanup

* update

* style

* load server certs from test fixtures

* sign ntf authPubKey

* fix onServerCertificate

* increase delay before sending messages

* require certificate with key in SMP server handshake

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>

* remove dhSecret from THandle

* remove v8, merge all changes to one version

* parameterize THandle

* rfc: transmission ecnryption

* Revert "parameterize THandle"

This reverts commit 75adfc94fb.

* use batch syntax for ntf server commands

* separate encodeTransmission when there is no key

* typo

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

* rename

* diff

---------

Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-02-16 11:45:54 +00:00
Evgeny Poberezkin 9254d8dac5 v5.5.3 2024-02-16 11:33:56 +00:00
Alexander BondarenkoandEvgeny Poberezkin 9ab34bca7d cli: add cert command to xftp and ntf servers (#991)
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-02-16 10:56:54 +00:00
spaced4ndy 6f62d7ff05 agent: add numRecipients parameter to send description (#993) 2024-02-15 13:24:46 +04:00
Alexander Bondarenko 7275714b8e cli: configure server paths from env (#992) 2024-02-14 20:31:06 +00:00
spaced4ndy 004597c764 agent: add index to file redirect migration (#988) 2024-02-14 18:47:39 +04:00
Alexander BondarenkoandEvgeny Poberezkin 2f7a288280 xftp: add sending and receiving via URI-encoded redirects (#968)
* xftp: add URI encoding for FileDescription

* tweak URI

* allow smaller blocks

* draft xftpReceiveFileFollow' and xftpSendFilePublic'

* add sending with redirect

* allow 64k chunks

* add migrations with redirect fields

* add test case

* fix deadlock

* revert CLI code

* WIP: working send/receive via URI

* fix field ambiguity

* cleanup

* update agent db schema

* update minimal chunk size

* add rfc

* apply suggestions from code review

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>

* add createRcvFileRedirect

* extract Simplex.Messaging.ServiceScheme and reuse for files

* update db schema

* check size/digest on receive complete

* cleanup

* use SIZE/DIGEST errors for redirects too

* split digest/size errors from redirect checks

* fix redirect error encoding

* rename RedirectMeta to RedirectFileInfo

* use query encoding for file URI

* group maybe fields under RcvFileRedirect

* add extras field

* update rfc

* add extras encoding and no-redirect tests

* fix toStrict for old ghc

* extra client data in file descr URI

* remove decoded yaml file

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-02-13 14:08:49 +00:00
Alexander BondarenkoandEvgeny Poberezkin 57e7c8ef6b smp-server: add cert CLI command to rotate online certificate (#984)
* smp-server: add gen-online CLI command

* use CN and algo from old certificate

* add cert checks to test

* rename command

* fix test

* cert

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-02-12 20:17:08 +00:00
spaced4ndy 8de23c15ad agent: return ntf server in getNtfToken (#986) 2024-02-12 16:51:37 +04:00
Evgeny Poberezkin e64b6cba4b 5.5.2.0 2024-02-10 22:07:02 +00:00
Evgeny Poberezkin 189885c50d protocol: ignore tail bytes in handshake blocks to allow future extension (#983)
* protocol: ignore tail bytes in handshake blocks to allow future extension

* cleanup
2024-02-05 21:01:20 +00:00
Evgeny Poberezkin a516c2f72c 5.5.1.2 2024-02-02 08:19:02 +00:00
Evgeny Poberezkin 2ae1c9f79d agent: only retry connecting SMP clients when there are pending subscriptions (#981)
* agent: only retry connecting SMP clients when there are pending subscriptions

* fix

* remove retry on creating clients

* simplify
2024-02-01 16:17:37 +00:00
Alexander BondarenkoandEvgeny Poberezkin 24b84106a6 core: add activity tests to workers details (#980)
* core: add activity tests to workers details

* remove names

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-02-01 11:02:31 +00:00
Alexander BondarenkoandEvgeny Poberezkin 15bc027f23 core: fix reconnection bugs (#979)
* core: fix reconnection bugs

* untangle newProtocolClient

* refactor

* report busy clientVar error

* log error

* comments

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-02-01 10:55:50 +00:00
Alexander Bondarenko cb64dabf75 tests: fix sporadic failures from reading stats (#975) 2024-01-30 11:36:07 +00:00
Evgeny Poberezkin 7a0cd8041b 5.5.1.1 2024-01-22 14:06:52 +00:00
Evgeny Poberezkinandspaced4ndy fd4eeb36db agent: optimize expired messages query (#976)
* agent: optimize expired messages query

* schema

* fix query

* fix

* typo

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

* fix

* refactor

* comment

* refactor2

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-01-22 14:04:57 +00:00
Evgeny Poberezkin 1e49f1c92d 5.5.1.0 2024-01-19 19:47:39 +00:00
f7cdec2f08 fix: support multiple notification servers in configuration (#971)
* tests: add trpl-keeps-server check

* add smp server switch check

* add connection test and fix withNtfServer

* Update src/Simplex/Messaging/Agent/NtfSubSupervisor.hs

Co-authored-by: Evgeny Poberezkin <e.poberezkin@me.com>

* use ntfServer from token

* rename

---------

Co-authored-by: Evgeny Poberezkin <e.poberezkin@me.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-01-19 19:42:28 +00:00
Evgeny Poberezkin 8ff89c19dc servers: do not keep stats file open during delays (#974) 2024-01-19 17:47:47 +00:00
Evgeny Poberezkin baf2c47065 agent: expire messages failed after quota exceeded after 7 days, and expire multiple messages at once (#973)
* agent: expire messages failed after quota exceeded after 7 days, and expire multiple messages at once

* fix, test

* refactor

* catch in loop
2024-01-19 17:21:25 +00:00
Alexander Bondarenko 40fa34c2d5 tests: test multiple Ntf servers (#966)
* tests: add ntf case with multiple Ntf servers

* simplify test

* fix for master

* add server switch test

* add server switch test

* add message test for ntf server switch
2024-01-17 19:47:47 +00:00
Alexander Bondarenko eb41abfb8f tests: fix random order in async commands case (#969) 2024-01-17 11:48:41 +00:00
Evgeny Poberezkin b547f34cc0 Revert "Revert "agent: do not create user record in new databases (#957)" (#963)" (#964)
* Revert "Revert "agent: do not create user record in new databases (#957)" (#963)"

This reverts commit aee9088417.

* tests

* remove logging
2024-01-17 11:01:04 +00:00
Evgeny Poberezkin f6ed4640d4 5.5.0.6 2024-01-16 17:07:15 +00:00
Evgeny Poberezkin a0b35cec4f agent: fix potential race when good client can be removed instead of bad for the same transport session (#967)
* agent: fix potential race when good client can be removed instead of bad for the same transport session

* tryAgentError

* case
2024-01-16 13:45:51 +00:00
Evgeny Poberezkin 00c4ff4a21 agent: fix race condition in subscription worker (results in stuck delivery), better error handling in batch operations (#962)
* debugging test: join connection when reply queue creation fails

* more logs

* more logs

* more logs, fixed bug

* cleanup

* catch all errors

* simplify

* comment

* remove client ID counter
2024-01-16 10:29:35 +00:00
Evgeny Poberezkin aee9088417 Revert "agent: do not create user record in new databases (#957)" (#963)
This reverts commit 9b9a0bd0df.
2024-01-14 22:28:28 +00:00
Evgeny Poberezkin 7f7a77c4eb SMP protocol: optimize batching transactions, remove Builder (#961)
* remove Builder

* fewer chunks

* remove lazy bytestrings

* optimize

* pad
2024-01-14 20:42:47 +00:00
Alexander Bondarenko cd4329f2de agent: collect worker stats (#959)
* agent: collect worker stats

* add more workers

* process asyncCmdWorkers as a Map
2024-01-12 23:38:35 +00:00
Alexander BondarenkoandEvgeny Poberezkin 68f5e189a6 smp: check for progress when resubscribing (#956)
* smp: check for progress when resubscribing

* add allowClose to reconnectServer to distinguish entry points

* resolve error todo

* make reconnect async per-session

* remove allowClose

* deregister reconnecter when it finishes

* signal/react more work explicitly

* fix restart condition

* wait for reconnecter to finish

* remove redundant reconnect locks

* rename getClientVar for expanded scope

* formatting

* remove withPending

* move pending check to tryReconnectSMPClient loop

* combine pending check and slot release transactions

* actually reserve the slot for async

* simplify

* refactor

* refactor

* use removeClientVar

* rename

* refactor

* test

* reduce MonadError scope

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-01-12 19:29:18 +00:00
Evgeny Poberezkin 17f64e1565 docs: URI schemes (#958) 2024-01-12 17:21:57 +00:00
Evgeny Poberezkin 9b9a0bd0df agent: do not create user record in new databases (#957)
* agent: do not create user record in new databases

* update
2024-01-12 16:57:57 +00:00
Evgeny Poberezkin ad8cd1d515 5.5.0.5 2024-01-09 19:44:15 +00:00
Evgeny Poberezkin ca527b4d6c use Builder for batching commands (#953)
* use Builder for batching commands

* refactor

* refactor

* increase timeout in test

* do not materialize builder

* use concat

* debug test

* debug2

* delays

* more delay, less worker delay

* remove logs

* reduce delay

* tests

* sized builder

* builder

* comment

* remove unsafe

* style

* remove fromIntegral
2024-01-08 22:54:27 +00:00
Evgeny Poberezkin 55808b0c82 5.5.0.4 2024-01-06 11:40:15 +00:00
Evgeny Poberezkin fa794d7878 agent: send errors on subscription timeouts (#937)
* agent: debug server reconnections

* update lock only if it has the same name already

* report subscription timeouts

* clean up
2024-01-05 17:36:57 +00:00
Alexander BondarenkoandEvgeny Poberezkin 34056b9d7b xftp-server: add control port (#951)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-01-05 17:36:19 +00:00
Alexander Bondarenko 87cec9ad16 control: try deleting from senders first (#950) 2024-01-05 17:07:29 +00:00
Evgeny Poberezkin 2488cf1e3a agent: restart worker when it terminated (#948)
* agent: restart worker when it terminated

* comment

* remove writeTMVar

* remove writeTMVar 2

* remove writeTMVar 3
2024-01-05 14:22:26 +00:00
Alexander BondarenkoandEvgeny Poberezkin b4495bb4f0 xftp-server: fix stats (#944)
* xftp-server: fix stats

* count file expirations

* rename

* simplify

* check files size

* store and fix fileSize check

* do

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-01-05 11:30:27 +00:00
Alexander Bondarenko 6d4834f306 core: publish sendMessagesB (#949) 2024-01-04 15:37:13 +00:00
Evgeny Poberezkin 37d30240fd 5.5.0.3 2024-01-03 20:36:54 +00:00
Alexander BondarenkoandEvgeny Poberezkin 8fd5e9f25a servers: make writeStoreLogRecord atomic (#946)
* servers: make writeStoreLogRecord atomic

* comment

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2024-01-03 20:29:09 +00:00
Alexander Bondarenko 36298f2cea smp-server: update stats (#943)
* smp-server: check queue balance in stats vs store

* smp-server: add msgExpired stats

* add msgExpired stats

* split expire/stats transactions

* count and pass msgExpired explicitly

* save/load qCount and use it for checking store
2024-01-02 21:24:17 +00:00
Evgeny Poberezkin f954c2cd17 rfc: SMP proxies key scope and other considerations (#927)
* rfc: SMP proxies key scope and other considerations

* split line
2023-12-28 23:29:51 +00:00
137 changed files with 11303 additions and 4939 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ jobs:
run: cabal build --enable-tests
- name: Test
timeout-minutes: 30
timeout-minutes: 40
shell: bash
run: cabal test --test-show-details=direct
+100
View File
@@ -1,3 +1,103 @@
# 5.6.2
Version 5.6.2.2.
SMP agent:
- Lower memory consumption (~20-25%).
- More stable XFTP file uploads and downloads.
- API to receive network connectivity changes from the apps.
- to reduce battery consumption: connection attempts interval growing to every 2 hours when app reports as offline.
- to reduce retries and traffic: 50% increased timeouts when on mobile network.
XFTP server:
- expire files on start.
- version negotiation based on TLS ALPN and handshake.
NTF server:
- reduced downtime by ~100x faster start time.
- exclude test tokens from statistics.
# 5.6.1
Version 5.6.1.0.
- Much faster iOS notification server start time (fewer skipped notifications).
- Fix SMP server stored message stats.
- Prevent overwriting uploaded XFTP files with subsequent upload attempts.
- Faster base64 encoding/parsing.
- Control port audit log and authentication.
# 5.6.0
Version 5.6.0.4.
SMP protocol/client/server:
- support deniable sender command authorization (to be enabled in the next version).
- remove support for SMP protocol versions (prior to v4, 07/2022).
Agent:
- optional post-quantum key agreement using sntrup761 in double ratchet protocol.
- improve performance of deleting multiple connections and files by batching database operations.
- delay connection deletion to deliver pending messages.
- API to test for notifications server.
- remove support for client protocols versions (prior to 10/2022).
XFTP server:
- restore storage quota in case of failed uploads.
Performance and stability improvements.
# 5.5.3
Agent:
- notification token API also returns active notifications server.
- support file descriptions with redirection and file URIs.
Servers:
- CLI commands for online key and certificate rotation.
- Configure config and log paths via environment variables.
# 5.5.2
Extensible handshake for clients and SMP/NTF servers (ignore extra data).
# 5.5.1
SMP servers:
- do not keep stats file open
- additional stats about currently stored messages
Agent:
- support multiple notification servers (only one can be used at a time).
- expire messages after "quota exceeded" error after 7 days (instead of 21 days previously).
- stabilize message delivery, remove unnecessary subscription retries and traffic.
- improve database performance for message delivery.
- fix sockets/memory leak - a very old bug "activated" by improvements in v5.5.0.
# 5.5.0
Code:
- compatible with GHC 8.10.7 to support compilation for armv7a.
- migrate to `crypton` from deprecated `cryptonite` (the seed for DRG is now sha512-hashed).
- use ChaChaDRG for all random IDs, keys and nonces, only using hashed entropy as seed.
- more efficient transaction batching in SMP protocol client and server.
Agent:
- stabilize message reception and delivery, migrate message delivery to database queue.
- additional event MSGNTF confirming that message received via notification is processed.
- efficient processing of messages sent to multiple recipients with batched database transactions.
- new worker abstraction for all queued tasks resilient to race conditions and some database errors.
- many fixed race conditions.
- background mode for iOS NSE.
- additional error reporting to client on critical errors (to be show as alert in the clients).
- functional api to get worker statistics.
SMP/XFTP servers:
- fix socket and memory leak on servers with high load (inactive clients without subscriptions are disconnected after set time of inactivity).
- control port improvements.
- fix statistics for stored queues, messages and files.
- make writing to store log atomic (fixes a rare bug in XFTP server).
# 5.4.0
Migrate to GHC 9.6.3
+7 -4
View File
@@ -1,13 +1,14 @@
module Main where
import Control.Logger.Simple
import Simplex.Messaging.Server.CLI (getEnvPath)
import Simplex.Messaging.Notifications.Server.Main
cfgPath :: FilePath
cfgPath = "/etc/opt/simplex-notifications"
defaultCfgPath :: FilePath
defaultCfgPath = "/etc/opt/simplex-notifications"
logPath :: FilePath
logPath = "/var/opt/simplex-notifications"
defaultLogPath :: FilePath
defaultLogPath = "/var/opt/simplex-notifications"
logCfg :: LogConfig
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
@@ -15,4 +16,6 @@ logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
main :: IO ()
main = do
setLogLevel LogDebug -- change to LogError in production
cfgPath <- getEnvPath "NTF_SERVER_CFG_PATH" defaultCfgPath
logPath <- getEnvPath "NTF_SERVER_LOG_PATH" defaultLogPath
withGlobalLogging logCfg $ ntfServerCLI cfgPath logPath
+3 -1
View File
@@ -1,4 +1,5 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
@@ -38,7 +39,8 @@ logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
-- Warning: this SMP agent server is experimental - it does not work correctly with multiple connected TCP clients in some cases.
main :: IO ()
main = do
putStrLn $ "SMP agent listening on port " ++ tcpPort (cfg :: AgentConfig)
let AgentConfig {tcpPort} = cfg
putStrLn $ maybe (error "no agent port") (\port -> "SMP agent listening on port " ++ port) tcpPort
setLogLevel LogInfo -- LogError
Right st <- createAgentStore agentDbFile agentDbKey False MCConsole
withGlobalLogging logCfg $ runSMPAgent (transport @TLS) cfg servers st
+1 -4
View File
@@ -3,8 +3,8 @@
module Main where
import Control.Logger.Simple
import Simplex.Messaging.Server.CLI (getEnvPath)
import Simplex.Messaging.Server.Main
import System.Environment
defaultCfgPath :: FilePath
defaultCfgPath = "/etc/opt/simplex"
@@ -21,6 +21,3 @@ main = do
cfgPath <- getEnvPath "SMP_SERVER_CFG_PATH" defaultCfgPath
logPath <- getEnvPath "SMP_SERVER_LOG_PATH" defaultLogPath
withGlobalLogging logCfg $ smpServerCLI cfgPath logPath
getEnvPath :: String -> FilePath -> IO FilePath
getEnvPath name def = maybe def (\case "" -> def; f -> f) <$> lookupEnv name
+7 -4
View File
@@ -1,13 +1,14 @@
module Main where
import Control.Logger.Simple
import Simplex.Messaging.Server.CLI (getEnvPath)
import Simplex.FileTransfer.Server.Main
cfgPath :: FilePath
cfgPath = "/etc/opt/simplex-xftp"
defaultCfgPath :: FilePath
defaultCfgPath = "/etc/opt/simplex-xftp"
logPath :: FilePath
logPath = "/var/opt/simplex-xftp"
defaultLogPath :: FilePath
defaultLogPath = "/var/opt/simplex-xftp"
logCfg :: LogConfig
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
@@ -15,4 +16,6 @@ logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
main :: IO ()
main = do
setLogLevel LogDebug -- change to LogError in production
cfgPath <- getEnvPath "XFTP_SERVER_CFG_PATH" defaultCfgPath
logPath <- getEnvPath "XFTP_SERVER_LOG_PATH" defaultLogPath
withGlobalLogging logCfg $ xftpServerCLI cfgPath logPath
+23
View File
@@ -0,0 +1,23 @@
common:
corrId - random BS, used as CbNonce
entityId - p2r tlsUniq
# setup
s->p: "proxy", uri, auth?
# unless connected
p->r: "p_handshake"
p<-r: "r_key", tls-signed dh pub
s<-r: "r_key", tls-signed dh pub # reply entityId contains tlsUniq
# working
s ; generate random dh priv, make shared secret
s->p: s2r("forward", random dh pub, SEND command blob)
p->r: p2r("forward", random dh pub, s2r("forward", ...)))
r->c@ "msg", ...
p<-r: p2r("r_res", s2r("ok" / "error", error))
s<-p@ s2r("ok" / "error", error)
# expired
p<-r@ p2r("error", "key expired")
s<-p@ "error", "key expired"
s ; reconnect
+11 -1
View File
@@ -1,5 +1,5 @@
name: simplexmq
version: 5.5.0.2
version: 5.6.2.2
synopsis: SimpleXMQ message broker
description: |
This package includes <./docs/Simplex-Messaging-Server.html server>,
@@ -73,6 +73,7 @@ dependencies:
- unliftio-core == 0.2.*
- websockets == 0.12.*
- yaml == 0.11.*
- zstd == 0.1.3.*
flags:
swift:
@@ -168,6 +169,11 @@ tests:
- silently == 1.2.*
- main-tester == 0.2.*
- timeit == 2.0.*
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-A64M
- -with-rtsopts=-N1
ghc-options:
# - -haddock
@@ -178,3 +184,7 @@ ghc-options:
- -Wincomplete-record-updates
- -Wincomplete-uni-patterns
- -Wunused-type-patterns
- -O2
default-extensions:
- StrictData
+16
View File
@@ -0,0 +1,16 @@
Scheme name: xftp
Status: Provisional
Applications/protocols that use this scheme name:
This scheme is used for URIs of XFTP (SimpleX File Transfer Protocol) servers,
a client-server protocol for asynchronous file transfer via relays,
preserving file meta-data (including size and name) and content privacy.
Contact: Evgeny Poberezkin <ep@simplex.chat>
Change controller: Evgeny Poberezkin <ep@simplex.chat>
References:
The syntax for server URIs in the provisional specification for SimpleX File Transfer Protocol:
https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2022-12-26-simplex-file-transfer.md#server-address-syntax
+15
View File
@@ -0,0 +1,15 @@
Scheme name: xrcp
Status: Provisional
Applications/protocols that use this scheme name:
This scheme is used for URIs of controller sessions via SimpleX Remote Control protocol (XRCP),
a protocol for remote access and management of hosts via insecure network.
Contact: Evgeny Poberezkin <ep@simplex.chat>
Change controller: Evgeny Poberezkin <ep@simplex.chat>
References:
The syntax for server URIs in the provisional specification for SimpleX File Transfer Protocol:
https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2023-10-25-remote-control.md#session-invitation
+13
View File
@@ -149,6 +149,19 @@ parts:
This file description is sent to all recipients via normal messages, split to 15780 byte chunks if needed.
### Server address syntax
The server address is a URI with the following format:
```abnf
xftpServerURI = %s"xftp://" xftpServer
xftpServer = serverIdentity "@" srvHost [":" port]
srvHost = <hostname> ; RFC1123, RFC5891
port = 1*DIGIT
serverIdentity = base64url
base64url = <base64url encoded binary> ; RFC4648, section 5
```
### Receiving file
Having received the description, the recipient will:
+53 -42
View File
@@ -2,9 +2,9 @@
## Problem
SMP protocol relays are chosen and can be controlled by the message recipients. It means that the recipients can find out IP addresses of message senders by modifying SMP relay code (or by using proxies and timing correlation), unless the senders use VPN or some overlay network. Tor is an audequate solution in most cases to mitigate it, but it requires additional technical knowledge to install and configure (even installing Orbot on Android is seen as "complex" by many users), and reduces usability because of higher latency.
SMP protocol relays are chosen and can be controlled by the message recipients. It means that the recipients can find out IP addresses of message senders by modifying SMP relay code (or by using proxies and timing correlation), unless the senders use VPN or some overlay network. Tor is an adequate solution in most cases to mitigate it, but it requires additional technical knowledge to install and configure (even installing Orbot on Android is seen as "complex" by many users), and reduces usability because of higher latency.
The lack of in-built IP address protection is the main concern of many users, particularly given that most people do not realise that it is lacking by default - without transport protection SimpleX is not perceived as a "whole product".
The lack of in-built IP address protection is the main concern of many users, particularly given that most people do not realize that it is lacking by default - without transport protection SimpleX is not perceived as a "whole product".
Similarly, XFTP protocol relays are chosen by senders, and they can be used to detect file recipients' IP addresses.
@@ -43,7 +43,7 @@ Overall, this is not a viable or even appropriate option for the current stage.
3. SMP / XFTP proxy.
Introduce SMP and XFTP protocol extenstions to allow message senders and file recipients to delegate the tasks of sending messages and receiving files to the proxies, so that peer-chosen relays can only observe IP addresses of the proxies and not of the users.
Introduce SMP and XFTP protocol extensions to allow message senders and file recipients to delegate the tasks of sending messages and receiving files to the proxies, so that peer-chosen relays can only observe IP addresses of the proxies and not of the users.
Pros:
- no dependency on and lower latency than via Tor
@@ -68,12 +68,14 @@ Below considers this design.
2. SMP proxy should not be able to observe queue addresses and their count on the destination relays. This requirement is not needed for XFTP proxies, as each file chunk is downloaded only once, so there is no need to hide its address.
3. There must be no identifiers and cyphertext in common in outgoing and incoming traffic inside TLS (the current designs have this quality).
3. There must be no identifiers and ciphertext in common in outgoing and incoming traffic inside TLS (the current designs have this quality).
4. Traffic between the client and destination relays must be e2e encrypted, with MITM-by-proxy mitigated, relying on the relay identity (certificate fingerprint), ideally without any additional fingerprint in relay address.
5. SMP proxy should implement retry logic and hold messages while they are delivered. They also should return relay replies to the client. To avoid any additional traffic the client should just add "sent to proxy" status and only show "sent" once proxy returns the response from the destination relay - there should be no additional response from the proxy confirming acceptance to delivery.
This would also reduce the difference in how the traffic looks to the observer - sending via proxy may look similar to sending to the usual server (which can be further supported by friendly destination relays that could add latency for direct requests and reply quickly when response came via proxy and be undermined by hostile relays that would introduce some latency pattern to help traffic correlation. The latter problem can be mitigated by having a fixed response latency from proxy that may be "come back later for destination response").
6. Sending messages to groups have to be batched in the client to avoid multiple requests for destination relay sessions - such requests can be batched to proxy, even though it leaks _some_ metadata - which destination relays are used by a given sender's IP address, it also reduces the overhead from proxies it could be an option based on the privacy slider.
6. SMP proxy may also increase utility and privacy of the platform:
@@ -89,17 +91,17 @@ Below considers this design.
3. We probably should aim to avoid changing agent/client logic and see it instead as transport concern that can be dynamically decided at a point of sending a message, based on the current configuration.
4. Configuration should probably allow to choose between not using proxies (particularly, during testing, when it would be the default), using proxies only for unknown relays, and using proxies for all relays (extra traffic, but more complex transport correlation). The clients can aim to use proxy from another provider, to reduce the risks of sharing the information.
4. Configuration should probably allow to choose between not using proxies (particularly, during testing, when it would be the default), using proxies only for unknown relays, and using proxies for all relays (extra traffic, but more complex transport correlation - although randomizing this choice can be more beneficial to the transport privacy). The clients can aim to use proxy from another provider, to reduce the risks of sharing the information.
### SMP-proxy protocol
The flow of the messages will be:
1. Client requests proxy to create session with the relay by sending `server` command with the SMP relay address and optional proxy basic AUTH (below). It should be possible to batch multiple session requests into one block, to reduce traffic.
1. Client requests proxy to create session with the relay by sending `PRXY` command with the SMP relay address and optional proxy basic AUTH (below). It should be possible to batch multiple session requests into one block, to reduce traffic.
2. Proxy connects to SMP relay, negotiating a shared secret in the handshake that will be used to encrypt all sender blocks inside TLS (proxy-relay encryption). SMP relay also returns in handshake its temporary DH key to agree e2e encryption with the client (sender-relay encryption, to protect metadata from proxy).
2. Proxy connects to SMP relay, negotiating a shared secret via a handshake headers - it will be used to encrypt all sender blocks inside TLS (proxy-relay encryption). DH key returned by SMP relay in handshake will also be used to encrypt client commands, combining it with random per-command keys (sender-relay encryption, to hide metadata sent to the destination relay from proxy).
3. Proxy replies with `server_id` command including relay session ID to identify it in further requests, relay DH key for e2e encryption with the client - this key is signed with the TLS online private key associated with the certificate (its fingerprint is included in the relay address), and the TLS session ID between proxy and relay (this session ID must be used in transmissions, to mitigate replay attacks as before).
3. Proxy replies to sender with `PKEY` message using "entityId" transmission field to indicate session ID for using in further requests, relay DH key for _s2r_ encryption with the client - this key is signed with the TLS online private key associated with the certificate (its fingerprint is included in the relay address), and the TLS session ID between proxy and relay (this session ID must be used in transmissions, to mitigate replay attacks as before).
A possible attack here is that proxy can use this TLS session to replay commands received from the client. Possibly, it could be mitigated with a bloom filter per proxy/SMP relay connection that would reject the repeated DH keys (that need to be used for replay), and also with DH key expiration (this mitigation should allow some acceptable rate of false positives from the bloom filter).
@@ -107,11 +109,15 @@ With 32 bits per key there will be ~1/1,000,000 false positives (see https://en.
Given that the client chooses proxy it has some trust to, maybe this replay attack risk can be accepted.
4. Now the client sends `forward` to proxy, which it then forwards to SMP relay, applying additional encryption layer.
It is important that the same public key from destination relay is returned to all clients, so proxy does not need to repeat this request to know relays while the key did not expire, as using different keys for different clients would allow destination relays to correlate requests to the clients. A proxy that colludes with the destination relay can pass different public keys to the same client, but it is not changing threat model as colluding proxy can share information as well. It is also important that the client uses a new random key for each command, as using the same key would allow the destination relay to identify these commands as comming from the same user, and using a different key for each queue while would protect privacy of the user from the destination relay, would make it visible to the proxy how many different queues the client has on destination relay.
5. SMP relay sends `response` to proxy applying additional encryption layer, which it then forwards to the client removing the additional encryption layer.
*Unrelated cosideration for SMP protocol privacy improvement*: instead of signing commands to the destination relay, the sender could have a ratchet per queue agreed with the destination relay that would simply use authenticated encryption with per-message symmetric key to encrypt the message on the way to relay, and this encryption would be used as a proof of sender.
Effectively it works as a simplified two-hop onion routing with the first relay (proxy) chosen by the sending client and the second relay chosen by the recipient, not only protecting senders' IP addresses from the recipients' relays, but also preventing recipients relays from correlating senders' traffic to different queues, as TLS session is owned by the proxy now and it mixes the traffic from multiple senders. To correlate traffic to users, proxy and relay would have to combine their information. SMP relays are still able to correlate traffic to receiving users via transport session.
4. Now the client sends `PFWD` to proxy, which it then forwards to SMP relay as `RFWD`, applying _p2r_ encryption layer.
5. SMP relay sends `RRES` to proxy applying _p2r_ encryption layer, which it then forwards to the client as `PRES`, removing the _p2r_ encryption layer.
Effectively it works as a simplified two-hop onion routing with the first relay (proxy) chosen by the sending client and the second relay chosen by the recipient, not only protecting senders' IP addresses from the recipients' relays, but also preventing recipients' relays from correlating senders' traffic to different queues, as TLS session is owned by the proxy now and it mixes the traffic from multiple senders. To correlate traffic to users, proxy and relay would have to combine their information. SMP relays are still able to correlate traffic to receiving users via transport session.
Sequence diagram for sending the message via SMP proxy:
@@ -120,33 +126,33 @@ Sequence diagram for sending the message via SMP proxy:
| sending | | SMP | | SMP | | receiving |
| client | | proxy | | relay | | client |
------------- ------------- ------------- -------------
| `server` | | |
| -------------------------> | create TLS session, get keys | |
| `PRXY` | | |
| -------------------------> | | |
| | ------------------------------> | |
| `server_id` | (if doesn't exist) | |
| | SMP handshake | |
| | <------------------------------ | |
| `PKEY` | | |
| <------------------------- | | |
| | | |
| TLS(F:s2r(SEND(e2e(msg)))) | | |
| -------------------------> | TLS(F:p2r(s2r(SEND(e2e(msg))))) | |
| `PFWD` (s2r) | | |
| -------------------------> | | |
| | `RFWD` (p2r) | |
| | ------------------------------> | |
| | | |
| | TLS(R:p2r(s2r(OK/ERR))) | |
| TLS(R:s2r(OK/ERR)) | <------------------------------ | |
| <------------------------- | | TLS(MSG(r2c(e2e(msg)))) |
| | | -----------------------> |
| | | |
| | | TLS(ACK) |
| | `RRES` (p2r) | |
| | <------------------------------ | |
| `PRES` (s2r) | | `MSG` |
| <------------------------- | | -----------------------> |
| | | `ACK` |
| | | <----------------------- |
| | | |
| | | |
```
Below diagram shows the encrypttion layers for `forward` and `response` commands:
Below diagram shows the encrypttion layers for `PFWD`/`RFWD` commands and `RRES`/`PRES` responses:
- s2r (added) - encryption between client and SMP relay, with relay key returned in server_id command, with MITM by proxy mitigated by verifying the certificate fingerprint included in the relay address.
- s2r (added) - encryption between client and SMP relay, with relay key returned in relay handshake, with MITM by proxy mitigated by verifying the certificate fingerprint included in the relay address.
- e2e (exists now) - end-to-end encryption per SMP queue, with double ratchet e2e encryption inside it.
- p2r (added) - additional encryption between proxy and SMP relay with key agreed in the handshake, to mitigate traffic correlation inside TLS. This key could also be signed by the same certificate, if we don't want to rely on TLS security.
- p2r (added) - additional encryption between proxy and SMP relay with the shared secret agreed in the handshake, to mitigate traffic correlation inside TLS.
- r2c (exists now) additional encryption between SMP relay and client to prevent traffic correlation inside TLS.
```
@@ -161,27 +167,32 @@ Below diagram shows the encrypttion layers for `forward` and `response` commands
----------------- ----------------- -- TLS -- ----------------- -----------------
```
When proxy connects to SMP relay it would indicate in the handshake that it will use proxy protocol and the SMP relay would expect the same `forward` commands and reply with `response`s.
Question: should proxy declare its role in handshake? When proxy connects to SMP relay it would indicate in the handshake that it will act as a proxy and the SMP relay would expect the same `forward` commands and reply with `response`s.
Below syntax aims to fit in 16kb block using spare capacity in SMP protocol.
Common SMP transmission format (v4), for reference:
```abnf
proxy_block = padded(proxy_transmission, 16384)
proxy_transmission = corr_id relay_session_id proxy_command
corr_id = length *8 OCTET
proxy_command = server / server_id / forward / response / error
server = "S" address [relay_basic_auth] ; creates transport session between proxy and relay
server_id = "I" relay_session_id tls_session_id signed_relay_key ;
; session_id is the TLS session ID between proxy and relay, it has to be included inside encrypted block to prevent replay attacks
forward = %s"F" random_dh_pub_key encrypted_block
response = %s"R" encrypted_block; response received from the destination SMP relay
relay_session_id = length *8 OCTET
error = %s"E" error
paddedTransmission = <padded(transmission), 16384>
transmission = signature signed
signature = 0 ; empty signatures here
signed = sessionIdentifier corrId entityId (smpCommand / brokerMsg)
```
The overhead is: 1+8 (corrId) + 1+8 (relay_session_id) + 1 (command) + 1+32 (random_dh_pub_key) + 2 (original length) + 16 (auth tag for e2e encryption) + 16 (auth tag for proxy to relay encryption) = 86 bytes. The reserve for sent messages in SMP is ~84 bytes, so it should about fit with some reduced bytes somewhere.
- `corrId` is fully random each time and used as a nonce for encrypted blocks.
- `entityId` carries tlsUniq from the current proxy-to-relay connection.
- `smpCommand` gets extended with `s2p_command / p2r_command`.
- `brokerMsg` gets extended with `r_key / r_response`.
Another possible design is to allow mixing sent messages and normal SMP commands in the same transport connection, but it can make fitting in the block a bit harder, additional overhead would be: 1 (transmission count) + 2 (transmission size) + 1 (empty signature) = 4 bytes.
```abnf
s2p_command = proxy / forward
p2r_command = p_handshake ; forward is
proxy = %s"PRXY" SP relayUri SP basicAuth
relayUri = length %s"smp://" serverIdentity "@" srvHost [":" port]
forward = %s"PFWD" SP dhPublic SP encryptedBlock
r_key = %s"PKEY" SP dhPublic
r_response = %s"RRES" SP encryptedBlock
dhPublic = length x509encoded
```
The above assumes that the client can only send one message to an SMP relay and then has to wait for response before sending the next message. Missing the response would cause re-delivery (further improvement is possible when proxy detects these redelieveries and not send them to relays but simply reply with the same response).
+36
View File
@@ -0,0 +1,36 @@
# Post-quantum double ratchet implementation
See [the previous doc](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/rfcs/2023-09-30-pq-double-ratchet.md).
The main implementation consideration is that it should be both backwards and forwards compatible, to allow changing the connection DR to/from using PQ primitives (although client version downgrade may be impossible in this case), and also to decide whether to use PQ primitive on per-connection basis:
- use without links (in SMP confirmation or in SMP invitation via address or via member), don't use with links (as they would be too large).
- use in small groups, don't use in large groups.
Also note that for DR to work we need to have 2 KEMs running in parallel.
Possible combinations (assuming both clients support PQ):
| Stage | No PQ kem | PQ key sent | PQ key + PQ ct sent |
|:------------:|:---------:|:-----------:|:-------------------:|
| inv | + | + | - |
| conf, in reply to: <br>no-pq inv <br>pq inv | &nbsp;<br>+<br>+ | &nbsp;<br>+<br>- | &nbsp;<br>-<br>+ |
| 1st msg, in reply to:<br>no-pq conf<br>pq/pq+ct conf | &nbsp;<br>+<br>+ | &nbsp;<br>+<br>- | &nbsp;<br>-<br>+ |
| Nth msg, in reply to:<br>no-pq msg <br>pq/pq+ct msg | &nbsp;<br>+<br>+ | &nbsp;<br>+<br>- | &nbsp;<br>-<br>+ |
These rules can be reduced to:
1. initial invitation optionally has PQ key, but must not have ciphertext.
2. all subsequent messages should be allowed without PQ key/ciphertext, but:
- if the previous message had PQ key or PQ key with ciphertext, they must either have no PQ key, or have PQ key with ciphertext (PQ key without ciphertext is an error).
- if the previous message had no PQ key, they must either have no PQ key, or have PQ key without ciphertext (PQ key with ciphertext is an error).
The rules for calculating the shared secret for received/sent messages are (assuming received message is valid according to the above rules):
| sent msg ><br>V received msg | no-pq | pq | pq+ct |
|:------------------------------:|:-----------:|:-------:|:---------------:|
| no-pq | DH / DH | DH / DH | err |
| pq (sent msg was NOT pq) | DH / DH | err | DH / DH+KEM |
| pq+ct (sent msg was NOT no-pq) | DH+KEM / DH | err | DH+KEM / DH+KEM |
To summarize, the upgrade to DH+KEM secret happens in a sent message that has PQ key with ciphertext sent in reply to message with PQ key only (without ciphertext), and the downgrade to DH secret happens in the message that has no PQ key.
The type for sending PQ key with optional ciphertext is `Maybe E2ERachetKEM` where `data E2ERachetKEM = E2ERachetKEM KEMPublicKey (Maybe KEMCiphertext)`, and for SMP invitation it will be simply `Maybe KEMPublicKey`. Possibly, there is a way to encode the rules above in the types, these types don't constrain possible transitions to valid ones.
+73
View File
@@ -0,0 +1,73 @@
# Sending large file descriptions
It is desirable to provide a QR code/URI from which a file can be downloaded. This way files may be addressed outside a chat client.
Currently the `xftp` CLI tool can generate YAML file descriptions that can be used to receive a file.
It is possible to pass such a description as an URI, but descriptions for files larger than ~8 MBs (two 4 MB chunks) would give QR codes that are difficult to process.
A user can manually upload description and get a shorter one. Typically descriptions for files that are up to ~20 GBs would still be small enough to not require another pass, and that is way beyond any current (or, reasonable, fwiw) limitations.
It is possible to streamline this process, so any application using simplexmq agent can easily send file descriptions and follow redirects.
A file description with a redirect contains an extra field with final file size and digest so it can be followed automatically.
The flow would be like this:
- Sending:
1. Upload file as usual with `xftpSendFile`, get recipient file descriptions in `SFDONE` message.
2. Upload one of the file descriptions with `xftpSendDescription`, get its redirect-description in its `SFDONE` message.
3. Wrap in `FileDescriptionURI` and use `strEncode` to get a QR-sized URI.
4. Show QR code / copy link.
- Receiving:
1. Scan QR code / paste link.
2. Use `strDecode` and unwrap `FileDescriptionURI` to get `ValidFileDescription 'FRecipient`.
3. Download it as usual with `xftpReceiveFile`, getting `RFDONE` message when the file is fully received.
It is not necesary to use redirect description if original description can be encoded to fit in 1002 characters. Beyond this size there is a significant jump in QR code complexity.
It is possible to call `encodeFileDescriptionURI` right after upload to test if the URI fits and skip step 2.
When `xftpReceiveFile` receives a decoded description that lacks `redirect` field, the procedure for downloading a file is the same as usual - download chunks and reassemble local file.
## Agent changes
### Sending
Sending and receiving files in agent is a multi-step process mediated by DB entries in `snd_files` and `rcv_files` tables.
`xftpSendDescription` is tasked with storing original description in a temporary locally-encrypted file, then creating upload task for it.
It is necessary to preserve redirect metadata so it can be attached to descriptions in the `SFDONE` message sent by a worker:
```sql
ALTER TABLE snd_files ADD COLUMN redirect_size INTEGER;
ALTER TABLE snd_files ADD COLUMN redirect_digest BLOB;
```
### Receiving
`xftpReceiveFile` gets a file description as an argument and knows if it should follow redirect procedure or run an ordinary download.
For redirects it will prepare a `RcvFile` for redirect and then a placeholder, for the final file.
Agent messages would be sent using the entity ID of the final file, which is stored along with redirect metadata in `RcvFile` for the redirect.
```sql
ALTER TABLE rcv_files ADD COLUMN redirect_id INTEGER REFERENCES rcv_files ON DELETE CASCADE; -- for later updates
ALTER TABLE rcv_files ADD COLUMN redirect_entity_id BLOB; -- for notifications
ALTER TABLE rcv_files ADD COLUMN redirect_size INTEGER;
ALTER TABLE rcv_files ADD COLUMN redirect_digest BLOB;
```
These additional fields will exist on the file that is a short description to receive an actual description of the final file.
While a description YAML is being downloaded, the application will get `RFPROG` messages tagged for final entity, containing bytes downloaded so far and the total size from the original file.
When the description is fully downloaded, the worker would decode description and check if the stated size and digest match the declared in redirect.
Then it will replace placeholder description in `rcv_files` for destination file with the actual data from downloaded description.
Finally, instead of sending `RFDONE` for redirect, it hands over work to chunk download worker, which will run exactly as if the user requested its download directly.
An application will then receive `RFPROG` and `RFDONE` messages as usual.
## URI encoding
File description URIs use the same service schema `simplex:` or its `https://simplex.chat` (or any custom host) equivalent as do contact links and can be extracted from text and processed the same way.
The path section is `/file` (with an optional trailing `/`).
The payload is encoded in the "fragment" part of the link, using `#/?`, followed by a query string.
File description is encoded first in a YAML document, then URL-encoded in under the key `d`.
An application may want to pass extra parameters not necessary to download a file. Those go in the `_` key, encoded as a JSON dictionary.
An example link:
`simplex:/file#/?d=chunkSize%3A%2064kb%0Adigest%3A%20OtpnXkECTW4a18Eots2m3O22maeOCMqPUX4ulugIjgMEJfCpTYc_-T257Uw7s9bW_F0G5WBg5BioBWd4Z_OoCw%3D%3D%0Akey%3A%20rNR8_2SJuH7Qve43gV3zszL0R6oY5HSdRZT_paB-wfE%3D%0Anonce%3A%202oKwfK-w75nwyWp8_1Lv6QnQonIRtJmG%0Aparty%3A%20recipient%0Areplicas%3A%0A-%20chunks%3A%0A%20%20-%201%3ATdvaxMnG2Ph1e3QCx3-rpA%3D%3D%3AMC4CAQAwBQYDK2VwBCIEILdErEICvgrBCajDLTX2h3LXyMB7z5vrtLa3XVigJuf-%3ANS46KuYdgOWs6dUeMp7p2oF8rBQ9wQ2Ez6TW6Y6gHg0%3D%0A%20%20-%202%3AH5SRbtKYrXWVXTthrkeWzw%3D%3D%3AMC4CAQAwBQYDK2VwBCIEIGeEPNLt7lUGPfplwsoJLCDFnbIc5Hm31kz5X6rWXmgu%3A7QNRI-gvFx9UM-baXp3YVDli9pcfh3HGFKDhsA9JQHY%3D%0A%20%20-%203%3A_xjukkIl9WZFryUXT0h_TQ%3D%3D%3AMC4CAQAwBQYDK2VwBCIEIIRFBaL1HvUfePvKLuggwUrC_q_ZHd7v08IL9jhM7teC%3Aid2lgLMMjTGsR8SUogJuRdLoEHAc5SDQKFDqlZRSuEY%3D%0A%20%20server%3A%20xftp%3A%2F%2FLcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI%3D%40localhost%3A7002%0Asize%3A%20192kb%0A&_=%7B%22k%22:%22test%22%7D`
+51
View File
@@ -0,0 +1,51 @@
# Repudiation for message senders
## Problem
We use double ratchet protocol to send messages. One of its important qualities is the use of symmetric encryption with forward secrecy, when the new key to encrypt the message is rotated after each message. This provides senders ability to plausibly deny having sent some messages, without denying having sent others. While the recipients can prove to themselves that the message was indeed sent by the sender, because it was encrypted using authenticated encryption with associated data, the recipients cannot prove it to any third party - as the message could have been encrypted by themselves, as they also have the same symmetric keys.
To receive the messages, the recipients agree a message queue with the senders, and the commands sent to this queue are signed by the senders using the cryptographic key (Edwards curve key) of which the public counterpart was shared with the recipient in the confirmation message of SMP protocol (this confirmation message itself is not signed).
While it was never claimed that the messaging protocol provides deniability, the deniability is often mentioned as one of the important qualities of double ratchet algorithm used in the innermost layer of e2e encryption, so without explicit disclaimer of deniability limitations, it may be assumed by the users that the system as a whole provides the same level of deniability as the double ratchet algorithm, which currently is not the case.
While societal understanding and legal acceptance of repudiation is arguable, there was less than a decade since this quality became widely available in Signal - legal systems take longer to evolve. While the argument that the message was forged is unlikely to be accepted in the usual court cases with the ordinary people, it is likely to be considered in cases with high profile defendants, who can reasonably claim that they are the target of smear campaign and are being attributed something that they never sent - the statement that the message is forged is reasonable in such cases, and it provides plausible deniability, and the cryptographic experts invited to the hearing would attest to that.
Its important to both continue providing repudiation quality in communication systems, when it is appropriate, and also to educate the users about when it can be used as a reasonable defence strategy, thus improving privacy of communication and making digital off-the-record communications possible and understood both by the society and by legal systems.
## Solution
The proposed solution is to avoid the use of signature algorithm for server command authorization, and instead use authenticated encryption to authorize the commands sent to the server queues. If this protocol change is adopted, it could be used both for senders and recipients commands, both for consistency, and also to provide the deniability to recipients about executing any commands on the servers, in a similar way.
The proposed approach is to use NaCl crypto_box that proves authentication and third party unforgeability and, unlike signature, repudiation guarantee. See [crypto_box docs](https://nacl.cr.yp.to/box.html):
> The crypto_box function is designed to meet the standard notions of privacy and third-party unforgeability for a public-key authenticated-encryption scheme using nonces. The crypto_box function is not meant to provide non-repudiation. On the contrary: the crypto_box function guarantees repudiability. A receiver can freely modify a boxed message, and therefore cannot convince third parties that this particular message came from the sender. The sender and receiver are nevertheless protected against forgeries by other parties. In the terminology of https://groups.google.com/group/sci.crypt/msg/ec5c18b23b11d82c, crypto_box uses "public-key authenticators" rather than "public-key signatures.”
DJB further writes in the link above:
> If you were already planning to encrypt the message, using another key derived from g^xy, then you don't have to do any extra public-key work. A secret-key authenticator is easier to implement than a public-key signature, and it takes less CPU time to compute.
So the proposed solution appears to have desired security qualities, without non-repudiation, that is undesirable in the context of private messaging.
When queue is created or secured, the recipient would provide a DH key (X25519) to the server (either their own or received from the sender), and the server would provide its own random X25519 key per session. Then, either the authenticator will be computed in this way:
```abnf
transmission = authenticator authorized
authenticator = crypto_box(sha512(authorized), secret = dh(client long term queue key, server session key), nonce = correlation ID)
authorized = tlsunique correlationId queueId protocol_command ; same as the currently signed part of the transmission
```
The authenticator is smaller in size than currently used signature size, freeing ~34 bytes from the transmission.
This allows to retain the protocol logic and make authentication scheme configurable, both by the clients and servers, e.g. some servers might be configured to use signature for non-repudiation, and clients may be configured to either agree or disagree to that, per conversation.
There is no required change in SMP command syntax other than allowing X25519 key instead of Ed signature keys passed to the server in NEW and KEY commands. We could add support for migration of the existing queues to the new authorization scheme, but it is not strictly required, as the clients provide a mechanism to rotate the receiving addresses (currently manually, and once automated all queues will be rotated). On another hand, per queue key and identifiers rotation is cheaper than negotiating the new queue (it can be done between client and server, without the involvement of another party), and could be considered as an independent improvement.
## Migration plan
As this new scheme breaks backward compatibility, as the new scheme requires additional keys in protocol handshake, and current implementation does not support forward compatible header extension, we have to migrate in multiple steps, to minimize any disruption to the users.
1. Upgrade clients for forward compatibility of the protocol handshake (ignore extra bytes) - 5.5.3.
2. Add support for handshake and version negotiation to XFTP - 5.5.4 or 5.6.
3. Upgrade clients to drop support of SMP earlier than v4 (batching) and also drop support of old double ratchet protocol and old handshake - 5.6.
4. Upgrade servers to offer SMP v7 with support for new authorization - by the time 5.6 is released.
5. Upgrade clients to require server support for SMP v7 / new authorization scheme and start using it - 5.7 or 5.8. At this point the old version of the servers will not be supported, as maintaining this backward compatibility would substantially increase the complexity and logic of the client - at the point of generating the key we do not even know which server version will be used.
+33
View File
@@ -0,0 +1,33 @@
# Transmission encryption
## Problems
### Protection of meta-data from sending proxy
The SEND commands and message queue IDs need to be encrypted so that sending proxy cannot see how many queues exist on each server.
Correlation IDs need to be random and can be re-used as nonces so that the destination relay cannot use the increasing correlation IDs that are sent in v6 of the protocol to track the sender.
### Protection of the traffic from the attacker who compromised TLS
Currently, even though different sending and receiving queue IDs are used, the attacker who compromised TLS could do statistical analysis and in this way correlate queue IDs of senders and recipients, and therefore correlate the senders and recipients.
## Possible solutions
1. Encrypt sent messages, other commands and their responses in the additional envelope, irrespective of whether proxy is used or not. In this case the requestion transmission could have this syntax:
```abnf
encReqTransmission = pubKey nonce encrypted(reqTransmission)
reqTransmission = respNonce entityId command
encRespTransmission = replyNonce encrypted(respTransmission)
respTransmission = entityId command
```
The keys to encrypt and decrypt both the command and responses would be computed as curve25519 from the key sent together with command and server session key. For the requests, the nonce has to be random and sent outside of the encrypted envelopt, but for the response respNonce would be taken from inside of the encrypted envelope and it would also be used for correlating commands and responses. This way the attacker who could compromise TLS would not be able to correlate the commands and responses, and also observe entity IDs.
2. The remaining question is to how encrypt and decrypt messages delivered not in response to the commands.
The possible options are:
- restore client session key only for that purpose, but do not forward this key to the destination proxy for sent messages. Then the messages can be sent with a random replyNonce and the key would be computed from session keys. The advantage here is that we won't need to parameterize handles as both client and server would have session keys. The downside that we would have to either somehow differentiate messages and responses, either by some flag that would allow some correlation or just by the absense of replyNonce in the lookup map - that is if the client can find replyNonce, it would use the associated key to decrypt, and if not it would use session key.
- use the same key that was sent with SUB or ACK command. This is much more complex, and would only have some upside if we were to introduce receiving proxies (to conceal transport sessions from the receiving relays for the recipients).
+92
View File
@@ -0,0 +1,92 @@
# Migrating existing connections to post-quantum double ratchet algorithm
## Problem
Post-quantum variant of double ratchet algorithm represents an almost full-stack change affecting all parts of the protocol stack except client-server protocol (SMP):
- double-ratchet end-to-end encryption: different encoding (additional large keys require byte-strings larger than 255 bytes with 2-byte length prefixes) and larger message headers (increased by ~2200 bytes).
- agent-agent protocol: a smaller maximum message size to accomodate larger headers and to fit in 16kb blocks, reduced by ~2200 bytes for the messages and by almost ~4000 bytes for connection information.
- chat protocol: also a smaller message size compensated by zstd comression of JSON messages.
We want the versioning that achieves these objectives:
- all changes in all protocol layers happen at the same time, when both clients support it.
- ability to downgrade the clients to the previous version without losing connection.
- ability to opt-in into this functionality via "experimental" feature toggle, that enables post-quantum encryption in connections when both contacts enable this toggle.
To have ability to downgrade the clients we have two options:
- roll-out this functionality in two stages: 1) roll-out clients support but do not enable the new version, and then 2) upgrade client version. The problem here is that the clients won't be able to opt-in into this experiment.
- make offered range dependent on experimental feature being enabled. Currently we have an option to enable PQ encryption in agent API, and this option can be used as a proxy to maxium supported protocol version - if the option is passed, it can be seen as an indication that higher version range (or version) should offered (or accepted).
## Solution
Currently ratchet state stores version range. It's unclear what was the intended semantics of that version range - it simply stores the offered/supported version range at the time ratchet was initialised, but only a high bound is used to send in message headers, and it is never upgraded. In JSON this range is encoded as tuple (an array of two elements in JSON).
We could continue using this range with the meaning of the lower bound to be "currently used ratchet version" and the meaning of higher boundary to be "maximum supported ratchet version". We could also use the version communicated in message headers to upgrade ratchet version, with the condition that upgrade should only happen if both sides want it. Currently it's defined by pqEnableKEM property in ratchet state. We could also make it more explicit by defining maximum version to which ratchet should upgrade. Given that irreversible upgrades are not very common, it is probably ok to keep it implicit.
We can define a better type than VersionRange to reflect semantics of the range in ratchet (current/max supported range), but for backward compatibility it needs to be encoded in the same way as now.
To summarize, the proposed solution for ratchet versioning is:
- define ratchet versions as new type to include current and maximum allowed versions, where maximum allowed will be either the same or lower than maximum supported based on PQ option (in 5.6), and in 5.7 it will be changed to maximum supported, so version starts upgrading independently from PQ being enabled.
- make encodings in ratchet depend on current version (in curent code it depends on max version).
- include max allowed in message header.
- upgrade current if in range on each new message if less than max and higher than current (same as we do for connections).
- increase max allowed once PQ is enabled (only in 5.6). Make max allowed the same as max supported (global constant).
```haskell
data RatchetVR = RatchetVR
{ currentVersion :: Version,
maxAllowedVersion :: Version
}
instance ToJSON RatchetVR where
toEncoding (RatchetVR v1 v2) = toEncoding (v1, v2)
toJSON (RatchetVR v1 v2) = toJSON (v1, v2)
instance FromJSON RatchetVR where
parseJSON v = do
-- this also verifies that v2 > v1 (although we could remove JSON instances for VersionRange)
VersionRange v1 v2 <- parseJSON v
pure $ RatchetVR v1 v2
```
For connections, we could also make version used for the purposes of encoding dependent on the PQ being enabled, and version for decoding taken from message header, but then we'd have to not only upgrade ratchets but the connection as well every time PQ mode changes.
Another suggestion to ensure that correct version range is used in correct contexts could be:
- using different newtypes for different version ranges.
- define generic type class for version aware encoding that would also accept only specific type class for the version to use the correct range. This may be justified as there will be several version-aware encodings, and not just the protocol as now.
```haskell
class Ord v => EncodingV v a where
{-# MINIMAL smpEncodeV, (smpDecodeV | smpVP) #-}
smpEncodeV :: v -> a -> ByteString
-- default decode uses parser
smpDecodeV :: v -> ByteString -> Either String a
smpDecodeV = parseAll . smpVP
-- default parser decodes from length-specified bytestring
smpVP :: v -> Parser a
smpVP v = smpDecodeV v <$?> smpP
```
The version will be passed from currently agreed version, it may only change when message is received, not when message is sent. The version will not be extracted from the encoding itself as it happens now in ratchet encodings.
## Various options how the problem can be simplified
1. Do not support connection downgrade once both devices upgraded. If applied to all existing connections then it is a bad option, as it would disrupt some important conversations.
2. Do not provide ability to opt-in into PQ encryption until v5.7 where it will be rolled out automatically. That is also suboptimal, as it won't allow announcing technology design and have testing outside of the team devices.
3. The logic explained above where connection upgrade and downgrade is possible and applied to all existing connections if both parties consent to it. There are these important downsides:
- complexity of this logic
- regression risks when this logic is removed.
- some non-coordinated upgrades of existing, potentially important conversations, simply because two users opt-in into the experiment without any expectation that another side also opts-in.
4. Apply upgrade/downgrade logic and enable PQ encryption as opt-in, based on the toggle in the UX, only for the new connections. This seems the least risky, and also simpler than option 3, as it would only apply to the new connections, and both users will have to enable experimental toggle prior to connecting.
Option 4 seems the best trade-off, and has these sub-options regarding where it is controlled:
a) in chat based on connection flag. Chat will pass PQ options only to connections that were created when experimental option was enabled.
b) in agent - there will be additional logic to ignore PQ option for existing connections.
c) both in chat and in agent.
Option 4a seems better, as it would:
- simplify agent code
- minimise required changes when releasing v5.7 (as we do want that all direct and small groups connections migrate to PQ encryption at the time, without any toggles)
- allow tests for connection upgrade in the currect code.
+123
View File
@@ -0,0 +1,123 @@
# Relay metadata and SimpleX network decentralization
## Problem
Currently, the clients configure/choose which servers to use, but they cannot see who operates them, in which geography and hosting provider, what is the server source code (in case it was modified from the reference implementation we provide) and also any administrative and feedback contacts.
Further, we currently use simplex.chat domain to host group links, and as diversity of the groups grows it is beginning to require managing feedback from the users about groups. It is important that this feedback is directed to relay owners and not to us, in case they are not our relays, as we are simply providing software here.
We also need to make it much easier to access administrative actions - e.g., if the relay owner wants to block some address based on the feedback, they should be able to do so effectively. While freedom of speech of users is important the autonomy and sovereignty of relay operators allowing them to decide what they want or don't want to host, irrespective of the reasons, is as important - public relays constitute a public space rather than a service, and while there is no authentication or authorization required to use them, the relay owners should be able to decide their conditions and limitations of use, as is the case for any public space.
## Why it is important
1. We are building network and not a service, so contrary to the traditional approach when service collects users metadata and conceals its own, it appears important to do the opposite - expose all relay metadata that may have any effect on users privacy and security and provide access to this metadata with the minimal friction, via the app UI.
2. We need to remove the only remaining bit of centralization - using simplex.chat website to show the QR code and help new users onboarding - this role can be delegated to the relays.
3. We need to make it easy for the users to submit feedback and to the relay operators to manage that feedback. That includes ourselves, as relay operators, and also any other person or organization that operates public relays.
4. It is important to stress the requirement of AGPLv3 license to provide access to the source code to the relay users, as any code modifications might undermine user privacy and security and must be made transparent to the relay users.
While this document is not the end of the journey to decentralize the network, it's an important first step.
## Proposed solution
The proposed solution consists of two parts:
- communicate server metadata via protocol, so it can be observed by the clients.
- create home page for the relays, with all the same metadata.
- create invitation and address links in the same domain name as the relay.
The latter point is important so it is clear to the users who operates and owns the relay and where the access point to the content or group is hosted. Even though simplex.chat domain is never accessed by the app, and the meaningful part of the address is never sent to the page hosting server, it creates an impression of centralization, and some dependency on simplex.chat domain for anything other that showing the link QR code.
Moving invitation links to the domain of the relay (primary relay, in case the link has redundancy) will both clarify relay ownership, solve the incorrect mis-perception of centralization, remove the dependency on simplex-chat domain without any user effort, and provides the means to submit content complaints to the relay operators (should they wish to receive them, which seems reasonable for large public relays, but may be unnecessary for private relays where unidentified parties cannot create links).
## Solution details
Extend server INI file with information section:
```
[INFORMATION]
# Please note that under AGPLv3 license conditions you MUST make
# any source code modifications available to the end users of the server.
# LICENSE: https://github.com/simplex-chat/simplexmq/blob/stable/LICENSE
# Not doing so would constitute a license violation.
# Declaring an incorrect information here amounts to a fraud.
# The license holders reserve the right to prosecute missing or incorrect
# information about the server source code to the fullest extent permitted by the law.
# The server will show warning on start if this field is absent
# and will not launch from v6.0 until this field is added.
# If any other information field is present, source code property also MUST be present.
source_code: https://github.com/simplex-chat/simplexmq
# Declaring all below information is optional, any of these fields can be omitted.
# Declaring an incorrect information here would amount to a fraud.
# We should split this document to the model one, where specific parameters will be external to the document,
# and specific to us, so that relay operators can adopt our recommended policy and publish any amendments separately.
conditions: https://github.com/simplex-chat/simplex-chat/blob/_archived-ep/ios-file-provider/PRIVACY.md
# conditions_amendments: link
server_country: SE
operator: SimpleX Chat Ltd.
operator_country: GB # operator country
website: https://simplex.chat
admin_simplex: administrative SimpleX address
admin_email: chat@simplex.chat
admin_pgp: PGP key
feedback_simplex: SimpleX address for feedback, comments and complaints
feedback_email: complaints@simplex.chat
feedback_pgp: PGP key
hosting: Linode / Akamai Inc.
hosting_country: US
```
Server home page would show whether queue creation is allowed and/or password protected, server retention policy (e.g., preserve messages on restart or not, and persist connections or not).
Server queue address/contact pages will optionally, provide the UI to submit feedback, comments and complaints directly from the web page (not an MVP, initially we would simply show addresses for feedback, and, probably, create link that opens in the app with pre-populated message, and we could also use this addresses defined in server meta-data to submit feedback from inside of the app - it's also out of MVP scope).
If server is available on .onion address, the web pages would show "open via .onion" in Tor browser.
Extend server handshake header with these information fields:
```haskell
data ServerHandshake = ServerHandshake
{ smpVersionRange :: VersionRangeSMP,
sessionId :: SessionId,
-- pub key to agree shared secrets for command authorization and entity ID encryption.
authPubKey :: Maybe (X.CertificateChain, X.SignedExact X.PubKey),
-- ^^ existing fields
-- vv new field that will be sent as length-prefixed JSON in the handshake
information :: Maybe ServerInformation
}
data ServerInformation = ServerInformation
{ -- below is based on the existing server configuration
persistence :: SMPServerPersistenceMode,
messageExpiration :: Int,
statsEnabled :: Bool,
newQueuesAllowed :: Bool,
basicAuthEnabled :: Bool, -- server is private if enabled
-- below is based on INFORMATION section of INI file
sourceCode :: Text, -- note that this property is not optional, in line with AGPLv3 license
-- all below properties are optional, except entity name MUST be present if any entity country is present
conditions :: Maybe ServerConditions,
operator :: Maybe Entity,
website :: Maybe Text,
admin :: Maybe ServerContactAddress,
feedback :: Maybe ServerContactAddress,
hosting :: Maybe Entity,
serverCountry :: Maybe Text
}
data ServerPersistenceMode = SMPMemoryOnly | SPMQueues | SPMQueuesMessages
data ServerConditions = ServerConditions {conditions :: Text, amendments :: Maybe Text}
data Entity = Entity {name :: Text, country :: Maybe Text}
data ServerContactAddress = ServerContactAddress
{ simplex :: Maybe Text,
email :: Maybe Text, -- it is recommended that it matches DNS email address, if either is present
pgp :: Maybe Text
}
```
This extended server information will be stored in the chat database every time it changes and shown in the UI of the server configuration.
+120
View File
@@ -0,0 +1,120 @@
# XFTP version agreement
## Problem
XFTP is using HTTP2 protocol for encoding requests and responses.
Unlike SMP which has a connection handshake initiated by a server and signals available versions XFTP/HTTP2 is almost entirely client-driven.
So, a client can only try to guess which protocol versions are supported by a server by sending a probe/hello request first.
Determining the endpoint for such a request is an implicit version agreement by itself.
Sending such a request to an old server would error out and requiring it from old clients would break them.
## Solution
The TLS layer used by the XFTP server has an optional [ALPN](https://datatracker.ietf.org/doc/html/rfc7301) extension which allows the client and server to negotiate protocols and store the decision in TLS session context.
Unless a client and a server run ALPN-aware versions, they would default to the old "unversioned" protocol.
TLS extension content is a 65kb chunk, but ALPN standard breaks it into 254b-sized chunks making it unusable for things like key exchange.
The exchange is still client-driven: the client proposes a list, and then a server callback picks one.
In effect, this makes it usable only to signal that some application-level handshake is desired and supported.
## Implementation
ALPN can be used to negotiate for any TLS-based protocol, but the description will focus on XFTP.
TransportClientConfig gets a new `alpn :: Maybe [ALPN]` field so a TLS transport can use it during TLS client creation.
XFTP client sets it to `Just ["xftp/1"]`.
The exact value is not important as long it is in agreement with the server side, but ALPN RFC insists on it being an IANA-registered identifier.
XFTP server sets `onALPNClientSuggest` TLS hook to pick the protocol when it is provided.
The `tls` library treats SHOULD from the RFC as MUST and does a client-side check that the server responded with one of the client-proposed protocols.
Upon connection, transport implementation invokes `getNegotiatedProtocol` and stores it in `tlsALPN :: Maybe ALPN` field of transport context.
HTTP2 transport implementation using `withHTTP2` passes negotiated "protocol" to client and server setup callbacks where they store it in their respective wrappers along with TLS session ID.
A server request handler then knows by looking at the `sessionALPN` if it should require a "handshake" request first.
A client code that got HTTP2Client with `sessionALPN` set knows if it has to proceed with handshake request.
A handshake request still has to be initiated by a client, so it should be kept minimal, just enough data to pass the initiative to a server.
A reply to that initial request should contain a server version range for the client to pick.
A client then commits to a version, sending its part of a handshake.
In the future ALPN negotiation can be dropped in favor of mandatory handshakes or used to signal further handshake schemes.
The XFTP handshake data types and validation code are cloned from SMP.
Currently they carry version information and session authentication parameters.
Authentication parameters made mandatory as this exchange is guarded by the handshake version.
### Server side
`runHTTP2Server` callback used by `xftpServer` should get access to the session state to track handshakes.
A local `TMap SessionId Handshake` is enough to switch request handlers.
The HTTP2 server framework is extended with a way to signal client disconnection to remove sessions from this map.
The `Handshake` type mimics implicit state in stream based handshakes of SMP and NTF.
```haskell
data Handshake
= HandshakeSent C.PrivateKeyX25519 -- server private key that will be merged with client public in `THandleAuth`
| HandshakeAccepted THandleAuth VersionXFTP -- session steady state after handshakes
```
An HTTP2 request without ALPN is treated as legacy and requires no session entry.
Its `Request`s are marked with `THandleParams {thVersion = VersionXFTP 1, ..}`.
An HTTP2 request with ALPN requires a session lookup.
- A lack of entry indicates that a client must send an empty request, to which the server replies with its "server handshake" block and stores its private state in `HandshakeSent`.
- If the session entry contains `HandshakeSent`, then the only valid request content is the "client handshake" block.
The server validates client handshake (in the same way as SMP) and stores authentication and version in `HandshakeAccepted`
- If the session entry contains `HandshakeAccepted`, then the server just passes it to `THandleParams`.
### Client side
`getXFTPClient` tweaks its transport config to include the ALPN marker and then checks if the client got its `sessionALPN` value.
If there's a value set, it then sends an initial block and checks out the server handshake in response.
After validation, it sends "client handshake" request to finish version negotiation.
```haskell
let tcConfig = (transportClientConfig xftpNetworkConfig) {alpn = Just ["xftp/1"]}
-- ...
http2Client <- liftEitherError xftpClientError $ getVerifiedHTTP2Client -- ...
thVersion <- case sessionALPN http2Client of
Nothing -> pure $ VersionXFTP 1
Just proto -> negotiate http2Client proto
```
The resulting `XFTPClient` then contains a negotiated version and can be used to send transmissions with a more recent encoding.
## Block encoding
### Client Hello (request)
A request with an empty body and no padding.
### Server handshake (response)
SMP-encoded and padded to `xftpBlockSize` (~16kb).
```haskell
data XFTPServerHandshake = XFTPServerHandshake
{ xftpVersionRange :: VersionRangeXFTP,
sessionId :: SessionId, -- validated by client against TLS unique
authPubKey ::
( X.CertificateChain, -- fingerprint validated by client against pre-shared hash
X.SignedExact X.PubKey -- signature validated by client against server key from TLS
)
}
```
### Client handshake (request)
SMP-encoded and padded to `xftpBlockSize` (~16kb).
```haskell
data XFTPClientHandshake = XFTPClientHandshake
{ xftpVersion :: VersionXFTP,
keyHash :: C.KeyHash, -- validated by server against its own cert fingerprint
authPubKey :: C.PublicKeyX25519
}
```
### Server confirmation (response)
A response with an empty body and no padding.
+42 -9
View File
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplexmq
version: 5.5.0.2
version: 5.6.2.2
synopsis: SimpleXMQ message broker
description: This package includes <./docs/Simplex-Messaging-Server.html server>,
<./docs/Simplex-Messaging-Client.html client> and
@@ -51,6 +51,7 @@ library
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
@@ -100,10 +101,14 @@ library
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.TAsyncs
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.TRcvQueues
Simplex.Messaging.Client
Simplex.Messaging.Client.Agent
Simplex.Messaging.Compression
Simplex.Messaging.Crypto
Simplex.Messaging.Crypto.File
Simplex.Messaging.Crypto.Lazy
@@ -141,6 +146,8 @@ library
Simplex.Messaging.Server.QueueStore.STM
Simplex.Messaging.Server.Stats
Simplex.Messaging.Server.StoreLog
Simplex.Messaging.ServiceScheme
Simplex.Messaging.Session
Simplex.Messaging.TMap
Simplex.Messaging.Transport
Simplex.Messaging.Transport.Buffer
@@ -155,6 +162,7 @@ library
Simplex.Messaging.Transport.WebSockets
Simplex.Messaging.Util
Simplex.Messaging.Version
Simplex.Messaging.Version.Internal
Simplex.RemoteControl.Client
Simplex.RemoteControl.Discovery
Simplex.RemoteControl.Discovery.Multicast
@@ -164,7 +172,9 @@ library
Paths_simplexmq
hs-source-dirs:
src
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns
default-extensions:
StrictData
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2
include-dirs:
cbits
c-sources:
@@ -222,6 +232,7 @@ library
, unliftio-core ==0.2.*
, websockets ==0.12.*
, yaml ==0.11.*
, zstd ==0.1.3.*
default-language: Haskell2010
if flag(swift)
cpp-options: -DswiftJSON
@@ -242,7 +253,9 @@ executable ntf-server
Paths_simplexmq
hs-source-dirs:
apps/ntf-server
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded -rtsopts
default-extensions:
StrictData
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
@@ -294,6 +307,7 @@ executable ntf-server
, unliftio-core ==0.2.*
, websockets ==0.12.*
, yaml ==0.11.*
, zstd ==0.1.3.*
default-language: Haskell2010
if flag(swift)
cpp-options: -DswiftJSON
@@ -314,7 +328,9 @@ executable smp-agent
Paths_simplexmq
hs-source-dirs:
apps/smp-agent
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded -rtsopts
default-extensions:
StrictData
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
@@ -366,6 +382,7 @@ executable smp-agent
, unliftio-core ==0.2.*
, websockets ==0.12.*
, yaml ==0.11.*
, zstd ==0.1.3.*
default-language: Haskell2010
if flag(swift)
cpp-options: -DswiftJSON
@@ -386,7 +403,9 @@ executable smp-server
Paths_simplexmq
hs-source-dirs:
apps/smp-server
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded -rtsopts
default-extensions:
StrictData
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
@@ -438,6 +457,7 @@ executable smp-server
, unliftio-core ==0.2.*
, websockets ==0.12.*
, yaml ==0.11.*
, zstd ==0.1.3.*
default-language: Haskell2010
if flag(swift)
cpp-options: -DswiftJSON
@@ -458,7 +478,9 @@ executable xftp
Paths_simplexmq
hs-source-dirs:
apps/xftp
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded -rtsopts
default-extensions:
StrictData
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
@@ -510,6 +532,7 @@ executable xftp
, unliftio-core ==0.2.*
, websockets ==0.12.*
, yaml ==0.11.*
, zstd ==0.1.3.*
default-language: Haskell2010
if flag(swift)
cpp-options: -DswiftJSON
@@ -530,7 +553,9 @@ executable xftp-server
Paths_simplexmq
hs-source-dirs:
apps/xftp-server
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded -rtsopts
default-extensions:
StrictData
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts
build-depends:
aeson ==2.2.*
, ansi-terminal >=0.10 && <0.12
@@ -582,6 +607,7 @@ executable xftp-server
, unliftio-core ==0.2.*
, websockets ==0.12.*
, yaml ==0.11.*
, zstd ==0.1.3.*
default-language: Haskell2010
if flag(swift)
cpp-options: -DswiftJSON
@@ -603,6 +629,7 @@ test-suite simplexmq-test
AgentTests
AgentTests.ConnectionRequestTests
AgentTests.DoubleRatchetTests
AgentTests.EqInstances
AgentTests.FunctionalAPITests
AgentTests.MigrationTests
AgentTests.NotificationTests
@@ -615,6 +642,7 @@ test-suite simplexmq-test
CoreTests.EncodingTests
CoreTests.ProtocolErrorTests
CoreTests.RetryIntervalTests
CoreTests.TRcvQueuesTests
CoreTests.UtilTests
CoreTests.VersionRangeTests
FileDescriptionTests
@@ -624,6 +652,8 @@ test-suite simplexmq-test
ServerTests
SMPAgentClient
SMPClient
SMPProxyTests
Util
XFTPAgent
XFTPCLI
XFTPClient
@@ -631,7 +661,9 @@ test-suite simplexmq-test
Paths_simplexmq
hs-source-dirs:
tests
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns
default-extensions:
StrictData
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -O2 -threaded -rtsopts -with-rtsopts=-A64M -with-rtsopts=-N1
build-depends:
HUnit ==1.6.*
, QuickCheck ==2.14.*
@@ -692,6 +724,7 @@ test-suite simplexmq-test
, unliftio-core ==0.2.*
, websockets ==0.12.*
, yaml ==0.11.*
, zstd ==0.1.3.*
default-language: Haskell2010
if flag(swift)
cpp-options: -DswiftJSON
+228 -133
View File
@@ -17,10 +17,14 @@ module Simplex.FileTransfer.Agent
-- Receiving files
xftpReceiveFile',
xftpDeleteRcvFile',
xftpDeleteRcvFiles',
-- Sending files
xftpSendFile',
xftpSendDescription',
deleteSndFileInternal,
deleteSndFilesInternal,
deleteSndFileRemote,
deleteSndFilesRemote,
)
where
@@ -28,14 +32,19 @@ import Control.Logger.Simple (logError)
import Control.Monad
import Control.Monad.Except
import Control.Monad.Reader
import Data.Bifunctor (first)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Coerce (coerce)
import Data.Composition ((.:))
import Data.Either (rights)
import Data.Int (Int64)
import Data.List (foldl', sortOn)
import Data.List (foldl', partition, sortOn)
import qualified Data.List.NonEmpty as L
import Data.Map (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (mapMaybe)
import qualified Data.Set as S
import Data.Text (Text)
import Data.Time.Clock (getCurrentTime)
import Data.Time.Format (defaultTimeLocale, formatTime)
@@ -45,6 +54,7 @@ import Simplex.FileTransfer.Crypto
import Simplex.FileTransfer.Description
import Simplex.FileTransfer.Protocol (FileParty (..), SFileParty (..))
import Simplex.FileTransfer.Transport (XFTPRcvChunkSpec (..))
import qualified Simplex.FileTransfer.Transport as XFTP
import Simplex.FileTransfer.Types
import Simplex.FileTransfer.Util (removePath, uniqueCombine)
import Simplex.Messaging.Agent.Client
@@ -52,18 +62,21 @@ import Simplex.Messaging.Agent.Env.SQLite
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.RetryInterval
import Simplex.Messaging.Agent.Store.SQLite
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.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, XFTPServer)
import Simplex.Messaging.Util (liftError, tshow, unlessM, whenM)
import Simplex.Messaging.Util (catchAll_, liftError, tshow, unlessM, whenM)
import System.FilePath (takeFileName, (</>))
import UnliftIO
import UnliftIO.Directory
import qualified UnliftIO.Exception as E
startXFTPWorkers :: AgentMonad m => AgentClient -> Maybe FilePath -> m ()
startXFTPWorkers :: AgentClient -> Maybe FilePath -> AM ()
startXFTPWorkers c workDir = do
wd <- asks $ xftpWorkDir . xftpAgent
atomically $ writeTVar wd workDir
@@ -72,23 +85,26 @@ startXFTPWorkers c workDir = do
startSndFiles cfg
startDelFiles cfg
where
startRcvFiles :: AgentConfig -> AM ()
startRcvFiles AgentConfig {rcvFilesTTL} = do
pendingRcvServers <- withStore' c (`getPendingRcvFilesServers` rcvFilesTTL)
forM_ pendingRcvServers $ \s -> resumeXFTPRcvWork c (Just s)
lift . forM_ pendingRcvServers $ \s -> resumeXFTPRcvWork c (Just s)
-- start local worker for files pending decryption,
-- no need to make an extra query for the check
-- as the worker will check the store anyway
resumeXFTPRcvWork c Nothing
lift $ resumeXFTPRcvWork c Nothing
startSndFiles :: AgentConfig -> AM ()
startSndFiles AgentConfig {sndFilesTTL} = do
-- start worker for files pending encryption/creation
resumeXFTPSndWork c Nothing
lift $ resumeXFTPSndWork c Nothing
pendingSndServers <- withStore' c (`getPendingSndFilesServers` sndFilesTTL)
forM_ pendingSndServers $ \s -> resumeXFTPSndWork c (Just s)
lift . forM_ pendingSndServers $ \s -> resumeXFTPSndWork c (Just s)
startDelFiles :: AgentConfig -> AM ()
startDelFiles AgentConfig {rcvFilesTTL} = do
pendingDelServers <- withStore' c (`getPendingDelFilesServers` rcvFilesTTL)
forM_ pendingDelServers $ resumeXFTPDelWork c
lift . forM_ pendingDelServers $ resumeXFTPDelWork c
closeXFTPAgent :: MonadUnliftIO m => XFTPAgent -> m ()
closeXFTPAgent :: XFTPAgent -> IO ()
closeXFTPAgent a = do
stopWorkers $ xftpRcvWorkers a
stopWorkers $ xftpSndWorkers a
@@ -96,91 +112,106 @@ closeXFTPAgent a = do
where
stopWorkers workers = atomically (swapTVar workers M.empty) >>= mapM_ (liftIO . cancelWorker)
xftpReceiveFile' :: AgentMonad m => AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> m RcvFileId
xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks}) cfArgs = do
xftpReceiveFile' :: AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> AM RcvFileId
xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks, redirect}) cfArgs = do
g <- asks random
prefixPath <- getPrefixPath "rcv.xftp"
prefixPath <- lift $ getPrefixPath "rcv.xftp"
createDirectory prefixPath
let relPrefixPath = takeFileName prefixPath
relTmpPath = relPrefixPath </> "xftp.encrypted"
relSavePath = relPrefixPath </> "xftp.decrypted"
createDirectory =<< toFSFilePath relTmpPath
createEmptyFile =<< toFSFilePath relSavePath
lift $ createDirectory =<< toFSFilePath relTmpPath
lift $ createEmptyFile =<< toFSFilePath relSavePath
let saveFile = CryptoFile relSavePath cfArgs
fId <- withStore c $ \db -> createRcvFile db g userId fd relPrefixPath relTmpPath saveFile
forM_ chunks downloadChunk
fId <- case redirect of
Nothing -> withStore c $ \db -> createRcvFile db g userId fd relPrefixPath relTmpPath saveFile
Just _ -> do
-- prepare description paths
let relTmpPathRedirect = relPrefixPath </> "xftp.redirect-encrypted"
relSavePathRedirect = relPrefixPath </> "xftp.redirect-decrypted"
lift $ createDirectory =<< toFSFilePath relTmpPathRedirect
lift $ createEmptyFile =<< toFSFilePath relSavePathRedirect
cfArgsRedirect <- atomically $ CF.randomArgs g
let saveFileRedirect = CryptoFile relSavePathRedirect $ Just cfArgsRedirect
-- create download tasks
withStore c $ \db -> createRcvFileRedirect db g userId fd relPrefixPath relTmpPathRedirect saveFileRedirect relTmpPath saveFile
forM_ chunks (downloadChunk c)
pure fId
where
downloadChunk :: AgentMonad m => FileChunk -> m ()
downloadChunk FileChunk {replicas = (FileChunkReplica {server} : _)} = do
void $ getXFTPRcvWorker True c (Just server)
downloadChunk _ = throwError $ INTERNAL "no replicas"
getPrefixPath :: AgentMonad m => String -> m FilePath
downloadChunk :: AgentClient -> FileChunk -> AM ()
downloadChunk c FileChunk {replicas = (FileChunkReplica {server} : _)} = do
lift . void $ getXFTPRcvWorker True c (Just server)
downloadChunk _ _ = throwError $ INTERNAL "no replicas"
getPrefixPath :: String -> AM' FilePath
getPrefixPath suffix = do
workPath <- getXFTPWorkPath
ts <- liftIO getCurrentTime
let isoTime = formatTime defaultTimeLocale "%Y%m%d_%H%M%S_%6q" ts
uniqueCombine workPath (isoTime <> "_" <> suffix)
toFSFilePath :: AgentMonad m => FilePath -> m FilePath
toFSFilePath :: FilePath -> AM' FilePath
toFSFilePath f = (</> f) <$> getXFTPWorkPath
createEmptyFile :: AgentMonad m => FilePath -> m ()
createEmptyFile :: FilePath -> AM' ()
createEmptyFile fPath = liftIO $ B.writeFile fPath ""
resumeXFTPRcvWork :: AgentMonad' m => AgentClient -> Maybe XFTPServer -> m ()
resumeXFTPRcvWork :: AgentClient -> Maybe XFTPServer -> AM' ()
resumeXFTPRcvWork = void .: getXFTPRcvWorker False
getXFTPRcvWorker :: AgentMonad' m => Bool -> AgentClient -> Maybe XFTPServer -> m Worker
getXFTPRcvWorker :: Bool -> AgentClient -> Maybe XFTPServer -> AM' Worker
getXFTPRcvWorker hasWork c server = do
ws <- asks $ xftpRcvWorkers . xftpAgent
getAgentWorker hasWork c server ws $
getAgentWorker "xftp_rcv" hasWork c server ws $
maybe (runXFTPRcvLocalWorker c) (runXFTPRcvWorker c) server
runXFTPRcvWorker :: forall m. AgentMonad m => AgentClient -> XFTPServer -> Worker -> m ()
runXFTPRcvWorker :: AgentClient -> XFTPServer -> Worker -> AM ()
runXFTPRcvWorker c srv Worker {doWork} = do
cfg <- asks config
forever $ do
waitForWork doWork
lift $ waitForWork doWork
atomically $ assertAgentForeground c
runXFTPOperation cfg
where
runXFTPOperation :: AgentConfig -> m ()
runXFTPOperation :: AgentConfig -> AM ()
runXFTPOperation AgentConfig {rcvFilesTTL, reconnectInterval = ri, xftpNotifyErrsOnRetry = notifyOnRetry, xftpConsecutiveRetries} =
withWork c doWork (\db -> getNextRcvChunkToDownload db srv rcvFilesTTL) $ \case
RcvFileChunk {rcvFileId, rcvFileEntityId, fileTmpPath, replicas = []} -> rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) "chunk has no replicas"
fc@RcvFileChunk {userId, rcvFileId, rcvFileEntityId, digest, fileTmpPath, replicas = replica@RcvFileChunkReplica {rcvChunkReplicaId, server, delay} : _} -> do
let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay
withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop ->
withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do
lift $ waitForUserNetwork c
downloadFileChunk fc replica
`catchAgentError` \e -> retryOnError "XFTP rcv worker" (retryLoop loop e delay') (retryDone e) e
where
retryLoop loop e replicaDelay = do
flip catchAgentError (\_ -> pure ()) $ do
when notifyOnRetry $ notify c rcvFileEntityId $ RFERR e
closeXFTPServerClient c userId server digest
liftIO $ closeXFTPServerClient c userId server digest
withStore' c $ \db -> updateRcvChunkReplicaDelay db rcvChunkReplicaId replicaDelay
atomically $ assertAgentForeground c
loop
retryDone e = rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) (show e)
downloadFileChunk :: RcvFileChunk -> RcvFileChunkReplica -> m ()
downloadFileChunk :: RcvFileChunk -> RcvFileChunkReplica -> AM ()
downloadFileChunk RcvFileChunk {userId, rcvFileId, rcvFileEntityId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath} replica = do
fsFileTmpPath <- toFSFilePath fileTmpPath
fsFileTmpPath <- lift $ toFSFilePath fileTmpPath
chunkPath <- uniqueCombine fsFileTmpPath $ show chunkNo
let chunkSpec = XFTPRcvChunkSpec chunkPath (unFileSize chunkSize) (unFileDigest digest)
relChunkPath = fileTmpPath </> takeFileName chunkPath
agentXFTPDownloadChunk c userId digest replica chunkSpec
atomically $ waitUntilForeground c
(complete, progress) <- withStore c $ \db -> runExceptT $ do
(entityId, complete, progress) <- withStore c $ \db -> runExceptT $ do
liftIO $ updateRcvFileChunkReceived db (rcvChunkReplicaId replica) rcvChunkId relChunkPath
RcvFile {size = FileSize total, chunks} <- ExceptT $ getRcvFile db rcvFileId
RcvFile {size = FileSize currentSize, chunks, redirect} <- ExceptT $ getRcvFile db rcvFileId
let rcvd = receivedSize chunks
complete = all chunkReceived chunks
(entityId, total) = case redirect of
Nothing -> (rcvFileEntityId, currentSize)
Just RcvFileRedirect {redirectFileInfo = RedirectFileInfo {size = FileSize finalSize}, redirectEntityId} -> (redirectEntityId, finalSize)
liftIO . when complete $ updateRcvFileStatus db rcvFileId RFSReceived
pure (complete, RFPROG rcvd total)
notify c rcvFileEntityId progress
when complete . void $
pure (entityId, complete, RFPROG rcvd total)
notify c entityId progress
when complete . lift . void $
getXFTPRcvWorker True c Nothing
where
receivedSize :: [RcvFileChunk] -> Int64
@@ -196,112 +227,158 @@ withRetryIntervalLimit maxN ri action =
withRetryIntervalCount ri $ \n delay loop ->
when (n < maxN) $ action delay loop
retryOnError :: AgentMonad m => Text -> m a -> m a -> AgentErrorType -> m a
retryOnError :: Text -> AM a -> AM a -> AgentErrorType -> AM a
retryOnError name loop done e = do
logError $ name <> " error: " <> tshow e
if temporaryAgentError e
then loop
else done
rcvWorkerInternalError :: AgentMonad m => AgentClient -> DBRcvFileId -> RcvFileId -> Maybe FilePath -> String -> m ()
rcvWorkerInternalError :: AgentClient -> DBRcvFileId -> RcvFileId -> Maybe FilePath -> String -> AM ()
rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath internalErrStr = do
forM_ tmpPath (removePath <=< toFSFilePath)
lift $ forM_ tmpPath (removePath <=< toFSFilePath)
withStore' c $ \db -> updateRcvFileError db rcvFileId internalErrStr
notify c rcvFileEntityId $ RFERR $ INTERNAL internalErrStr
runXFTPRcvLocalWorker :: forall m. AgentMonad m => AgentClient -> Worker -> m ()
runXFTPRcvLocalWorker :: AgentClient -> Worker -> AM ()
runXFTPRcvLocalWorker c Worker {doWork} = do
cfg <- asks config
forever $ do
waitForWork doWork
lift $ waitForWork doWork
atomically $ assertAgentForeground c
runXFTPOperation cfg
where
runXFTPOperation :: AgentConfig -> m ()
runXFTPOperation :: AgentConfig -> AM ()
runXFTPOperation AgentConfig {rcvFilesTTL} =
withWork c doWork (`getNextRcvFileToDecrypt` rcvFilesTTL) $
\f@RcvFile {rcvFileId, rcvFileEntityId, tmpPath} ->
decryptFile f `catchAgentError` (rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath . show)
decryptFile :: RcvFile -> m ()
decryptFile RcvFile {rcvFileId, rcvFileEntityId, key, nonce, tmpPath, saveFile, status, chunks} = do
decryptFile :: RcvFile -> AM ()
decryptFile RcvFile {rcvFileId, rcvFileEntityId, size, digest, key, nonce, tmpPath, saveFile, status, chunks, redirect} = do
let CryptoFile savePath cfArgs = saveFile
fsSavePath <- toFSFilePath savePath
when (status == RFSDecrypting) $
fsSavePath <- lift $ toFSFilePath savePath
lift . when (status == RFSDecrypting) $
whenM (doesFileExist fsSavePath) (removeFile fsSavePath >> createEmptyFile fsSavePath)
withStore' c $ \db -> updateRcvFileStatus db rcvFileId RFSDecrypting
chunkPaths <- getChunkPaths chunks
encSize <- liftIO $ foldM (\s path -> (s +) . fromIntegral <$> getFileSize path) 0 chunkPaths
when (FileSize encSize /= size) $ throwError $ XFTP XFTP.SIZE
encDigest <- liftIO $ LC.sha512Hash <$> readChunks chunkPaths
when (FileDigest encDigest /= digest) $ throwError $ XFTP XFTP.DIGEST
let destFile = CryptoFile fsSavePath cfArgs
void $ liftError (INTERNAL . show) $ decryptChunks encSize chunkPaths key nonce $ \_ -> pure destFile
notify c rcvFileEntityId $ RFDONE fsSavePath
forM_ tmpPath (removePath <=< toFSFilePath)
atomically $ waitUntilForeground c
withStore' c (`updateRcvFileComplete` rcvFileId)
case redirect of
Nothing -> do
notify c rcvFileEntityId $ RFDONE fsSavePath
lift $ forM_ tmpPath (removePath <=< toFSFilePath)
atomically $ 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
withStore' c (`updateRcvFileComplete` rcvFileId)
-- proceed with redirect
yaml <- liftError (INTERNAL . show) (CF.readFile $ CryptoFile fsSavePath cfArgs) `agentFinally` (lift $ toFSFilePath fsSavePath >>= removePath)
next@FileDescription {chunks = nextChunks} <- case strDecode (LB.toStrict yaml) of
Left _ -> throwError . XFTP $ XFTP.REDIRECT "decode error"
Right (ValidFileDescription fd@FileDescription {size = dstSize, digest = dstDigest})
| dstSize /= redirectSize -> throwError . XFTP $ XFTP.REDIRECT "size mismatch"
| dstDigest /= redirectDigest -> throwError . XFTP $ XFTP.REDIRECT "digest mismatch"
| otherwise -> pure fd
-- register and download chunks from the actual file
withStore c $ \db -> updateRcvFileRedirect db redirectDbId next
forM_ nextChunks (downloadChunk c)
where
getChunkPaths :: [RcvFileChunk] -> m [FilePath]
getChunkPaths :: [RcvFileChunk] -> AM [FilePath]
getChunkPaths [] = pure []
getChunkPaths (RcvFileChunk {chunkTmpPath = Just path} : cs) = do
ps <- getChunkPaths cs
fsPath <- toFSFilePath path
fsPath <- lift $ toFSFilePath path
pure $ fsPath : ps
getChunkPaths (RcvFileChunk {chunkTmpPath = Nothing} : _cs) =
throwError $ INTERNAL "no chunk path"
xftpDeleteRcvFile' :: AgentMonad m => AgentClient -> RcvFileId -> m ()
xftpDeleteRcvFile' c rcvFileEntityId = do
RcvFile {rcvFileId, prefixPath, status} <- withStore c $ \db -> getRcvFileByEntityId db rcvFileEntityId
if status == RFSComplete || status == RFSError
then do
removePath prefixPath
withStore' c (`deleteRcvFile'` rcvFileId)
else withStore' c (`updateRcvFileDeleted` rcvFileId)
xftpDeleteRcvFile' :: AgentClient -> RcvFileId -> AM' ()
xftpDeleteRcvFile' c rcvFileEntityId = xftpDeleteRcvFiles' c [rcvFileEntityId]
notify :: forall m e. (MonadUnliftIO m, AEntityI e) => AgentClient -> EntityId -> ACommand 'Agent e -> m ()
xftpDeleteRcvFiles' :: AgentClient -> [RcvFileId] -> AM' ()
xftpDeleteRcvFiles' c rcvFileEntityIds = do
rcvFiles <- rights <$> withStoreBatch c (\db -> map (fmap (first storeError) . getRcvFileByEntityId db) rcvFileEntityIds)
redirects <- rights <$> batchFiles getRcvFileRedirects rcvFiles
let (toDelete, toMarkDeleted) = partition fileComplete $ concat redirects <> rcvFiles
void $ batchFiles deleteRcvFile' toDelete
void $ batchFiles updateRcvFileDeleted toMarkDeleted
workPath <- getXFTPWorkPath
liftIO . forM_ toDelete $ \RcvFile {prefixPath} ->
(removePath . (workPath </>)) prefixPath `catchAll_` pure ()
where
fileComplete RcvFile {status} = status == RFSComplete || status == RFSError
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 -> ACommand 'Agent e -> m ()
notify c entId cmd = atomically $ writeTBQueue (subQ c) ("", entId, APC (sAEntity @e) cmd)
xftpSendFile' :: AgentMonad m => AgentClient -> UserId -> CryptoFile -> Int -> m SndFileId
xftpSendFile' :: AgentClient -> UserId -> CryptoFile -> Int -> AM SndFileId
xftpSendFile' c userId file numRecipients = do
g <- asks random
prefixPath <- getPrefixPath "snd.xftp"
prefixPath <- lift $ getPrefixPath "snd.xftp"
createDirectory prefixPath
let relPrefixPath = takeFileName prefixPath
key <- atomically $ C.randomSbKey g
nonce <- atomically $ C.randomCbNonce g
-- saving absolute filePath will not allow to restore file encryption after app update, but it's a short window
fId <- withStore c $ \db -> createSndFile db g userId file numRecipients relPrefixPath key nonce
void $ getXFTPSndWorker True c Nothing
fId <- withStore c $ \db -> createSndFile db g userId file numRecipients relPrefixPath key nonce Nothing
lift . void $ getXFTPSndWorker True c Nothing
pure fId
resumeXFTPSndWork :: AgentMonad' m => AgentClient -> Maybe XFTPServer -> m ()
xftpSendDescription' :: AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Int -> AM SndFileId
xftpSendDescription' c userId (ValidFileDescription fdDirect@FileDescription {size, digest}) numRecipients = do
g <- asks random
prefixPath <- lift $ getPrefixPath "snd.xftp"
createDirectory prefixPath
let relPrefixPath = takeFileName prefixPath
let directYaml = prefixPath </> "direct.yaml"
cfArgs <- atomically $ CF.randomArgs g
let file = CryptoFile directYaml (Just cfArgs)
liftError (INTERNAL . show) $ CF.writeFile file (LB.fromStrict $ strEncode fdDirect)
key <- atomically $ C.randomSbKey g
nonce <- atomically $ C.randomCbNonce g
fId <- withStore c $ \db -> createSndFile db g userId file numRecipients relPrefixPath key nonce $ Just RedirectFileInfo {size, digest}
lift . void $ getXFTPSndWorker True c Nothing
pure fId
resumeXFTPSndWork :: AgentClient -> Maybe XFTPServer -> AM' ()
resumeXFTPSndWork = void .: getXFTPSndWorker False
getXFTPSndWorker :: AgentMonad' m => Bool -> AgentClient -> Maybe XFTPServer -> m Worker
getXFTPSndWorker :: Bool -> AgentClient -> Maybe XFTPServer -> AM' Worker
getXFTPSndWorker hasWork c server = do
ws <- asks $ xftpSndWorkers . xftpAgent
getAgentWorker hasWork c server ws $
getAgentWorker "xftp_snd" hasWork c server ws $
maybe (runXFTPSndPrepareWorker c) (runXFTPSndWorker c) server
runXFTPSndPrepareWorker :: forall m. AgentMonad m => AgentClient -> Worker -> m ()
runXFTPSndPrepareWorker :: AgentClient -> Worker -> AM ()
runXFTPSndPrepareWorker c Worker {doWork} = do
cfg <- asks config
forever $ do
waitForWork doWork
lift $ waitForWork doWork
atomically $ assertAgentForeground c
runXFTPOperation cfg
where
runXFTPOperation :: AgentConfig -> m ()
runXFTPOperation :: AgentConfig -> AM ()
runXFTPOperation cfg@AgentConfig {sndFilesTTL} =
withWork c doWork (`getNextSndFileToPrepare` sndFilesTTL) $
\f@SndFile {sndFileId, sndFileEntityId, prefixPath} ->
prepareFile cfg f `catchAgentError` (sndWorkerInternalError c sndFileId sndFileEntityId prefixPath . show)
prepareFile :: AgentConfig -> SndFile -> m ()
prepareFile :: AgentConfig -> SndFile -> AM ()
prepareFile _ SndFile {prefixPath = Nothing} =
throwError $ INTERNAL "no prefix path"
prepareFile cfg sndFile@SndFile {sndFileId, userId, prefixPath = Just ppath, status} = do
SndFile {numRecipients, chunks} <-
if status /= SFSEncrypted -- status is SFSNew or SFSEncrypting
then do
fsEncPath <- toFSFilePath $ sndFileEncPath ppath
fsEncPath <- lift . toFSFilePath $ sndFileEncPath ppath
when (status == SFSEncrypting) . whenM (doesFileExist fsEncPath) $
removeFile fsEncPath
withStore' c $ \db -> updateSndFileStatus db sndFileId SFSEncrypting
@@ -313,39 +390,41 @@ runXFTPSndPrepareWorker c Worker {doWork} = do
let numRecipients' = min numRecipients maxRecipients
-- concurrently?
-- separate worker to create chunks? record retries and delay on snd_file_chunks?
forM_ (filter (not . chunkCreated) chunks) $ createChunk numRecipients'
forM_ (filter (\SndFileChunk {replicas} -> null replicas) chunks) $ createChunk numRecipients'
withStore' c $ \db -> updateSndFileStatus db sndFileId SFSUploading
where
AgentConfig {xftpMaxRecipientsPerRequest = maxRecipients, messageRetryInterval = ri} = cfg
encryptFileForUpload :: SndFile -> FilePath -> m (FileDigest, [(XFTPChunkSpec, FileDigest)])
encryptFileForUpload SndFile {key, nonce, srcFile} fsEncPath = do
encryptFileForUpload :: SndFile -> FilePath -> AM (FileDigest, [(XFTPChunkSpec, FileDigest)])
encryptFileForUpload SndFile {key, nonce, srcFile, redirect} fsEncPath = do
let CryptoFile {filePath} = srcFile
fileName = takeFileName filePath
fileSize <- liftIO $ fromInteger <$> CF.getFileContentsSize srcFile
when (fileSize > maxFileSize) $ throwError $ INTERNAL "max file size exceeded"
when (fileSize > maxFileSizeHard) $ throwError $ INTERNAL "max file size exceeded"
let fileHdr = smpEncode FileHeader {fileName, fileExtra = Nothing}
fileSize' = fromIntegral (B.length fileHdr) + fileSize
chunkSizes = prepareChunkSizes $ fileSize' + fileSizeLen + authTagSize
chunkSizes' = map fromIntegral chunkSizes
encSize = sum chunkSizes'
payloadSize = fileSize' + fileSizeLen + authTagSize
chunkSizes <- case redirect of
Nothing -> pure $ prepareChunkSizes payloadSize
Just _ -> case singleChunkSize payloadSize of
Nothing -> throwError $ INTERNAL "max file size exceeded for redirect"
Just chunkSize -> pure [chunkSize]
let encSize = sum $ map fromIntegral chunkSizes
void $ liftError (INTERNAL . show) $ encryptFile srcFile fileHdr key nonce fileSize' encSize fsEncPath
digest <- liftIO $ LC.sha512Hash <$> LB.readFile fsEncPath
let chunkSpecs = prepareChunkSpecs fsEncPath chunkSizes
chunkDigests <- map FileDigest <$> mapM (liftIO . getChunkDigest) chunkSpecs
pure (FileDigest digest, zip chunkSpecs chunkDigests)
chunkCreated :: SndFileChunk -> Bool
chunkCreated SndFileChunk {replicas} =
any (\SndFileChunkReplica {replicaStatus} -> replicaStatus == SFRSCreated) replicas
createChunk :: Int -> SndFileChunk -> m ()
chunkDigests <- liftIO $ mapM getChunkDigest chunkSpecs
pure (FileDigest digest, zip chunkSpecs $ coerce chunkDigests)
createChunk :: Int -> SndFileChunk -> AM ()
createChunk numRecipients' ch = do
atomically $ assertAgentForeground c
(replica, ProtoServerWithAuth srv _) <- tryCreate
withStore' c $ \db -> createSndFileReplica db ch replica
void $ getXFTPSndWorker True c (Just srv)
lift . void $ getXFTPSndWorker True c (Just srv)
where
tryCreate = do
usedSrvs <- newTVarIO ([] :: [XFTPServer])
withRetryInterval (riFast ri) $ \_ loop ->
withRetryInterval (riFast ri) $ \_ loop -> do
lift $ waitForUserNetwork c
createWithNextSrv usedSrvs
`catchAgentError` \e -> retryOnError "XFTP prepare worker" (retryLoop loop) (throwError e) e
where
@@ -357,42 +436,43 @@ runXFTPSndPrepareWorker c Worker {doWork} = do
replica <- agentXFTPNewChunk c ch numRecipients' srvAuth
pure (replica, srvAuth)
sndWorkerInternalError :: AgentMonad m => AgentClient -> DBSndFileId -> SndFileId -> Maybe FilePath -> String -> m ()
sndWorkerInternalError :: AgentClient -> DBSndFileId -> SndFileId -> Maybe FilePath -> String -> AM ()
sndWorkerInternalError c sndFileId sndFileEntityId prefixPath internalErrStr = do
forM_ prefixPath $ removePath <=< toFSFilePath
lift . forM_ prefixPath $ removePath <=< toFSFilePath
withStore' c $ \db -> updateSndFileError db sndFileId internalErrStr
notify c sndFileEntityId $ SFERR $ INTERNAL internalErrStr
runXFTPSndWorker :: forall m. AgentMonad m => AgentClient -> XFTPServer -> Worker -> m ()
runXFTPSndWorker :: AgentClient -> XFTPServer -> Worker -> AM ()
runXFTPSndWorker c srv Worker {doWork} = do
cfg <- asks config
forever $ do
waitForWork doWork
lift $ waitForWork doWork
atomically $ assertAgentForeground c
runXFTPOperation cfg
where
runXFTPOperation :: AgentConfig -> m ()
runXFTPOperation :: AgentConfig -> AM ()
runXFTPOperation cfg@AgentConfig {sndFilesTTL, reconnectInterval = ri, xftpNotifyErrsOnRetry = notifyOnRetry, xftpConsecutiveRetries} = do
withWork c doWork (\db -> getNextSndChunkToUpload db srv sndFilesTTL) $ \case
SndFileChunk {sndFileId, sndFileEntityId, filePrefixPath, replicas = []} -> sndWorkerInternalError c sndFileId sndFileEntityId (Just filePrefixPath) "chunk has no replicas"
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 ->
withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do
lift $ waitForUserNetwork c
uploadFileChunk cfg fc replica
`catchAgentError` \e -> retryOnError "XFTP snd worker" (retryLoop loop e delay') (retryDone e) e
where
retryLoop loop e replicaDelay = do
flip catchAgentError (\_ -> pure ()) $ do
when notifyOnRetry $ notify c sndFileEntityId $ SFERR e
closeXFTPServerClient c userId server digest
liftIO $ closeXFTPServerClient c userId server digest
withStore' c $ \db -> updateSndChunkReplicaDelay db sndChunkReplicaId replicaDelay
atomically $ assertAgentForeground c
loop
retryDone e = sndWorkerInternalError c sndFileId sndFileEntityId (Just filePrefixPath) (show e)
uploadFileChunk :: AgentConfig -> SndFileChunk -> SndFileChunkReplica -> m ()
uploadFileChunk :: AgentConfig -> SndFileChunk -> SndFileChunkReplica -> AM ()
uploadFileChunk AgentConfig {xftpMaxRecipientsPerRequest = maxRecipients} sndFileChunk@SndFileChunk {sndFileId, userId, chunkSpec = chunkSpec@XFTPChunkSpec {filePath}, digest = chunkDigest} replica = do
replica'@SndFileChunkReplica {sndChunkReplicaId} <- addRecipients sndFileChunk replica
fsFilePath <- toFSFilePath filePath
fsFilePath <- lift $ toFSFilePath filePath
unlessM (doesFileExist fsFilePath) $ throwError $ INTERNAL "encrypted file doesn't exist on upload"
let chunkSpec' = chunkSpec {filePath = fsFilePath} :: XFTPChunkSpec
atomically $ assertAgentForeground c
@@ -408,10 +488,10 @@ runXFTPSndWorker c srv Worker {doWork} = do
when complete $ do
(sndDescr, rcvDescrs) <- sndFileToDescrs sf
notify c sndFileEntityId $ SFDONE sndDescr rcvDescrs
forM_ prefixPath $ removePath <=< toFSFilePath
lift . forM_ prefixPath $ removePath <=< toFSFilePath
withStore' c $ \db -> updateSndFileComplete db sndFileId
where
addRecipients :: SndFileChunk -> SndFileChunkReplica -> m SndFileChunkReplica
addRecipients :: SndFileChunk -> SndFileChunkReplica -> AM SndFileChunkReplica
addRecipients ch@SndFileChunk {numRecipients} cr@SndFileChunkReplica {rcvIdsKeys}
| length rcvIdsKeys > numRecipients = throwError $ INTERNAL "too many recipients"
| length rcvIdsKeys == numRecipients = pure cr
@@ -420,22 +500,22 @@ runXFTPSndWorker c srv Worker {doWork} = do
rcvIdsKeys' <- agentXFTPAddRecipients c userId chunkDigest cr numRecipients'
cr' <- withStore' c $ \db -> addSndChunkReplicaRecipients db cr $ L.toList rcvIdsKeys'
addRecipients ch cr'
sndFileToDescrs :: SndFile -> m (ValidFileDescription 'FSender, [ValidFileDescription 'FRecipient])
sndFileToDescrs :: SndFile -> AM (ValidFileDescription 'FSender, [ValidFileDescription 'FRecipient])
sndFileToDescrs SndFile {digest = Nothing} = throwError $ INTERNAL "snd file has no digest"
sndFileToDescrs SndFile {chunks = []} = throwError $ INTERNAL "snd file has no chunks"
sndFileToDescrs SndFile {digest = Just digest, key, nonce, chunks = chunks@(fstChunk : _)} = do
sndFileToDescrs SndFile {digest = Just digest, key, nonce, chunks = chunks@(fstChunk : _), redirect} = do
let chunkSize = FileSize $ sndChunkSize fstChunk
size = FileSize $ sum $ map (fromIntegral . sndChunkSize) chunks
-- snd description
sndDescrChunks <- mapM toSndDescrChunk chunks
let fdSnd = FileDescription {party = SFSender, size, digest, key, nonce, chunkSize, chunks = sndDescrChunks}
let fdSnd = FileDescription {party = SFSender, size, digest, key, nonce, chunkSize, chunks = sndDescrChunks, redirect = Nothing}
validFdSnd <- either (throwError . INTERNAL) pure $ validateFileDescription fdSnd
-- rcv descriptions
let fdRcv = FileDescription {party = SFRecipient, size, digest, key, nonce, chunkSize, chunks = []}
let fdRcv = FileDescription {party = SFRecipient, size, digest, key, nonce, chunkSize, chunks = [], redirect}
fdRcvs = createRcvFileDescriptions fdRcv chunks
validFdRcvs <- either (throwError . INTERNAL) pure $ mapM validateFileDescription fdRcvs
pure (validFdSnd, validFdRcvs)
toSndDescrChunk :: SndFileChunk -> m FileChunk
toSndDescrChunk :: SndFileChunk -> AM FileChunk
toSndDescrChunk SndFileChunk {replicas = []} = throwError $ INTERNAL "snd file chunk has no replicas"
toSndDescrChunk ch@SndFileChunk {chunkNo, digest = chDigest, replicas = (SndFileChunkReplica {server, replicaId, replicaKey} : _)} = do
let chunkSize = FileSize $ sndChunkSize ch
@@ -486,57 +566,72 @@ runXFTPSndWorker c srv Worker {doWork} = do
chunkUploaded SndFileChunk {replicas} =
any (\SndFileChunkReplica {replicaStatus} -> replicaStatus == SFRSUploaded) replicas
deleteSndFileInternal :: AgentMonad m => AgentClient -> SndFileId -> m ()
deleteSndFileInternal c sndFileEntityId = do
SndFile {sndFileId, prefixPath, status} <- withStore c $ \db -> getSndFileByEntityId db sndFileEntityId
if status == SFSComplete || status == SFSError
then do
forM_ prefixPath $ removePath <=< toFSFilePath
withStore' c (`deleteSndFile'` sndFileId)
else withStore' c (`updateSndFileDeleted` sndFileId)
deleteSndFileInternal :: AgentClient -> SndFileId -> AM' ()
deleteSndFileInternal c sndFileEntityId = deleteSndFilesInternal c [sndFileEntityId]
deleteSndFileRemote :: forall m. AgentMonad m => AgentClient -> UserId -> SndFileId -> ValidFileDescription 'FSender -> m ()
deleteSndFileRemote c userId sndFileEntityId (ValidFileDescription FileDescription {chunks}) = do
deleteSndFileInternal c sndFileEntityId `catchAgentError` (notify c sndFileEntityId . SFERR)
forM_ chunks $ \ch -> deleteFileChunk ch `catchAgentError` (notify c sndFileEntityId . SFERR)
deleteSndFilesInternal :: AgentClient -> [SndFileId] -> AM' ()
deleteSndFilesInternal c sndFileEntityIds = do
sndFiles <- rights <$> withStoreBatch c (\db -> map (fmap (first storeError) . getSndFileByEntityId db) sndFileEntityIds)
let (toDelete, toMarkDeleted) = partition fileComplete sndFiles
workPath <- getXFTPWorkPath
liftIO . forM_ toDelete $ \SndFile {prefixPath} ->
mapM_ (removePath . (workPath </>)) prefixPath `catchAll_` pure ()
batchFiles_ deleteSndFile' toDelete
batchFiles_ updateSndFileDeleted toMarkDeleted
where
deleteFileChunk :: FileChunk -> m ()
deleteFileChunk FileChunk {digest, replicas = replica@FileChunkReplica {server} : _} = do
withStore' c $ \db -> createDeletedSndChunkReplica db userId replica digest
void $ getXFTPDelWorker True c server
deleteFileChunk _ = pure ()
fileComplete SndFile {status} = status == SFSComplete || status == SFSError
batchFiles_ :: (DB.Connection -> DBSndFileId -> IO a) -> [SndFile] -> AM' ()
batchFiles_ f sndFiles = void $ withStoreBatch' c $ \db -> map (\SndFile {sndFileId} -> f db sndFileId) sndFiles
resumeXFTPDelWork :: AgentMonad' m => AgentClient -> XFTPServer -> m ()
deleteSndFileRemote :: AgentClient -> UserId -> SndFileId -> ValidFileDescription 'FSender -> AM' ()
deleteSndFileRemote c userId sndFileEntityId sfd = deleteSndFilesRemote c userId [(sndFileEntityId, sfd)]
deleteSndFilesRemote :: AgentClient -> UserId -> [(SndFileId, ValidFileDescription 'FSender)] -> AM' ()
deleteSndFilesRemote c userId sndFileIdsDescrs = do
deleteSndFilesInternal c (map fst sndFileIdsDescrs) `E.catchAny` (notify c "" . SFERR . INTERNAL . show)
let rs = concatMap (mapMaybe chunkReplica . fdChunks . snd) sndFileIdsDescrs
void $ withStoreBatch' c (\db -> map (uncurry $ createDeletedSndChunkReplica db userId) rs)
let servers = S.fromList $ map (\(FileChunkReplica {server}, _) -> server) rs
mapM_ (getXFTPDelWorker True c) servers
where
fdChunks (ValidFileDescription FileDescription {chunks}) = chunks
chunkReplica :: FileChunk -> Maybe (FileChunkReplica, FileDigest)
chunkReplica = \case
FileChunk {digest, replicas = replica : _} -> Just (replica, digest)
_ -> Nothing
resumeXFTPDelWork :: AgentClient -> XFTPServer -> AM' ()
resumeXFTPDelWork = void .: getXFTPDelWorker False
getXFTPDelWorker :: AgentMonad' m => Bool -> AgentClient -> XFTPServer -> m Worker
getXFTPDelWorker :: Bool -> AgentClient -> XFTPServer -> AM' Worker
getXFTPDelWorker hasWork c server = do
ws <- asks $ xftpDelWorkers . xftpAgent
getAgentWorker hasWork c server ws $ runXFTPDelWorker c server
getAgentWorker "xftp_del" hasWork c server ws $ runXFTPDelWorker c server
runXFTPDelWorker :: forall m. AgentMonad m => AgentClient -> XFTPServer -> Worker -> m ()
runXFTPDelWorker :: AgentClient -> XFTPServer -> Worker -> AM ()
runXFTPDelWorker c srv Worker {doWork} = do
cfg <- asks config
forever $ do
waitForWork doWork
lift $ waitForWork doWork
atomically $ assertAgentForeground c
runXFTPOperation cfg
where
runXFTPOperation :: AgentConfig -> m ()
runXFTPOperation :: AgentConfig -> AM ()
runXFTPOperation AgentConfig {rcvFilesTTL, reconnectInterval = ri, xftpNotifyErrsOnRetry = notifyOnRetry, xftpConsecutiveRetries} = do
-- no point in deleting files older than rcv ttl, as they will be expired on server
withWork c doWork (\db -> getNextDeletedSndChunkReplica db srv rcvFilesTTL) processDeletedReplica
where
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 ->
withRetryIntervalLimit xftpConsecutiveRetries ri' $ \delay' loop -> do
lift $ waitForUserNetwork c
deleteChunkReplica
`catchAgentError` \e -> retryOnError "XFTP del worker" (retryLoop loop e delay') (retryDone e) e
where
retryLoop loop e replicaDelay = do
flip catchAgentError (\_ -> pure ()) $ do
when notifyOnRetry $ notify c "" $ SFERR e
closeXFTPServerClient c userId server chunkDigest
liftIO $ closeXFTPServerClient c userId server chunkDigest
withStore' c $ \db -> updateDeletedSndChunkReplicaDelay db deletedSndChunkReplicaId replicaDelay
atomically $ assertAgentForeground c
loop
@@ -545,7 +640,7 @@ runXFTPDelWorker c srv Worker {doWork} = do
agentXFTPDeleteChunk c userId replica
withStore' c $ \db -> deleteDeletedSndChunkReplica db deletedSndChunkReplicaId
delWorkerInternalError :: AgentMonad m => AgentClient -> Int64 -> AgentErrorType -> m ()
delWorkerInternalError :: AgentClient -> Int64 -> AgentErrorType -> AM ()
delWorkerInternalError c deletedSndChunkReplicaId e = do
withStore' c $ \db -> deleteDeletedSndChunkReplica db deletedSndChunkReplicaId
notify c "" $ SFERR e
+89 -30
View File
@@ -4,11 +4,14 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
module Simplex.FileTransfer.Client where
import Control.Logger.Simple
import Control.Monad
import Control.Monad.Except
import Crypto.Random (ChaChaDRG)
@@ -20,10 +23,12 @@ import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Time (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.Description (mb)
import Simplex.FileTransfer.Protocol
import Simplex.FileTransfer.Server.Env (supportedXFTPhandshakes)
import Simplex.FileTransfer.Transport
import Simplex.Messaging.Client
( NetworkConfig (..),
@@ -37,6 +42,7 @@ import Simplex.Messaging.Client
import Simplex.Messaging.Client.Agent ()
import qualified Simplex.Messaging.Crypto as C
import qualified Simplex.Messaging.Crypto.Lazy as LC
import Simplex.Messaging.Encoding (smpDecode, smpEncode)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol
( BasicAuth,
@@ -45,24 +51,27 @@ import Simplex.Messaging.Protocol
RecipientId,
SenderId,
)
import Simplex.Messaging.Transport (supportedParameters)
import Simplex.Messaging.Transport.Client (TransportClientConfig, TransportHost)
import Simplex.Messaging.Transport (ALPN, HandshakeError (VERSION), THandleAuth (..), THandleParams (..), TransportError (..), TransportPeer (..), supportedParameters)
import Simplex.Messaging.Transport.Client (TransportClientConfig, TransportHost, alpn)
import Simplex.Messaging.Transport.HTTP2
import Simplex.Messaging.Transport.HTTP2.Client
import Simplex.Messaging.Transport.HTTP2.File
import Simplex.Messaging.Util (bshow, liftEitherError, whenM)
import Simplex.Messaging.Util (bshow, liftEitherWith, liftError', tshow, whenM)
import Simplex.Messaging.Version (compatibleVersion, pattern Compatible)
import UnliftIO
import UnliftIO.Directory
data XFTPClient = XFTPClient
{ http2Client :: HTTP2Client,
transportSession :: TransportSession FileResponse,
thParams :: THandleParams XFTPVersion 'TClient,
config :: XFTPClientConfig
}
data XFTPClientConfig = XFTPClientConfig
{ xftpNetworkConfig :: NetworkConfig,
uploadTimeoutPerMb :: Int64
serverVRange :: VersionRangeXFTP,
clientALPN :: Maybe [ALPN]
}
data XFTPChunkBody = XFTPChunkBody
@@ -84,12 +93,13 @@ defaultXFTPClientConfig :: XFTPClientConfig
defaultXFTPClientConfig =
XFTPClientConfig
{ xftpNetworkConfig = defaultNetworkConfig,
uploadTimeoutPerMb = 10000000 -- 10 seconds
serverVRange = supportedFileServerVRange,
clientALPN = Just supportedXFTPhandshakes
}
getXFTPClient :: TransportSession FileResponse -> XFTPClientConfig -> (XFTPClient -> IO ()) -> IO (Either XFTPClientError XFTPClient)
getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {xftpNetworkConfig} disconnected = runExceptT $ do
let tcConfig = transportClientConfig xftpNetworkConfig
getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, xftpNetworkConfig, serverVRange} disconnected = runExceptT $ do
let tcConfig = (transportClientConfig xftpNetworkConfig) {alpn = clientALPN}
http2Config = xftpHTTP2Config tcConfig config
username = proxyUsername transportSession
ProtocolServer _ host port keyHash = srv
@@ -97,11 +107,53 @@ getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {xftpNetworkC
clientVar <- newTVarIO Nothing
let usePort = if null port then "443" else port
clientDisconnected = readTVarIO clientVar >>= mapM_ disconnected
http2Client <- liftEitherError xftpClientError $ getVerifiedHTTP2Client (Just username) useHost usePort (Just keyHash) Nothing http2Config clientDisconnected
let c = XFTPClient {http2Client, transportSession, config}
http2Client <- liftError' xftpClientError $ getVerifiedHTTP2Client (Just username) useHost usePort (Just keyHash) Nothing http2Config clientDisconnected
let HTTP2Client {sessionId, sessionALPN} = http2Client
thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = VersionXFTP 1, thAuth = Nothing, implySessId = False, batch = True}
logDebug $ "Client negotiated handshake protocol: " <> tshow sessionALPN
thParams@THandleParams {thVersion} <- case sessionALPN of
Just "xftp/1" -> xftpClientHandshakeV1 serverVRange keyHash http2Client thParams0
Nothing -> pure thParams0
_ -> throwError $ PCETransportError (TEHandshake VERSION)
logDebug $ "Client negotiated protocol: " <> tshow thVersion
let c = XFTPClient {http2Client, thParams, transportSession, config}
atomically $ writeTVar clientVar $ Just c
pure c
xftpClientHandshakeV1 :: VersionRangeXFTP -> C.KeyHash -> HTTP2Client -> THandleParamsXFTP 'TClient -> ExceptT XFTPClientError IO (THandleParamsXFTP 'TClient)
xftpClientHandshakeV1 serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {sessionId, serverKey} thParams0 = do
shs@XFTPServerHandshake {authPubKey = ck} <- getServerHandshake
(v, sk) <- processServerHandshake shs
sendClientHandshake XFTPClientHandshake {xftpVersion = v, keyHash}
pure thParams0 {thAuth = Just THAuthClient {serverPeerPubKey = sk, serverCertKey = ck, sessSecret = Nothing}, thVersion = v}
where
getServerHandshake :: ExceptT XFTPClientError IO XFTPServerHandshake
getServerHandshake = do
let helloReq = H.requestNoBody "POST" "/" []
HTTP2Response {respBody = HTTP2Body {bodyHead = shsBody}} <-
liftError' (const $ PCEResponseError HANDSHAKE) $ sendRequest c helloReq Nothing
liftHS . smpDecode =<< liftHS (C.unPad shsBody)
processServerHandshake :: XFTPServerHandshake -> ExceptT XFTPClientError IO (VersionXFTP, C.PublicKeyX25519)
processServerHandshake XFTPServerHandshake {xftpVersionRange, sessionId = serverSessId, authPubKey = serverAuth} = do
unless (sessionId == serverSessId) $ throwError $ PCEResponseError SESSION
case xftpVersionRange `compatibleVersion` serverVRange of
Nothing -> throwError $ PCEResponseError HANDSHAKE
Just (Compatible v) ->
fmap (v,) . liftHS $ do
let (X.CertificateChain cert, exact) = serverAuth
case cert of
[_leaf, ca] | XV.Fingerprint kh == XV.getFingerprint ca X.HashSHA256 -> pure ()
_ -> throwError "bad certificate"
pubKey <- maybe (throwError "bad server key type") (`C.verifyX509` exact) serverKey
C.x509ToPublic (pubKey, []) >>= C.pubKey
sendClientHandshake :: XFTPClientHandshake -> ExceptT XFTPClientError IO ()
sendClientHandshake chs = do
chs' <- liftHS $ C.pad (smpEncode chs) xftpBlockSize
let chsReq = H.requestBuilder "POST" "/" [] $ byteString chs'
HTTP2Response {respBody = HTTP2Body {bodyHead}} <- liftError' (const $ PCEResponseError HANDSHAKE) $ sendRequest c chsReq Nothing
unless (B.null bodyHead) $ throwError $ PCEResponseError HANDSHAKE
liftHS = liftEitherWith (const $ PCEResponseError HANDSHAKE)
closeXFTPClient :: XFTPClient -> IO ()
closeXFTPClient XFTPClient {http2Client} = closeHTTP2Client http2Client
@@ -131,21 +183,23 @@ xftpClientError = \case
HCNetworkError -> PCENetworkError
HCIOError e -> PCEIOError e
sendXFTPCommand :: forall p. FilePartyI p => XFTPClient -> C.APrivateSignKey -> XFTPFileId -> FileCommand p -> Maybe XFTPChunkSpec -> ExceptT XFTPClientError IO (FileResponse, HTTP2Body)
sendXFTPCommand c@XFTPClient {http2Client = HTTP2Client {sessionId}} pKey fId cmd chunkSpec_ = do
sendXFTPCommand :: forall p. FilePartyI p => XFTPClient -> C.APrivateAuthKey -> XFTPFileId -> FileCommand p -> Maybe XFTPChunkSpec -> ExceptT XFTPClientError IO (FileResponse, HTTP2Body)
sendXFTPCommand c@XFTPClient {thParams} pKey fId cmd chunkSpec_ = do
-- TODO random corrId
let corrIdUsedAsNonce = ""
t <-
liftEither . first PCETransportError $
xftpEncodeTransmission sessionId (Just pKey) ("", fId, FileCmd (sFileParty @p) cmd)
xftpEncodeAuthTransmission thParams pKey (corrIdUsedAsNonce, fId, FileCmd (sFileParty @p) cmd)
sendXFTPTransmission c t chunkSpec_
sendXFTPTransmission :: XFTPClient -> ByteString -> Maybe XFTPChunkSpec -> ExceptT XFTPClientError IO (FileResponse, HTTP2Body)
sendXFTPTransmission XFTPClient {config, http2Client = http2@HTTP2Client {sessionId}} t chunkSpec_ = do
sendXFTPTransmission XFTPClient {config, thParams, http2Client} t chunkSpec_ = do
let req = H.requestStreaming N.methodPost "/" [] streamBody
reqTimeout = (\XFTPChunkSpec {chunkSize} -> chunkTimeout config chunkSize) <$> chunkSpec_
HTTP2Response {respBody = body@HTTP2Body {bodyHead}} <- liftEitherError xftpClientError $ sendRequest http2 req reqTimeout
reqTimeout = xftpReqTimeout config $ (\XFTPChunkSpec {chunkSize} -> chunkSize) <$> chunkSpec_
HTTP2Response {respBody = body@HTTP2Body {bodyHead}} <- withExceptT xftpClientError . ExceptT $ sendRequest http2Client req (Just reqTimeout)
when (B.length bodyHead /= xftpBlockSize) $ throwError $ PCEResponseError BLOCK
-- TODO validate that the file ID is the same as in the request?
(_, _, (_, _fId, respOrErr)) <- liftEither . first PCEResponseError $ xftpDecodeTransmission sessionId bodyHead
(_, _, (_, _fId, respOrErr)) <- liftEither . first PCEResponseError $ xftpDecodeTransmission thParams bodyHead
case respOrErr of
Right r -> case protocolError r of
Just e -> throwError $ PCEProtocolError e
@@ -163,9 +217,9 @@ sendXFTPTransmission XFTPClient {config, http2Client = http2@HTTP2Client {sessio
createXFTPChunk ::
XFTPClient ->
C.APrivateSignKey ->
C.APrivateAuthKey ->
FileInfo ->
NonEmpty C.APublicVerifyKey ->
NonEmpty C.APublicAuthKey ->
Maybe BasicAuth ->
ExceptT XFTPClientError IO (SenderId, NonEmpty RecipientId)
createXFTPChunk c spKey file rcps auth_ =
@@ -173,17 +227,17 @@ createXFTPChunk c spKey file rcps auth_ =
(FRSndIds sId rIds, body) -> noFile body (sId, rIds)
(r, _) -> throwError . PCEUnexpectedResponse $ bshow r
addXFTPRecipients :: XFTPClient -> C.APrivateSignKey -> XFTPFileId -> NonEmpty C.APublicVerifyKey -> ExceptT XFTPClientError IO (NonEmpty RecipientId)
addXFTPRecipients :: XFTPClient -> C.APrivateAuthKey -> XFTPFileId -> NonEmpty C.APublicAuthKey -> ExceptT XFTPClientError IO (NonEmpty RecipientId)
addXFTPRecipients c spKey fId rcps =
sendXFTPCommand c spKey fId (FADD rcps) Nothing >>= \case
(FRRcvIds rIds, body) -> noFile body rIds
(r, _) -> throwError . PCEUnexpectedResponse $ bshow r
uploadXFTPChunk :: XFTPClient -> C.APrivateSignKey -> XFTPFileId -> XFTPChunkSpec -> ExceptT XFTPClientError IO ()
uploadXFTPChunk :: XFTPClient -> C.APrivateAuthKey -> XFTPFileId -> XFTPChunkSpec -> ExceptT XFTPClientError IO ()
uploadXFTPChunk c spKey fId chunkSpec =
sendXFTPCommand c spKey fId FPUT (Just chunkSpec) >>= okResponse
downloadXFTPChunk :: TVar ChaChaDRG -> XFTPClient -> C.APrivateSignKey -> XFTPFileId -> XFTPRcvChunkSpec -> ExceptT XFTPClientError IO ()
downloadXFTPChunk :: TVar ChaChaDRG -> XFTPClient -> C.APrivateAuthKey -> XFTPFileId -> XFTPRcvChunkSpec -> ExceptT XFTPClientError IO ()
downloadXFTPChunk g c@XFTPClient {config} rpKey fId chunkSpec@XFTPRcvChunkSpec {filePath, chunkSize} = do
(rDhKey, rpDhKey) <- atomically $ C.generateKeyPair g
sendXFTPCommand c rpKey fId (FGET rDhKey) Nothing >>= \case
@@ -193,29 +247,34 @@ downloadXFTPChunk g c@XFTPClient {config} rpKey fId chunkSpec@XFTPRcvChunkSpec {
let dhSecret = C.dh' sDhKey rpDhKey
cbState <- liftEither . first PCECryptoError $ LC.cbInit dhSecret cbNonce
let t = chunkTimeout config chunkSize
t `timeout` download cbState >>= maybe (throwError PCEResponseTimeout) pure
ExceptT (sequence <$> (t `timeout` download cbState)) >>= maybe (throwError PCEResponseTimeout) pure
where
download cbState =
withExceptT PCEResponseError $
runExceptT . withExceptT PCEResponseError $
receiveEncFile chunkPart cbState chunkSpec `catchError` \e ->
whenM (doesFileExist filePath) (removeFile filePath) >> throwError e
_ -> throwError $ PCEResponseError NO_FILE
(r, _) -> throwError . PCEUnexpectedResponse $ bshow r
chunkTimeout :: XFTPClientConfig -> Word32 -> Int
chunkTimeout config chunkSize = fromIntegral $ (fromIntegral chunkSize * uploadTimeoutPerMb config) `div` mb 1
xftpReqTimeout :: XFTPClientConfig -> Maybe Word32 -> Int
xftpReqTimeout cfg@XFTPClientConfig {xftpNetworkConfig = NetworkConfig {tcpTimeout}} chunkSize_ =
maybe tcpTimeout (chunkTimeout cfg) chunkSize_
deleteXFTPChunk :: XFTPClient -> C.APrivateSignKey -> SenderId -> ExceptT XFTPClientError IO ()
chunkTimeout :: XFTPClientConfig -> Word32 -> Int
chunkTimeout XFTPClientConfig {xftpNetworkConfig = NetworkConfig {tcpTimeout, tcpTimeoutPerKb}} sz =
tcpTimeout + fromIntegral (min ((fromIntegral sz `div` 1024) * tcpTimeoutPerKb) (fromIntegral (maxBound :: Int)))
deleteXFTPChunk :: XFTPClient -> C.APrivateAuthKey -> SenderId -> ExceptT XFTPClientError IO ()
deleteXFTPChunk c spKey sId = sendXFTPCommand c spKey sId FDEL Nothing >>= okResponse
ackXFTPChunk :: XFTPClient -> C.APrivateSignKey -> RecipientId -> ExceptT XFTPClientError IO ()
ackXFTPChunk :: XFTPClient -> C.APrivateAuthKey -> RecipientId -> ExceptT XFTPClientError IO ()
ackXFTPChunk c rpKey rId = sendXFTPCommand c rpKey rId FACK Nothing >>= okResponse
pingXFTP :: XFTPClient -> ExceptT XFTPClientError IO ()
pingXFTP c@XFTPClient {http2Client = HTTP2Client {sessionId}} = do
pingXFTP c@XFTPClient {thParams} = do
t <-
liftEither . first PCETransportError $
xftpEncodeTransmission sessionId Nothing ("", "", FileCmd SFRecipient PING)
xftpEncodeTransmission thParams ("", "", FileCmd SFRecipient PING)
(r, _) <- sendXFTPTransmission c t Nothing
case r of
FRPong -> pure ()
+3 -2
View File
@@ -10,6 +10,7 @@ module Simplex.FileTransfer.Client.Agent where
import Control.Logger.Simple (logInfo)
import Control.Monad
import Control.Monad.Except
import Control.Monad.Trans (lift)
import Data.Bifunctor (first)
import qualified Data.ByteString.Char8 as B
import Data.Text (Text)
@@ -86,7 +87,7 @@ getXFTPServerClient XFTPClientAgent {xftpClients, config} srv = do
waitForXFTPClient :: XFTPClientVar -> ME XFTPClient
waitForXFTPClient clientVar = do
let XFTPClientConfig {xftpNetworkConfig = NetworkConfig {tcpConnectTimeout}} = xftpConfig config
client_ <- tcpConnectTimeout `timeout` atomically (readTMVar clientVar)
client_ <- liftIO $ tcpConnectTimeout `timeout` atomically (readTMVar clientVar)
liftEither $ case client_ of
Just (Right c) -> Right c
Just (Left e) -> Left e
@@ -110,7 +111,7 @@ getXFTPServerClient XFTPClientAgent {xftpClients, config} srv = do
TM.delete srv xftpClients
throwError e
tryConnectAsync :: ME ()
tryConnectAsync = void . async $ do
tryConnectAsync = void . lift . async . runExceptT $ do
withRetryInterval (reconnectInterval config) $ \_ loop -> void $ tryConnectClient loop
showServer :: XFTPServer -> Text
+37 -18
View File
@@ -15,9 +15,12 @@ module Simplex.FileTransfer.Client.Main
CLIError (..),
xftpClientCLI,
cliSendFile,
cliSendFileOpts,
singleChunkSize,
prepareChunkSizes,
prepareChunkSpecs,
maxFileSize,
maxFileSizeHard,
fileSizeLen,
getChunkDigest,
SentRecipientReplica (..),
@@ -34,13 +37,14 @@ import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Char (toLower)
import Data.Either (partitionEithers)
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)
import Data.Maybe (fromMaybe, listToMaybe)
import qualified Data.Text as T
import Data.Word (Word32)
import GHC.Records (HasField (getField))
@@ -62,7 +66,7 @@ import qualified Simplex.Messaging.Crypto.Lazy as LC
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String (StrEncoding (..))
import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), SenderId, SndPrivateSignKey, XFTPServer, XFTPServerWithAuth)
import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), SenderId, SndPrivateAuthKey, XFTPServer, XFTPServerWithAuth)
import Simplex.Messaging.Server.CLI (getCliCommand')
import Simplex.Messaging.Util (groupAllOn, ifM, tshow, whenM)
import System.Exit (exitFailure)
@@ -75,12 +79,17 @@ 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
@@ -208,25 +217,25 @@ cliCommandP =
data SentFileChunk = SentFileChunk
{ chunkNo :: Int,
sndId :: SenderId,
sndPrivateKey :: SndPrivateSignKey,
sndPrivateKey :: SndPrivateAuthKey,
chunkSize :: FileSize Word32,
digest :: FileDigest,
replicas :: [SentFileChunkReplica]
}
deriving (Eq, Show)
deriving (Show)
data SentFileChunkReplica = SentFileChunkReplica
{ server :: XFTPServer,
recipients :: [(ChunkReplicaId, C.APrivateSignKey)]
recipients :: [(ChunkReplicaId, C.APrivateAuthKey)]
}
deriving (Eq, Show)
deriving (Show)
data SentRecipientReplica = SentRecipientReplica
{ chunkNo :: Int,
server :: XFTPServer,
rcvNo :: Int,
replicaId :: ChunkReplicaId,
replicaKey :: C.APrivateSignKey,
replicaKey :: C.APrivateAuthKey,
digest :: FileDigest,
chunkSize :: FileSize Word32
}
@@ -297,8 +306,8 @@ cliSendFileOpts SendOptions {filePath, outputDir, numRecipients, xftpServers, re
withExceptT (CLIError . show) $ encryptFile srcFile fileHdr key nonce fileSize' encSize encPath
digest <- liftIO $ LC.sha512Hash <$> LB.readFile encPath
let chunkSpecs = prepareChunkSpecs encPath chunkSizes
fdRcv = FileDescription {party = SFRecipient, size = FileSize encSize, digest = FileDigest digest, key, nonce, chunkSize = FileSize defChunkSize, chunks = []}
fdSnd = FileDescription {party = SFSender, size = FileSize encSize, digest = FileDigest digest, key, nonce, chunkSize = FileSize defChunkSize, chunks = []}
fdRcv = FileDescription {party = SFRecipient, size = FileSize encSize, digest = FileDigest digest, key, nonce, chunkSize = FileSize defChunkSize, chunks = [], redirect = Nothing}
fdSnd = FileDescription {party = SFSender, size = FileSize encSize, digest = FileDigest digest, key, nonce, chunkSize = FileSize defChunkSize, chunks = [], redirect = Nothing}
logInfo $ "encrypted file to " <> tshow encPath
pure (encPath, fdRcv, fdSnd, chunkSpecs, encSize)
uploadFile :: TVar ChaChaDRG -> [XFTPChunkSpec] -> TVar [Int64] -> Int64 -> ExceptT CLIError IO [SentFileChunk]
@@ -313,13 +322,15 @@ cliSendFileOpts SendOptions {filePath, outputDir, numRecipients, xftpServers, re
-- the reason we don't do pooled downloads here within one server is that http2 library doesn't handle cleint concurrency, even though
-- upload doesn't allow other requests within the same client until complete (but download does allow).
logInfo $ "uploading " <> tshow (length chunks) <> " chunks..."
map snd . sortOn fst . concat <$> pooledForConcurrentlyN 16 chunks' (mapM $ uploadFileChunk a)
(errs, rs) <- partitionEithers . concat <$> liftIO (pooledForConcurrentlyN 16 chunks' . mapM $ runExceptT . uploadFileChunk a)
mapM_ throwError errs
pure $ map snd (sortOn fst rs)
where
uploadFileChunk :: XFTPClientAgent -> (Int, XFTPChunkSpec, XFTPServerWithAuth) -> ExceptT CLIError IO (Int, SentFileChunk)
uploadFileChunk a (chunkNo, chunkSpec@XFTPChunkSpec {chunkSize}, ProtoServerWithAuth xftpServer auth) = do
logInfo $ "uploading chunk " <> tshow chunkNo <> " to " <> showServer xftpServer <> "..."
(sndKey, spKey) <- atomically $ C.generateSignatureKeyPair C.SEd25519 g
rKeys <- atomically $ L.fromList <$> replicateM numRecipients (C.generateSignatureKeyPair C.SEd25519 g)
(sndKey, spKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
rKeys <- atomically $ L.fromList <$> replicateM numRecipients (C.generateAuthKeyPair C.SEd25519 g)
digest <- liftIO $ getChunkDigest chunkSpec
let ch = FileInfo {sndKey, size = fromIntegral chunkSize, digest}
c <- withRetry retryCount $ getXFTPServerClient a xftpServer
@@ -387,7 +398,7 @@ cliSendFileOpts SendOptions {filePath, outputDir, numRecipients, xftpServers, re
sentChunks
-- SentFileChunk having sndId and sndPrivateKey represents the current implementation's limitation
-- that sender uploads each chunk only to one server, so we can use the first replica's server for FileChunkReplica
sndReplicas :: [SentFileChunkReplica] -> ChunkReplicaId -> C.APrivateSignKey -> [FileChunkReplica]
sndReplicas :: [SentFileChunkReplica] -> ChunkReplicaId -> C.APrivateAuthKey -> [FileChunkReplica]
sndReplicas [] _ _ = []
sndReplicas (SentFileChunkReplica {server} : _) replicaId replicaKey = [FileChunkReplica {server, replicaId, replicaKey}]
writeFileDescriptions :: String -> [FileDescription 'FRecipient] -> FileDescription 'FSender -> IO ([FilePath], FilePath)
@@ -406,7 +417,8 @@ getChunkDigest :: XFTPChunkSpec -> IO ByteString
getChunkDigest XFTPChunkSpec {filePath = chunkPath, chunkOffset, chunkSize} =
withFile chunkPath ReadMode $ \h -> do
hSeek h AbsoluteSeek $ fromIntegral chunkOffset
LC.sha256Hash <$> LB.hGet h (fromIntegral chunkSize)
chunk <- LB.hGet h (fromIntegral chunkSize)
pure $! LC.sha256Hash chunk
cliReceiveFile :: ReceiveOptions -> ExceptT CLIError IO ()
cliReceiveFile ReceiveOptions {fileDescription, filePath, retryCount, tempPath, verbose, yes} =
@@ -424,7 +436,9 @@ cliReceiveFile ReceiveOptions {fileDescription, filePath, retryCount, tempPath,
FileChunkReplica {server} : _ -> server
srvChunks = groupAllOn srv chunks
g <- liftIO C.newRandom
chunkPaths <- map snd . sortOn fst . concat <$> pooledForConcurrentlyN 16 srvChunks (mapM $ downloadFileChunk g a encPath size downloadedChunks)
(errs, rs) <- partitionEithers . concat <$> liftIO (pooledForConcurrentlyN 16 srvChunks $ mapM $ runExceptT . downloadFileChunk g a encPath size downloadedChunks)
mapM_ throwError errs
let chunkPaths = map snd $ sortOn fst rs
encDigest <- liftIO $ LC.sha512Hash <$> readChunks chunkPaths
when (encDigest /= unFileDigest digest) $ throwError $ CLIError "File digest mismatch"
encSize <- liftIO $ foldM (\s path -> (s +) . fromIntegral <$> getFileSize path) 0 chunkPaths
@@ -521,14 +535,19 @@ getFileDescription' path =
getFileDescription path >>= \case
AVFD fd -> either (throwError . 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)
| otherwise = (chunkSize1, chunkSize2)
-- | size' > size34 chunkSize2 = (chunkSize1, chunkSize2)
-- | otherwise = (chunkSize0, chunkSize1)
| size' > size34 chunkSize2 = (chunkSize1, chunkSize2)
| otherwise = (chunkSize0, chunkSize1)
size34 sz = (fromIntegral sz * 3) `div` 4
prepareSizes 0 = []
prepareSizes size
+1 -1
View File
@@ -29,7 +29,7 @@ import UnliftIO.Directory (removeFile)
encryptFile :: CryptoFile -> ByteString -> C.SbKey -> C.CbNonce -> Int64 -> Int64 -> FilePath -> ExceptT FTCryptoError IO ()
encryptFile srcFile fileHdr key nonce fileSize' encSize encFile = do
sb <- liftEitherWith FTCECryptoError $ LC.sbInit key nonce
CF.withFile srcFile ReadMode $ \r -> withFile encFile WriteMode $ \w -> do
CF.withFile srcFile ReadMode $ \r -> ExceptT . withFile encFile WriteMode $ \w -> runExceptT $ do
let lenStr = smpEncode fileSize'
(hdr, !sb') = LC.sbEncryptChunk sb $ lenStr <> fileHdr
padLen = encSize - authTagSize - fileSize' - 8
+66 -10
View File
@@ -12,6 +12,7 @@
module Simplex.FileTransfer.Description
( FileDescription (..),
RedirectFileInfo (..),
AFileDescription (..),
ValidFileDescription, -- constructor is not exported, use pattern
pattern ValidFileDescription,
@@ -30,12 +31,17 @@ module Simplex.FileTransfer.Description
kb,
mb,
gb,
FileDescriptionURI (..),
FileClientData,
fileDescriptionURI,
qrSizeLimit,
)
where
import Control.Applicative (optional)
import Control.Monad ((<=<))
import Data.Aeson (FromJSON, ToJSON)
import qualified Data.Aeson as J
import qualified Data.Aeson.TH as J
import Data.Attoparsec.ByteString.Char8 (Parser)
import qualified Data.Attoparsec.ByteString.Char8 as A
@@ -50,17 +56,21 @@ import Data.Map (Map)
import qualified Data.Map 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 qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, parseAll)
import Simplex.Messaging.Protocol (XFTPServer)
import Simplex.Messaging.Util (bshow, (<$?>))
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
import Simplex.Messaging.Util (bshow, safeDecodeUtf8, (<$?>))
data FileDescription (p :: FileParty) = FileDescription
{ party :: SFileParty p,
@@ -69,7 +79,14 @@ data FileDescription (p :: FileParty) = FileDescription
key :: C.SbKey,
nonce :: C.CbNonce,
chunkSize :: FileSize Word32,
chunks :: [FileChunk]
chunks :: [FileChunk],
redirect :: Maybe RedirectFileInfo
}
deriving (Eq, Show)
data RedirectFileInfo = RedirectFileInfo
{ size :: FileSize Int64,
digest :: FileDigest
}
deriving (Eq, Show)
@@ -118,7 +135,7 @@ data FileChunk = FileChunk
data FileChunkReplica = FileChunkReplica
{ server :: XFTPServer,
replicaId :: ChunkReplicaId,
replicaKey :: C.APrivateSignKey
replicaKey :: C.APrivateAuthKey
}
deriving (Eq, Show)
@@ -147,7 +164,8 @@ data YAMLFileDescription = YAMLFileDescription
key :: C.SbKey,
nonce :: C.CbNonce,
chunkSize :: String,
replicas :: [YAMLServerReplicas]
replicas :: [YAMLServerReplicas],
redirect :: Maybe RedirectFileInfo
}
deriving (Eq, Show)
@@ -161,7 +179,7 @@ data FileServerReplica = FileServerReplica
{ chunkNo :: Int,
server :: XFTPServer,
replicaId :: ChunkReplicaId,
replicaKey :: C.APrivateSignKey,
replicaKey :: C.APrivateAuthKey,
digest :: Maybe FileDigest,
chunkSize :: Maybe (FileSize Word32)
}
@@ -170,8 +188,16 @@ data FileServerReplica = FileServerReplica
newtype FileSize a = FileSize {unFileSize :: a}
deriving (Eq, Show)
instance FromJSON a => FromJSON (FileSize a) where
parseJSON v = FileSize <$> Y.parseJSON v
instance ToJSON a => ToJSON (FileSize a) where
toJSON = Y.toJSON . unFileSize
$(J.deriveJSON defaultJSON ''YAMLServerReplicas)
$(J.deriveJSON defaultJSON ''RedirectFileInfo)
$(J.deriveJSON defaultJSON ''YAMLFileDescription)
instance FilePartyI p => StrEncoding (ValidFileDescription p) where
@@ -201,10 +227,10 @@ validateFileDescription fd@FileDescription {size, chunks}
| otherwise = Right $ ValidFD fd
where
chunkNos = map (\FileChunk {chunkNo} -> chunkNo) chunks
chunksSize = fromIntegral . foldl' (\s FileChunk {chunkSize} -> s + unFileSize chunkSize) 0
chunksSize = foldl' (\(s :: Int64) FileChunk {chunkSize} -> s + fromIntegral (unFileSize chunkSize)) 0
encodeFileDescription :: FileDescription p -> YAMLFileDescription
encodeFileDescription FileDescription {party, size, digest, key, nonce, chunkSize, chunks} =
encodeFileDescription FileDescription {party, size, digest, key, nonce, chunkSize, chunks, redirect} =
YAMLFileDescription
{ party = toFileParty party,
size = B.unpack $ strEncode size,
@@ -212,9 +238,39 @@ encodeFileDescription FileDescription {party, size, digest, key, nonce, chunkSiz
key,
nonce,
chunkSize = B.unpack $ strEncode chunkSize,
replicas = encodeFileReplicas chunkSize chunks
replicas = encodeFileReplicas chunkSize chunks,
redirect
}
data FileDescriptionURI = FileDescriptionURI
{ scheme :: ServiceScheme,
description :: ValidFileDescription 'FRecipient,
clientData :: Maybe FileClientData -- JSON-encoded extensions to pass in a link
}
deriving (Eq, Show)
type FileClientData = Text
fileDescriptionURI :: ValidFileDescription 'FRecipient -> FileDescriptionURI
fileDescriptionURI vfd = FileDescriptionURI SSSimplex vfd mempty
instance StrEncoding FileDescriptionURI where
strEncode FileDescriptionURI {scheme, description, clientData} = mconcat [strEncode scheme, "/file", "#/?", queryStr]
where
queryStr = strEncode $ QSP QEscape qs
qs = ("desc", strEncode description) : maybe [] (\cd -> [("data", encodeUtf8 cd)]) clientData
strP = do
scheme <- strP
_ <- "/file" <* optional (A.char '/') <* "#/?"
query <- strP
description <- queryParam "desc" query
let clientData = safeDecodeUtf8 <$> queryParamStr "data" query
pure FileDescriptionURI {scheme, description, clientData}
-- | URL length in QR code before jumping up to a next size.
qrSizeLimit :: Int
qrSizeLimit = 1002 -- ~2 chunks in URLencoded YAML with some spare size for server hosts
instance (Integral a, Show a) => StrEncoding (FileSize a) where
strEncode (FileSize b)
| b' /= 0 = bshow b
@@ -285,13 +341,13 @@ unfoldChunksToReplicas defChunkSize = concatMap chunkReplicas
in FileServerReplica {chunkNo, server, replicaId, replicaKey, digest = digest', chunkSize = chunkSize'}
decodeFileDescription :: YAMLFileDescription -> Either String AFileDescription
decodeFileDescription YAMLFileDescription {party, size, digest, key, nonce, chunkSize, replicas} = do
decodeFileDescription YAMLFileDescription {party, size, digest, key, nonce, chunkSize, replicas, redirect} = do
size' <- strDecode $ B.pack size
chunkSize' <- strDecode $ B.pack chunkSize
replicas' <- decodeFileParts replicas
chunks <- foldReplicasToChunks chunkSize' replicas'
pure $ case aFileParty party of
AFP party' -> AFD FileDescription {party = party', size = size', digest, key, nonce, chunkSize = chunkSize', chunks}
AFP party' -> AFD FileDescription {party = party', size = size', digest, key, nonce, chunkSize = chunkSize', chunks, redirect}
where
decodeFileParts = fmap concat . mapM decodeYAMLServerReplicas
+43 -113
View File
@@ -1,21 +1,22 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
module Simplex.FileTransfer.Protocol where
import Control.Applicative ((<|>))
import qualified Data.Aeson.TH as J
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bifunctor (first)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
@@ -24,10 +25,11 @@ 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.Messaging.Client (authTransmission)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Notifications.Transport (ntfClientHandshake)
import Simplex.Messaging.Parsers
import Simplex.Messaging.Protocol
( BasicAuth,
@@ -37,27 +39,25 @@ import Simplex.Messaging.Protocol
ProtocolErrorType (..),
ProtocolMsgTag (..),
ProtocolType (..),
RcvPublicAuthKey,
RcvPublicDhKey,
RcvPublicVerifyKey,
RecipientId,
SenderId,
SentRawTransmission,
SignedTransmission,
SndPublicVerifyKey,
SndPublicAuthKey,
Transmission,
TransmissionForAuth (..),
CorrId (..),
encodeTransmission,
encodeTransmissionForAuth,
messageTagP,
tDecodeParseValidate,
tEncode,
tEncodeBatch,
tEncodeBatch1,
tParse,
)
import Simplex.Messaging.Transport (SessionId, TransportError (..))
import Simplex.Messaging.Util (bshow, (<$?>))
import Simplex.Messaging.Version
currentXFTPVersion :: Version
currentXFTPVersion = 1
import Simplex.Messaging.Transport (THandleParams (..), TransportError (..), TransportPeer (..))
import Simplex.Messaging.Util ((<$?>))
xftpBlockSize :: Int
xftpBlockSize = 16384
@@ -139,18 +139,18 @@ instance ProtocolMsgTag FileCmdTag where
instance FilePartyI p => ProtocolMsgTag (FileCommandTag p) where
decodeTag s = decodeTag s >>= (\(FCT _ t) -> checkParty' t)
instance Protocol XFTPErrorType FileResponse where
instance Protocol XFTPVersion XFTPErrorType FileResponse where
type ProtoCommand FileResponse = FileCmd
type ProtoType FileResponse = 'PXFTP
protocolClientHandshake = ntfClientHandshake
protocolClientHandshake = xftpClientHandshakeStub
protocolPing = FileCmd SFRecipient PING
protocolError = \case
FRErr e -> Just e
_ -> Nothing
data FileCommand (p :: FileParty) where
FNEW :: FileInfo -> NonEmpty RcvPublicVerifyKey -> Maybe BasicAuth -> FileCommand FSender
FADD :: NonEmpty RcvPublicVerifyKey -> FileCommand FSender
FNEW :: FileInfo -> NonEmpty RcvPublicAuthKey -> Maybe BasicAuth -> FileCommand FSender
FADD :: NonEmpty RcvPublicAuthKey -> FileCommand FSender
FPUT :: FileCommand FSender
FDEL :: FileCommand FSender
FGET :: RcvPublicDhKey -> FileCommand FRecipient
@@ -164,15 +164,15 @@ data FileCmd = forall p. FilePartyI p => FileCmd (SFileParty p) (FileCommand p)
deriving instance Show FileCmd
data FileInfo = FileInfo
{ sndKey :: SndPublicVerifyKey,
{ sndKey :: SndPublicAuthKey,
size :: Word32,
digest :: ByteString
}
deriving (Eq, Show)
deriving (Show)
type XFTPFileId = ByteString
instance FilePartyI p => ProtocolEncoding XFTPErrorType (FileCommand p) where
instance FilePartyI p => ProtocolEncoding XFTPVersion XFTPErrorType (FileCommand p) where
type Tag (FileCommand p) = FileCommandTag p
encodeProtocol _v = \case
FNEW file rKeys auth_ -> e (FNEW_, ' ', file, rKeys, auth_)
@@ -188,24 +188,24 @@ instance FilePartyI p => ProtocolEncoding XFTPErrorType (FileCommand p) where
protocolP v tag = (\(FileCmd _ c) -> checkParty c) <$?> protocolP v (FCT (sFileParty @p) tag)
fromProtocolError = fromProtocolError @XFTPErrorType @FileResponse
fromProtocolError = fromProtocolError @XFTPVersion @XFTPErrorType @FileResponse
{-# INLINE fromProtocolError #-}
checkCredentials (sig, _, fileId, _) cmd = case cmd of
checkCredentials (auth, _, fileId, _) cmd = case cmd of
-- FNEW must not have signature and chunk ID
FNEW {}
| isNothing sig -> Left $ CMD NO_AUTH
| isNothing auth -> Left $ CMD NO_AUTH
| not (B.null fileId) -> Left $ CMD HAS_AUTH
| otherwise -> Right cmd
PING
| isNothing sig && B.null fileId -> Right cmd
| isNothing auth && B.null fileId -> Right cmd
| otherwise -> Left $ CMD HAS_AUTH
-- other client commands must have both signature and queue ID
_
| isNothing sig || B.null fileId -> Left $ CMD NO_AUTH
| isNothing auth || B.null fileId -> Left $ CMD NO_AUTH
| otherwise -> Right cmd
instance ProtocolEncoding XFTPErrorType FileCmd where
instance ProtocolEncoding XFTPVersion XFTPErrorType FileCmd where
type Tag FileCmd = FileCmdTag
encodeProtocol _v (FileCmd _ c) = encodeProtocol _v c
@@ -222,7 +222,7 @@ instance ProtocolEncoding XFTPErrorType FileCmd where
FACK_ -> pure FACK
PING_ -> pure PING
fromProtocolError = fromProtocolError @XFTPErrorType @FileResponse
fromProtocolError = fromProtocolError @XFTPVersion @XFTPErrorType @FileResponse
{-# INLINE fromProtocolError #-}
checkCredentials t (FileCmd p c) = FileCmd p <$> checkCredentials t c
@@ -273,7 +273,7 @@ data FileResponse
| FRPong
deriving (Show)
instance ProtocolEncoding XFTPErrorType FileResponse where
instance ProtocolEncoding XFTPVersion XFTPErrorType FileResponse where
type Tag FileResponse = FileResponseTag
encodeProtocol _v = \case
FRSndIds fId rIds -> e (FRSndIds_, ' ', fId, rIds)
@@ -316,74 +316,6 @@ instance ProtocolEncoding XFTPErrorType FileResponse where
| B.null entId = Right cmd
| otherwise = Left $ CMD HAS_AUTH
data XFTPErrorType
= -- | incorrect block format, encoding or signature size
BLOCK
| -- | incorrect SMP session ID (TLS Finished message / tls-unique binding RFC5929)
SESSION
| -- | SMP command is unknown or has invalid syntax
CMD {cmdErr :: CommandError}
| -- | command authorization error - bad signature or non-existing SMP queue
AUTH
| -- | incorrent file size
SIZE
| -- | storage quota exceeded
QUOTA
| -- | incorrent file digest
DIGEST
| -- | file encryption/decryption failed
CRYPTO
| -- | no expected file body in request/response or no file on the server
NO_FILE
| -- | unexpected file body
HAS_FILE
| -- | file IO error
FILE_IO
| -- | internal server error
INTERNAL
| -- | used internally, never returned by the server (to be removed)
DUPLICATE_ -- not part of SMP protocol, used internally
deriving (Eq, Read, Show)
instance StrEncoding XFTPErrorType where
strEncode = \case
CMD e -> "CMD " <> bshow e
e -> bshow e
strP = "CMD " *> (CMD <$> parseRead1) <|> parseRead1
instance Encoding XFTPErrorType where
smpEncode = \case
BLOCK -> "BLOCK"
SESSION -> "SESSION"
CMD err -> "CMD " <> smpEncode err
AUTH -> "AUTH"
SIZE -> "SIZE"
QUOTA -> "QUOTA"
DIGEST -> "DIGEST"
CRYPTO -> "CRYPTO"
NO_FILE -> "NO_FILE"
HAS_FILE -> "HAS_FILE"
FILE_IO -> "FILE_IO"
INTERNAL -> "INTERNAL"
DUPLICATE_ -> "DUPLICATE_"
smpP =
A.takeTill (== ' ') >>= \case
"BLOCK" -> pure BLOCK
"SESSION" -> pure SESSION
"CMD" -> CMD <$> _smpP
"AUTH" -> pure AUTH
"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
"INTERNAL" -> pure INTERNAL
"DUPLICATE_" -> pure DUPLICATE_
_ -> fail "bad error type"
checkParty :: forall t p p'. (FilePartyI p, FilePartyI p') => t p' -> Either String (t p)
checkParty c = case testEquality (sFileParty @p) (sFileParty @p') of
Just Refl -> Right c
@@ -394,27 +326,25 @@ checkParty' c = case testEquality (sFileParty @p) (sFileParty @p') of
Just Refl -> Just c
_ -> Nothing
xftpEncodeTransmission :: ProtocolEncoding e c => SessionId -> Maybe C.APrivateSignKey -> Transmission c -> Either TransportError ByteString
xftpEncodeTransmission sessionId pKey (corrId, fId, msg) = do
let t = encodeTransmission currentXFTPVersion sessionId (corrId, fId, msg)
xftpEncodeBatch1 $ signTransmission t
where
signTransmission :: ByteString -> SentRawTransmission
signTransmission t = ((`C.sign` t) <$> pKey, t)
xftpEncodeAuthTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion 'TClient -> C.APrivateAuthKey -> Transmission c -> Either TransportError ByteString
xftpEncodeAuthTransmission thParams@THandleParams {thAuth} pKey (corrId, fId, msg) = do
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (corrId, fId, msg)
xftpEncodeBatch1 . (,tToSend) =<< authTransmission thAuth (Just pKey) (C.cbNonce $ bs corrId) tForAuth
xftpEncodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion p -> Transmission c -> Either TransportError ByteString
xftpEncodeTransmission thParams (corrId, fId, msg) = do
let t = encodeTransmission thParams (corrId, fId, msg)
xftpEncodeBatch1 (Nothing, t)
-- this function uses batch syntax but puts only one transmission in the batch
xftpEncodeBatch1 :: (Maybe C.ASignature, ByteString) -> Either TransportError ByteString
xftpEncodeBatch1 (sig, t) =
let t' = tEncodeBatch 1 . smpEncode . Large $ tEncode (sig, t)
in first (const TELargeMsg) $ C.pad t' xftpBlockSize
xftpEncodeBatch1 :: SentRawTransmission -> Either TransportError ByteString
xftpEncodeBatch1 t = first (const TELargeMsg) $ C.pad (tEncodeBatch1 t) xftpBlockSize
xftpDecodeTransmission :: ProtocolEncoding e c => SessionId -> ByteString -> Either XFTPErrorType (SignedTransmission e c)
xftpDecodeTransmission sessionId t = do
xftpDecodeTransmission :: ProtocolEncoding XFTPVersion e c => THandleParams XFTPVersion p -> ByteString -> Either XFTPErrorType (SignedTransmission e c)
xftpDecodeTransmission thParams t = do
t' <- first (const BLOCK) $ C.unPad t
case tParse True t' of
t'' :| [] -> Right $ tDecodeParseValidate sessionId currentXFTPVersion t''
case tParse thParams t' of
t'' :| [] -> Right $ tDecodeParseValidate thParams t''
_ -> Left BLOCK
$(J.deriveJSON (enumJSON $ dropPrefix "F") ''FileParty)
$(J.deriveJSON (sumTypeJSON id) ''XFTPErrorType)
+285 -112
View File
@@ -3,38 +3,44 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
module Simplex.FileTransfer.Server where
import Control.Logger.Simple
import Control.Monad
import Control.Monad.Except
import Control.Monad.IO.Unlift (MonadUnliftIO)
import Control.Monad.Reader
import Data.Bifunctor (first)
import qualified Data.ByteString.Base64.URL as B64
import Data.ByteString.Builder (byteString)
import Data.ByteString.Builder (Builder, byteString)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Functor (($>))
import Data.Int (Int64)
import Data.List (intercalate)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as L
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.Stats (getRTSStats)
import qualified Network.HTTP.Types as N
import qualified Network.HTTP2.Server as H
import Network.Socket
import Simplex.FileTransfer.Protocol
import Simplex.FileTransfer.Server.Control
import Simplex.FileTransfer.Server.Env
import Simplex.FileTransfer.Server.Stats
import Simplex.FileTransfer.Server.Store
@@ -42,25 +48,39 @@ import Simplex.FileTransfer.Server.StoreLog
import Simplex.FileTransfer.Transport
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, RcvPublicDhKey, RcvPublicVerifyKey, RecipientId)
import Simplex.Messaging.Server (dummyVerifyCmd, verifyCmdSignature)
import Simplex.Messaging.Protocol (CorrId (..), RcvPublicAuthKey, RcvPublicDhKey, RecipientId, TransmissionAuth)
import Simplex.Messaging.Server (dummyVerifyCmd, verifyCmdAuthorization)
import Simplex.Messaging.Server.Expiration
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.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.Util
import Simplex.Messaging.Version (isCompatible)
import System.Exit (exitFailure)
import System.FilePath ((</>))
import System.IO (BufferMode (..), hPutStrLn, hSetBuffering)
import UnliftIO (IOMode (..), withFile)
import System.IO (hPrint, hPutStrLn, universalNewlineMode)
import UnliftIO
import UnliftIO.Concurrent (threadDelay)
import UnliftIO.Directory (doesFileExist, removeFile, renameFile)
import UnliftIO.Exception
import UnliftIO.STM
import qualified UnliftIO.Exception as E
type M a = ReaderT XFTPEnv IO a
data XFTPTransportRequest = XFTPTransportRequest
{ thParams :: THandleParamsXFTP 'TServer,
reqBody :: HTTP2Body,
request :: H.Request,
sendResponse :: H.Response -> IO ()
}
runXFTPServer :: XFTPServerConfig -> IO ()
runXFTPServer cfg = do
started <- newEmptyTMVarIO
@@ -69,19 +89,75 @@ runXFTPServer cfg = do
runXFTPServerBlocking :: TMVar Bool -> XFTPServerConfig -> IO ()
runXFTPServerBlocking started cfg = newXFTPServerEnv cfg >>= runReaderT (xftpServer cfg started)
data Handshake
= HandshakeSent C.PrivateKeyX25519
| HandshakeAccepted (THandleAuth 'TServer) VersionXFTP
xftpServer :: XFTPServerConfig -> TMVar Bool -> M ()
xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration} started = do
xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration, fileExpiration} started = do
mapM_ (expireServerFiles Nothing) fileExpiration
restoreServerStats
raceAny_ (runServer : expireFilesThread_ cfg <> serverStatsThread_ cfg) `finally` stopServer
raceAny_ (runServer : expireFilesThread_ cfg <> serverStatsThread_ cfg <> controlPortThread_ cfg) `finally` stopServer
where
runServer :: M ()
runServer = do
serverParams <- asks tlsServerParams
let (chain, pk) = tlsServerCredentials serverParams
signKey <- liftIO $ case C.x509ToPrivate (pk, []) >>= C.privKey of
Right pk' -> pure pk'
Left e -> putStrLn ("servers has no valid key: " <> show e) >> exitFailure
env <- ask
liftIO $
runHTTP2Server started xftpPort defaultHTTP2BufferSize serverParams transportConfig inactiveClientExpiration $ \sessionId r sendResponse -> do
reqBody <- getHTTP2Body r xftpBlockSize
processRequest HTTP2Request {sessionId, request = r, reqBody, sendResponse} `runReaderT` env
sessions <- atomically TM.empty
let cleanup sessionId = atomically $ TM.delete sessionId sessions
liftIO . runHTTP2Server started xftpPort defaultHTTP2BufferSize serverParams transportConfig inactiveClientExpiration cleanup $ \sessionId sessionALPN r sendResponse -> do
reqBody <- getHTTP2Body r xftpBlockSize
let thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = VersionXFTP 1, thAuth = Nothing, implySessId = False, batch = True}
req0 = XFTPTransportRequest {thParams = thParams0, request = r, reqBody, sendResponse}
flip runReaderT env $ case sessionALPN of
Nothing -> processRequest req0
Just "xftp/1" ->
xftpServerHandshakeV1 chain signKey sessions req0 >>= \case
Nothing -> pure () -- handshake response sent
Just thParams -> processRequest req0 {thParams} -- proceed with new version (XXX: may as well switch the request handler here)
_ -> liftIO . sendResponse $ H.responseNoBody N.ok200 [] -- shouldn't happen: means server picked handshake protocol it doesn't know about
xftpServerHandshakeV1 :: X.CertificateChain -> C.APrivateSignKey -> TMap SessionId Handshake -> XFTPTransportRequest -> M (Maybe (THandleParams XFTPVersion 'TServer))
xftpServerHandshakeV1 chain serverSignKey sessions XFTPTransportRequest {thParams = thParams@THandleParams {sessionId}, reqBody = HTTP2Body {bodyHead}, sendResponse} = do
s <- atomically $ TM.lookup sessionId sessions
r <- runExceptT $ case s of
Nothing -> processHello
Just (HandshakeSent pk) -> processClientHandshake pk
Just (HandshakeAccepted auth v) -> pure $ Just thParams {thAuth = Just auth, thVersion = v}
either sendError pure r
where
processHello = do
unless (B.null bodyHead) $ throwError HANDSHAKE
(k, pk) <- atomically . C.generateKeyPair =<< asks random
atomically $ TM.insert sessionId (HandshakeSent pk) sessions
let authPubKey = (chain, C.signX509 serverSignKey $ C.publicToX509 k)
let hs = XFTPServerHandshake {xftpVersionRange = supportedFileServerVRange, sessionId, authPubKey}
shs <- encodeXftp hs
liftIO . sendResponse $ H.responseBuilder N.ok200 [] shs
pure Nothing
processClientHandshake pk = do
unless (B.length bodyHead == xftpBlockSize) $ throwError HANDSHAKE
body <- liftHS $ C.unPad bodyHead
XFTPClientHandshake {xftpVersion, keyHash} <- liftHS $ smpDecode body
kh <- asks serverIdentity
unless (keyHash == kh) $ throwError HANDSHAKE
unless (xftpVersion `isCompatible` supportedFileServerVRange) $ throwError HANDSHAKE
let auth = THAuthServer {serverPrivKey = pk, sessSecret' = Nothing}
atomically $ TM.insert sessionId (HandshakeAccepted auth xftpVersion) sessions
liftIO . sendResponse $ H.responseNoBody N.ok200 []
pure Nothing
sendError :: XFTPErrorType -> M (Maybe (THandleParams XFTPVersion 'TServer))
sendError err = do
runExceptT (encodeXftp err) >>= \case
Right bs -> liftIO . sendResponse $ H.responseBuilder N.ok200 [] bs
Left _ -> logError $ "Error encoding handshake error: " <> tshow err
pure Nothing
encodeXftp :: Encoding a => a -> ExceptT XFTPErrorType (ReaderT XFTPEnv IO) Builder
encodeXftp a = byteString <$> liftHS (C.pad (smpEncode a) xftpBlockSize)
liftHS = liftEitherWith (const HANDSHAKE)
stopServer :: M ()
stopServer = do
@@ -94,25 +170,10 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
expireFiles :: ExpirationConfig -> M ()
expireFiles expCfg = do
st <- asks store
let interval = checkInterval expCfg * 1000000
forever $ do
liftIO $ threadDelay' interval
old <- liftIO $ expireBeforeEpoch expCfg
sIds <- M.keysSet <$> readTVarIO (files st)
forM_ sIds $ \sId -> do
threadDelay 100000
atomically (expiredFilePath st sId old)
>>= mapM_ (remove $ delete st sId)
where
remove del filePath =
ifM
(doesFileExist filePath)
(removeFile filePath >> del `catch` \(e :: SomeException) -> logError $ "failed to remove expired file " <> tshow filePath <> ": " <> tshow e)
del
delete st sId = do
withFileLog (`logDeleteFile` sId)
void $ atomically $ deleteFile st sId
expireServerFiles (Just 100000) expCfg
serverStatsThread_ :: XFTPServerConfig -> [M ()]
serverStatsThread_ XFTPServerConfig {logStatsInterval = Just interval, logStatsStartTime, serverStatsLogFile} =
@@ -124,7 +185,7 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
initialDelay <- (startAt -) . fromIntegral . (`div` 1000000_000000) . diffTimeToPicoseconds . utctDayTime <$> liftIO getCurrentTime
liftIO $ putStrLn $ "server stats log enabled: " <> statsFilePath
liftIO $ threadDelay' $ 1_000_000 * (initialDelay + if initialDelay < 0 then 86_400 else 0)
FileServerStats {fromTime, filesCreated, fileRecipients, filesUploaded, filesDeleted, filesDownloaded, fileDownloads, fileDownloadAcks, filesCount, filesSize} <- asks serverStats
FileServerStats {fromTime, filesCreated, fileRecipients, filesUploaded, filesExpired, filesDeleted, filesDownloaded, fileDownloads, fileDownloadAcks, filesCount, filesSize} <- asks serverStats
let interval = 1_000_000 * logInterval
forever $ do
withFile statsFilePath AppendMode $ \h -> liftIO $ do
@@ -134,12 +195,13 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
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' <- atomically $ swapTVar filesCount 0
filesSize' <- atomically $ swapTVar filesSize 0
filesCount' <- readTVarIO filesCount
filesSize' <- readTVarIO filesSize
hPutStrLn h $
intercalate
","
@@ -154,25 +216,91 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
show fileDownloads',
show fileDownloadAcks',
show filesCount',
show filesSize'
show filesSize',
show filesExpired'
]
liftIO $ threadDelay' interval
controlPortThread_ :: XFTPServerConfig -> [M ()]
controlPortThread_ XFTPServerConfig {controlPort = Just port} = [runCPServer port]
controlPortThread_ _ = []
runCPServer :: ServiceName -> M ()
runCPServer port = do
cpStarted <- newEmptyTMVarIO
u <- askUnliftIO
liftIO $ do
labelMyThread "control port server"
runTCPServer cpStarted port $ runCPClient u
where
runCPClient :: UnliftIO (ReaderT XFTPEnv IO) -> Socket -> IO ()
runCPClient u sock = do
labelMyThread "control port client"
h <- socketToHandle sock ReadWriteMode
hSetBuffering h LineBuffering
hSetNewlineMode h universalNewlineMode
hPutStrLn h "XFTP server control port\n'help' for supported commands"
role <- newTVarIO CPRNone
cpLoop h role
where
cpLoop h role = do
s <- trimCR <$> B.hGetLine h
case strDecode s of
Right CPQuit -> hClose h
Right cmd -> logCmd s cmd >> processCP h role cmd >> cpLoop h role
Left err -> hPutStrLn h ("error: " <> err) >> cpLoop h role
logCmd s cmd = when shouldLog $ logWarn $ "ControlPort: " <> tshow s
where
shouldLog = case cmd of
CPAuth _ -> False
CPHelp -> False
CPQuit -> False
CPSkip -> False
_ -> True
processCP h role = \case
CPAuth auth -> atomically $ writeTVar role $! newRole cfg
where
newRole XFTPServerConfig {controlPortUserAuth = user, controlPortAdminAuth = admin}
| Just auth == admin = CPRAdmin
| Just auth == user = CPRUser
| otherwise = CPRNone
CPStatsRTS -> E.tryAny getRTSStats >>= either (hPrint h) (hPrint h)
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
ExceptT $ deleteServerFile_ fr
liftIO . hPutStrLn h $ either (\e -> "error: " <> show e) (\() -> "ok") r
CPHelp -> hPutStrLn h "commands: stats-rts, delete, help, quit"
CPQuit -> pure ()
CPSkip -> pure ()
where
withUserRole action =
readTVarIO role >>= \case
CPRAdmin -> action
CPRUser -> action
_ -> do
logError "Unauthorized control port command"
hPutStrLn h "AUTH"
data ServerFile = ServerFile
{ filePath :: FilePath,
fileSize :: Word32,
sbState :: LC.SbState
}
processRequest :: HTTP2Request -> M ()
processRequest HTTP2Request {sessionId, reqBody = body@HTTP2Body {bodyHead}, sendResponse}
processRequest :: XFTPTransportRequest -> M ()
processRequest XFTPTransportRequest {thParams, reqBody = body@HTTP2Body {bodyHead}, sendResponse}
| B.length bodyHead /= xftpBlockSize = sendXFTPResponse ("", "", FRErr BLOCK) Nothing
| otherwise = do
case xftpDecodeTransmission sessionId bodyHead of
Right (sig_, signed, (corrId, fId, cmdOrErr)) -> do
case xftpDecodeTransmission thParams bodyHead of
Right (sig_, signed, (corrId, fId, cmdOrErr)) ->
case cmdOrErr of
Right cmd -> do
verifyXFTPTransmission sig_ signed fId cmd >>= \case
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
Left e -> send (FRErr e) Nothing
@@ -180,9 +308,8 @@ processRequest HTTP2Request {sessionId, reqBody = body@HTTP2Body {bodyHead}, sen
send resp = sendXFTPResponse (corrId, fId, resp)
Left e -> sendXFTPResponse ("", "", FRErr e) Nothing
where
sendXFTPResponse :: (CorrId, XFTPFileId, FileResponse) -> Maybe ServerFile -> M ()
sendXFTPResponse (corrId, fId, resp) serverFile_ = do
let t_ = xftpEncodeTransmission sessionId Nothing (corrId, fId, resp)
let t_ = xftpEncodeTransmission thParams (corrId, fId, resp)
liftIO $ sendResponse $ H.responseStreaming N.ok200 [] $ streamBody t_
where
streamBody t_ send done = do
@@ -199,10 +326,10 @@ processRequest HTTP2Request {sessionId, reqBody = body@HTTP2Body {bodyHead}, sen
data VerificationResult = VRVerified XFTPRequest | VRFailed
verifyXFTPTransmission :: Maybe C.ASignature -> ByteString -> XFTPFileId -> FileCmd -> M VerificationResult
verifyXFTPTransmission sig_ signed fId cmd =
verifyXFTPTransmission :: Maybe (THandleAuth 'TServer, C.CbNonce) -> Maybe TransmissionAuth -> ByteString -> XFTPFileId -> FileCmd -> M VerificationResult
verifyXFTPTransmission auth_ tAuth authorized fId cmd =
case cmd of
FileCmd SFSender (FNEW file rcps auth) -> pure $ XFTPReqNew file rcps auth `verifyWith` sndKey file
FileCmd SFSender (FNEW file rcps auth') -> pure $ XFTPReqNew file rcps auth' `verifyWith` sndKey file
FileCmd SFRecipient PING -> pure $ VRVerified XFTPReqPing
FileCmd party _ -> verifyCmd party
where
@@ -213,8 +340,9 @@ verifyXFTPTransmission sig_ signed fId cmd =
where
verify = \case
Right (fr, k) -> XFTPReqCmd fId fr cmd `verifyWith` k
_ -> maybe False (dummyVerifyCmd signed) sig_ `seq` VRFailed
req `verifyWith` k = if verifyCmdSignature sig_ signed k then VRVerified req else VRFailed
_ -> maybe False (dummyVerifyCmd Nothing authorized) tAuth `seq` VRFailed
-- TODO verify with DH authorization
req `verifyWith` k = if verifyCmdAuthorization auth_ tAuth authorized k then VRVerified req else VRFailed
processXFTPRequest :: HTTP2Body -> XFTPRequest -> M (FileResponse, Maybe ServerFile)
processXFTPRequest HTTP2Body {bodyPart} = \case
@@ -235,7 +363,7 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
XFTPReqPing -> noFile FRPong
where
noFile resp = pure (resp, Nothing)
createFile :: FileInfo -> NonEmpty RcvPublicVerifyKey -> M FileResponse
createFile :: FileInfo -> NonEmpty RcvPublicAuthKey -> M FileResponse
createFile file rks = do
st <- asks store
r <- runExceptT $ do
@@ -245,7 +373,7 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
-- TODO validate body empty
sId <- ExceptT $ addFileRetry st file 3 ts
rcps <- mapM (ExceptT . addRecipientRetry st 3 sId) rks
withFileLog $ \sl -> do
lift $ withFileLog $ \sl -> do
logAddFile sl sId file ts
logAddRecipients sl sId rcps
stats <- asks serverStats
@@ -259,7 +387,7 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
retryAdd n $ \sId -> runExceptT $ do
ExceptT $ addFile st sId file ts
pure sId
addRecipientRetry :: FileStore -> Int -> XFTPFileId -> RcvPublicVerifyKey -> M (Either XFTPErrorType FileRecipient)
addRecipientRetry :: FileStore -> Int -> XFTPFileId -> RcvPublicAuthKey -> M (Either XFTPErrorType FileRecipient)
addRecipientRetry st n sId rpk =
retryAdd n $ \rId -> runExceptT $ do
let rcp = FileRecipient rId rpk
@@ -272,80 +400,80 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
atomically (add fId) >>= \case
Left DUPLICATE_ -> retryAdd (n - 1) add
r -> pure r
addRecipients :: XFTPFileId -> NonEmpty RcvPublicVerifyKey -> M FileResponse
addRecipients :: XFTPFileId -> NonEmpty RcvPublicAuthKey -> M FileResponse
addRecipients sId rks = do
st <- asks store
r <- runExceptT $ do
rcps <- mapM (ExceptT . addRecipientRetry st 3 sId) rks
withFileLog $ \sl -> logAddRecipients sl sId rcps
lift $ withFileLog $ \sl -> logAddRecipients sl sId rcps
stats <- asks serverStats
atomically $ modifyTVar' (fileRecipients stats) (+ length rks)
let rIds = L.map (\(FileRecipient rId _) -> rId) rcps
pure $ FRRcvIds rIds
pure $ either FRErr id r
receiveServerFile :: FileRec -> M FileResponse
receiveServerFile fr@FileRec {senderId, fileInfo} = case bodyPart of
-- TODO do not allow repeated file upload
receiveServerFile FileRec {senderId, fileInfo = FileInfo {size, digest}, filePath} = case bodyPart of
Nothing -> pure $ FRErr SIZE
Just getBody -> do
-- TODO validate body size before downloading, once it's populated
path <- asks $ filesPath . config
let fPath = path </> B.unpack (B64.encode senderId)
FileInfo {size, digest} = fileInfo
withFileLog $ \sl -> logPutFile sl senderId fPath
st <- asks store
quota_ <- asks $ fileSizeQuota . config
-- TODO timeout file upload, remove partially uploaded files
stats <- asks serverStats
liftIO $
runExceptT (receiveFile getBody (XFTPRcvChunkSpec fPath size digest)) >>= \case
Right () -> do
used <- readTVarIO $ usedStorage st
if maybe False (used + fromIntegral size >) quota_
then remove fPath $> FRErr QUOTA
else do
atomically (setFilePath' st fr fPath)
atomically $ modifyTVar' (filesUploaded stats) (+ 1)
atomically $ modifyTVar' (filesCount stats) (+ 1)
atomically $ modifyTVar' (filesSize stats) (+ fromIntegral size)
pure FROk
Left e -> remove fPath $> FRErr e
-- TODO validate body size from request before downloading, once it's populated
Just getBody -> skipCommitted $ ifM reserve receive (pure $ FRErr QUOTA)
where
remove fPath = whenM (doesFileExist fPath) (removeFile fPath) `catch` logFileError
-- having a filePath means the file is already uploaded and committed, must not change anything
skipCommitted = ifM (isJust <$> readTVarIO filePath) (liftIO $ drain $ fromIntegral size)
where
-- can't send FROk without reading the request body or a client will block on sending it
-- can't send any old error as the client would fail or restart indefinitely
drain s = do
bs <- B.length <$> getBody fileBlockSize
if
| bs == s -> pure FROk
| bs == 0 || bs > s -> pure $ FRErr SIZE
| otherwise -> drain (s - bs)
reserve = do
us <- asks $ usedStorage . store
quota <- asks $ fromMaybe maxBound . fileSizeQuota . config
atomically . stateTVar us $
\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)
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)
pure FROk
Left e -> do
us <- asks $ usedStorage . store
atomically . modifyTVar' us $ subtract (fromIntegral size)
liftIO $ whenM (doesFileExist fPath) (removeFile fPath) `catch` logFileError
pure $ FRErr e
receiveChunk spec = do
t <- asks $ fileTimeout . config
liftIO $ fromMaybe (Left TIMEOUT) <$> timeout t (runExceptT (receiveFile getBody spec) `catchAll_` pure (Left FILE_IO))
sendServerFile :: FileRec -> RcvPublicDhKey -> M (FileResponse, Maybe ServerFile)
sendServerFile FileRec {senderId, filePath, fileInfo = FileInfo {size}} rDhKey = do
readTVarIO filePath >>= \case
Just path -> do
g <- asks random
(sDhKey, spDhKey) <- atomically $ C.generateKeyPair g
let dhSecret = C.dh' rDhKey spDhKey
cbNonce <- atomically $ C.randomCbNonce g
case LC.cbInit dhSecret cbNonce of
Right sbState -> do
stats <- asks serverStats
atomically $ modifyTVar' (fileDownloads stats) (+ 1)
atomically $ updatePeriodStats (filesDownloaded stats) senderId
pure (FRFile sDhKey cbNonce, Just ServerFile {filePath = path, fileSize = size, sbState})
_ -> pure (FRErr INTERNAL, Nothing)
Just path -> ifM (doesFileExist path) sendFile (pure (FRErr AUTH, Nothing))
where
sendFile = do
g <- asks random
(sDhKey, spDhKey) <- atomically $ C.generateKeyPair g
let dhSecret = C.dh' rDhKey spDhKey
cbNonce <- atomically $ C.randomCbNonce g
case LC.cbInit dhSecret cbNonce of
Right sbState -> do
stats <- asks serverStats
atomically $ modifyTVar' (fileDownloads stats) (+ 1)
atomically $ updatePeriodStats (filesDownloaded stats) senderId
pure (FRFile sDhKey cbNonce, Just ServerFile {filePath = path, fileSize = size, sbState})
_ -> pure (FRErr INTERNAL, Nothing)
_ -> pure (FRErr NO_FILE, Nothing)
deleteServerFile :: FileRec -> M FileResponse
deleteServerFile FileRec {senderId, fileInfo, filePath} = do
withFileLog (`logDeleteFile` senderId)
r <- 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)
pure FROk
either (pure . FRErr) pure r
where
deletedStats stats = do
atomically $ modifyTVar' (filesCount stats) (subtract 1)
atomically $ modifyTVar' (filesSize stats) (subtract $ fromIntegral $ size fileInfo)
deleteServerFile fr = either FRErr (\() -> FROk) <$> deleteServerFile_ fr
logFileError :: SomeException -> IO ()
logFileError e = logError $ "Error deleting file: " <> tshow e
@@ -359,7 +487,49 @@ processXFTPRequest HTTP2Body {bodyPart} = \case
atomically $ modifyTVar' (fileDownloadAcks stats) (+ 1)
pure FROk
randomId :: (MonadUnliftIO m, MonadReader XFTPEnv m) => Int -> m ByteString
deleteServerFile_ :: FileRec -> M (Either XFTPErrorType ())
deleteServerFile_ FileRec {senderId, fileInfo, filePath} = 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)
where
deletedStats stats = do
atomically $ modifyTVar' (filesCount stats) (subtract 1)
atomically $ modifyTVar' (filesSize stats) (subtract $ fromIntegral $ size fileInfo)
expireServerFiles :: Maybe Int -> ExpirationConfig -> M ()
expireServerFiles itemDelay expCfg = do
st <- asks store
usedStart <- readTVarIO $ usedStorage st
old <- liftIO $ expireBeforeEpoch expCfg
files' <- readTVarIO (files st)
logInfo $ "Expiration check: " <> tshow (M.size files') <> " files"
forM_ (M.keys files') $ \sId -> do
mapM_ threadDelay itemDelay
atomically (expiredFilePath st sId old)
>>= mapM_ (maybeRemove $ delete st sId)
usedEnd <- readTVarIO $ usedStorage st
logInfo $ "Used " <> mbs usedStart <> " -> " <> mbs usedEnd <> ", " <> mbs (usedStart - usedEnd) <> " reclaimed."
where
mbs bs = tshow (bs `div` 1048576) <> "mb"
maybeRemove del = maybe del (remove del)
remove del filePath =
ifM
(doesFileExist filePath)
((removeFile filePath >> del) `catch` \(e :: SomeException) -> logError $ "failed to remove expired file " <> tshow filePath <> ": " <> tshow e)
del
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)
randomId :: Int -> M ByteString
randomId n = atomically . C.randomBytes n =<< asks random
getFileId :: M XFTPFileId
@@ -367,7 +537,7 @@ getFileId = do
size <- asks (fileIdSize . config)
atomically . C.randomBytes size =<< asks random
withFileLog :: (MonadIO m, MonadReader XFTPEnv m) => (StoreLog 'WriteMode -> IO a) -> m ()
withFileLog :: (StoreLog 'WriteMode -> IO a) -> M ()
withFileLog action = liftIO . mapM_ action =<< asks storeLog
incFileStat :: (FileServerStats -> TVar Int) -> M ()
@@ -391,14 +561,17 @@ restoreServerStats = asks (serverStatsBackupFile . config) >>= mapM_ restoreStat
restoreStats f = whenM (doesFileExist f) $ do
logInfo $ "restoring server stats from file " <> T.pack f
liftIO (strDecode <$> B.readFile f) >>= \case
Right d -> do
Right d@FileServerStatsData {_filesCount = statsFilesCount, _filesSize = statsFilesSize} -> do
s <- asks serverStats
fs <- readTVarIO . files =<< asks store
let _filesCount = length $ M.keys fs
_filesSize = M.foldl' (\n -> (n +) . fromIntegral . size . fileInfo) 0 fs
FileStore {files, usedStorage} <- asks store
_filesCount <- M.size <$> readTVarIO files
_filesSize <- readTVarIO usedStorage
atomically $ setFileServerStats s d {_filesCount, _filesSize}
renameFile f $ f <> ".bak"
logInfo "server stats restored"
when (statsFilesCount /= _filesCount) $ logWarn $ "Files count differs: stats: " <> tshow statsFilesCount <> ", store: " <> tshow _filesCount
when (statsFilesSize /= _filesSize) $ logWarn $ "Files size differs: stats: " <> tshow statsFilesSize <> ", store: " <> tshow _filesSize
logInfo $ "Restored " <> tshow (_filesSize `div` 1048576) <> " MBs in " <> tshow _filesCount <> " files"
Left e -> do
logInfo $ "error restoring server stats: " <> T.pack e
liftIO exitFailure
@@ -0,0 +1,37 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Simplex.FileTransfer.Server.Control where
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.ByteString (ByteString)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (BasicAuth)
data CPClientRole = CPRNone | CPRUser | CPRAdmin
data ControlProtocol
= CPAuth BasicAuth
| CPStatsRTS
| CPDelete ByteString
| CPHelp
| CPQuit
| CPSkip
instance StrEncoding ControlProtocol where
strEncode = \case
CPAuth tok -> "auth " <> strEncode tok
CPStatsRTS -> "stats-rts"
CPDelete fId -> strEncode (Str "delete", fId)
CPHelp -> "help"
CPQuit -> "quit"
CPSkip -> ""
strP =
A.takeTill (== ' ') >>= \case
"auth" -> CPAuth <$> _strP
"stats-rts" -> pure CPStatsRTS
"delete" -> CPDelete <$> _strP
"help" -> pure CPHelp
"quit" -> pure CPQuit
"" -> pure CPSkip
_ -> fail "bad ControlProtocol command"
+37 -8
View File
@@ -2,37 +2,44 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StrictData #-}
module Simplex.FileTransfer.Server.Env where
import Control.Logger.Simple (logInfo)
import Control.Logger.Simple
import Control.Monad
import Control.Monad.IO.Unlift
import Crypto.Random
import Data.Default (def)
import Data.Int (Int64)
import Data.List (find)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe)
import Data.Time.Clock (getCurrentTime)
import Data.Word (Word32)
import Data.X509.Validation (Fingerprint (..))
import Network.Socket
import qualified Network.TLS as T
import Simplex.FileTransfer.Protocol (FileCmd, FileInfo, XFTPFileId)
import Simplex.FileTransfer.Protocol (FileCmd, FileInfo (..), XFTPFileId)
import Simplex.FileTransfer.Server.Stats
import Simplex.FileTransfer.Server.Store
import Simplex.FileTransfer.Server.StoreLog
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Protocol (BasicAuth, RcvPublicVerifyKey)
import Simplex.Messaging.Protocol (BasicAuth, RcvPublicAuthKey)
import Simplex.Messaging.Server.Expiration
import Simplex.Messaging.Transport.Server (TransportServerConfig, loadFingerprint, loadTLSServerParams)
import Simplex.Messaging.Transport (ALPN)
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), loadFingerprint, loadTLSServerParams)
import Simplex.Messaging.Util (tshow)
import System.IO (IOMode (..))
import UnliftIO.STM
data XFTPServerConfig = XFTPServerConfig
{ xftpPort :: ServiceName,
controlPort :: Maybe ServiceName,
fileIdSize :: Int,
storeLogFile :: Maybe FilePath,
filesPath :: FilePath,
@@ -44,8 +51,13 @@ data XFTPServerConfig = XFTPServerConfig
allowNewFiles :: Bool,
-- | simple password that the clients need to pass in handshake to be able to create new files
newFileBasicAuth :: Maybe BasicAuth,
-- | control port passwords,
controlPortUserAuth :: Maybe BasicAuth,
controlPortAdminAuth :: Maybe BasicAuth,
-- | time after which the files can be removed and check interval, seconds
fileExpiration :: Maybe ExpirationConfig,
-- | timeout to receive file
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
@@ -87,21 +99,38 @@ defaultFileExpiration =
checkInterval = 2 * 3600 -- seconds, 2 hours
}
newXFTPServerEnv :: (MonadUnliftIO m, MonadRandom m) => XFTPServerConfig -> m XFTPEnv
supportedXFTPhandshakes :: [ALPN]
supportedXFTPhandshakes = ["xftp/1"]
newXFTPServerEnv :: XFTPServerConfig -> IO XFTPEnv
newXFTPServerEnv config@XFTPServerConfig {storeLogFile, fileSizeQuota, caCertificateFile, certificateFile, privateKeyFile} = do
random <- liftIO C.newRandom
store <- atomically newFileStore
storeLog <- liftIO $ mapM (`readWriteFileStore` store) storeLogFile
used <- readTVarIO (usedStorage store)
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
tlsServerParams' <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile
let TransportServerConfig {alpn} = transportConfig config
let tlsServerParams = case alpn of
Nothing -> tlsServerParams'
Just supported ->
tlsServerParams'
{ T.serverHooks =
def
{ T.onALPNClientSuggest = Just $ pure . fromMaybe "" . find (`elem` supported)
}
}
Fingerprint fp <- liftIO $ loadFingerprint caCertificateFile
serverStats <- atomically . newFileServerStats =<< liftIO getCurrentTime
pure XFTPEnv {config, store, storeLog, random, tlsServerParams, serverIdentity = C.KeyHash fp, serverStats}
countUsedStorage :: M.Map k FileRec -> Int64
countUsedStorage = M.foldl' (\acc FileRec {fileInfo = FileInfo {size}} -> acc + fromIntegral size) 0
data XFTPRequest
= XFTPReqNew FileInfo (NonEmpty RcvPublicVerifyKey) (Maybe BasicAuth)
= XFTPReqNew FileInfo (NonEmpty RcvPublicAuthKey) (Maybe BasicAuth)
| XFTPReqCmd XFTPFileId FileRec FileCmd
| XFTPReqPing
+15 -4
View File
@@ -25,6 +25,7 @@ import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), pattern XFTPServer)
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 System.Directory (createDirectoryIfMissing, doesFileExist)
@@ -32,9 +33,6 @@ import System.FilePath (combine)
import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
import Text.Read (readMaybe)
xftpServerVersion :: String
xftpServerVersion = "1.2.0.2"
xftpServerCLI :: FilePath -> FilePath -> IO ()
xftpServerCLI cfgPath logPath = do
getCliCommand' (cliCommandP cfgPath logPath iniFile) serverVersion >>= \case
@@ -42,6 +40,10 @@ xftpServerCLI cfgPath logPath = do
doesFileExist iniFile >>= \case
True -> exitError $ "Error: server is already initialized (" <> iniFile <> " exists).\nRun `" <> executableName <> " start`."
_ -> initializeServer opts
OnlineCert certOpts ->
doesFileExist iniFile >>= \case
True -> genOnline cfgPath certOpts
_ -> exitError $ "Error: server is not initialized (" <> iniFile <> " does not exist).\nRun `" <> executableName <> " init`."
Start ->
doesFileExist iniFile >>= \case
True -> readIniFile iniFile >>= either exitError runServer
@@ -53,7 +55,7 @@ xftpServerCLI cfgPath logPath = do
putStrLn "Deleted configuration and log files"
where
iniFile = combine cfgPath "file-server.ini"
serverVersion = "SimpleX XFTP server v" <> xftpServerVersion
serverVersion = "SimpleX XFTP server v" <> simplexMQVersion
defaultServerPort = "443"
executableName = "file-server"
storeLogFilePath = combine logPath "file-server-store.log"
@@ -95,11 +97,14 @@ xftpServerCLI cfgPath logPath = do
\# with the users who you want to allow uploading files to your server.\n\
\# create_password: password to upload files (any printable ASCII characters without whitespace, '@', ':' and '/')\n\
\\n\
\# control_port_admin_password:\n\
\# control_port_user_password:\n\
\[TRANSPORT]\n\
\# host is only used to print server address on start\n"
<> ("host: " <> host <> "\n")
<> ("port: " <> defaultServerPort <> "\n")
<> "log_tls_errors: off\n\
\# control_port: 5226\n\
\\n\
\[FILES]\n"
<> ("path: " <> filesPath <> "\n")
@@ -144,6 +149,7 @@ xftpServerCLI cfgPath logPath = do
serverConfig =
XFTPServerConfig
{ xftpPort = T.unpack $ strictIni "TRANSPORT" "port" ini,
controlPort = either (const Nothing) (Just . T.unpack) $ lookupValue "TRANSPORT" "control_port" ini,
fileIdSize = 16,
storeLogFile = enableStoreLog $> storeLogFilePath,
filesPath = T.unpack $ strictIni "FILES" "path" ini,
@@ -151,11 +157,14 @@ xftpServerCLI cfgPath logPath = do
allowedChunkSizes = serverChunkSizes,
allowNewFiles = fromMaybe True $ iniOnOff "AUTH" "new_files" ini,
newFileBasicAuth = either error id <$> strDecodeIni "AUTH" "create_password" ini,
controlPortAdminAuth = either error id <$> strDecodeIni "AUTH" "control_port_admin_password" ini,
controlPortUserAuth = either error id <$> strDecodeIni "AUTH" "control_port_user_password" ini,
fileExpiration =
Just
defaultFileExpiration
{ ttl = 3600 * readIniDefault defFileExpirationHours "STORE_LOG" "expire_files_hours" ini
},
fileTimeout = 10 * 60 * 1000000, -- 10 mins to send 4mb chunk
inactiveClientExpiration =
settingIsOn "INACTIVE_CLIENTS" "disconnect" ini
$> ExpirationConfig
@@ -177,6 +186,7 @@ xftpServerCLI cfgPath logPath = do
data CliCommand
= Init InitOptions
| OnlineCert CertOptions
| Start
| Delete
@@ -194,6 +204,7 @@ cliCommandP :: FilePath -> FilePath -> FilePath -> Parser CliCommand
cliCommandP cfgPath logPath iniFile =
hsubparser
( command "init" (info (Init <$> initP) (progDesc $ "Initialize server - creates " <> cfgPath <> " and " <> logPath <> " directories and configuration files"))
<> command "cert" (info (OnlineCert <$> certOptionsP) (progDesc $ "Generate new online TLS server credentials (configuration: " <> iniFile <> ")"))
<> command "start" (info (pure Start) (progDesc $ "Start server (configuration: " <> iniFile <> ")"))
<> command "delete" (info (pure Delete) (progDesc "Delete configuration and log files"))
)
+16 -4
View File
@@ -4,6 +4,7 @@
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.Int (Int64)
@@ -18,6 +19,7 @@ data FileServerStats = FileServerStats
filesCreated :: TVar Int,
fileRecipients :: TVar Int,
filesUploaded :: TVar Int,
filesExpired :: TVar Int,
filesDeleted :: TVar Int,
filesDownloaded :: PeriodStats SenderId,
fileDownloads :: TVar Int,
@@ -31,6 +33,7 @@ data FileServerStatsData = FileServerStatsData
_filesCreated :: Int,
_fileRecipients :: Int,
_filesUploaded :: Int,
_filesExpired :: Int,
_filesDeleted :: Int,
_filesDownloaded :: PeriodStatsData SenderId,
_fileDownloads :: Int,
@@ -46,13 +49,14 @@ newFileServerStats ts = do
filesCreated <- newTVar 0
fileRecipients <- newTVar 0
filesUploaded <- newTVar 0
filesExpired <- newTVar 0
filesDeleted <- newTVar 0
filesDownloaded <- newPeriodStats
fileDownloads <- newTVar 0
fileDownloadAcks <- newTVar 0
filesCount <- newTVar 0
filesSize <- newTVar 0
pure FileServerStats {fromTime, filesCreated, fileRecipients, filesUploaded, filesDeleted, filesDownloaded, fileDownloads, fileDownloadAcks, filesCount, filesSize}
pure FileServerStats {fromTime, filesCreated, fileRecipients, filesUploaded, filesExpired, filesDeleted, filesDownloaded, fileDownloads, fileDownloadAcks, filesCount, filesSize}
getFileServerStatsData :: FileServerStats -> STM FileServerStatsData
getFileServerStatsData s = do
@@ -60,13 +64,14 @@ getFileServerStatsData s = do
_filesCreated <- readTVar $ filesCreated s
_fileRecipients <- readTVar $ fileRecipients s
_filesUploaded <- readTVar $ filesUploaded s
_filesExpired <- readTVar $ filesExpired s
_filesDeleted <- readTVar $ filesDeleted s
_filesDownloaded <- getPeriodStatsData $ filesDownloaded s
_fileDownloads <- readTVar $ fileDownloads s
_fileDownloadAcks <- readTVar $ fileDownloadAcks s
_filesCount <- readTVar $ filesCount s
_filesSize <- readTVar $ filesSize s
pure FileServerStatsData {_fromTime, _filesCreated, _fileRecipients, _filesUploaded, _filesDeleted, _filesDownloaded, _fileDownloads, _fileDownloadAcks, _filesCount, _filesSize}
pure FileServerStatsData {_fromTime, _filesCreated, _fileRecipients, _filesUploaded, _filesExpired, _filesDeleted, _filesDownloaded, _fileDownloads, _fileDownloadAcks, _filesCount, _filesSize}
setFileServerStats :: FileServerStats -> FileServerStatsData -> STM ()
setFileServerStats s d = do
@@ -74,6 +79,7 @@ setFileServerStats s d = do
writeTVar (filesCreated s) $! _filesCreated d
writeTVar (fileRecipients s) $! _fileRecipients d
writeTVar (filesUploaded s) $! _filesUploaded d
writeTVar (filesExpired s) $! _filesExpired d
writeTVar (filesDeleted s) $! _filesDeleted d
setPeriodStats (filesDownloaded s) $! _filesDownloaded d
writeTVar (fileDownloads s) $! _fileDownloads d
@@ -82,13 +88,16 @@ setFileServerStats s d = do
writeTVar (filesSize s) $! _filesSize d
instance StrEncoding FileServerStatsData where
strEncode FileServerStatsData {_fromTime, _filesCreated, _fileRecipients, _filesUploaded, _filesDeleted, _filesDownloaded, _fileDownloads, _fileDownloadAcks} =
strEncode FileServerStatsData {_fromTime, _filesCreated, _fileRecipients, _filesUploaded, _filesExpired, _filesDeleted, _filesDownloaded, _fileDownloads, _fileDownloadAcks, _filesCount, _filesSize} =
B.unlines
[ "fromTime=" <> strEncode _fromTime,
"filesCreated=" <> strEncode _filesCreated,
"fileRecipients=" <> strEncode _fileRecipients,
"filesUploaded=" <> strEncode _filesUploaded,
"filesExpired=" <> strEncode _filesExpired,
"filesDeleted=" <> strEncode _filesDeleted,
"filesCount=" <> strEncode _filesCount,
"filesSize=" <> strEncode _filesSize,
"filesDownloaded:",
strEncode _filesDownloaded,
"fileDownloads=" <> strEncode _fileDownloads,
@@ -99,8 +108,11 @@ instance StrEncoding FileServerStatsData where
_filesCreated <- "filesCreated=" *> strP <* A.endOfLine
_fileRecipients <- "fileRecipients=" *> strP <* A.endOfLine
_filesUploaded <- "filesUploaded=" *> strP <* A.endOfLine
_filesExpired <- "filesExpired=" *> strP <* A.endOfLine <|> pure 0
_filesDeleted <- "filesDeleted=" *> strP <* A.endOfLine
_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, _filesDeleted, _filesDownloaded, _fileDownloads, _fileDownloadAcks, _filesCount = 0, _filesSize = 0}
pure FileServerStatsData {_fromTime, _filesCreated, _fileRecipients, _filesUploaded, _filesExpired, _filesDeleted, _filesDownloaded, _fileDownloads, _fileDownloadAcks, _filesCount, _filesSize}
+12 -16
View File
@@ -11,7 +11,6 @@ module Simplex.FileTransfer.Server.Store
newFileStore,
addFile,
setFilePath,
setFilePath',
addRecipient,
deleteFile,
deleteRecipient,
@@ -23,22 +22,22 @@ where
import Control.Concurrent.STM
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Functor (($>))
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 (..), XFTPErrorType (..), XFTPFileId)
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 (RcvPublicVerifyKey, RecipientId, SenderId)
import Simplex.Messaging.Protocol (RcvPublicAuthKey, RecipientId, SenderId)
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Util (ifM, ($>>=))
data FileStore = FileStore
{ files :: TMap SenderId FileRec,
recipients :: TMap RecipientId (SenderId, RcvPublicVerifyKey),
recipients :: TMap RecipientId (SenderId, RcvPublicAuthKey),
usedStorage :: TVar Int64
}
@@ -49,9 +48,8 @@ data FileRec = FileRec
recipientIds :: TVar (Set RecipientId),
createdAt :: SystemTime
}
deriving (Eq)
data FileRecipient = FileRecipient RecipientId RcvPublicVerifyKey
data FileRecipient = FileRecipient RecipientId RcvPublicAuthKey
instance StrEncoding FileRecipient where
strEncode (FileRecipient rId rKey) = strEncode rId <> ":" <> strEncode rKey
@@ -79,12 +77,10 @@ newFileRec senderId fileInfo createdAt = do
setFilePath :: FileStore -> SenderId -> FilePath -> STM (Either XFTPErrorType ())
setFilePath st sId fPath =
withFile st sId $ \fr -> setFilePath' st fr fPath $> Right ()
setFilePath' :: FileStore -> FileRec -> FilePath -> STM ()
setFilePath' st FileRec {fileInfo, filePath} fPath = do
writeTVar filePath (Just fPath)
modifyTVar' (usedStorage st) (+ fromIntegral (size fileInfo))
withFile st sId $ \FileRec {fileInfo, filePath} -> do
writeTVar filePath (Just fPath)
modifyTVar' (usedStorage st) (+ fromIntegral (size fileInfo))
pure $ Right ()
addRecipient :: FileStore -> SenderId -> FileRecipient -> STM (Either XFTPErrorType ())
addRecipient st@FileStore {recipients} senderId (FileRecipient rId rKey) =
@@ -113,7 +109,7 @@ deleteRecipient FileStore {recipients} rId FileRec {recipientIds} = do
TM.delete rId recipients
modifyTVar' recipientIds $ S.delete rId
getFile :: FileStore -> SFileParty p -> XFTPFileId -> STM (Either XFTPErrorType (FileRec, C.APublicVerifyKey))
getFile :: FileStore -> SFileParty p -> XFTPFileId -> STM (Either XFTPErrorType (FileRec, C.APublicAuthKey))
getFile st party fId = case party of
SFSender -> withFile st fId $ pure . Right . (\f -> (f, sndKey $ fileInfo f))
SFRecipient ->
@@ -121,12 +117,12 @@ getFile st party fId = case party of
Just (sId, rKey) -> withFile st sId $ pure . Right . (,rKey)
_ -> pure $ Left AUTH
expiredFilePath :: FileStore -> XFTPFileId -> Int64 -> STM (Maybe FilePath)
expiredFilePath :: FileStore -> XFTPFileId -> Int64 -> STM (Maybe (Maybe FilePath))
expiredFilePath FileStore {files} sId old =
TM.lookup sId files
$>>= \FileRec {filePath, createdAt} ->
if systemSeconds createdAt < old
then readTVar filePath
then Just <$> readTVar filePath
else pure Nothing
ackFile :: FileStore -> RecipientId -> STM (Either XFTPErrorType ())
+4 -3
View File
@@ -22,6 +22,7 @@ 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.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as L
@@ -31,7 +32,7 @@ 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 (RcvPublicVerifyKey, RecipientId, SenderId)
import Simplex.Messaging.Protocol (RcvPublicAuthKey, RecipientId, SenderId)
import Simplex.Messaging.Server.StoreLog
import Simplex.Messaging.Util (bshow, whenM)
import System.Directory (doesFileExist, renameFile)
@@ -88,7 +89,7 @@ readWriteFileStore f st = do
pure s
readFileStore :: FilePath -> FileStore -> IO ()
readFileStore f st = mapM_ addFileLogRecord . B.lines =<< B.readFile f
readFileStore f st = mapM_ (addFileLogRecord . LB.toStrict) . LB.lines =<< LB.readFile f
where
addFileLogRecord s = case strDecode s of
Left e -> B.putStrLn $ "Log parsing error (" <> B.pack e <> "): " <> B.take 100 s
@@ -109,7 +110,7 @@ writeFileStore s FileStore {files, recipients} = do
allRcps <- readTVarIO recipients
readTVarIO files >>= mapM_ (logFile allRcps)
where
logFile :: Map RecipientId (SenderId, RcvPublicVerifyKey) -> FileRec -> IO ()
logFile :: Map RecipientId (SenderId, RcvPublicAuthKey) -> FileRec -> IO ()
logFile allRcps FileRec {senderId, fileInfo, filePath, recipientIds, createdAt} = do
logAddFile s senderId fileInfo createdAt
(rcpErrs, rcps) <- M.mapEither getRcp . M.fromSet id <$> readTVarIO recipientIds
+180 -5
View File
@@ -1,10 +1,28 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
module Simplex.FileTransfer.Transport
( supportedFileServerVRange,
authCmdsXFTPVersion,
xftpClientHandshakeStub,
XFTPClientHandshake (..),
-- xftpClientHandshake,
XFTPServerHandshake (..),
-- xftpServerHandshake,
THandleXFTP,
THandleParamsXFTP,
VersionXFTP,
VersionRangeXFTP,
XFTPVersion,
pattern VersionXFTP,
XFTPErrorType (..),
XFTPRcvChunkSpec (..),
ReceiveFileError (..),
receiveFile,
@@ -14,22 +32,32 @@ module Simplex.FileTransfer.Transport
)
where
import Control.Applicative ((<|>))
import qualified Control.Exception as E
import Control.Monad
import Control.Monad.Except
import Control.Monad.IO.Class
import Data.Bifunctor (first)
import qualified Data.Aeson.TH as J
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bifunctor (bimap, first)
import qualified Data.ByteArray as BA
import Data.ByteString.Builder (Builder, byteString)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Word (Word32)
import Simplex.FileTransfer.Protocol (XFTPErrorType (..))
import Data.Word (Word16, Word32)
import qualified Data.X509 as X
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.Parsers
import Simplex.Messaging.Protocol (CommandError)
import Simplex.Messaging.Transport (HandshakeError (..), SessionId, THandle (..), THandleParams (..), TransportError (..), TransportPeer (..))
import Simplex.Messaging.Transport.HTTP2.File
import Simplex.Messaging.Util (bshow)
import Simplex.Messaging.Version
import Simplex.Messaging.Version.Internal
import System.IO (Handle, IOMode (..), withFile)
data XFTPRcvChunkSpec = XFTPRcvChunkSpec
@@ -39,8 +67,69 @@ data XFTPRcvChunkSpec = XFTPRcvChunkSpec
}
deriving (Show)
supportedFileServerVRange :: VersionRange
supportedFileServerVRange = mkVersionRange 1 1
data XFTPVersion
instance VersionScope XFTPVersion
type VersionXFTP = Version XFTPVersion
type VersionRangeXFTP = VersionRange XFTPVersion
pattern VersionXFTP :: Word16 -> VersionXFTP
pattern VersionXFTP v = Version v
type THandleXFTP c p = THandle XFTPVersion c p
type THandleParamsXFTP p = THandleParams XFTPVersion p
initialXFTPVersion :: VersionXFTP
initialXFTPVersion = VersionXFTP 1
authCmdsXFTPVersion :: VersionXFTP
authCmdsXFTPVersion = VersionXFTP 2
currentXFTPVersion :: VersionXFTP
currentXFTPVersion = VersionXFTP 2
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 = throwError $ TEHandshake VERSION
data XFTPServerHandshake = XFTPServerHandshake
{ xftpVersionRange :: VersionRangeXFTP,
sessionId :: SessionId,
-- | pub key to agree shared secrets for command authorization and entity ID encryption.
authPubKey :: (X.CertificateChain, X.SignedExact X.PubKey)
}
data XFTPClientHandshake = XFTPClientHandshake
{ -- | agreed XFTP server protocol version
xftpVersion :: VersionXFTP,
-- | server identity - CA certificate fingerprint
keyHash :: C.KeyHash
}
instance Encoding XFTPClientHandshake where
smpEncode XFTPClientHandshake {xftpVersion, keyHash} =
smpEncode (xftpVersion, keyHash)
smpP = do
(xftpVersion, keyHash) <- smpP
Tail _compat <- smpP
pure XFTPClientHandshake {xftpVersion, keyHash}
instance Encoding XFTPServerHandshake where
smpEncode XFTPServerHandshake {xftpVersionRange, sessionId, authPubKey} =
smpEncode (xftpVersionRange, sessionId, auth)
where
auth = bimap C.encodeCertChain C.SignedObject authPubKey
smpP = do
(xftpVersionRange, sessionId) <- smpP
cert <- C.certChainP
C.SignedObject key <- smpP
Tail _compat <- smpP
pure XFTPServerHandshake {xftpVersionRange, sessionId, authPubKey = (cert, key)}
sendEncFile :: Handle -> (Builder -> IO ()) -> LC.SbState -> Word32 -> IO ()
sendEncFile h send = go
@@ -97,3 +186,89 @@ receiveFile_ receive XFTPRcvChunkSpec {filePath, chunkSize, chunkDigest} = do
ExceptT $ withFile filePath WriteMode (`receive` chunkSize)
digest' <- liftIO $ LC.sha256Hash <$> LB.readFile filePath
when (digest' /= chunkDigest) $ throwError DIGEST
data XFTPErrorType
= -- | incorrect block format, encoding or signature size
BLOCK
| -- | incorrect SMP session ID (TLS Finished message / tls-unique binding RFC5929)
SESSION
| -- | incorrect handshake command
HANDSHAKE
| -- | SMP command is unknown or has invalid syntax
CMD {cmdErr :: CommandError}
| -- | command authorization error - bad signature or non-existing SMP queue
AUTH
| -- | incorrent file size
SIZE
| -- | storage quota exceeded
QUOTA
| -- | incorrent file digest
DIGEST
| -- | file encryption/decryption failed
CRYPTO
| -- | no expected file body in request/response or no file on the server
NO_FILE
| -- | unexpected file body
HAS_FILE
| -- | file IO error
FILE_IO
| -- | file sending timeout
TIMEOUT
| -- | bad redirect data
REDIRECT {redirectError :: String}
| -- | internal server error
INTERNAL
| -- | used internally, never returned by the server (to be removed)
DUPLICATE_ -- not part of SMP protocol, used internally
deriving (Eq, Read, Show)
instance StrEncoding XFTPErrorType where
strEncode = \case
CMD e -> "CMD " <> bshow e
REDIRECT e -> "REDIRECT " <> bshow e
e -> bshow e
strP =
"CMD " *> (CMD <$> parseRead1)
<|> "REDIRECT " *> (REDIRECT <$> parseRead A.takeByteString)
<|> parseRead1
instance Encoding XFTPErrorType where
smpEncode = \case
BLOCK -> "BLOCK"
SESSION -> "SESSION"
HANDSHAKE -> "HANDSHAKE"
CMD err -> "CMD " <> smpEncode err
AUTH -> "AUTH"
SIZE -> "SIZE"
QUOTA -> "QUOTA"
DIGEST -> "DIGEST"
CRYPTO -> "CRYPTO"
NO_FILE -> "NO_FILE"
HAS_FILE -> "HAS_FILE"
FILE_IO -> "FILE_IO"
TIMEOUT -> "TIMEOUT"
REDIRECT err -> "REDIRECT " <> smpEncode err
INTERNAL -> "INTERNAL"
DUPLICATE_ -> "DUPLICATE_"
smpP =
A.takeTill (== ' ') >>= \case
"BLOCK" -> pure BLOCK
"SESSION" -> pure SESSION
"HANDSHAKE" -> pure HANDSHAKE
"CMD" -> CMD <$> _smpP
"AUTH" -> pure AUTH
"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
"REDIRECT" -> REDIRECT <$> _smpP
"INTERNAL" -> pure INTERNAL
"DUPLICATE_" -> pure DUPLICATE_
_ -> fail "bad error type"
$(J.deriveJSON (sumTypeJSON id) ''XFTPErrorType)
+24 -15
View File
@@ -47,6 +47,7 @@ data RcvFile = RcvFile
key :: C.SbKey,
nonce :: C.CbNonce,
chunkSize :: FileSize Word32,
redirect :: Maybe RcvFileRedirect,
chunks :: [RcvFileChunk],
prefixPath :: FilePath,
tmpPath :: Maybe FilePath,
@@ -54,7 +55,7 @@ data RcvFile = RcvFile
status :: RcvFileStatus,
deleted :: Bool
}
deriving (Eq, Show)
deriving (Show)
data RcvFileStatus
= RFSReceiving
@@ -95,18 +96,25 @@ data RcvFileChunk = RcvFileChunk
fileTmpPath :: FilePath,
chunkTmpPath :: Maybe FilePath
}
deriving (Eq, Show)
deriving (Show)
data RcvFileChunkReplica = RcvFileChunkReplica
{ rcvChunkReplicaId :: Int64,
server :: XFTPServer,
replicaId :: ChunkReplicaId,
replicaKey :: C.APrivateSignKey,
replicaKey :: C.APrivateAuthKey,
received :: Bool,
delay :: Maybe Int64,
retries :: Int
}
deriving (Eq, Show)
deriving (Show)
data RcvFileRedirect = RcvFileRedirect
{ redirectDbId :: DBRcvFileId,
redirectEntityId :: RcvFileId,
redirectFileInfo :: RedirectFileInfo
}
deriving (Show)
-- Sending files
@@ -124,9 +132,10 @@ data SndFile = SndFile
srcFile :: CryptoFile,
prefixPath :: Maybe FilePath,
status :: SndFileStatus,
deleted :: Bool
deleted :: Bool,
redirect :: Maybe RedirectFileInfo
}
deriving (Eq, Show)
deriving (Show)
sndFileEncPath :: FilePath -> FilePath
sndFileEncPath prefixPath = prefixPath </> "xftp.encrypted"
@@ -173,7 +182,7 @@ data SndFileChunk = SndFileChunk
digest :: FileDigest,
replicas :: [SndFileChunkReplica]
}
deriving (Eq, Show)
deriving (Show)
sndChunkSize :: SndFileChunk -> Word32
sndChunkSize SndFileChunk {chunkSpec = XFTPChunkSpec {chunkSize}} = chunkSize
@@ -181,22 +190,22 @@ sndChunkSize SndFileChunk {chunkSpec = XFTPChunkSpec {chunkSize}} = chunkSize
data NewSndChunkReplica = NewSndChunkReplica
{ server :: XFTPServer,
replicaId :: ChunkReplicaId,
replicaKey :: C.APrivateSignKey,
rcvIdsKeys :: [(ChunkReplicaId, C.APrivateSignKey)]
replicaKey :: C.APrivateAuthKey,
rcvIdsKeys :: [(ChunkReplicaId, C.APrivateAuthKey)]
}
deriving (Eq, Show)
deriving (Show)
data SndFileChunkReplica = SndFileChunkReplica
{ sndChunkReplicaId :: Int64,
server :: XFTPServer,
replicaId :: ChunkReplicaId,
replicaKey :: C.APrivateSignKey,
rcvIdsKeys :: [(ChunkReplicaId, C.APrivateSignKey)],
replicaKey :: C.APrivateAuthKey,
rcvIdsKeys :: [(ChunkReplicaId, C.APrivateAuthKey)],
replicaStatus :: SndFileReplicaStatus,
delay :: Maybe Int64,
retries :: Int
}
deriving (Eq, Show)
deriving (Show)
data SndFileReplicaStatus
= SFRSCreated
@@ -221,9 +230,9 @@ data DeletedSndChunkReplica = DeletedSndChunkReplica
userId :: Int64,
server :: XFTPServer,
replicaId :: ChunkReplicaId,
replicaKey :: C.APrivateSignKey,
replicaKey :: C.APrivateAuthKey,
chunkDigest :: FileDigest,
delay :: Maybe Int64,
retries :: Int
}
deriving (Eq, Show)
deriving (Show)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+81 -38
View File
@@ -11,15 +11,17 @@
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
module Simplex.Messaging.Agent.Env.SQLite
( AgentMonad,
AgentMonad',
( AM',
AM,
AgentConfig (..),
InitialAgentServers (..),
NetworkConfig (..),
defaultAgentConfig,
defaultReconnectInterval,
tryAgentError,
tryAgentError',
catchAgentError,
catchAgentError',
agentFinally,
Env (..),
newSMPAgentEnv,
@@ -28,6 +30,8 @@ module Simplex.Messaging.Agent.Env.SQLite
NtfSupervisorCommand (..),
XFTPAgent (..),
Worker (..),
RestartCount (..),
updateRestartCount,
)
where
@@ -40,6 +44,7 @@ import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty)
import Data.Map (Map)
import Data.Time.Clock (NominalDiffTime, nominalDay)
import Data.Time.Clock.System (SystemTime (..))
import Data.Word (Word16)
import Network.Socket
import Numeric.Natural
@@ -51,22 +56,23 @@ import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
import Simplex.Messaging.Client
import Simplex.Messaging.Client.Agent ()
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.Ratchet (supportedE2EEncryptVRange)
import Simplex.Messaging.Crypto.Ratchet (PQSupport, VersionRangeE2E, supportedE2EEncryptVRange)
import Simplex.Messaging.Notifications.Client (defaultNTFClientConfig)
import Simplex.Messaging.Notifications.Transport (NTFVersion)
import Simplex.Messaging.Notifications.Types
import Simplex.Messaging.Protocol (NtfServer, XFTPServer, XFTPServerWithAuth, supportedSMPClientVRange)
import Simplex.Messaging.Protocol (NtfServer, VersionRangeSMPC, XFTPServer, XFTPServerWithAuth, supportedSMPClientVRange)
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport (TLS, Transport (..))
import Simplex.Messaging.Transport (SMPVersion, TLS, Transport (..))
import Simplex.Messaging.Transport.Client (defaultSMPPort)
import Simplex.Messaging.Util (allFinally, catchAllErrors, tryAllErrors)
import Simplex.Messaging.Version
import Simplex.Messaging.Util (allFinally, catchAllErrors, catchAllErrors', tryAllErrors, tryAllErrors')
import System.Random (StdGen, newStdGen)
import UnliftIO (Async, SomeException)
import UnliftIO.STM
type AgentMonad' m = (MonadUnliftIO m, MonadReader Env m)
type AM' a = ReaderT Env IO a
type AgentMonad m = (AgentMonad' m, MonadError AgentErrorType m)
type AM a = ExceptT AgentErrorType (ReaderT Env IO) a
data InitialAgentServers = InitialAgentServers
{ smp :: Map UserId (NonEmpty SMPServerWithAuth),
@@ -76,20 +82,26 @@ data InitialAgentServers = InitialAgentServers
}
data AgentConfig = AgentConfig
{ tcpPort :: ServiceName,
cmdSignAlg :: C.SignAlg,
{ tcpPort :: Maybe ServiceName,
rcvAuthAlg :: C.AuthAlg,
sndAuthAlg :: C.AuthAlg,
connIdBytes :: Int,
tbqSize :: Natural,
smpCfg :: ProtocolClientConfig,
ntfCfg :: ProtocolClientConfig,
smpCfg :: ProtocolClientConfig SMPVersion,
ntfCfg :: ProtocolClientConfig NTFVersion,
xftpCfg :: XFTPClientConfig,
reconnectInterval :: RetryInterval,
messageRetryInterval :: RetryInterval2,
userNetworkInterval :: RetryInterval,
messageTimeout :: NominalDiffTime,
connDeleteDeliveryTimeout :: NominalDiffTime,
helloTimeout :: NominalDiffTime,
quotaExceededTimeout :: NominalDiffTime,
initialCleanupDelay :: Int64,
cleanupInterval :: Int64,
cleanupStepInterval :: Int,
maxWorkerRestartsPerMin :: Int,
maxSubscriptionTimeouts :: Int,
storedMsgDataTTL :: NominalDiffTime,
rcvFilesTTL :: NominalDiffTime,
sndFilesTTL :: NominalDiffTime,
@@ -105,10 +117,9 @@ data AgentConfig = AgentConfig
caCertificateFile :: FilePath,
privateKeyFile :: FilePath,
certificateFile :: FilePath,
e2eEncryptVRange :: VersionRange,
smpAgentVRange :: VersionRange,
smpClientVRange :: VersionRange,
initialClientId :: Int
e2eEncryptVRange :: PQSupport -> VersionRangeE2E,
smpAgentVRange :: PQSupport -> VersionRangeSMPA,
smpClientVRange :: VersionRangeSMPC
}
defaultReconnectInterval :: RetryInterval
@@ -116,7 +127,7 @@ defaultReconnectInterval =
RetryInterval
{ initialInterval = 2_000000,
increaseAfter = 10_000000,
maxInterval = 180_000000
maxInterval = 60_000000
}
defaultMessageRetryInterval :: RetryInterval2
@@ -124,38 +135,53 @@ defaultMessageRetryInterval =
RetryInterval2
{ riFast =
RetryInterval
{ initialInterval = 1_000000,
{ initialInterval = 2_000000,
increaseAfter = 10_000000,
maxInterval = 60_000000
},
riSlow =
-- TODO: these timeouts can be increased in v5.0 once most clients are updated
-- to resume sending on QCONT messages.
-- After that local message expiration period should be also increased.
RetryInterval
{ initialInterval = 60_000000,
{ initialInterval = 300_000000, -- 5 minutes
increaseAfter = 60_000000,
maxInterval = 3600_000000 -- 1 hour
maxInterval = 6 * 3600_000000 -- 6 hours
}
}
defaultUserNetworkInterval :: RetryInterval
defaultUserNetworkInterval =
RetryInterval
{ initialInterval = 1200_000000, -- 20 minutes
increaseAfter = 0,
maxInterval = 7200_000000 -- 2 hours
}
defaultAgentConfig :: AgentConfig
defaultAgentConfig =
AgentConfig
{ tcpPort = "5224",
cmdSignAlg = C.SignAlg C.SEd448,
{ tcpPort = Just "5224",
-- while the current client version supports X25519, it can only be enabled once support for SMP v6 is dropped,
-- and all servers are required to support v7 to be compatible.
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 = defaultClientConfig {defaultTransport = (show defaultSMPPort, transport @TLS)},
ntfCfg = defaultClientConfig {defaultTransport = ("443", transport @TLS)},
smpCfg = defaultSMPClientConfig {defaultTransport = (show defaultSMPPort, transport @TLS)},
ntfCfg = defaultNTFClientConfig {defaultTransport = ("443", transport @TLS)},
xftpCfg = defaultXFTPClientConfig,
reconnectInterval = defaultReconnectInterval,
messageRetryInterval = defaultMessageRetryInterval,
userNetworkInterval = defaultUserNetworkInterval,
messageTimeout = 2 * nominalDay,
connDeleteDeliveryTimeout = 2 * nominalDay,
helloTimeout = 2 * nominalDay,
quotaExceededTimeout = 7 * nominalDay,
initialCleanupDelay = 30 * 1000000, -- 30 seconds
cleanupInterval = 30 * 60 * 1000000, -- 30 minutes
cleanupStepInterval = 200000, -- 200ms
maxWorkerRestartsPerMin = 5,
-- 3 consecutive subscription timeouts will result in alert to the user
-- this is a fallback, as the timeout set to 3x of expected timeout, to avoid potential locking.
maxSubscriptionTimeouts = 3,
storedMsgDataTTL = 21 * nominalDay,
rcvFilesTTL = 2 * nominalDay,
sndFilesTTL = nominalDay,
@@ -175,15 +201,13 @@ defaultAgentConfig =
certificateFile = "/etc/opt/simplex-agent/agent.crt",
e2eEncryptVRange = supportedE2EEncryptVRange,
smpAgentVRange = supportedSMPAgentVRange,
smpClientVRange = supportedSMPClientVRange,
initialClientId = 0
smpClientVRange = supportedSMPClientVRange
}
data Env = Env
{ config :: AgentConfig,
store :: SQLiteStore,
random :: TVar ChaChaDRG,
clientCounter :: TVar Int,
randomServer :: TVar StdGen,
ntfSupervisor :: NtfSupervisor,
xftpAgent :: XFTPAgent,
@@ -191,14 +215,13 @@ data Env = Env
}
newSMPAgentEnv :: AgentConfig -> SQLiteStore -> IO Env
newSMPAgentEnv config@AgentConfig {initialClientId} store = do
newSMPAgentEnv config store = do
random <- C.newRandom
clientCounter <- newTVarIO initialClientId
randomServer <- newTVarIO =<< liftIO newStdGen
ntfSupervisor <- atomically . newNtfSubSupervisor $ tbqSize config
xftpAgent <- atomically newXFTPAgent
multicastSubscribers <- newTMVarIO 0
pure Env {config, store, random, clientCounter, randomServer, ntfSupervisor, xftpAgent, multicastSubscribers}
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
@@ -237,15 +260,24 @@ newXFTPAgent = do
xftpDelWorkers <- TM.empty
pure XFTPAgent {xftpWorkDir, xftpRcvWorkers, xftpSndWorkers, xftpDelWorkers}
tryAgentError :: AgentMonad m => m a -> m (Either AgentErrorType a)
tryAgentError :: AM a -> AM (Either AgentErrorType a)
tryAgentError = tryAllErrors mkInternal
{-# INLINE tryAgentError #-}
catchAgentError :: AgentMonad m => m a -> (AgentErrorType -> m a) -> m a
-- unlike runExceptT, this ensures we catch IO exceptions as well
tryAgentError' :: AM a -> AM' (Either AgentErrorType a)
tryAgentError' = tryAllErrors' mkInternal
{-# INLINE tryAgentError' #-}
catchAgentError :: AM a -> (AgentErrorType -> AM a) -> AM a
catchAgentError = catchAllErrors mkInternal
{-# INLINE catchAgentError #-}
agentFinally :: AgentMonad m => m a -> m b -> m a
catchAgentError' :: AM a -> (AgentErrorType -> AM' a) -> AM' a
catchAgentError' = catchAllErrors' mkInternal
{-# INLINE catchAgentError' #-}
agentFinally :: AM a -> AM b -> AM a
agentFinally = allFinally mkInternal
{-# INLINE agentFinally #-}
@@ -256,5 +288,16 @@ mkInternal = INTERNAL . show
data Worker = Worker
{ workerId :: Int,
doWork :: TMVar (),
action :: TMVar (Maybe (Async ()))
action :: TMVar (Maybe (Async ())),
restarts :: TVar RestartCount
}
data RestartCount = RestartCount
{ restartMinute :: Int64,
restartCount :: Int
}
updateRestartCount :: SystemTime -> RestartCount -> RestartCount
updateRestartCount t (RestartCount minute count) = do
let min' = systemSeconds t `div` 60
in RestartCount min' $ if minute == min' then count + 1 else 1
+8 -4
View File
@@ -1,15 +1,15 @@
{-# LANGUAGE NamedFieldPuns #-}
module Simplex.Messaging.Agent.Lock
( Lock,
createLock,
withLock,
withLock',
withGetLock,
withGetLocks,
)
where
import Control.Monad (void)
import Control.Monad.Except (ExceptT (..), runExceptT)
import Control.Monad.IO.Unlift
import Data.Functor (($>))
import UnliftIO.Async (forConcurrently)
@@ -22,8 +22,12 @@ createLock :: STM Lock
createLock = newEmptyTMVar
{-# INLINE createLock #-}
withLock :: MonadUnliftIO m => Lock -> String -> m a -> m a
withLock lock name =
withLock :: MonadUnliftIO m => Lock -> String -> ExceptT e m a -> ExceptT e m a
withLock lock name = ExceptT . withLock' lock name . runExceptT
{-# INLINE withLock #-}
withLock' :: MonadUnliftIO m => Lock -> String -> m a -> m a
withLock' lock name =
E.bracket_
(atomically $ putTMVar lock name)
(void . atomically $ takeTMVar lock)
+57 -52
View File
@@ -44,7 +44,7 @@ import UnliftIO
import UnliftIO.Concurrent (forkIO, threadDelay)
import qualified UnliftIO.Exception as E
runNtfSupervisor :: forall m. AgentMonad' m => AgentClient -> m ()
runNtfSupervisor :: AgentClient -> AM' ()
runNtfSupervisor c = do
ns <- asks ntfSupervisor
forever $ do
@@ -54,13 +54,13 @@ runNtfSupervisor c = do
Left e -> notifyErr connId e
Right _ -> return ()
where
handleErr :: ConnId -> m () -> m ()
handleErr :: ConnId -> AM' () -> AM' ()
handleErr connId = E.handle $ \(e :: E.SomeException) -> do
logError $ "runNtfSupervisor error " <> tshow e
notifyErr connId e
notifyErr connId e = notifyInternalError c connId $ "runNtfSupervisor error " <> show e
processNtfSub :: forall m. AgentMonad m => AgentClient -> (ConnId, NtfSupervisorCommand) -> m ()
processNtfSub :: AgentClient -> (ConnId, NtfSupervisorCommand) -> AM ()
processNtfSub c (connId, cmd) = do
logInfo $ "processNtfSub - connId = " <> tshow connId <> " - cmd = " <> tshow cmd
case cmd of
@@ -72,16 +72,16 @@ processNtfSub c (connId, cmd) = do
logInfo $ "processNtfSub, NSCCreate - a = " <> tshow a
case a of
Nothing -> do
withNtfServer c $ \ntfServer -> 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
void $ getNtfNTFWorker True c ntfServer
lift . void $ getNtfNTFWorker True c ntfServer
Nothing -> do
let newSub = newNtfSubscription connId smpServer Nothing ntfServer NASNew
withStore c $ \db -> createNtfSubscription db newSub $ NtfSubSMPAction NSASmpKey
void $ getNtfSMPWorker True c smpServer
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')
@@ -90,7 +90,7 @@ processNtfSub c (connId, cmd) = do
(Nothing, Nothing) -> create
_ -> rotate
where
create :: m ()
create :: AM ()
create = case action_ of
-- action was set to NULL after worker internal error
Nothing -> resetSubscription
@@ -99,77 +99,78 @@ processNtfSub c (connId, cmd) = do
| isDeleteNtfSubAction action -> do
if ntfSubStatus == NASNew || ntfSubStatus == NASOff || ntfSubStatus == NASDeleted
then resetSubscription
else withNtfServer c $ \ntfServer -> do
else withTokenServer $ \ntfServer -> do
withStore' c $ \db -> supervisorUpdateNtfSub db sub {ntfServer} (NtfSubNTFAction NSACreate)
void $ getNtfNTFWorker True c ntfServer
lift . void $ getNtfNTFWorker True c ntfServer
| otherwise -> case action of
NtfSubNTFAction _ -> void $ getNtfNTFWorker True c subNtfServer
NtfSubSMPAction _ -> void $ getNtfSMPWorker True c smpServer
rotate :: m ()
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)
void $ getNtfNTFWorker True c subNtfServer
resetSubscription :: m ()
lift . void $ getNtfNTFWorker True c subNtfServer
resetSubscription :: AM ()
resetSubscription =
withNtfServer c $ \ntfServer -> do
withTokenServer $ \ntfServer -> do
let sub' = sub {ntfQueueId = Nothing, ntfServer, ntfSubId = Nothing, ntfSubStatus = NASNew}
withStore' c $ \db -> supervisorUpdateNtfSub db sub' (NtfSubSMPAction NSASmpKey)
void $ getNtfSMPWorker True c smpServer
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}, _)) -> void $ getNtfNTFWorker True c ntfServer
(Just (NtfSubscription {ntfServer}, _)) -> lift . void $ getNtfNTFWorker True c ntfServer
_ -> pure () -- err "NSCDelete - no subscription"
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)
void $ getNtfSMPWorker True c smpServer
lift . void $ getNtfSMPWorker True c smpServer
_ -> notifyInternalError c connId "NSCSmpDelete - no rcv queue"
NSCNtfWorker ntfServer -> void $ getNtfNTFWorker True c ntfServer
NSCNtfSMPWorker smpServer -> void $ getNtfSMPWorker True c smpServer
NSCNtfWorker ntfServer -> lift . void $ getNtfNTFWorker True c ntfServer
NSCNtfSMPWorker smpServer -> lift . void $ getNtfSMPWorker True c smpServer
getNtfNTFWorker :: AgentMonad' m => Bool -> AgentClient -> NtfServer -> m Worker
getNtfNTFWorker :: Bool -> AgentClient -> NtfServer -> AM' Worker
getNtfNTFWorker hasWork c server = do
ws <- asks $ ntfWorkers . ntfSupervisor
getAgentWorker hasWork c server ws $ runNtfWorker c server
getAgentWorker "ntf_ntf" hasWork c server ws $ runNtfWorker c server
getNtfSMPWorker :: AgentMonad' m => Bool -> AgentClient -> SMPServer -> m Worker
getNtfSMPWorker :: Bool -> AgentClient -> SMPServer -> AM' Worker
getNtfSMPWorker hasWork c server = do
ws <- asks $ ntfSMPWorkers . ntfSupervisor
getAgentWorker hasWork c server ws $ runNtfSMPWorker c server
getAgentWorker "ntf_smp" hasWork c server ws $ runNtfSMPWorker c server
withNtfServer :: AgentMonad' m => AgentClient -> (NtfServer -> m ()) -> m ()
withNtfServer c action = getNtfServer c >>= mapM_ action
withTokenServer :: (NtfServer -> AM ()) -> AM ()
withTokenServer action = lift getNtfToken >>= mapM_ (\NtfToken {ntfServer} -> action ntfServer)
runNtfWorker :: forall m. AgentMonad m => AgentClient -> NtfServer -> Worker -> m ()
runNtfWorker :: AgentClient -> NtfServer -> Worker -> AM ()
runNtfWorker c srv Worker {doWork} = do
delay <- asks $ ntfWorkerDelay . config
forever $ do
waitForWork doWork
agentOperationBracket c AONtfNetwork throwWhenInactive runNtfOperation
ExceptT $ agentOperationBracket c AONtfNetwork throwWhenInactive $ runExceptT runNtfOperation
threadDelay delay
where
runNtfOperation :: m ()
runNtfOperation :: AM ()
runNtfOperation =
withWork c doWork (`getNextNtfSubNTFAction` srv) $
\nextSub@(NtfSubscription {connId}, _, _) -> do
logInfo $ "runNtfWorker, nextSub " <> tshow nextSub
ri <- asks $ reconnectInterval . config
withRetryInterval ri $ \_ loop ->
withRetryInterval ri $ \_ loop -> do
lift $ waitForUserNetwork c
processSub nextSub
`catchAgentError` retryOnError c "NtfWorker" loop (workerInternalError c connId . show)
processSub :: (NtfSubscription, NtfSubNTFAction, NtfActionTs) -> m ()
processSub :: (NtfSubscription, NtfSubNTFAction, NtfActionTs) -> AM ()
processSub (sub@NtfSubscription {connId, smpServer, ntfSubId}, action, actionTs) = do
ts <- liftIO getCurrentTime
unlessM (rescheduleAction doWork ts actionTs) $
unlessM (lift $ rescheduleAction doWork ts actionTs) $
case action of
NSACreate ->
getNtfToken >>= \case
lift getNtfToken >>= \case
Just tkn@NtfToken {ntfTokenId = Just tknId, ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
RcvQueue {clientNtfCreds} <- withStore c (`getPrimaryRcvQueue` connId)
case clientNtfCreds of
@@ -182,13 +183,13 @@ runNtfWorker c srv Worker {doWork} = do
_ -> workerInternalError c connId "NSACreate - no notifier queue credentials"
_ -> workerInternalError c connId "NSACreate - no active token"
NSACheck ->
getNtfToken >>= \case
lift getNtfToken >>= \case
Just tkn ->
case ntfSubId of
Just nSubId ->
agentNtfCheckSubscription c nSubId tkn >>= \case
NSAuth -> do
getNtfServer c >>= \case
lift (getNtfServer c) >>= \case
Just ntfServer -> do
withStore' c $ \db ->
updateNtfSubscription db sub {ntfServer, ntfQueueId = Nothing, ntfSubId = Nothing, ntfSubStatus = NASNew} (NtfSubSMPAction NSASmpKey) ts
@@ -200,7 +201,7 @@ runNtfWorker c srv Worker {doWork} = do
_ -> workerInternalError c connId "NSACheck - no active token"
NSADelete -> case ntfSubId of
Just nSubId ->
(getNtfToken >>= mapM_ (agentNtfDeleteSubscription c nSubId))
(lift getNtfToken >>= mapM_ (agentNtfDeleteSubscription c nSubId))
`agentFinally` continueDeletion
_ -> continueDeletion
where
@@ -211,7 +212,7 @@ runNtfWorker c srv Worker {doWork} = do
atomically $ writeTBQueue (ntfSubQ ns) (connId, NSCNtfSMPWorker smpServer)
NSARotate -> case ntfSubId of
Just nSubId ->
(getNtfToken >>= mapM_ (agentNtfDeleteSubscription c nSubId))
(lift getNtfToken >>= mapM_ (agentNtfDeleteSubscription c nSubId))
`agentFinally` deleteCreate
_ -> deleteCreate
where
@@ -228,12 +229,14 @@ runNtfWorker c srv Worker {doWork} = do
withStore' c $ \db ->
updateNtfSubscription db sub {ntfSubStatus = toStatus} toAction actionTs'
runNtfSMPWorker :: forall m. AgentMonad m => AgentClient -> SMPServer -> Worker -> m ()
runNtfSMPWorker :: AgentClient -> SMPServer -> Worker -> AM ()
runNtfSMPWorker c srv Worker {doWork} = do
env <- ask
delay <- asks $ ntfSMPWorkerDelay . config
forever $ do
waitForWork doWork
agentOperationBracket c AONtfNetwork throwWhenInactive runNtfSMPOperation
ExceptT . liftIO . agentOperationBracket c AONtfNetwork throwWhenInactive $
runReaderT (runExceptT runNtfSMPOperation) env
threadDelay delay
where
runNtfSMPOperation =
@@ -241,21 +244,22 @@ runNtfSMPWorker c srv Worker {doWork} = do
\nextSub@(NtfSubscription {connId}, _, _) -> do
logInfo $ "runNtfSMPWorker, nextSub " <> tshow nextSub
ri <- asks $ reconnectInterval . config
withRetryInterval ri $ \_ loop ->
withRetryInterval ri $ \_ loop -> do
lift $ waitForUserNetwork c
processSub nextSub
`catchAgentError` retryOnError c "NtfSMPWorker" loop (workerInternalError c connId . show)
processSub :: (NtfSubscription, NtfSubSMPAction, NtfActionTs) -> m ()
processSub :: (NtfSubscription, NtfSubSMPAction, NtfActionTs) -> AM ()
processSub (sub@NtfSubscription {connId, ntfServer}, smpAction, actionTs) = do
ts <- liftIO getCurrentTime
unlessM (rescheduleAction doWork ts actionTs) $
unlessM (lift $ rescheduleAction doWork ts actionTs) $
case smpAction of
NSASmpKey ->
getNtfToken >>= \case
lift getNtfToken >>= \case
Just NtfToken {ntfTknStatus = NTActive, ntfMode = NMInstant} -> do
rq <- withStore c (`getPrimaryRcvQueue` connId)
C.SignAlg a <- asks (cmdSignAlg . config)
C.AuthAlg a <- asks (rcvAuthAlg . config)
g <- asks random
(ntfPublicKey, ntfPrivateKey) <- atomically $ C.generateSignatureKeyPair a g
(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
@@ -272,7 +276,7 @@ runNtfSMPWorker c srv Worker {doWork} = do
mapM_ (disableQueueNotifications c) rq_
withStore' c $ \db -> deleteNtfSubscription db connId
rescheduleAction :: AgentMonad' m => TMVar () -> UTCTime -> UTCTime -> m Bool
rescheduleAction :: TMVar () -> UTCTime -> UTCTime -> AM' Bool
rescheduleAction doWork ts actionTs
| actionTs <= ts = pure False
| otherwise = do
@@ -282,7 +286,7 @@ rescheduleAction doWork ts actionTs
atomically $ hasWorkToDo' doWork
pure True
retryOnError :: AgentMonad' m => AgentClient -> Text -> m () -> (AgentErrorType -> m ()) -> AgentErrorType -> m ()
retryOnError :: AgentClient -> Text -> AM () -> (AgentErrorType -> AM ()) -> AgentErrorType -> AM ()
retryOnError c name loop done e = do
logError $ name <> " error: " <> tshow e
case e of
@@ -296,16 +300,17 @@ retryOnError c name loop done e = do
atomically $ beginAgentOperation c AONtfNetwork
loop
workerInternalError :: AgentMonad m => AgentClient -> ConnId -> String -> m ()
workerInternalError :: AgentClient -> ConnId -> String -> AM ()
workerInternalError c connId internalErrStr = do
withStore' c $ \db -> setNullNtfSubscriptionAction db connId
notifyInternalError c connId internalErrStr
-- TODO change error
notifyInternalError :: MonadUnliftIO m => AgentClient -> ConnId -> String -> m ()
notifyInternalError :: MonadIO m => AgentClient -> ConnId -> String -> m ()
notifyInternalError AgentClient {subQ} connId internalErrStr = atomically $ writeTBQueue subQ ("", connId, APC SAEConn $ ERR $ INTERNAL internalErrStr)
{-# INLINE notifyInternalError #-}
getNtfToken :: AgentMonad' m => m (Maybe NtfToken)
getNtfToken :: AM' (Maybe NtfToken)
getNtfToken = do
tkn <- asks $ ntfTkn . ntfSupervisor
readTVarIO tkn
@@ -326,14 +331,14 @@ instantNotifications = \case
Just NtfToken {ntfTknStatus = NTActive, ntfMode = NMInstant} -> True
_ -> False
closeNtfSupervisor :: MonadUnliftIO m => NtfSupervisor -> m ()
closeNtfSupervisor :: NtfSupervisor -> IO ()
closeNtfSupervisor ns = do
stopWorkers $ ntfWorkers ns
stopWorkers $ ntfSMPWorkers ns
where
stopWorkers workers = atomically (swapTVar workers M.empty) >>= mapM_ (liftIO . cancelWorker)
getNtfServer :: AgentMonad' m => AgentClient -> m (Maybe NtfServer)
getNtfServer :: AgentClient -> AM' (Maybe NtfServer)
getNtfServer c = do
ntfServers <- readTVarIO $ ntfServers c
case ntfServers of
+207 -134
View File
@@ -4,6 +4,7 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
@@ -33,6 +34,14 @@
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md
module Simplex.Messaging.Agent.Protocol
( -- * Protocol parameters
VersionSMPA,
VersionRangeSMPA,
pattern VersionSMPA,
duplexHandshakeSMPAgentVersion,
ratchetSyncSMPAgentVersion,
deliveryRcptsSMPAgentVersion,
pqdrSMPAgentVersion,
currentSMPAgentVersion,
supportedSMPAgentVRange,
e2eEncConnInfoLength,
e2eEncUserMsgLength,
@@ -97,7 +106,7 @@ module Simplex.Messaging.Agent.Protocol
AConnectionRequestUri (..),
ConnReqUriData (..),
CRClientData,
ConnReqScheme (..),
ServiceScheme,
simplexChat,
AgentErrorType (..),
CommandErrorType (..),
@@ -164,7 +173,7 @@ import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as L
import Data.Map (Map)
import qualified Data.Map as M
import Data.Maybe (isJust)
import Data.Maybe (fromMaybe, isJust)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
@@ -173,14 +182,25 @@ import Data.Time.Clock.System (SystemTime)
import Data.Time.ISO8601
import Data.Type.Equality
import Data.Typeable ()
import Data.Word (Word32)
import Data.Word (Word16, Word32)
import Database.SQLite.Simple.FromField
import Database.SQLite.Simple.ToField
import Simplex.FileTransfer.Description
import Simplex.FileTransfer.Protocol (FileParty (..), XFTPErrorType)
import Simplex.FileTransfer.Protocol (FileParty (..))
import Simplex.FileTransfer.Transport (XFTPErrorType)
import Simplex.Messaging.Agent.QueryString
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.Ratchet (E2ERatchetParams, E2ERatchetParamsUri)
import Simplex.Messaging.Crypto.Ratchet
( InitialKeys (..),
PQEncryption (..),
pattern PQEncOff,
PQSupport,
pattern PQSupportOn,
pattern PQSupportOff,
RcvE2ERatchetParams,
RcvE2ERatchetParamsUri,
SndE2ERatchetParams
)
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers
@@ -196,40 +216,87 @@ import Simplex.Messaging.Protocol
SMPMsgMeta,
SMPServer,
SMPServerWithAuth,
SndPublicVerifyKey,
SrvLoc (..),
SndPublicAuthKey,
SubscriptionMode,
SMPClientVersion,
VersionSMPC,
VersionRangeSMPC,
initialSMPClientVersion,
legacyEncodeServer,
legacyServerP,
legacyStrEncodeServer,
noAuthSrv,
sameSrvAddr,
srvHostnamesSMPClientVersion,
pattern ProtoServerWithAuth,
pattern SMPServer,
)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.ServiceScheme
import Simplex.Messaging.Transport (Transport (..), TransportError, serializeTransportError, transportErrorP)
import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts_ (..))
import Simplex.Messaging.Util
import Simplex.Messaging.Version
import Simplex.Messaging.Version.Internal
import Simplex.RemoteControl.Types
import Text.Read
import UnliftIO.Exception (Exception)
currentSMPAgentVersion :: Version
currentSMPAgentVersion = 4
-- SMP agent protocol version history:
-- 1 - binary protocol encoding (1/1/2022)
-- 2 - "duplex" (more efficient) connection handshake (6/9/2022)
-- 3 - support ratchet renegotiation (6/30/2023)
-- 4 - delivery receipts (7/13/2023)
-- 5 - post-quantum double ratchet (3/14/2024)
supportedSMPAgentVRange :: VersionRange
supportedSMPAgentVRange = mkVersionRange 1 currentSMPAgentVersion
data SMPAgentVersion
instance VersionScope SMPAgentVersion
type VersionSMPA = Version SMPAgentVersion
type VersionRangeSMPA = VersionRange SMPAgentVersion
pattern VersionSMPA :: Word16 -> VersionSMPA
pattern VersionSMPA v = Version v
duplexHandshakeSMPAgentVersion :: VersionSMPA
duplexHandshakeSMPAgentVersion = VersionSMPA 2
ratchetSyncSMPAgentVersion :: VersionSMPA
ratchetSyncSMPAgentVersion = VersionSMPA 3
deliveryRcptsSMPAgentVersion :: VersionSMPA
deliveryRcptsSMPAgentVersion = VersionSMPA 4
pqdrSMPAgentVersion :: VersionSMPA
pqdrSMPAgentVersion = VersionSMPA 5
-- TODO v5.7 increase to 5
currentSMPAgentVersion :: VersionSMPA
currentSMPAgentVersion = VersionSMPA 4
-- TODO v5.7 remove dependency of version range on whether PQ support is needed
supportedSMPAgentVRange :: PQSupport -> VersionRangeSMPA
supportedSMPAgentVRange pq =
mkVersionRange duplexHandshakeSMPAgentVersion $ case pq of
PQSupportOn -> pqdrSMPAgentVersion
PQSupportOff -> currentSMPAgentVersion
-- it is shorter to allow all handshake headers,
-- including E2E (double-ratchet) parameters and
-- signing key of the sender for the server
e2eEncConnInfoLength :: Int
e2eEncConnInfoLength = 14848
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
e2eEncUserMsgLength :: Int
e2eEncUserMsgLength = 15856
e2eEncUserMsgLength :: VersionSMPA -> PQSupport -> Int
e2eEncUserMsgLength v = \case
-- reduced by 2222 (the increase of message ratchet header size)
PQSupportOn | v >= pqdrSMPAgentVersion -> 13634
_ -> 15856
-- | Raw (unparsed) SMP agent protocol transmission.
type ARawTransmission = (ByteString, ByteString, ByteString)
@@ -255,8 +322,6 @@ data SAParty :: AParty -> Type where
deriving instance Show (SAParty p)
deriving instance Eq (SAParty p)
instance TestEquality SAParty where
testEquality SAgent SAgent = Just Refl
testEquality SClient SClient = Just Refl
@@ -279,8 +344,6 @@ data SAEntity :: AEntity -> Type where
deriving instance Show (SAEntity e)
deriving instance Eq (SAEntity e)
instance TestEquality SAEntity where
testEquality SAEConn SAEConn = Just Refl
testEquality SAERcvFile SAERcvFile = Just Refl
@@ -315,16 +378,16 @@ type ConnInfo = ByteString
-- | Parameterized type for SMP agent protocol commands and responses from all participants.
data ACommand (p :: AParty) (e :: AEntity) where
NEW :: Bool -> AConnectionMode -> SubscriptionMode -> ACommand Client AEConn -- response INV
NEW :: Bool -> AConnectionMode -> InitialKeys -> SubscriptionMode -> ACommand Client AEConn -- response INV
INV :: AConnectionRequestUri -> ACommand Agent AEConn
JOIN :: Bool -> AConnectionRequestUri -> SubscriptionMode -> ConnInfo -> ACommand Client AEConn -- response OK
CONF :: ConfirmationId -> [SMPServer] -> ConnInfo -> ACommand Agent AEConn -- ConnInfo is from sender, [SMPServer] will be empty only in v1 handshake
JOIN :: Bool -> AConnectionRequestUri -> PQSupport -> SubscriptionMode -> ConnInfo -> ACommand Client AEConn -- response OK
CONF :: ConfirmationId -> PQSupport -> [SMPServer] -> ConnInfo -> ACommand Agent AEConn -- ConnInfo is from sender, [SMPServer] will be empty only in v1 handshake
LET :: ConfirmationId -> ConnInfo -> ACommand Client AEConn -- ConnInfo is from client
REQ :: InvitationId -> NonEmpty SMPServer -> ConnInfo -> ACommand Agent AEConn -- ConnInfo is from sender
ACPT :: InvitationId -> ConnInfo -> ACommand Client AEConn -- ConnInfo is from client
REQ :: InvitationId -> PQSupport -> NonEmpty SMPServer -> ConnInfo -> ACommand Agent AEConn -- ConnInfo is from sender
ACPT :: InvitationId -> PQSupport -> ConnInfo -> ACommand Client AEConn -- ConnInfo is from client
RJCT :: InvitationId -> ACommand Client AEConn
INFO :: ConnInfo -> ACommand Agent AEConn
CON :: ACommand Agent AEConn -- notification that connection is established
INFO :: PQSupport -> ConnInfo -> ACommand Agent AEConn
CON :: PQEncryption -> ACommand Agent AEConn -- notification that connection is established
SUB :: ACommand Client AEConn
END :: ACommand Agent AEConn
CONNECT :: AProtocolType -> TransportHost -> ACommand Agent AENone
@@ -333,14 +396,16 @@ data ACommand (p :: AParty) (e :: AEntity) where
UP :: SMPServer -> [ConnId] -> ACommand Agent AENone
SWITCH :: QueueDirection -> SwitchPhase -> ConnectionStats -> ACommand Agent AEConn
RSYNC :: RatchetSyncState -> Maybe AgentCryptoError -> ConnectionStats -> ACommand Agent AEConn
SEND :: MsgFlags -> MsgBody -> ACommand Client AEConn
MID :: AgentMsgId -> ACommand Agent AEConn
SEND :: PQEncryption -> MsgFlags -> MsgBody -> ACommand Client AEConn
MID :: AgentMsgId -> PQEncryption -> ACommand Agent AEConn
SENT :: AgentMsgId -> ACommand Agent AEConn
MERR :: AgentMsgId -> AgentErrorType -> ACommand Agent AEConn
MERRS :: NonEmpty AgentMsgId -> AgentErrorType -> ACommand Agent AEConn
MSG :: MsgMeta -> MsgFlags -> MsgBody -> ACommand Agent AEConn
MSGNTF :: SMPMsgMeta -> ACommand Agent AEConn
ACK :: AgentMsgId -> Maybe MsgReceiptInfo -> ACommand Client AEConn
RCVD :: MsgMeta -> NonEmpty MsgReceipt -> ACommand Agent AEConn
QCONT :: ACommand Agent AEConn
SWCH :: ACommand Client AEConn
OFF :: ACommand Client AEConn
DEL :: ACommand Client AEConn
@@ -398,10 +463,12 @@ data ACommandTag (p :: AParty) (e :: AEntity) where
MID_ :: ACommandTag Agent AEConn
SENT_ :: ACommandTag Agent AEConn
MERR_ :: ACommandTag Agent AEConn
MERRS_ :: ACommandTag Agent AEConn
MSG_ :: ACommandTag Agent AEConn
MSGNTF_ :: ACommandTag Agent AEConn
ACK_ :: ACommandTag Client AEConn
RCVD_ :: ACommandTag Agent AEConn
QCONT_ :: ACommandTag Agent AEConn
SWCH_ :: ACommandTag Client AEConn
OFF_ :: ACommandTag Client AEConn
DEL_ :: ACommandTag Client AEConn
@@ -438,8 +505,8 @@ aCommandTag = \case
REQ {} -> REQ_
ACPT {} -> ACPT_
RJCT _ -> RJCT_
INFO _ -> INFO_
CON -> CON_
INFO {} -> INFO_
CON _ -> CON_
SUB -> SUB_
END -> END_
CONNECT {} -> CONNECT_
@@ -449,13 +516,15 @@ aCommandTag = \case
SWITCH {} -> SWITCH_
RSYNC {} -> RSYNC_
SEND {} -> SEND_
MID _ -> MID_
MID {} -> MID_
SENT _ -> SENT_
MERR {} -> MERR_
MERRS {} -> MERRS_
MSG {} -> MSG_
MSGNTF {} -> MSGNTF_
ACK {} -> ACK_
RCVD {} -> RCVD_
QCONT -> QCONT_
SWCH -> SWCH_
OFF -> OFF_
DEL -> DEL_
@@ -644,7 +713,7 @@ instance StrEncoding SndQueueInfo where
pure SndQueueInfo {sndServer, sndSwitchStatus}
data ConnectionStats = ConnectionStats
{ connAgentVersion :: Version,
{ connAgentVersion :: VersionSMPA,
rcvQueuesInfo :: [RcvQueueInfo],
sndQueuesInfo :: [SndQueueInfo],
ratchetSyncState :: RatchetSyncState,
@@ -748,17 +817,19 @@ data MsgMeta = MsgMeta
{ integrity :: MsgIntegrity,
recipient :: (AgentMsgId, UTCTime),
broker :: (MsgId, UTCTime),
sndMsgId :: AgentMsgId
sndMsgId :: AgentMsgId,
pqEncryption :: PQEncryption
}
deriving (Eq, Show)
instance StrEncoding MsgMeta where
strEncode MsgMeta {integrity, recipient = (rmId, rTs), broker = (bmId, bTs), sndMsgId} =
strEncode MsgMeta {integrity, recipient = (rmId, rTs), broker = (bmId, bTs), sndMsgId, pqEncryption} =
B.unwords
[ strEncode integrity,
"R=" <> bshow rmId <> "," <> showTs rTs,
"B=" <> encode bmId <> "," <> showTs bTs,
"S=" <> bshow sndMsgId
"S=" <> bshow sndMsgId,
"PQ=" <> strEncode pqEncryption
]
where
showTs = B.pack . formatISO8601Millis
@@ -767,13 +838,14 @@ instance StrEncoding MsgMeta where
recipient <- " R=" *> partyMeta A.decimal
broker <- " B=" *> partyMeta base64P
sndMsgId <- " S=" *> A.decimal
pure MsgMeta {integrity, recipient, broker, sndMsgId}
pqEncryption <- " PQ=" *> strP
pure MsgMeta {integrity, recipient, broker, sndMsgId, pqEncryption}
where
partyMeta idParser = (,) <$> idParser <* A.char ',' <*> tsISO8601P
data SMPConfirmation = SMPConfirmation
{ -- | sender's public key to use for authentication of sender's commands at the recepient's server
senderKey :: SndPublicVerifyKey,
senderKey :: SndPublicAuthKey,
-- | sender's DH public key for simple per-queue e2e encryption
e2ePubKey :: C.PublicKeyX25519,
-- | sender's information to be associated with the connection, e.g. sender's profile information
@@ -781,28 +853,28 @@ data SMPConfirmation = SMPConfirmation
-- | optional reply queues included in confirmation (added in agent protocol v2)
smpReplyQueues :: [SMPQueueInfo],
-- | SMP client version
smpClientVersion :: Version
smpClientVersion :: VersionSMPC
}
deriving (Show)
data AgentMsgEnvelope
= AgentConfirmation
{ agentVersion :: Version,
e2eEncryption_ :: Maybe (E2ERatchetParams 'C.X448),
{ agentVersion :: VersionSMPA,
e2eEncryption_ :: Maybe (SndE2ERatchetParams 'C.X448),
encConnInfo :: ByteString
}
| AgentMsgEnvelope
{ agentVersion :: Version,
{ agentVersion :: VersionSMPA,
encAgentMessage :: ByteString
}
| AgentInvitation -- the connInfo in contactInvite is only encrypted with per-queue E2E, not with double ratchet,
{ agentVersion :: Version,
{ agentVersion :: VersionSMPA,
connReq :: ConnectionRequestUri 'CMInvitation,
connInfo :: ByteString -- this message is only encrypted with per-queue E2E, not with double ratchet,
}
| AgentRatchetKey
{ agentVersion :: Version,
e2eEncryption :: E2ERatchetParams 'C.X448,
{ agentVersion :: VersionSMPA,
e2eEncryption :: RcvE2ERatchetParams 'C.X448,
info :: ByteString
}
deriving (Show)
@@ -840,9 +912,10 @@ instance Encoding AgentMsgEnvelope where
-- or in case of AgentInvitation - in plain text body)
-- AgentRatchetInfo is not encrypted with double ratchet, but with per-queue E2E encryption
data AgentMessage
= AgentConnInfo ConnInfo
| -- AgentConnInfoReply is only used in duplexHandshake mode (v2), allowing to include reply queue(s) in the initial confirmation.
-- It makes REPLY message unnecessary.
= -- used by the initiating party when confirming reply queue
AgentConnInfo ConnInfo
| -- AgentConnInfoReply is used by accepting party in duplexHandshake mode (v2), allowing to include reply queue(s) in the initial confirmation.
-- It made removed REPLY message unnecessary.
AgentConnInfoReply (NonEmpty SMPQueueInfo) ConnInfo
| AgentRatchetInfo ByteString
| AgentMessage APrivHeader AMessage
@@ -924,11 +997,9 @@ agentMessageType = \case
-- 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_
-- REPLY is only used in v1
REPLY _ -> AM_REPLY_
A_MSG _ -> AM_A_MSG_
A_RCVD {} -> AM_A_RCVD_
QCONT _ -> AM_QCONT_
A_QCONT _ -> AM_QCONT_
QADD _ -> AM_QADD_
QKEY _ -> AM_QKEY_
QUSE _ -> AM_QUSE_
@@ -950,10 +1021,9 @@ instance Encoding APrivHeader where
data AMsgType
= HELLO_
| REPLY_
| A_MSG_
| A_RCVD_
| QCONT_
| A_QCONT_
| QADD_
| QKEY_
| QUSE_
@@ -964,10 +1034,9 @@ data AMsgType
instance Encoding AMsgType where
smpEncode = \case
HELLO_ -> "H"
REPLY_ -> "R"
A_MSG_ -> "M"
A_RCVD_ -> "V"
QCONT_ -> "QC"
A_QCONT_ -> "QC"
QADD_ -> "QA"
QKEY_ -> "QK"
QUSE_ -> "QU"
@@ -976,12 +1045,11 @@ instance Encoding AMsgType where
smpP =
A.anyChar >>= \case
'H' -> pure HELLO_
'R' -> pure REPLY_
'M' -> pure A_MSG_
'V' -> pure A_RCVD_
'Q' ->
A.anyChar >>= \case
'C' -> pure QCONT_
'C' -> pure A_QCONT_
'A' -> pure QADD_
'K' -> pure QKEY_
'U' -> pure QUSE_
@@ -996,18 +1064,16 @@ instance Encoding AMsgType where
data AMessage
= -- | the first message in the queue to validate it is secured
HELLO
| -- | reply queues information
REPLY (NonEmpty SMPQueueInfo)
| -- | agent envelope for the client message
A_MSG MsgBody
| -- | agent envelope for delivery receipt
A_RCVD (NonEmpty AMessageReceipt)
| -- | the message instructing the client to continue sending messages (after ERR QUOTA)
QCONT SndQAddr
A_QCONT SndQAddr
| -- add queue to connection (sent by recipient), with optional address of the replaced queue
QADD (NonEmpty (SMPQueueUri, Maybe SndQAddr))
| -- key to secure the added queues and agree e2e encryption key (sent by sender)
QKEY (NonEmpty (SMPQueueInfo, SndPublicVerifyKey))
QKEY (NonEmpty (SMPQueueInfo, SndPublicAuthKey))
| -- inform that the queues are ready to use (sent by recipient)
QUSE (NonEmpty (SndQAddr, Bool))
| -- sent by the sender to test new queues and to complete switching
@@ -1059,10 +1125,9 @@ type SndQAddr = (SMPServer, SMP.SenderId)
instance Encoding AMessage where
smpEncode = \case
HELLO -> smpEncode HELLO_
REPLY smpQueues -> smpEncode (REPLY_, smpQueues)
A_MSG body -> smpEncode (A_MSG_, Tail body)
A_RCVD mrs -> smpEncode (A_RCVD_, mrs)
QCONT addr -> smpEncode (QCONT_, addr)
A_QCONT addr -> smpEncode (A_QCONT_, addr)
QADD qs -> smpEncode (QADD_, qs)
QKEY qs -> smpEncode (QKEY_, qs)
QUSE qs -> smpEncode (QUSE_, qs)
@@ -1072,10 +1137,9 @@ instance Encoding AMessage where
smpP
>>= \case
HELLO_ -> pure HELLO
REPLY_ -> REPLY <$> smpP
A_MSG_ -> A_MSG . unTail <$> smpP
A_RCVD_ -> A_RCVD <$> smpP
QCONT_ -> QCONT <$> smpP
A_QCONT_ -> A_QCONT <$> smpP
QADD_ -> QADD <$> smpP
QKEY_ -> QKEY <$> smpP
QUSE_ -> QUSE <$> smpP
@@ -1102,7 +1166,7 @@ instance forall m. ConnectionModeI m => StrEncoding (ConnectionRequestUri m) whe
CRInvitationUri crData e2eParams -> crEncode "invitation" crData (Just e2eParams)
CRContactUri crData -> crEncode "contact" crData Nothing
where
crEncode :: ByteString -> ConnReqUriData -> Maybe (E2ERatchetParamsUri 'C.X448) -> ByteString
crEncode :: ByteString -> ConnReqUriData -> Maybe (RcvE2ERatchetParamsUri 'C.X448) -> ByteString
crEncode crMode ConnReqUriData {crScheme, crAgentVRange, crSmpQueues, crClientData} e2eParams =
strEncode crScheme <> "/" <> crMode <> "#/?" <> queryStr
where
@@ -1120,20 +1184,25 @@ instance forall m. ConnectionModeI m => StrEncoding (ConnectionRequestUri m) whe
instance StrEncoding AConnectionRequestUri where
strEncode (ACR _ cr) = strEncode cr
strP = do
_crScheme :: ConnReqScheme <- strP
_crScheme :: ServiceScheme <- strP
crMode <- A.char '/' *> crModeP <* optional (A.char '/') <* "#/?"
query <- strP
crAgentVRange <- queryParam "v" query
aVRange <- queryParam "v" query
crSmpQueues <- queryParam "smp" query
let crClientData = safeDecodeUtf8 <$> queryParamStr "data" query
let crData = ConnReqUriData {crScheme = CRSSimplex, crAgentVRange, crSmpQueues, crClientData}
let crData = ConnReqUriData {crScheme = SSSimplex, crAgentVRange = aVRange, crSmpQueues, crClientData}
case crMode of
CMInvitation -> do
crE2eParams <- queryParam "e2e" query
pure . ACR SCMInvitation $ CRInvitationUri crData crE2eParams
CMContact -> pure . ACR SCMContact $ CRContactUri crData
-- contact links are adjusted to the minimum version supported by the agent
-- to preserve compatibility with the old links published online
CMContact -> pure . ACR SCMContact $ CRContactUri crData {crAgentVRange = adjustAgentVRange aVRange}
where
crModeP = "invitation" $> CMInvitation <|> "contact" $> CMContact
adjustAgentVRange vr =
let v = max duplexHandshakeSMPAgentVersion $ minVersion vr
in fromMaybe vr $ safeVersionRange v (max v $ maxVersion vr)
instance ConnectionModeI m => FromJSON (ConnectionRequestUri m) where
parseJSON = strParseJSON "ConnectionRequestUri"
@@ -1210,16 +1279,16 @@ sameQueue :: SMPQueue q => (SMPServer, SMP.QueueId) -> q -> Bool
sameQueue addr q = sameQAddress addr (qAddress q)
{-# INLINE sameQueue #-}
data SMPQueueInfo = SMPQueueInfo {clientVersion :: Version, queueAddress :: SMPQueueAddress}
data SMPQueueInfo = SMPQueueInfo {clientVersion :: VersionSMPC, queueAddress :: SMPQueueAddress}
deriving (Eq, Show)
instance Encoding SMPQueueInfo where
smpEncode (SMPQueueInfo clientVersion SMPQueueAddress {smpServer, senderId, dhPublicKey})
| clientVersion > 1 = smpEncode (clientVersion, smpServer, senderId, dhPublicKey)
| clientVersion > initialSMPClientVersion = smpEncode (clientVersion, smpServer, senderId, dhPublicKey)
| otherwise = smpEncode clientVersion <> legacyEncodeServer smpServer <> smpEncode (senderId, dhPublicKey)
smpP = do
clientVersion <- smpP
smpServer <- if clientVersion > 1 then smpP else updateSMPServerHosts <$> legacyServerP
smpServer <- if clientVersion > initialSMPClientVersion then smpP else updateSMPServerHosts <$> legacyServerP
(senderId, dhPublicKey) <- smpP
pure $ SMPQueueInfo clientVersion SMPQueueAddress {smpServer, senderId, dhPublicKey}
@@ -1227,20 +1296,20 @@ instance Encoding SMPQueueInfo where
-- But this is created to allow backward and forward compatibility where SMPQueueUri
-- could have more fields to convert to different versions of SMPQueueInfo in a different way,
-- and this instance would become non-trivial.
instance VersionI SMPQueueInfo where
type VersionRangeT SMPQueueInfo = SMPQueueUri
instance VersionI SMPClientVersion SMPQueueInfo where
type VersionRangeT SMPClientVersion SMPQueueInfo = SMPQueueUri
version = clientVersion
toVersionRangeT (SMPQueueInfo _v addr) vr = SMPQueueUri vr addr
instance VersionRangeI SMPQueueUri where
type VersionT SMPQueueUri = SMPQueueInfo
instance VersionRangeI SMPClientVersion SMPQueueUri where
type VersionT SMPClientVersion SMPQueueUri = SMPQueueInfo
versionRange = clientVRange
toVersionT (SMPQueueUri _vr addr) v = SMPQueueInfo v addr
-- | SMP queue information sent out-of-band.
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#out-of-band-messages
data SMPQueueUri = SMPQueueUri {clientVRange :: VersionRange, queueAddress :: SMPQueueAddress}
data SMPQueueUri = SMPQueueUri {clientVRange :: VersionRangeSMPC, queueAddress :: SMPQueueAddress}
deriving (Eq, Show)
data SMPQueueAddress = SMPQueueAddress
@@ -1274,7 +1343,7 @@ sameQAddress (srv, qId) (srv', qId') = sameSrvAddr srv srv' && qId == qId'
instance StrEncoding SMPQueueUri where
strEncode (SMPQueueUri vr SMPQueueAddress {smpServer = srv, senderId = qId, dhPublicKey})
| minVersion vr > 1 = strEncode srv <> "/" <> strEncode qId <> "#/?" <> query queryParams
| minVersion vr >= srvHostnamesSMPClientVersion = strEncode srv <> "/" <> strEncode qId <> "#/?" <> query queryParams
| otherwise = legacyStrEncodeServer srv <> "/" <> strEncode qId <> "#/?" <> query (queryParams <> srvParam)
where
query = strEncode . QSP QEscape
@@ -1286,10 +1355,10 @@ instance StrEncoding SMPQueueUri where
senderId <- strP <* optional (A.char '/') <* A.char '#'
(vr, hs, dhPublicKey) <- unversioned <|> versioned
let srv' = srv {host = h :| host <> hs}
smpServer = if maxVersion vr == 1 then updateSMPServerHosts srv' else srv'
smpServer = if maxVersion vr < srvHostnamesSMPClientVersion then updateSMPServerHosts srv' else srv'
pure $ SMPQueueUri vr SMPQueueAddress {smpServer, senderId, dhPublicKey}
where
unversioned = (versionToRange 1,[],) <$> strP <* A.endOfInput
unversioned = (versionToRange initialSMPClientVersion,[],) <$> strP <* A.endOfInput
versioned = do
dhKey_ <- optional strP
query <- optional (A.char '/') *> A.char '?' *> strP
@@ -1306,8 +1375,8 @@ instance Encoding SMPQueueUri where
pure $ SMPQueueUri clientVRange SMPQueueAddress {smpServer, senderId, dhPublicKey}
data ConnectionRequestUri (m :: ConnectionMode) where
CRInvitationUri :: ConnReqUriData -> E2ERatchetParamsUri 'C.X448 -> ConnectionRequestUri CMInvitation
-- contact connection request does NOT contain E2E encryption parameters -
CRInvitationUri :: ConnReqUriData -> RcvE2ERatchetParamsUri 'C.X448 -> ConnectionRequestUri CMInvitation
-- contact connection request does NOT contain E2E encryption parameters for double ratchet -
-- they are passed in AgentInvitation message
CRContactUri :: ConnReqUriData -> ConnectionRequestUri CMContact
@@ -1318,15 +1387,15 @@ deriving instance Show (ConnectionRequestUri m)
data AConnectionRequestUri = forall m. ConnectionModeI m => ACR (SConnectionMode m) (ConnectionRequestUri m)
instance Eq AConnectionRequestUri where
ACR m cr == ACR m' cr' = case testEquality m m' of
Just Refl -> cr == cr'
_ -> False
ACR m cr == ACR m' cr' = case testEquality m m' of
Just Refl -> cr == cr'
_ -> False
deriving instance Show AConnectionRequestUri
data ConnReqUriData = ConnReqUriData
{ crScheme :: ConnReqScheme,
crAgentVRange :: VersionRange,
{ crScheme :: ServiceScheme,
crAgentVRange :: VersionRangeSMPA,
crSmpQueues :: NonEmpty SMPQueueUri,
crClientData :: Maybe CRClientData
}
@@ -1334,20 +1403,6 @@ data ConnReqUriData = ConnReqUriData
type CRClientData = Text
data ConnReqScheme = CRSSimplex | CRSAppServer SrvLoc
deriving (Eq, Show)
instance StrEncoding ConnReqScheme where
strEncode = \case
CRSSimplex -> "simplex:"
CRSAppServer srv -> "https://" <> strEncode srv
strP =
"simplex:" $> CRSSimplex
<|> "https://" *> (CRSAppServer <$> strP)
simplexChat :: ConnReqScheme
simplexChat = CRSAppServer $ SrvLoc "simplex.chat" ""
-- | SMP queue status.
data QueueStatus
= -- | queue is created
@@ -1432,6 +1487,8 @@ data AgentErrorType
AGENT {agentErr :: SMPAgentError}
| -- | agent implementation or dependency errors
INTERNAL {internalErr :: String}
| -- | critical agent errors that should be shown to the user, optionally with restart button
CRITICAL {offerRestart :: Bool, criticalErr :: String}
| -- | agent inactive
INACTIVE
deriving (Eq, Show, Exception)
@@ -1543,6 +1600,7 @@ instance StrEncoding AgentErrorType where
<|> "AGENT QUEUE " *> (AGENT . A_QUEUE <$> parseRead A.takeByteString)
<|> "AGENT " *> (AGENT <$> parseRead1)
<|> "INTERNAL " *> (INTERNAL <$> parseRead A.takeByteString)
<|> "CRITICAL " *> (CRITICAL <$> parseRead1 <* A.space <*> parseRead A.takeByteString)
<|> "INACTIVE" $> INACTIVE
where
textP = T.unpack . safeDecodeUtf8 <$> A.takeTill (== ' ')
@@ -1560,6 +1618,7 @@ instance StrEncoding AgentErrorType where
AGENT (A_QUEUE e) -> "AGENT QUEUE " <> bshow e
AGENT e -> "AGENT " <> bshow e
INTERNAL e -> "INTERNAL " <> bshow e
CRITICAL restart e -> "CRITICAL " <> bshow restart <> " " <> bshow e
INACTIVE -> "INACTIVE"
where
text = encodeUtf8 . T.pack
@@ -1607,10 +1666,12 @@ instance StrEncoding ACmdTag where
"MID" -> ct MID_
"SENT" -> ct SENT_
"MERR" -> ct MERR_
"MERRS" -> ct MERRS_
"MSG" -> ct MSG_
"MSGNTF" -> ct MSGNTF_
"ACK" -> t ACK_
"RCVD" -> ct RCVD_
"QCONT" -> ct QCONT_
"SWCH" -> t SWCH_
"OFF" -> t OFF_
"DEL" -> t DEL_
@@ -1663,10 +1724,12 @@ instance (APartyI p, AEntityI e) => StrEncoding (ACommandTag p e) where
MID_ -> "MID"
SENT_ -> "SENT"
MERR_ -> "MERR"
MERRS_ -> "MERRS"
MSG_ -> "MSG"
MSGNTF_ -> "MSGNTF"
ACK_ -> "ACK"
RCVD_ -> "RCVD"
QCONT_ -> "QCONT"
SWCH_ -> "SWCH"
OFF_ -> "OFF"
DEL_ -> "DEL"
@@ -1703,13 +1766,13 @@ commandP binaryP =
>>= \case
ACmdTag SClient e cmd ->
ACmd SClient e <$> case cmd of
NEW_ -> s (NEW <$> strP_ <*> strP_ <*> (strP <|> pure SMP.SMSubscribe))
JOIN_ -> s (JOIN <$> strP_ <*> strP_ <*> (strP_ <|> pure SMP.SMSubscribe) <*> binaryP)
NEW_ -> s (NEW <$> strP_ <*> strP_ <*> pqIKP <*> (strP <|> pure SMP.SMSubscribe))
JOIN_ -> s (JOIN <$> strP_ <*> strP_ <*> pqSupP <*> (strP_ <|> pure SMP.SMSubscribe) <*> binaryP)
LET_ -> s (LET <$> A.takeTill (== ' ') <* A.space <*> binaryP)
ACPT_ -> s (ACPT <$> A.takeTill (== ' ') <* A.space <*> binaryP)
ACPT_ -> s (ACPT <$> A.takeTill (== ' ') <* A.space <*> pqSupP <*> binaryP)
RJCT_ -> s (RJCT <$> A.takeByteString)
SUB_ -> pure SUB
SEND_ -> s (SEND <$> smpP <* A.space <*> binaryP)
SEND_ -> s (SEND <$> pqEncP <*> smpP <* A.space <*> binaryP)
ACK_ -> s (ACK <$> A.decimal <*> optional (A.space *> binaryP))
SWCH_ -> pure SWCH
OFF_ -> pure OFF
@@ -1718,10 +1781,10 @@ commandP binaryP =
ACmdTag SAgent e cmd ->
ACmd SAgent e <$> case cmd of
INV_ -> s (INV <$> strP)
CONF_ -> s (CONF <$> A.takeTill (== ' ') <* A.space <*> strListP <* A.space <*> binaryP)
REQ_ -> s (REQ <$> A.takeTill (== ' ') <* A.space <*> strP_ <*> binaryP)
INFO_ -> s (INFO <$> binaryP)
CON_ -> pure CON
CONF_ -> s (CONF <$> A.takeTill (== ' ') <* A.space <*> pqSupP <*> strListP <* A.space <*> binaryP)
REQ_ -> s (REQ <$> A.takeTill (== ' ') <* A.space <*> pqSupP <*> strP_ <*> binaryP)
INFO_ -> s (INFO <$> pqSupP <*> binaryP)
CON_ -> s (CON <$> strP)
END_ -> pure END
CONNECT_ -> s (CONNECT <$> strP_ <*> strP)
DISCONNECT_ -> s (DISCONNECT <$> strP_ <*> strP)
@@ -1729,12 +1792,14 @@ commandP binaryP =
UP_ -> s (UP <$> strP_ <*> connections)
SWITCH_ -> s (SWITCH <$> strP_ <*> strP_ <*> strP)
RSYNC_ -> s (RSYNC <$> strP_ <*> strP <*> strP)
MID_ -> s (MID <$> A.decimal)
MID_ -> s (MID <$> A.decimal <*> _strP)
SENT_ -> s (SENT <$> A.decimal)
MERR_ -> s (MERR <$> A.decimal <* A.space <*> strP)
MERRS_ -> s (MERRS <$> strP_ <*> strP)
MSG_ -> s (MSG <$> strP <* A.space <*> smpP <* A.space <*> binaryP)
MSGNTF_ -> s (MSGNTF <$> strP)
RCVD_ -> s (RCVD <$> strP <* A.space <*> strP)
QCONT_ -> pure QCONT
DEL_RCVQ_ -> s (DEL_RCVQ <$> strP_ <*> strP_ <*> strP)
DEL_CONN_ -> pure DEL_CONN
DEL_USER_ -> s (DEL_USER <$> strP)
@@ -1751,6 +1816,12 @@ commandP binaryP =
where
s :: Parser a -> Parser a
s p = A.space *> p
pqIKP :: Parser InitialKeys
pqIKP = strP_ <|> pure (IKNoPQ PQSupportOff)
pqSupP :: Parser PQSupport
pqSupP = strP_ <|> pure PQSupportOff
pqEncP :: Parser PQEncryption
pqEncP = strP_ <|> pure PQEncOff
connections :: Parser [ConnId]
connections = strP `A.sepBy'` A.char ','
sfDone :: Text -> Either String (ACommand 'Agent 'AESndFile)
@@ -1766,15 +1837,15 @@ parseCommand = parse (commandP A.takeByteString) $ CMD SYNTAX
-- | Serialize SMP agent command.
serializeCommand :: ACommand p e -> ByteString
serializeCommand = \case
NEW ntfs cMode subMode -> s (NEW_, ntfs, cMode, subMode)
NEW ntfs cMode pqIK subMode -> s (NEW_, ntfs, cMode, pqIK, subMode)
INV cReq -> s (INV_, cReq)
JOIN ntfs cReq subMode cInfo -> s (JOIN_, ntfs, cReq, subMode, Str $ serializeBinary cInfo)
CONF confId srvs cInfo -> B.unwords [s CONF_, confId, strEncodeList srvs, serializeBinary cInfo]
JOIN ntfs cReq pqSup subMode cInfo -> s (JOIN_, ntfs, cReq, pqSup, subMode, Str $ serializeBinary cInfo)
CONF confId pqSup srvs cInfo -> B.unwords [s CONF_, confId, s pqSup, strEncodeList srvs, serializeBinary cInfo]
LET confId cInfo -> B.unwords [s LET_, confId, serializeBinary cInfo]
REQ invId srvs cInfo -> B.unwords [s REQ_, invId, s srvs, serializeBinary cInfo]
ACPT invId cInfo -> B.unwords [s ACPT_, invId, serializeBinary cInfo]
REQ invId pqSup srvs cInfo -> B.unwords [s REQ_, invId, s pqSup, s srvs, serializeBinary cInfo]
ACPT invId pqSup cInfo -> B.unwords [s ACPT_, invId, s pqSup, serializeBinary cInfo]
RJCT invId -> B.unwords [s RJCT_, invId]
INFO cInfo -> B.unwords [s INFO_, serializeBinary cInfo]
INFO pqSup cInfo -> B.unwords [s INFO_, s pqSup, serializeBinary cInfo]
SUB -> s SUB_
END -> s END_
CONNECT p h -> s (CONNECT_, p, h)
@@ -1783,14 +1854,16 @@ serializeCommand = \case
UP srv conns -> B.unwords [s UP_, s srv, connections conns]
SWITCH dir phase srvs -> s (SWITCH_, dir, phase, srvs)
RSYNC rrState cryptoErr cstats -> s (RSYNC_, rrState, cryptoErr, cstats)
SEND msgFlags msgBody -> B.unwords [s SEND_, smpEncode msgFlags, serializeBinary msgBody]
MID mId -> s (MID_, Str $ bshow mId)
SENT mId -> s (SENT_, Str $ bshow mId)
MERR mId e -> s (MERR_, Str $ bshow mId, e)
SEND pqEnc msgFlags msgBody -> B.unwords [s SEND_, s pqEnc, smpEncode msgFlags, serializeBinary msgBody]
MID mId pqEnc -> s (MID_, mId, pqEnc)
SENT mId -> s (SENT_, mId)
MERR mId e -> s (MERR_, mId, e)
MERRS mIds e -> s (MERRS_, mIds, e)
MSG msgMeta msgFlags msgBody -> B.unwords [s MSG_, s msgMeta, smpEncode msgFlags, serializeBinary msgBody]
MSGNTF smpMsgMeta -> s (MSGNTF_, smpMsgMeta)
ACK mId rcptInfo_ -> s (ACK_, Str $ bshow mId) <> maybe "" (B.cons ' ' . serializeBinary) rcptInfo_
ACK mId rcptInfo_ -> s (ACK_, mId) <> maybe "" (B.cons ' ' . serializeBinary) rcptInfo_
RCVD msgMeta rcpts -> s (RCVD_, msgMeta, rcpts)
QCONT -> s QCONT_
SWCH -> s SWCH_
OFF -> s OFF_
DEL -> s DEL_
@@ -1799,7 +1872,7 @@ serializeCommand = \case
DEL_USER userId -> s (DEL_USER_, userId)
CHK -> s CHK_
STAT srvs -> s (STAT_, srvs)
CON -> s CON_
CON pqEnc -> s (CON_, pqEnc)
ERR e -> s (ERR_, e)
OK -> s OK_
SUSPENDED -> s SUSPENDED_
@@ -1831,15 +1904,15 @@ tGetRaw :: Transport c => c -> IO ARawTransmission
tGetRaw h = (,,) <$> getLn h <*> getLn h <*> getLn h
-- | Send SMP agent protocol command (or response) to TCP connection.
tPut :: (Transport c, MonadIO m) => c -> ATransmission p -> m ()
tPut :: Transport c => c -> ATransmission p -> IO ()
tPut h (corrId, connId, APC _ cmd) =
liftIO $ tPutRaw h (corrId, connId, serializeCommand cmd)
tPutRaw h (corrId, connId, serializeCommand cmd)
-- | Receive client and agent transmissions from TCP connection.
tGet :: forall c m p. (Transport c, MonadIO m) => SAParty p -> c -> m (ATransmissionOrError p)
tGet :: forall c p. Transport c => SAParty p -> c -> IO (ATransmissionOrError p)
tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody
where
tParseLoadBody :: ARawTransmission -> m (ATransmissionOrError p)
tParseLoadBody :: ARawTransmission -> IO (ATransmissionOrError p)
tParseLoadBody t@(corrId, entId, command) = do
let cmd = parseCommand command >>= fromParty >>= tConnId t
fullCmd <- either (return . Left) cmdWithMsgBody cmd
@@ -1869,20 +1942,20 @@ tGet party h = liftIO (tGetRaw h) >>= tParseLoadBody
| B.null entId -> Left $ CMD NO_CONN
| otherwise -> Right cmd
cmdWithMsgBody :: APartyCmd p -> m (Either AgentErrorType (APartyCmd p))
cmdWithMsgBody :: APartyCmd p -> IO (Either AgentErrorType (APartyCmd p))
cmdWithMsgBody (APC e cmd) =
APC e <$$> case cmd of
SEND msgFlags body -> SEND msgFlags <$$> getBody body
SEND pqEnc msgFlags body -> SEND pqEnc msgFlags <$$> getBody body
MSG msgMeta msgFlags body -> MSG msgMeta msgFlags <$$> getBody body
JOIN ntfs qUri subMode cInfo -> JOIN ntfs qUri subMode <$$> getBody cInfo
CONF confId srvs cInfo -> CONF confId srvs <$$> getBody cInfo
JOIN ntfs qUri pqSup subMode cInfo -> JOIN ntfs qUri pqSup subMode <$$> getBody cInfo
CONF confId pqSup srvs cInfo -> CONF confId pqSup srvs <$$> getBody cInfo
LET confId cInfo -> LET confId <$$> getBody cInfo
REQ invId srvs cInfo -> REQ invId srvs <$$> getBody cInfo
ACPT invId cInfo -> ACPT invId <$$> getBody cInfo
INFO cInfo -> INFO <$$> getBody cInfo
REQ invId pqSup srvs cInfo -> REQ invId pqSup srvs <$$> getBody cInfo
ACPT invId pqSup cInfo -> ACPT invId pqSup <$$> getBody cInfo
INFO pqSup cInfo -> INFO pqSup <$$> getBody cInfo
_ -> pure $ Right cmd
getBody :: ByteString -> m (Either AgentErrorType ByteString)
getBody :: ByteString -> IO (Either AgentErrorType ByteString)
getBody binary =
case B.unpack binary of
':' : body -> return . Right $ B.pack body
+5 -4
View File
@@ -11,6 +11,7 @@ module Simplex.Messaging.Agent.RetryInterval
withRetryIntervalCount,
withRetryLock2,
updateRetryInterval2,
nextRetryDelay,
)
where
@@ -60,7 +61,7 @@ withRetryIntervalCount ri action = callAction 0 0 $ initialInterval ri
loop = do
liftIO $ threadDelay' delay
let elapsed' = elapsed + delay
callAction (n + 1) elapsed' $ nextDelay elapsed' delay ri
callAction (n + 1) elapsed' $ nextRetryDelay elapsed' delay ri
-- This function allows action to toggle between slow and fast retry intervals.
withRetryLock2 :: forall m. MonadIO m => RetryInterval2 -> TMVar () -> (RI2State -> (RetryIntervalMode -> m ()) -> m ()) -> m ()
@@ -76,7 +77,7 @@ withRetryLock2 RetryInterval2 {riSlow, riFast} lock action =
run (elapsed, delay) ri call = do
wait delay
let elapsed' = elapsed + delay
delay' = nextDelay elapsed' delay ri
delay' = nextRetryDelay elapsed' delay ri
call (elapsed', delay')
wait delay = do
waiting <- newTVarIO True
@@ -87,8 +88,8 @@ withRetryLock2 RetryInterval2 {riSlow, riFast} lock action =
takeTMVar lock
writeTVar waiting False
nextDelay :: Int64 -> Int64 -> RetryInterval -> Int64
nextDelay elapsed delay RetryInterval {increaseAfter, maxInterval} =
nextRetryDelay :: Int64 -> Int64 -> RetryInterval -> Int64
nextRetryDelay elapsed delay RetryInterval {increaseAfter, maxInterval} =
if elapsed < increaseAfter || delay == maxInterval
then delay
else min (delay * 3 `div` 2) maxInterval
+24 -20
View File
@@ -12,13 +12,13 @@ where
import Control.Logger.Simple (logInfo)
import Control.Monad
import Control.Monad.IO.Unlift (MonadUnliftIO)
import Control.Monad.Reader
import Crypto.Random (MonadRandom)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Text.Encoding (decodeUtf8)
import Network.Socket (ServiceName)
import Simplex.Messaging.Agent
import Simplex.Messaging.Agent.Client (newAgentClient)
import Simplex.Messaging.Agent.Env.SQLite
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore)
@@ -32,50 +32,54 @@ import UnliftIO.STM
-- | Runs an SMP agent as a TCP service using passed configuration.
--
-- See a full agent executable here: https://github.com/simplex-chat/simplexmq/blob/master/apps/smp-agent/Main.hs
runSMPAgent :: (MonadRandom m, MonadUnliftIO m) => ATransport -> AgentConfig -> InitialAgentServers -> SQLiteStore -> m ()
runSMPAgent :: ATransport -> AgentConfig -> InitialAgentServers -> SQLiteStore -> IO ()
runSMPAgent t cfg initServers store =
runSMPAgentBlocking t cfg initServers store =<< newEmptyTMVarIO
runSMPAgentBlocking t cfg initServers store 0 =<< newEmptyTMVarIO
-- | Runs an SMP agent as a TCP service using passed configuration with signalling.
--
-- This function uses passed TMVar to signal when the server is ready to accept TCP requests (True)
-- and when it is disconnected from the TCP socket once the server thread is killed (False).
runSMPAgentBlocking :: (MonadRandom m, MonadUnliftIO m) => ATransport -> AgentConfig -> InitialAgentServers -> SQLiteStore -> TMVar Bool -> m ()
runSMPAgentBlocking (ATransport t) cfg@AgentConfig {tcpPort, caCertificateFile, certificateFile, privateKeyFile} initServers store started = do
liftIO (newSMPAgentEnv cfg store) >>= runReaderT (smpAgent t)
runSMPAgentBlocking :: ATransport -> AgentConfig -> InitialAgentServers -> SQLiteStore -> Int -> TMVar Bool -> IO ()
runSMPAgentBlocking (ATransport t) cfg@AgentConfig {tcpPort, caCertificateFile, certificateFile, privateKeyFile} initServers store initClientId started =
case tcpPort of
Just port -> newSMPAgentEnv cfg store >>= smpAgent t port
Nothing -> E.throwIO $ userError "no agent port"
where
smpAgent :: forall c m'. (Transport c, MonadUnliftIO m', MonadReader Env m') => TProxy c -> m' ()
smpAgent _ = do
smpAgent :: forall c. Transport c => TProxy c -> ServiceName -> Env -> IO ()
smpAgent _ port env = do
-- tlsServerParams is not in Env to avoid breaking functional API w/t key and certificate generation
tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile
runTransportServer started tcpPort tlsServerParams defaultTransportServerConfig $ \(h :: c) -> do
liftIO . putLn h $ "Welcome to SMP agent v" <> B.pack simplexMQVersion
c <- getAgentClient initServers
tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile
clientId <- newTVarIO initClientId
runTransportServer started port tlsServerParams defaultTransportServerConfig $ \(h :: c) -> do
putLn h $ "Welcome to SMP agent v" <> B.pack simplexMQVersion
cId <- atomically $ stateTVar clientId $ \i -> (i + 1, i + 1)
c <- atomically $ newAgentClient cId initServers env
logConnection c True
race_ (connectClient h c) (runAgentClient c)
`E.finally` disconnectAgentClient c
race_ (connectClient h c) (runAgentClient c `runReaderT` env)
`E.finally` (disconnectAgentClient c)
connectClient :: Transport c => MonadUnliftIO m => c -> AgentClient -> m ()
connectClient :: Transport c => c -> AgentClient -> IO ()
connectClient h c = race_ (send h c) (receive h c)
receive :: forall c m. (Transport c, MonadUnliftIO m) => c -> AgentClient -> m ()
receive :: forall c. Transport c => c -> AgentClient -> IO ()
receive h c@AgentClient {rcvQ, subQ} = forever $ do
(corrId, entId, cmdOrErr) <- tGet SClient h
case cmdOrErr of
Right cmd -> write rcvQ (corrId, entId, cmd)
Left e -> write subQ (corrId, entId, APC SAEConn $ ERR e)
where
write :: TBQueue (ATransmission p) -> ATransmission p -> m ()
write :: TBQueue (ATransmission p) -> ATransmission p -> IO ()
write q t = do
logClient c "-->" t
atomically $ writeTBQueue q t
send :: (Transport c, MonadUnliftIO m) => c -> AgentClient -> m ()
send :: Transport c => c -> AgentClient -> IO ()
send h c@AgentClient {subQ} = forever $ do
t <- atomically $ readTBQueue subQ
tPut h t
logClient c "<--" t
logClient :: MonadUnliftIO m => AgentClient -> ByteString -> ATransmission a -> m ()
logClient :: AgentClient -> ByteString -> ATransmission a -> IO ()
logClient AgentClient {clientId} dir (corrId, connId, APC _ cmd) = do
logInfo . decodeUtf8 $ B.unwords [bshow clientId, dir, "A :", corrId, connId, B.takeWhile (/= ' ') $ serializeCommand cmd]
+31 -44
View File
@@ -30,23 +30,24 @@ import Data.Type.Equality
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.RetryInterval (RI2State)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.Ratchet (RatchetX448)
import Simplex.Messaging.Crypto.Ratchet (RatchetX448, PQEncryption, PQSupport)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol
( MsgBody,
MsgFlags,
MsgId,
NotifierId,
NtfPrivateSignKey,
NtfPublicVerifyKey,
NtfPrivateAuthKey,
NtfPublicAuthKey,
RcvDhSecret,
RcvNtfDhSecret,
RcvPrivateSignKey,
SndPrivateSignKey,
RcvPrivateAuthKey,
SndPrivateAuthKey,
SndPublicAuthKey,
VersionSMPC,
)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.Util ((<$?>))
import Simplex.Messaging.Version
-- * Queue types
@@ -60,8 +61,6 @@ data DBQueueId (q :: QueueStored) where
DBQueueId :: Int64 -> DBQueueId 'QSStored
DBNewQueue :: DBQueueId 'QSNew
deriving instance Eq (DBQueueId q)
deriving instance Show (DBQueueId q)
type RcvQueue = StoredRcvQueue 'QSStored
@@ -75,8 +74,8 @@ data StoredRcvQueue (q :: QueueStored) = RcvQueue
server :: SMPServer,
-- | recipient queue ID
rcvId :: SMP.RecipientId,
-- | key used by the recipient to sign transmissions
rcvPrivateKey :: RcvPrivateSignKey,
-- | key used by the recipient to authorize transmissions
rcvPrivateKey :: RcvPrivateAuthKey,
-- | shared DH secret used to encrypt/decrypt message bodies from server to recipient
rcvDhSecret :: RcvDhSecret,
-- | private DH key related to public sent to sender out-of-band (to agree simple per-queue e2e)
@@ -95,12 +94,12 @@ data StoredRcvQueue (q :: QueueStored) = RcvQueue
dbReplaceQueueId :: Maybe Int64,
rcvSwchStatus :: Maybe RcvSwitchStatus,
-- | SMP client version
smpClientVersion :: Version,
smpClientVersion :: VersionSMPC,
-- | credentials used in context of notifications
clientNtfCreds :: Maybe ClientNtfCreds,
deleteErrors :: Int
}
deriving (Eq, Show)
deriving (Show)
rcvQueueInfo :: RcvQueue -> RcvQueueInfo
rcvQueueInfo rq@RcvQueue {server, rcvSwchStatus} =
@@ -119,15 +118,15 @@ canAbortRcvSwitch = maybe False canAbort . rcvSwchStatus
RSReceivedMessage -> False
data ClientNtfCreds = ClientNtfCreds
{ -- | key pair to be used by the notification server to sign transmissions
ntfPublicKey :: NtfPublicVerifyKey,
ntfPrivateKey :: NtfPrivateSignKey,
{ -- | key pair to be used by the notification server to authorize transmissions
ntfPublicKey :: NtfPublicAuthKey,
ntfPrivateKey :: NtfPrivateAuthKey,
-- | queue ID to be used by the notification server for NSUB command
notifierId :: NotifierId,
-- | shared DH secret used to encrypt/decrypt notification metadata (NMsgMeta) from server to recipient
rcvNtfDhSecret :: RcvNtfDhSecret
}
deriving (Eq, Show)
deriving (Show)
type SndQueue = StoredSndQueue 'QSStored
@@ -140,9 +139,10 @@ data StoredSndQueue (q :: QueueStored) = SndQueue
server :: SMPServer,
-- | sender queue ID
sndId :: SMP.SenderId,
-- | key pair used by the sender to sign transmissions
sndPublicKey :: Maybe C.APublicVerifyKey,
sndPrivateKey :: SndPrivateSignKey,
-- | key pair used by the sender to authorize transmissions
-- TODO combine keys to key pair so that types match
sndPublicKey :: Maybe SndPublicAuthKey,
sndPrivateKey :: SndPrivateAuthKey,
-- | DH public key used to negotiate per-queue e2e encryption
e2ePubKey :: Maybe C.PublicKeyX25519,
-- | shared DH secret agreed for simple per-queue e2e encryption
@@ -157,9 +157,9 @@ data StoredSndQueue (q :: QueueStored) = SndQueue
dbReplaceQueueId :: Maybe Int64,
sndSwchStatus :: Maybe SndSwitchStatus,
-- | SMP client version
smpClientVersion :: Version
smpClientVersion :: VersionSMPC
}
deriving (Eq, Show)
deriving (Show)
sndQueueInfo :: SndQueue -> SndQueueInfo
sndQueueInfo SndQueue {server, sndSwchStatus} =
@@ -254,8 +254,6 @@ data Connection (d :: ConnType) where
DuplexConnection :: ConnData -> NonEmpty RcvQueue -> NonEmpty SndQueue -> Connection CDuplex
ContactConnection :: ConnData -> RcvQueue -> Connection CContact
deriving instance Eq (Connection d)
deriving instance Show (Connection d)
toConnData :: Connection d -> ConnData
@@ -288,8 +286,6 @@ connType SCSnd = CSnd
connType SCDuplex = CDuplex
connType SCContact = CContact
deriving instance Eq (SConnType d)
deriving instance Show (SConnType d)
instance TestEquality SConnType where
@@ -303,35 +299,24 @@ instance TestEquality SConnType where
-- Used to refer to an arbitrary connection when retrieving from store.
data SomeConn = forall d. SomeConn (SConnType d) (Connection d)
instance Eq SomeConn where
SomeConn d c == SomeConn d' c' = case testEquality d d' of
Just Refl -> c == c'
_ -> False
deriving instance Show SomeConn
data ConnData = ConnData
{ connId :: ConnId,
userId :: UserId,
connAgentVersion :: Version,
connAgentVersion :: VersionSMPA,
enableNtfs :: Bool,
duplexHandshake :: Maybe Bool, -- added in agent protocol v2
lastExternalSndId :: PrevExternalSndId,
deleted :: Bool,
ratchetSyncState :: RatchetSyncState
ratchetSyncState :: RatchetSyncState,
pqSupport :: PQSupport
}
deriving (Eq, Show)
-- this function should be mirrored in the clients
ratchetSyncAllowed :: ConnData -> Bool
ratchetSyncAllowed cData@ConnData {ratchetSyncState} =
ratchetSyncSupported' cData && (ratchetSyncState `elem` ([RSAllowed, RSRequired] :: [RatchetSyncState]))
ratchetSyncSupported' :: ConnData -> Bool
ratchetSyncSupported' ConnData {connAgentVersion} = connAgentVersion >= 3
messageRcptsSupported :: ConnData -> Bool
messageRcptsSupported ConnData {connAgentVersion} = connAgentVersion >= 4
ratchetSyncAllowed ConnData {ratchetSyncState, connAgentVersion} =
connAgentVersion >= ratchetSyncSMPAgentVersion && (ratchetSyncState `elem` ([RSAllowed, RSRequired] :: [RatchetSyncState]))
-- this function should be mirrored in the clients
ratchetSyncSendProhibited :: ConnData -> Bool
@@ -388,11 +373,11 @@ instance StrEncoding AgentCommandTag where
data InternalCommand
= ICAck SMP.RecipientId MsgId
| ICAckDel SMP.RecipientId MsgId InternalId
| ICAllowSecure SMP.RecipientId SMP.SndPublicVerifyKey
| ICDuplexSecure SMP.RecipientId SMP.SndPublicVerifyKey
| ICAllowSecure SMP.RecipientId SMP.SndPublicAuthKey
| ICDuplexSecure SMP.RecipientId SMP.SndPublicAuthKey
| ICDeleteConn
| ICDeleteRcvQueue SMP.RecipientId
| ICQSecure SMP.RecipientId SMP.SndPublicVerifyKey
| ICQSecure SMP.RecipientId SMP.SndPublicAuthKey
| ICQDelete SMP.RecipientId
data InternalCommandTag
@@ -539,6 +524,7 @@ data SndMsgData = SndMsgData
msgType :: AgentMessageType,
msgFlags :: MsgFlags,
msgBody :: MsgBody,
pqEncryption :: PQEncryption,
internalHash :: MsgHash,
prevMsgHash :: MsgHash
}
@@ -556,6 +542,7 @@ data PendingMsgData = PendingMsgData
msgType :: AgentMessageType,
msgFlags :: MsgFlags,
msgBody :: MsgBody,
pqEncryption :: PQEncryption,
msgRetryState :: Maybe RI2State,
internalTs :: InternalTs
}
+247 -159
View File
@@ -50,7 +50,6 @@ module Simplex.Messaging.Agent.Store.SQLite
createNewConn,
updateNewConnRcv,
updateNewConnSnd,
createRcvConn, -- no longer used
createSndConn,
getConn,
getDeletedConn,
@@ -59,7 +58,9 @@ module Simplex.Messaging.Agent.Store.SQLite
getConnData,
setConnDeleted,
setConnAgentVersion,
setConnPQSupport,
getDeletedConnIds,
getDeletedWaitingDeliveryConnIds,
setConnRatchetSync,
addProcessedRatchetKeyHash,
checkRatchetKeyHashExists,
@@ -92,7 +93,6 @@ module Simplex.Messaging.Agent.Store.SQLite
acceptConfirmation,
getAcceptedConfirmation,
removeConfirmations,
setHandshakeVersion,
-- Invitations - sent via Contact connections
createInvitation,
getInvitation,
@@ -110,6 +110,7 @@ module Simplex.Messaging.Agent.Store.SQLite
getPendingQueueMsg,
updatePendingMsgRIState,
deletePendingMsgs,
getExpiredSndMessages,
setMsgUserAck,
getRcvMsg,
getLastMsg,
@@ -122,8 +123,6 @@ module Simplex.Messaging.Agent.Store.SQLite
-- Double ratchet persistence
createRatchetX3dhKeys,
getRatchetX3dhKeys,
createRatchetX3dhKeys',
getRatchetX3dhKeys',
setRatchetX3dhKeys,
createRatchet,
deleteRatchet,
@@ -163,13 +162,16 @@ module Simplex.Messaging.Agent.Store.SQLite
-- Rcv files
createRcvFile,
createRcvFileRedirect,
getRcvFile,
getRcvFileByEntityId,
getRcvFileRedirects,
updateRcvChunkReplicaDelay,
updateRcvFileChunkReceived,
updateRcvFileStatus,
updateRcvFileError,
updateRcvFileComplete,
updateRcvFileRedirect,
updateRcvFileNoTmpPath,
updateRcvFileDeleted,
deleteRcvFile',
@@ -230,6 +232,7 @@ import Data.ByteArray (ScrubbedBytes)
import qualified Data.ByteArray as BA
import Data.ByteString (ByteString)
import qualified Data.ByteString.Base64.URL as U
import qualified Data.ByteString.Char8 as B
import Data.Char (toLower)
import Data.Functor (($>))
import Data.IORef
@@ -238,7 +241,7 @@ import Data.List (foldl', intercalate, sortBy)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as L
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe, isJust, listToMaybe)
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe)
import Data.Ord (Down (..))
import Data.Text (Text)
import qualified Data.Text as T
@@ -254,7 +257,7 @@ import qualified Database.SQLite3 as SQLite3
import Network.Socket (ServiceName)
import Simplex.FileTransfer.Client (XFTPChunkSpec (..))
import Simplex.FileTransfer.Description
import Simplex.FileTransfer.Protocol (FileParty (..))
import Simplex.FileTransfer.Protocol (FileParty (..), SFileParty (..))
import Simplex.FileTransfer.Types
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.RetryInterval (RI2State (..))
@@ -265,7 +268,8 @@ import Simplex.Messaging.Agent.Store.SQLite.Migrations (DownMigration (..), MTRE
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
import Simplex.Messaging.Crypto.Ratchet (RatchetX448, SkippedMsgDiff (..), SkippedMsgKeys)
import Simplex.Messaging.Crypto.Ratchet (RatchetX448, SkippedMsgDiff (..), SkippedMsgKeys, PQEncryption (..), PQSupport (..))
import qualified Simplex.Messaging.Crypto.Ratchet as CR
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfSubscriptionId, NtfTknStatus (..), NtfTokenId, SMPQueueNtf (..))
@@ -275,7 +279,7 @@ import Simplex.Messaging.Protocol
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.Transport.Client (TransportHost)
import Simplex.Messaging.Util (bshow, catchAllErrors, eitherToMaybe, ifM, safeDecodeUtf8, ($>>=), (<$$>))
import Simplex.Messaging.Version
import Simplex.Messaging.Version.Internal
import System.Directory (copyFile, createDirectoryIfMissing, doesFileExist)
import System.Exit (exitFailure)
import System.FilePath (takeDirectory)
@@ -539,11 +543,8 @@ createConn_ gVar cData create = checkConstraint SEConnDuplicate $ case cData of
ConnData {connId} -> Right . (connId,) <$> create connId
createNewConn :: DB.Connection -> TVar ChaChaDRG -> ConnData -> SConnectionMode c -> IO (Either StoreError ConnId)
createNewConn db gVar cData@ConnData {userId, connAgentVersion, enableNtfs, duplexHandshake} cMode = do
fst <$$> createConn_ gVar cData create
where
create connId =
DB.execute db "INSERT INTO connections (user_id, conn_id, conn_mode, smp_agent_version, enable_ntfs, duplex_handshake) VALUES (?,?,?,?,?,?)" (userId, connId, cMode, connAgentVersion, enableNtfs, duplexHandshake)
createNewConn db gVar cData cMode = do
fst <$$> createConn_ gVar cData (\connId -> createConnRecord db connId cData cMode)
updateNewConnRcv :: DB.Connection -> ConnId -> NewRcvQueue -> IO (Either StoreError RcvQueue)
updateNewConnRcv db connId rq =
@@ -565,22 +566,25 @@ updateNewConnSnd db connId sq =
updateConn :: IO (Either StoreError SndQueue)
updateConn = Right <$> addConnSndQueue_ db connId sq
createRcvConn :: DB.Connection -> TVar ChaChaDRG -> ConnData -> NewRcvQueue -> SConnectionMode c -> IO (Either StoreError (ConnId, RcvQueue))
createRcvConn db gVar cData@ConnData {userId, connAgentVersion, enableNtfs, duplexHandshake} q@RcvQueue {server} cMode =
createConn_ gVar cData $ \connId -> do
serverKeyHash_ <- createServer_ db server
DB.execute db "INSERT INTO connections (user_id, conn_id, conn_mode, smp_agent_version, enable_ntfs, duplex_handshake) VALUES (?,?,?,?,?,?)" (userId, connId, cMode, connAgentVersion, enableNtfs, duplexHandshake)
insertRcvQueue_ db connId q serverKeyHash_
createSndConn :: DB.Connection -> TVar ChaChaDRG -> ConnData -> NewSndQueue -> IO (Either StoreError (ConnId, SndQueue))
createSndConn db gVar cData@ConnData {userId, connAgentVersion, enableNtfs, duplexHandshake} q@SndQueue {server} =
createSndConn db gVar cData q@SndQueue {server} =
-- check confirmed snd queue doesn't already exist, to prevent it being deleted by REPLACE in insertSndQueue_
ifM (liftIO $ checkConfirmedSndQueueExists_ db q) (pure $ Left SESndQueueExists) $
createConn_ gVar cData $ \connId -> do
serverKeyHash_ <- createServer_ db server
DB.execute db "INSERT INTO connections (user_id, conn_id, conn_mode, smp_agent_version, enable_ntfs, duplex_handshake) VALUES (?,?,?,?,?,?)" (userId, connId, SCMInvitation, connAgentVersion, enableNtfs, duplexHandshake)
createConnRecord db connId cData SCMInvitation
insertSndQueue_ db connId q serverKeyHash_
createConnRecord :: DB.Connection -> ConnId -> ConnData -> SConnectionMode c -> IO ()
createConnRecord db connId ConnData {userId, connAgentVersion, enableNtfs, pqSupport} cMode =
DB.execute
db
[sql|
INSERT INTO connections
(user_id, conn_id, conn_mode, smp_agent_version, enable_ntfs, pq_support, duplex_handshake) VALUES (?,?,?,?,?,?,?)
|]
(userId, connId, cMode, connAgentVersion, enableNtfs, pqSupport, True)
checkConfirmedSndQueueExists_ :: DB.Connection -> NewSndQueue -> IO Bool
checkConfirmedSndQueueExists_ db SndQueue {server, sndId} = do
fromMaybe False
@@ -599,12 +603,32 @@ getRcvConn db ProtocolServer {host, port} rcvId = runExceptT $ do
DB.query db (rcvQueueQuery <> " WHERE q.host = ? AND q.port = ? AND q.rcv_id = ? AND q.deleted = 0") (host, port, rcvId)
(rq,) <$> ExceptT (getConn db connId)
deleteConn :: DB.Connection -> ConnId -> IO ()
deleteConn db connId =
DB.executeNamed
db
"DELETE FROM connections WHERE conn_id = :conn_id;"
[":conn_id" := connId]
-- | Deletes connection, optionally checking for pending snd message deliveries; returns connection id if it was deleted
deleteConn :: DB.Connection -> Maybe NominalDiffTime -> ConnId -> IO (Maybe ConnId)
deleteConn db waitDeliveryTimeout_ connId = case waitDeliveryTimeout_ of
Nothing -> delete
Just timeout ->
ifM
checkNoPendingDeliveries_
delete
( ifM
(checkWaitDeliveryTimeout_ timeout)
delete
(pure Nothing)
)
where
delete = DB.execute db "DELETE FROM connections WHERE conn_id = ?" (Only connId) $> Just connId
checkNoPendingDeliveries_ = do
r :: (Maybe Int64) <-
maybeFirstRow fromOnly $
DB.query db "SELECT 1 FROM snd_message_deliveries WHERE conn_id = ? AND failed = 0 LIMIT 1" (Only connId)
pure $ isNothing r
checkWaitDeliveryTimeout_ timeout = do
cutoffTs <- addUTCTime (-timeout) <$> getCurrentTime
r :: (Maybe Int64) <-
maybeFirstRow fromOnly $
DB.query db "SELECT 1 FROM connections WHERE conn_id = ? AND deleted_at_wait_delivery < ? LIMIT 1" (connId, cutoffTs)
pure $ isJust r
upgradeRcvConnToDuplex :: DB.Connection -> ConnId -> NewSndQueue -> IO (Either StoreError SndQueue)
upgradeRcvConnToDuplex db connId sq =
@@ -678,7 +702,7 @@ setRcvQueueDeleted db RcvQueue {rcvId, server = ProtocolServer {host, port}} = d
|]
(host, port, rcvId)
setRcvQueueConfirmedE2E :: DB.Connection -> RcvQueue -> C.DhSecretX25519 -> Version -> IO ()
setRcvQueueConfirmedE2E :: DB.Connection -> RcvQueue -> C.DhSecretX25519 -> VersionSMPC -> IO ()
setRcvQueueConfirmedE2E db RcvQueue {rcvId, server = ProtocolServer {host, port}} e2eDhSecret smpClientVersion =
DB.executeNamed
db
@@ -779,7 +803,7 @@ setRcvQueueNtfCreds db connId clientNtfCreds =
Just ClientNtfCreds {ntfPublicKey, ntfPrivateKey, notifierId, rcvNtfDhSecret} -> (Just ntfPublicKey, Just ntfPrivateKey, Just notifierId, Just rcvNtfDhSecret)
Nothing -> (Nothing, Nothing, Nothing, Nothing)
type SMPConfirmationRow = (SndPublicVerifyKey, C.PublicKeyX25519, ConnInfo, Maybe [SMPQueueInfo], Maybe Version)
type SMPConfirmationRow = (SndPublicAuthKey, C.PublicKeyX25519, ConnInfo, Maybe [SMPQueueInfo], Maybe VersionSMPC)
smpConfirmation :: SMPConfirmationRow -> SMPConfirmation
smpConfirmation (senderKey, e2ePubKey, connInfo, smpReplyQueues_, smpClientVersion_) =
@@ -788,7 +812,7 @@ smpConfirmation (senderKey, e2ePubKey, connInfo, smpReplyQueues_, smpClientVersi
e2ePubKey,
connInfo,
smpReplyQueues = fromMaybe [] smpReplyQueues_,
smpClientVersion = fromMaybe 1 smpClientVersion_
smpClientVersion = fromMaybe initialSMPClientVersion smpClientVersion_
}
createConfirmation :: DB.Connection -> TVar ChaChaDRG -> NewConfirmation -> IO (Either StoreError ConfirmationId)
@@ -865,10 +889,6 @@ removeConfirmations db connId =
|]
[":conn_id" := connId]
setHandshakeVersion :: DB.Connection -> ConnId -> Version -> Bool -> IO ()
setHandshakeVersion db connId aVersion duplexHS =
DB.execute db "UPDATE connections SET smp_agent_version = ?, duplex_handshake = ? WHERE conn_id = ?" (aVersion, duplexHS, connId)
createInvitation :: DB.Connection -> TVar ChaChaDRG -> NewInvitation -> IO (Either StoreError InvitationId)
createInvitation db gVar NewInvitation {contactConnId, connReq, recipientConnInfo} =
createWithRandomId gVar $ \invitationId ->
@@ -1005,18 +1025,18 @@ getPendingQueueMsg db connId SndQueue {dbQueueId} =
DB.query
db
[sql|
SELECT m.msg_type, m.msg_flags, m.msg_body, m.internal_ts, s.retry_int_slow, s.retry_int_fast
SELECT m.msg_type, m.msg_flags, m.msg_body, m.pq_encryption, m.internal_ts, s.retry_int_slow, s.retry_int_fast
FROM messages m
JOIN snd_messages s ON s.conn_id = m.conn_id AND s.internal_id = m.internal_id
WHERE m.conn_id = ? AND m.internal_id = ?
|]
(connId, msgId)
err = SEInternal $ "msg delivery " <> bshow msgId <> " returned []"
pendingMsgData :: (AgentMessageType, Maybe MsgFlags, MsgBody, InternalTs, Maybe Int64, Maybe Int64) -> PendingMsgData
pendingMsgData (msgType, msgFlags_, msgBody, internalTs, riSlow_, riFast_) =
pendingMsgData :: (AgentMessageType, Maybe MsgFlags, MsgBody, PQEncryption, InternalTs, Maybe Int64, Maybe Int64) -> PendingMsgData
pendingMsgData (msgType, msgFlags_, msgBody, pqEncryption, internalTs, riSlow_, riFast_) =
let msgFlags = fromMaybe SMP.noMsgFlags msgFlags_
msgRetryState = RI2State <$> riSlow_ <*> riFast_
in PendingMsgData {msgId, msgType, msgFlags, msgBody, msgRetryState, internalTs}
in PendingMsgData {msgId, msgType, msgFlags, msgBody, pqEncryption, msgRetryState, internalTs}
markMsgFailed msgId = DB.execute db "UPDATE snd_message_deliveries SET failed = 1 WHERE conn_id = ? AND internal_id = ?" (connId, msgId)
getWorkItem :: Show i => ByteString -> IO (Maybe i) -> (i -> IO (Either StoreError a)) -> (i -> IO ()) -> IO (Either StoreError (Maybe a))
@@ -1041,6 +1061,33 @@ deletePendingMsgs :: DB.Connection -> ConnId -> SndQueue -> IO ()
deletePendingMsgs db connId SndQueue {dbQueueId} =
DB.execute db "DELETE FROM snd_message_deliveries WHERE conn_id = ? AND snd_queue_id = ?" (connId, dbQueueId)
getExpiredSndMessages :: DB.Connection -> ConnId -> SndQueue -> UTCTime -> IO [InternalId]
getExpiredSndMessages db connId SndQueue {dbQueueId} expireTs = do
-- type is Maybe InternalId because MAX always returns one row, possibly with NULL value
maxId :: [Maybe InternalId] <-
map fromOnly
<$> DB.query
db
[sql|
SELECT MAX(internal_id)
FROM messages
WHERE conn_id = ? AND internal_snd_id IS NOT NULL AND internal_ts < ?
|]
(connId, expireTs)
case maxId of
Just msgId : _ ->
map fromOnly
<$> DB.query
db
[sql|
SELECT internal_id
FROM snd_message_deliveries
WHERE conn_id = ? AND snd_queue_id = ? AND failed = 0 AND internal_id <= ?
ORDER BY internal_id ASC
|]
(connId, dbQueueId, msgId)
_ -> pure []
setMsgUserAck :: DB.Connection -> ConnId -> InternalId -> IO (Either StoreError (RcvQueue, SMP.MsgId))
setMsgUserAck db connId agentMsgId = runExceptT $ do
(dbRcvId, srvMsgId) <-
@@ -1058,7 +1105,7 @@ getRcvMsg db connId agentMsgId =
[sql|
SELECT
r.internal_id, m.internal_ts, r.broker_id, r.broker_ts, r.external_snd_id, r.integrity, r.internal_hash,
m.msg_type, m.msg_body, s.internal_id, s.rcpt_status, r.user_ack
m.msg_type, m.msg_body, m.pq_encryption, s.internal_id, s.rcpt_status, r.user_ack
FROM rcv_messages r
JOIN messages m ON r.conn_id = m.conn_id AND r.internal_id = m.internal_id
LEFT JOIN snd_messages s ON s.conn_id = r.conn_id AND s.rcpt_internal_id = r.internal_id
@@ -1074,7 +1121,7 @@ getLastMsg db connId msgId =
[sql|
SELECT
r.internal_id, m.internal_ts, r.broker_id, r.broker_ts, r.external_snd_id, r.integrity, r.internal_hash,
m.msg_type, m.msg_body, s.internal_id, s.rcpt_status, r.user_ack
m.msg_type, m.msg_body, m.pq_encryption, s.internal_id, s.rcpt_status, r.user_ack
FROM rcv_messages r
JOIN messages m ON r.conn_id = m.conn_id AND r.internal_id = m.internal_id
JOIN connections c ON r.conn_id = c.conn_id AND c.last_internal_msg_id = r.internal_id
@@ -1083,9 +1130,9 @@ getLastMsg db connId msgId =
|]
(connId, msgId)
toRcvMsg :: (Int64, InternalTs, BrokerId, BrokerTs, AgentMsgId, MsgIntegrity, MsgHash, AgentMessageType, MsgBody, Maybe AgentMsgId, Maybe MsgReceiptStatus, Bool) -> RcvMsg
toRcvMsg (agentMsgId, internalTs, brokerId, brokerTs, sndMsgId, integrity, internalHash, msgType, msgBody, rcptInternalId_, rcptStatus_, userAck) =
let msgMeta = MsgMeta {recipient = (agentMsgId, internalTs), broker = (brokerId, brokerTs), sndMsgId, integrity}
toRcvMsg :: (Int64, InternalTs, BrokerId, BrokerTs) :. (AgentMsgId, MsgIntegrity, MsgHash, AgentMessageType, MsgBody, PQEncryption, Maybe AgentMsgId, Maybe MsgReceiptStatus, Bool) -> RcvMsg
toRcvMsg ((agentMsgId, internalTs, brokerId, brokerTs) :. (sndMsgId, integrity, internalHash, msgType, msgBody, pqEncryption, rcptInternalId_, rcptStatus_, userAck)) =
let msgMeta = MsgMeta {recipient = (agentMsgId, internalTs), broker = (brokerId, brokerTs), sndMsgId, integrity, pqEncryption}
msgReceipt = MsgReceipt <$> rcptInternalId_ <*> rcptStatus_
in RcvMsg {internalId = InternalId agentMsgId, msgMeta, msgType, msgBody, internalHash, msgReceipt, userAck}
@@ -1145,49 +1192,34 @@ deleteSndMsgsExpired db ttl = do
"DELETE FROM messages WHERE internal_ts < ? AND internal_snd_id IS NOT NULL"
(Only cutoffTs)
createRatchetX3dhKeys :: DB.Connection -> ConnId -> C.PrivateKeyX448 -> C.PrivateKeyX448 -> IO ()
createRatchetX3dhKeys db connId x3dhPrivKey1 x3dhPrivKey2 =
DB.execute db "INSERT INTO ratchets (conn_id, x3dh_priv_key_1, x3dh_priv_key_2) VALUES (?, ?, ?)" (connId, x3dhPrivKey1, x3dhPrivKey2)
createRatchetX3dhKeys :: DB.Connection -> ConnId -> C.PrivateKeyX448 -> C.PrivateKeyX448 -> Maybe CR.RcvPrivRKEMParams -> IO ()
createRatchetX3dhKeys db connId x3dhPrivKey1 x3dhPrivKey2 pqPrivKem =
DB.execute db "INSERT INTO ratchets (conn_id, x3dh_priv_key_1, x3dh_priv_key_2, pq_priv_kem) VALUES (?, ?, ?, ?)" (connId, x3dhPrivKey1, x3dhPrivKey2, pqPrivKem)
getRatchetX3dhKeys :: DB.Connection -> ConnId -> IO (Either StoreError (C.PrivateKeyX448, C.PrivateKeyX448))
getRatchetX3dhKeys :: DB.Connection -> ConnId -> IO (Either StoreError (C.PrivateKeyX448, C.PrivateKeyX448, Maybe CR.RcvPrivRKEMParams))
getRatchetX3dhKeys db connId =
fmap hasKeys $
firstRow id SEX3dhKeysNotFound $
DB.query db "SELECT x3dh_priv_key_1, x3dh_priv_key_2 FROM ratchets WHERE conn_id = ?" (Only connId)
firstRow' keys SEX3dhKeysNotFound $
DB.query db "SELECT x3dh_priv_key_1, x3dh_priv_key_2, pq_priv_kem FROM ratchets WHERE conn_id = ?" (Only connId)
where
hasKeys = \case
Right (Just k1, Just k2) -> Right (k1, k2)
_ -> Left SEX3dhKeysNotFound
createRatchetX3dhKeys' :: DB.Connection -> ConnId -> C.PrivateKeyX448 -> C.PrivateKeyX448 -> C.PublicKeyX448 -> C.PublicKeyX448 -> IO ()
createRatchetX3dhKeys' db connId x3dhPrivKey1 x3dhPrivKey2 x3dhPubKey1 x3dhPubKey2 =
DB.execute
db
"INSERT INTO ratchets (conn_id, x3dh_priv_key_1, x3dh_priv_key_2, x3dh_pub_key_1, x3dh_pub_key_2) VALUES (?,?,?,?,?)"
(connId, x3dhPrivKey1, x3dhPrivKey2, x3dhPubKey1, x3dhPubKey2)
getRatchetX3dhKeys' :: DB.Connection -> ConnId -> IO (Either StoreError (C.PrivateKeyX448, C.PrivateKeyX448, C.PublicKeyX448, C.PublicKeyX448))
getRatchetX3dhKeys' db connId =
fmap hasKeys $
firstRow id SEX3dhKeysNotFound $
DB.query db "SELECT x3dh_priv_key_1, x3dh_priv_key_2, x3dh_pub_key_1, x3dh_pub_key_2 FROM ratchets WHERE conn_id = ?" (Only connId)
where
hasKeys = \case
Right (Just pk1, Just pk2, Just k1, Just k2) -> Right (pk1, pk2, k1, k2)
keys = \case
(Just k1, Just k2, pKem) -> Right (k1, k2, pKem)
_ -> Left SEX3dhKeysNotFound
-- used to remember new keys when starting ratchet re-synchronization
setRatchetX3dhKeys :: DB.Connection -> ConnId -> C.PrivateKeyX448 -> C.PrivateKeyX448 -> C.PublicKeyX448 -> C.PublicKeyX448 -> IO ()
setRatchetX3dhKeys db connId x3dhPrivKey1 x3dhPrivKey2 x3dhPubKey1 x3dhPubKey2 =
-- TODO remove the columns for public keys in v5.7.
-- Currently, the keys are not used but still stored to support app downgrade to the previous version.
setRatchetX3dhKeys :: DB.Connection -> ConnId -> C.PrivateKeyX448 -> C.PrivateKeyX448 -> Maybe CR.RcvPrivRKEMParams -> IO ()
setRatchetX3dhKeys db connId x3dhPrivKey1 x3dhPrivKey2 pqPrivKem =
DB.execute
db
[sql|
UPDATE ratchets
SET x3dh_priv_key_1 = ?, x3dh_priv_key_2 = ?, x3dh_pub_key_1 = ?, x3dh_pub_key_2 = ?
SET x3dh_priv_key_1 = ?, x3dh_priv_key_2 = ?, x3dh_pub_key_1 = ?, x3dh_pub_key_2 = ?, pq_priv_kem = ?
WHERE conn_id = ?
|]
(x3dhPrivKey1, x3dhPrivKey2, x3dhPubKey1, x3dhPubKey2, connId)
(x3dhPrivKey1, x3dhPrivKey2, C.publicKey x3dhPrivKey1, C.publicKey x3dhPrivKey2, pqPrivKem, connId)
-- TODO remove the columns for public keys in v5.7.
createRatchet :: DB.Connection -> ConnId -> RatchetX448 -> IO ()
createRatchet db connId rc =
DB.executeNamed
@@ -1200,7 +1232,8 @@ createRatchet db connId rc =
x3dh_priv_key_1 = NULL,
x3dh_priv_key_2 = NULL,
x3dh_pub_key_1 = NULL,
x3dh_pub_key_2 = NULL
x3dh_pub_key_2 = NULL,
pq_priv_kem = NULL
|]
[":conn_id" := connId, ":ratchet_state" := rc]
@@ -1739,6 +1772,10 @@ instance ToField MsgReceiptStatus where toField = toField . decodeLatin1 . strEn
instance FromField MsgReceiptStatus where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8
instance ToField (Version v) where toField (Version v) = toField v
instance FromField (Version v) where fromField f = Version <$> fromField f
listToEither :: e -> [a] -> Either e a
listToEither _ (x : _) = Right x
listToEither e _ = Left e
@@ -1889,26 +1926,39 @@ getConnData db connId' =
db
[sql|
SELECT
user_id, conn_id, conn_mode, smp_agent_version, enable_ntfs, duplex_handshake,
last_external_snd_msg_id, deleted, ratchet_sync_state
user_id, conn_id, conn_mode, smp_agent_version, enable_ntfs,
last_external_snd_msg_id, deleted, ratchet_sync_state, pq_support
FROM connections
WHERE conn_id = ?
|]
(Only connId')
where
cData (userId, connId, cMode, connAgentVersion, enableNtfs_, duplexHandshake, lastExternalSndId, deleted, ratchetSyncState) =
(ConnData {userId, connId, connAgentVersion, enableNtfs = fromMaybe True enableNtfs_, duplexHandshake, lastExternalSndId, deleted, ratchetSyncState}, cMode)
cData (userId, connId, cMode, connAgentVersion, enableNtfs_, lastExternalSndId, deleted, ratchetSyncState, pqSupport) =
(ConnData {userId, connId, connAgentVersion, enableNtfs = fromMaybe True enableNtfs_, lastExternalSndId, deleted, ratchetSyncState, pqSupport}, cMode)
setConnDeleted :: DB.Connection -> ConnId -> IO ()
setConnDeleted db connId = DB.execute db "UPDATE connections SET deleted = ? WHERE conn_id = ?" (True, connId)
setConnDeleted :: DB.Connection -> Bool -> ConnId -> IO ()
setConnDeleted db waitDelivery connId
| waitDelivery = do
currentTs <- getCurrentTime
DB.execute db "UPDATE connections SET deleted_at_wait_delivery = ? WHERE conn_id = ?" (currentTs, connId)
| otherwise =
DB.execute db "UPDATE connections SET deleted = ? WHERE conn_id = ?" (True, connId)
setConnAgentVersion :: DB.Connection -> ConnId -> Version -> IO ()
setConnAgentVersion :: DB.Connection -> ConnId -> VersionSMPA -> IO ()
setConnAgentVersion db connId aVersion =
DB.execute db "UPDATE connections SET smp_agent_version = ? WHERE conn_id = ?" (aVersion, connId)
setConnPQSupport :: DB.Connection -> ConnId -> PQSupport -> IO ()
setConnPQSupport db connId pqSupport =
DB.execute db "UPDATE connections SET pq_support = ? WHERE conn_id = ?" (pqSupport, connId)
getDeletedConnIds :: DB.Connection -> IO [ConnId]
getDeletedConnIds db = map fromOnly <$> DB.query db "SELECT conn_id FROM connections WHERE deleted = ?" (Only True)
getDeletedWaitingDeliveryConnIds :: DB.Connection -> IO [ConnId]
getDeletedWaitingDeliveryConnIds db =
map fromOnly <$> DB.query_ db "SELECT conn_id FROM connections WHERE deleted_at_wait_delivery IS NOT NULL"
setConnRatchetSync :: DB.Connection -> ConnId -> RatchetSyncState -> IO ()
setConnRatchetSync db connId ratchetSyncState =
DB.execute db "UPDATE connections SET ratchet_sync_state = ? WHERE conn_id = ?" (ratchetSyncState, connId)
@@ -1956,13 +2006,13 @@ rcvQueueQuery =
|]
toRcvQueue ::
(UserId, C.KeyHash, ConnId, NonEmpty TransportHost, ServiceName, SMP.RecipientId, SMP.RcvPrivateSignKey, SMP.RcvDhSecret, C.PrivateKeyX25519, Maybe C.DhSecretX25519, SMP.SenderId, QueueStatus)
:. (DBQueueId 'QSStored, Bool, Maybe Int64, Maybe RcvSwitchStatus, Maybe Version, Int)
:. (Maybe SMP.NtfPublicVerifyKey, Maybe SMP.NtfPrivateSignKey, Maybe SMP.NotifierId, Maybe RcvNtfDhSecret) ->
(UserId, C.KeyHash, ConnId, NonEmpty TransportHost, ServiceName, SMP.RecipientId, SMP.RcvPrivateAuthKey, SMP.RcvDhSecret, C.PrivateKeyX25519, Maybe C.DhSecretX25519, SMP.SenderId, QueueStatus)
:. (DBQueueId 'QSStored, Bool, Maybe Int64, Maybe RcvSwitchStatus, Maybe VersionSMPC, Int)
:. (Maybe SMP.NtfPublicAuthKey, Maybe SMP.NtfPrivateAuthKey, Maybe SMP.NotifierId, Maybe RcvNtfDhSecret) ->
RcvQueue
toRcvQueue ((userId, keyHash, connId, host, port, rcvId, rcvPrivateKey, rcvDhSecret, e2ePrivKey, e2eDhSecret, sndId, status) :. (dbQueueId, primary, dbReplaceQueueId, rcvSwchStatus, smpClientVersion_, deleteErrors) :. (ntfPublicKey_, ntfPrivateKey_, notifierId_, rcvNtfDhSecret_)) =
let server = SMPServer host port keyHash
smpClientVersion = fromMaybe 1 smpClientVersion_
smpClientVersion = fromMaybe initialSMPClientVersion smpClientVersion_
clientNtfCreds = case (ntfPublicKey_, ntfPrivateKey_, notifierId_, rcvNtfDhSecret_) of
(Just ntfPublicKey, Just ntfPrivateKey, Just notifierId, Just rcvNtfDhSecret) -> Just $ ClientNtfCreds {ntfPublicKey, ntfPrivateKey, notifierId, rcvNtfDhSecret}
_ -> Nothing
@@ -1997,8 +2047,8 @@ sndQueueQuery =
toSndQueue ::
(UserId, C.KeyHash, ConnId, NonEmpty TransportHost, ServiceName, SenderId)
:. (Maybe C.APublicVerifyKey, SndPrivateSignKey, Maybe C.PublicKeyX25519, C.DhSecretX25519, QueueStatus)
:. (DBQueueId 'QSStored, Bool, Maybe Int64, Maybe SndSwitchStatus, Version) ->
:. (Maybe SndPublicAuthKey, SndPrivateAuthKey, Maybe C.PublicKeyX25519, C.DhSecretX25519, QueueStatus)
:. (DBQueueId 'QSStored, Bool, Maybe Int64, Maybe SndSwitchStatus, VersionSMPC) ->
SndQueue
toSndQueue
( (userId, keyHash, connId, host, port, sndId)
@@ -2047,23 +2097,15 @@ updateLastIdsRcv_ dbConn connId newInternalId newInternalRcvId =
insertRcvMsgBase_ :: DB.Connection -> ConnId -> RcvMsgData -> IO ()
insertRcvMsgBase_ dbConn connId RcvMsgData {msgMeta, msgType, msgFlags, msgBody, internalRcvId} = do
let MsgMeta {recipient = (internalId, internalTs)} = msgMeta
DB.executeNamed
let MsgMeta {recipient = (internalId, internalTs), pqEncryption} = msgMeta
DB.execute
dbConn
[sql|
INSERT INTO messages
( conn_id, internal_id, internal_ts, internal_rcv_id, internal_snd_id, msg_type, msg_flags, msg_body)
VALUES
(:conn_id,:internal_id,:internal_ts,:internal_rcv_id, NULL,:msg_type,:msg_flags,:msg_body);
(conn_id, internal_id, internal_ts, internal_rcv_id, internal_snd_id, msg_type, msg_flags, msg_body, pq_encryption)
VALUES (?,?,?,?,?,?,?,?,?);
|]
[ ":conn_id" := connId,
":internal_id" := internalId,
":internal_ts" := internalTs,
":internal_rcv_id" := internalRcvId,
":msg_type" := msgType,
":msg_flags" := msgFlags,
":msg_body" := msgBody
]
(connId, internalId, internalTs, internalRcvId, Nothing :: Maybe Int64, msgType, msgFlags, msgBody, pqEncryption)
insertRcvMsgDetails_ :: DB.Connection -> ConnId -> RcvQueue -> RcvMsgData -> IO ()
insertRcvMsgDetails_ db connId RcvQueue {dbQueueId} RcvMsgData {msgMeta, internalRcvId, internalHash, externalPrevSndHash, encryptedMsgHash} = do
@@ -2144,23 +2186,16 @@ updateLastIdsSnd_ dbConn connId newInternalId newInternalSndId =
-- * createSndMsg helpers
insertSndMsgBase_ :: DB.Connection -> ConnId -> SndMsgData -> IO ()
insertSndMsgBase_ dbConn connId SndMsgData {..} = do
DB.executeNamed
dbConn
insertSndMsgBase_ db connId SndMsgData {internalId, internalTs, internalSndId, msgType, msgFlags, msgBody, pqEncryption} = do
DB.execute
db
[sql|
INSERT INTO messages
( conn_id, internal_id, internal_ts, internal_rcv_id, internal_snd_id, msg_type, msg_flags, msg_body)
(conn_id, internal_id, internal_ts, internal_rcv_id, internal_snd_id, msg_type, msg_flags, msg_body, pq_encryption)
VALUES
(:conn_id,:internal_id,:internal_ts, NULL,:internal_snd_id,:msg_type,:msg_flags,:msg_body);
(?,?,?,?,?,?,?,?,?);
|]
[ ":conn_id" := connId,
":internal_id" := internalId,
":internal_ts" := internalTs,
":internal_snd_id" := internalSndId,
":msg_type" := msgType,
":msg_flags" := msgFlags,
":msg_body" := msgBody
]
(connId, internalId, internalTs, Nothing :: Maybe Int64, internalSndId, msgType, msgFlags, msgBody, pqEncryption)
insertSndMsgDetails_ :: DB.Connection -> ConnId -> SndMsgData -> IO ()
insertSndMsgDetails_ dbConn connId SndMsgData {..} =
@@ -2235,38 +2270,67 @@ getXFTPServerId_ db ProtocolServer {host, port, keyHash} = do
DB.query db "SELECT xftp_server_id FROM xftp_servers WHERE xftp_host = ? AND xftp_port = ? AND xftp_key_hash = ?" (host, port, keyHash)
createRcvFile :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription 'FRecipient -> FilePath -> FilePath -> CryptoFile -> IO (Either StoreError RcvFileId)
createRcvFile db gVar userId fd@FileDescription {chunks} prefixPath tmpPath (CryptoFile savePath cfArgs) = runExceptT $ do
(rcvFileEntityId, rcvFileId) <- ExceptT $ insertRcvFile fd
createRcvFile db gVar userId fd@FileDescription {chunks} prefixPath tmpPath file = runExceptT $ do
(rcvFileEntityId, rcvFileId) <- ExceptT $ insertRcvFile db gVar userId fd prefixPath tmpPath file Nothing Nothing
liftIO $
forM_ chunks $ \fc@FileChunk {replicas} -> do
chunkId <- insertChunk fc rcvFileId
forM_ (zip [1 ..] replicas) $ \(rno, replica) -> insertReplica rno replica chunkId
chunkId <- insertRcvFileChunk db fc rcvFileId
forM_ (zip [1 ..] replicas) $ \(rno, replica) -> insertRcvFileChunkReplica db rno replica chunkId
pure rcvFileEntityId
createRcvFileRedirect :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription FRecipient -> FilePath -> FilePath -> CryptoFile -> FilePath -> CryptoFile -> IO (Either StoreError RcvFileId)
createRcvFileRedirect _ _ _ FileDescription {redirect = Nothing} _ _ _ _ _ = pure $ Left $ SEInternal "createRcvFileRedirect called without redirect"
createRcvFileRedirect db gVar userId redirectFd@FileDescription {chunks = redirectChunks, redirect = Just RedirectFileInfo {size, digest}} prefixPath redirectPath redirectFile dstPath dstFile = runExceptT $ do
(dstEntityId, dstId) <- ExceptT $ insertRcvFile db gVar userId dummyDst prefixPath dstPath dstFile Nothing Nothing
(_, redirectId) <- ExceptT $ insertRcvFile db gVar userId redirectFd prefixPath redirectPath redirectFile (Just dstId) (Just dstEntityId)
liftIO $
forM_ redirectChunks $ \fc@FileChunk {replicas} -> do
chunkId <- insertRcvFileChunk db fc redirectId
forM_ (zip [1 ..] replicas) $ \(rno, replica) -> insertRcvFileChunkReplica db rno replica chunkId
pure dstEntityId
where
insertRcvFile :: FileDescription 'FRecipient -> IO (Either StoreError (RcvFileId, DBRcvFileId))
insertRcvFile FileDescription {size, digest, key, nonce, chunkSize} = runExceptT $ do
rcvFileEntityId <- ExceptT $
createWithRandomId gVar $ \rcvFileEntityId ->
DB.execute
db
"INSERT INTO rcv_files (rcv_file_entity_id, user_id, size, digest, key, nonce, chunk_size, prefix_path, tmp_path, save_path, save_file_key, save_file_nonce, status) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)"
((rcvFileEntityId, userId, size, digest, key, nonce, chunkSize) :. (prefixPath, tmpPath, savePath, fileKey <$> cfArgs, fileNonce <$> cfArgs, RFSReceiving))
rcvFileId <- liftIO $ insertedRowId db
pure (rcvFileEntityId, rcvFileId)
insertChunk :: FileChunk -> DBRcvFileId -> IO Int64
insertChunk FileChunk {chunkNo, chunkSize, digest} rcvFileId = do
dummyDst =
FileDescription
{ party = SFRecipient,
size,
digest,
redirect = Nothing,
-- updated later with updateRcvFileRedirect
key = C.unsafeSbKey $ B.replicate 32 '#',
nonce = C.cbNonce "",
chunkSize = FileSize 0,
chunks = []
}
insertRcvFile :: DB.Connection -> TVar ChaChaDRG -> UserId -> FileDescription 'FRecipient -> FilePath -> FilePath -> CryptoFile -> Maybe DBRcvFileId -> Maybe RcvFileId -> IO (Either StoreError (RcvFileId, DBRcvFileId))
insertRcvFile db gVar userId FileDescription {size, digest, key, nonce, chunkSize, redirect} prefixPath tmpPath (CryptoFile savePath cfArgs) redirectId_ redirectEntityId_ = runExceptT $ do
let (redirectDigest_, redirectSize_) = case redirect of
Just RedirectFileInfo {digest = d, size = s} -> (Just d, Just s)
Nothing -> (Nothing, Nothing)
rcvFileEntityId <- ExceptT $
createWithRandomId gVar $ \rcvFileEntityId ->
DB.execute
db
"INSERT INTO rcv_file_chunks (rcv_file_id, chunk_no, chunk_size, digest) VALUES (?,?,?,?)"
(rcvFileId, chunkNo, chunkSize, digest)
insertedRowId db
insertReplica :: Int -> FileChunkReplica -> Int64 -> IO ()
insertReplica replicaNo FileChunkReplica {server, replicaId, replicaKey} chunkId = do
srvId <- createXFTPServer_ db server
DB.execute
db
"INSERT INTO rcv_file_chunk_replicas (replica_number, rcv_file_chunk_id, xftp_server_id, replica_id, replica_key) VALUES (?,?,?,?,?)"
(replicaNo, chunkId, srvId, replicaId, replicaKey)
"INSERT INTO rcv_files (rcv_file_entity_id, user_id, size, digest, key, nonce, chunk_size, prefix_path, tmp_path, save_path, save_file_key, save_file_nonce, status, redirect_id, redirect_entity_id, redirect_digest, redirect_size) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
((rcvFileEntityId, userId, size, digest, key, nonce, chunkSize, prefixPath, tmpPath) :. (savePath, fileKey <$> cfArgs, fileNonce <$> cfArgs, RFSReceiving, redirectId_, redirectEntityId_, redirectDigest_, redirectSize_))
rcvFileId <- liftIO $ insertedRowId db
pure (rcvFileEntityId, rcvFileId)
insertRcvFileChunk :: DB.Connection -> FileChunk -> DBRcvFileId -> IO Int64
insertRcvFileChunk db FileChunk {chunkNo, chunkSize, digest} rcvFileId = do
DB.execute
db
"INSERT INTO rcv_file_chunks (rcv_file_id, chunk_no, chunk_size, digest) VALUES (?,?,?,?)"
(rcvFileId, chunkNo, chunkSize, digest)
insertedRowId db
insertRcvFileChunkReplica :: DB.Connection -> Int -> FileChunkReplica -> Int64 -> IO ()
insertRcvFileChunkReplica db replicaNo FileChunkReplica {server, replicaId, replicaKey} chunkId = do
srvId <- createXFTPServer_ db server
DB.execute
db
"INSERT INTO rcv_file_chunk_replicas (replica_number, rcv_file_chunk_id, xftp_server_id, replica_id, replica_key) VALUES (?,?,?,?,?)"
(replicaNo, chunkId, srvId, replicaId, replicaKey)
getRcvFileByEntityId :: DB.Connection -> RcvFileId -> IO (Either StoreError RcvFile)
getRcvFileByEntityId db rcvFileEntityId = runExceptT $ do
@@ -2278,6 +2342,11 @@ getRcvFileIdByEntityId_ db rcvFileEntityId =
firstRow fromOnly SEFileNotFound $
DB.query db "SELECT rcv_file_id FROM rcv_files WHERE rcv_file_entity_id = ?" (Only rcvFileEntityId)
getRcvFileRedirects :: DB.Connection -> DBRcvFileId -> IO [RcvFile]
getRcvFileRedirects db rcvFileId = do
redirects <- fromOnly <$$> DB.query db "SELECT rcv_file_id FROM rcv_files WHERE redirect_id = ?" (Only rcvFileId)
fmap catMaybes . forM redirects $ getRcvFile db >=> either (const $ pure Nothing) (pure . Just)
getRcvFile :: DB.Connection -> DBRcvFileId -> IO (Either StoreError RcvFile)
getRcvFile db rcvFileId = runExceptT $ do
f@RcvFile {rcvFileEntityId, userId, tmpPath} <- ExceptT getFile
@@ -2290,17 +2359,22 @@ getRcvFile db rcvFileId = runExceptT $ do
DB.query
db
[sql|
SELECT rcv_file_entity_id, user_id, size, digest, key, nonce, chunk_size, prefix_path, tmp_path, save_path, save_file_key, save_file_nonce, status, deleted
SELECT rcv_file_entity_id, user_id, size, digest, key, nonce, chunk_size, prefix_path, tmp_path, save_path, save_file_key, save_file_nonce, status, deleted, redirect_id, redirect_entity_id, redirect_size, redirect_digest
FROM rcv_files
WHERE rcv_file_id = ?
|]
(Only rcvFileId)
where
toFile :: (RcvFileId, UserId, FileSize Int64, FileDigest, C.SbKey, C.CbNonce, FileSize Word32, FilePath, Maybe FilePath) :. (FilePath, Maybe C.SbKey, Maybe C.CbNonce, RcvFileStatus, Bool) -> RcvFile
toFile ((rcvFileEntityId, userId, size, digest, key, nonce, chunkSize, prefixPath, tmpPath) :. (savePath, saveKey_, saveNonce_, status, deleted)) =
toFile :: (RcvFileId, UserId, FileSize Int64, FileDigest, C.SbKey, C.CbNonce, FileSize Word32, FilePath, Maybe FilePath) :. (FilePath, Maybe C.SbKey, Maybe C.CbNonce, RcvFileStatus, Bool, Maybe DBRcvFileId, Maybe RcvFileId, Maybe (FileSize Int64), Maybe FileDigest) -> RcvFile
toFile ((rcvFileEntityId, userId, size, digest, key, nonce, chunkSize, prefixPath, tmpPath) :. (savePath, saveKey_, saveNonce_, status, deleted, redirectDbId, redirectEntityId, redirectSize_, redirectDigest_)) =
let cfArgs = CFArgs <$> saveKey_ <*> saveNonce_
saveFile = CryptoFile savePath cfArgs
in RcvFile {rcvFileId, rcvFileEntityId, userId, size, digest, key, nonce, chunkSize, prefixPath, tmpPath, saveFile, status, deleted, chunks = []}
redirect =
RcvFileRedirect
<$> redirectDbId
<*> redirectEntityId
<*> (RedirectFileInfo <$> redirectSize_ <*> redirectDigest_)
in RcvFile {rcvFileId, rcvFileEntityId, userId, size, digest, key, nonce, chunkSize, redirect, prefixPath, tmpPath, saveFile, status, deleted, chunks = []}
getChunks :: RcvFileId -> UserId -> FilePath -> IO [RcvFileChunk]
getChunks rcvFileEntityId userId fileTmpPath = do
chunks <-
@@ -2335,7 +2409,7 @@ getRcvFile db rcvFileId = runExceptT $ do
|]
(Only chunkId)
where
toReplica :: (Int64, ChunkReplicaId, C.APrivateSignKey, Bool, Maybe Int64, Int, NonEmpty TransportHost, ServiceName, C.KeyHash) -> RcvFileChunkReplica
toReplica :: (Int64, ChunkReplicaId, C.APrivateAuthKey, Bool, Maybe Int64, Int, NonEmpty TransportHost, ServiceName, C.KeyHash) -> RcvFileChunkReplica
toReplica (rcvChunkReplicaId, replicaId, replicaKey, received, delay, retries, host, port, keyHash) =
let server = XFTPServer host port keyHash
in RcvFileChunkReplica {rcvChunkReplicaId, server, replicaId, replicaKey, received, delay, retries}
@@ -2366,6 +2440,14 @@ updateRcvFileComplete db rcvFileId = do
updatedAt <- getCurrentTime
DB.execute db "UPDATE rcv_files SET tmp_path = NULL, status = ?, updated_at = ? WHERE rcv_file_id = ?" (RFSComplete, updatedAt, rcvFileId)
updateRcvFileRedirect :: DB.Connection -> DBRcvFileId -> FileDescription 'FRecipient -> IO (Either StoreError ())
updateRcvFileRedirect db rcvFileId FileDescription {key, nonce, chunkSize, chunks} = runExceptT $ do
updatedAt <- liftIO getCurrentTime
liftIO $ DB.execute db "UPDATE rcv_files SET key = ?, nonce = ?, chunk_size = ?, updated_at = ? WHERE rcv_file_id = ?" (key, nonce, chunkSize, updatedAt, rcvFileId)
liftIO $ forM_ chunks $ \fc@FileChunk {replicas} -> do
chunkId <- insertRcvFileChunk db fc rcvFileId
forM_ (zip [1 ..] replicas) $ \(rno, replica) -> insertRcvFileChunkReplica db rno replica chunkId
updateRcvFileNoTmpPath :: DB.Connection -> DBRcvFileId -> IO ()
updateRcvFileNoTmpPath db rcvFileId = do
updatedAt <- getCurrentTime
@@ -2421,7 +2503,7 @@ getNextRcvChunkToDownload db server@ProtocolServer {host, port, keyHash} ttl = d
|]
(Only rcvFileChunkReplicaId)
where
toChunk :: ((DBRcvFileId, RcvFileId, UserId, Int64, Int, FileSize Word32, FileDigest, FilePath, Maybe FilePath) :. (Int64, ChunkReplicaId, C.APrivateSignKey, Bool, Maybe Int64, Int)) -> RcvFileChunk
toChunk :: ((DBRcvFileId, RcvFileId, UserId, Int64, Int, FileSize Word32, FileDigest, FilePath, Maybe FilePath) :. (Int64, ChunkReplicaId, C.APrivateAuthKey, Bool, Maybe Int64, Int)) -> RcvFileChunk
toChunk ((rcvFileId, rcvFileEntityId, userId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath, chunkTmpPath) :. (rcvChunkReplicaId, replicaId, replicaKey, received, delay, retries)) =
RcvFileChunk
{ rcvFileId,
@@ -2513,13 +2595,18 @@ getRcvFilesExpired db ttl = do
|]
(Only cutoffTs)
createSndFile :: DB.Connection -> TVar ChaChaDRG -> UserId -> CryptoFile -> Int -> FilePath -> C.SbKey -> C.CbNonce -> IO (Either StoreError SndFileId)
createSndFile db gVar userId (CryptoFile path cfArgs) numRecipients prefixPath key nonce =
createSndFile :: DB.Connection -> TVar ChaChaDRG -> UserId -> CryptoFile -> Int -> FilePath -> C.SbKey -> C.CbNonce -> Maybe RedirectFileInfo -> IO (Either StoreError SndFileId)
createSndFile db gVar userId (CryptoFile path cfArgs) numRecipients prefixPath key nonce redirect_ =
createWithRandomId gVar $ \sndFileEntityId ->
DB.execute
db
"INSERT INTO snd_files (snd_file_entity_id, user_id, path, src_file_key, src_file_nonce, num_recipients, prefix_path, key, nonce, status) VALUES (?,?,?,?,?,?,?,?,?,?)"
(sndFileEntityId, userId, path, fileKey <$> cfArgs, fileNonce <$> cfArgs, numRecipients, prefixPath, key, nonce, SFSNew)
"INSERT INTO snd_files (snd_file_entity_id, user_id, path, src_file_key, src_file_nonce, num_recipients, prefix_path, key, nonce, status, redirect_size, redirect_digest) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"
((sndFileEntityId, userId, path, fileKey <$> cfArgs, fileNonce <$> cfArgs, numRecipients) :. (prefixPath, key, nonce, SFSNew, redirectSize_, redirectDigest_))
where
(redirectSize_, redirectDigest_) =
case redirect_ of
Nothing -> (Nothing, Nothing)
Just RedirectFileInfo {size, digest} -> (Just size, Just digest)
getSndFileByEntityId :: DB.Connection -> SndFileId -> IO (Either StoreError SndFile)
getSndFileByEntityId db sndFileEntityId = runExceptT $ do
@@ -2543,17 +2630,18 @@ getSndFile db sndFileId = runExceptT $ do
DB.query
db
[sql|
SELECT snd_file_entity_id, user_id, path, src_file_key, src_file_nonce, num_recipients, digest, prefix_path, key, nonce, status, deleted
SELECT snd_file_entity_id, user_id, path, src_file_key, src_file_nonce, num_recipients, digest, prefix_path, key, nonce, status, deleted, redirect_size, redirect_digest
FROM snd_files
WHERE snd_file_id = ?
|]
(Only sndFileId)
where
toFile :: (SndFileId, UserId, FilePath, Maybe C.SbKey, Maybe C.CbNonce, Int, Maybe FileDigest, Maybe FilePath, C.SbKey, C.CbNonce, SndFileStatus, Bool) -> SndFile
toFile (sndFileEntityId, userId, srcPath, srcKey_, srcNonce_, numRecipients, digest, prefixPath, key, nonce, status, deleted) =
toFile :: (SndFileId, UserId, FilePath, Maybe C.SbKey, Maybe C.CbNonce, Int, Maybe FileDigest, Maybe FilePath, C.SbKey, C.CbNonce) :. (SndFileStatus, Bool, Maybe (FileSize Int64), Maybe FileDigest) -> SndFile
toFile ((sndFileEntityId, userId, srcPath, srcKey_, srcNonce_, numRecipients, digest, prefixPath, key, nonce) :. (status, deleted, redirectSize_, redirectDigest_)) =
let cfArgs = CFArgs <$> srcKey_ <*> srcNonce_
srcFile = CryptoFile srcPath cfArgs
in SndFile {sndFileId, sndFileEntityId, userId, srcFile, numRecipients, digest, prefixPath, key, nonce, status, deleted, chunks = []}
redirect = RedirectFileInfo <$> redirectSize_ <*> redirectDigest_
in SndFile {sndFileId, sndFileEntityId, userId, srcFile, numRecipients, digest, prefixPath, key, nonce, status, deleted, redirect, chunks = []}
getChunks :: SndFileId -> UserId -> Int -> FilePath -> IO [SndFileChunk]
getChunks sndFileEntityId userId numRecipients filePrefixPath = do
chunks <-
@@ -2593,12 +2681,12 @@ getSndFile db sndFileId = runExceptT $ do
rcvIdsKeys <- getChunkReplicaRecipients_ db sndChunkReplicaId
pure (replica :: SndFileChunkReplica) {rcvIdsKeys}
where
toReplica :: (Int64, ChunkReplicaId, C.APrivateSignKey, SndFileReplicaStatus, Maybe Int64, Int, NonEmpty TransportHost, ServiceName, C.KeyHash) -> SndFileChunkReplica
toReplica :: (Int64, ChunkReplicaId, C.APrivateAuthKey, SndFileReplicaStatus, Maybe Int64, Int, NonEmpty TransportHost, ServiceName, C.KeyHash) -> SndFileChunkReplica
toReplica (sndChunkReplicaId, replicaId, replicaKey, replicaStatus, delay, retries, host, port, keyHash) =
let server = XFTPServer host port keyHash
in SndFileChunkReplica {sndChunkReplicaId, server, replicaId, replicaKey, replicaStatus, delay, retries, rcvIdsKeys = []}
getChunkReplicaRecipients_ :: DB.Connection -> Int64 -> IO [(ChunkReplicaId, C.APrivateSignKey)]
getChunkReplicaRecipients_ :: DB.Connection -> Int64 -> IO [(ChunkReplicaId, C.APrivateAuthKey)]
getChunkReplicaRecipients_ db replicaId =
DB.query
db
@@ -2746,7 +2834,7 @@ getNextSndChunkToUpload db server@ProtocolServer {host, port, keyHash} ttl = do
pure (replica :: SndFileChunkReplica) {rcvIdsKeys}
pure (chunk {replicas = replicas'} :: SndFileChunk)
where
toChunk :: ((DBSndFileId, SndFileId, UserId, Int, FilePath) :. (Int64, Int, Int64, Word32, FileDigest) :. (Int64, ChunkReplicaId, C.APrivateSignKey, SndFileReplicaStatus, Maybe Int64, Int)) -> SndFileChunk
toChunk :: ((DBSndFileId, SndFileId, UserId, Int, FilePath) :. (Int64, Int, Int64, Word32, FileDigest) :. (Int64, ChunkReplicaId, C.APrivateAuthKey, SndFileReplicaStatus, Maybe Int64, Int)) -> SndFileChunk
toChunk ((sndFileId, sndFileEntityId, userId, numRecipients, filePrefixPath) :. (sndChunkId, chunkNo, chunkOffset, chunkSize, digest) :. (sndChunkReplicaId, replicaId, replicaKey, replicaStatus, delay, retries)) =
let chunkSpec = XFTPChunkSpec {filePath = sndFileEncPath filePrefixPath, chunkOffset, chunkSize}
in SndFileChunk
@@ -2767,7 +2855,7 @@ updateSndChunkReplicaDelay db replicaId delay = do
updatedAt <- getCurrentTime
DB.execute db "UPDATE snd_file_chunk_replicas SET delay = ?, retries = retries + 1, updated_at = ? WHERE snd_file_chunk_replica_id = ?" (delay, updatedAt, replicaId)
addSndChunkReplicaRecipients :: DB.Connection -> SndFileChunkReplica -> [(ChunkReplicaId, C.APrivateSignKey)] -> IO SndFileChunkReplica
addSndChunkReplicaRecipients :: DB.Connection -> SndFileChunkReplica -> [(ChunkReplicaId, C.APrivateAuthKey)] -> IO SndFileChunkReplica
addSndChunkReplicaRecipients db r@SndFileChunkReplica {sndChunkReplicaId} rcvIdsKeys = do
forM_ rcvIdsKeys $ \(rcvId, rcvKey) -> do
DB.execute
@@ -2860,7 +2948,7 @@ getDeletedSndChunkReplica db deletedSndChunkReplicaId =
|]
(Only deletedSndChunkReplicaId)
where
toReplica :: (UserId, ChunkReplicaId, C.APrivateSignKey, FileDigest, Maybe Int64, Int, NonEmpty TransportHost, ServiceName, C.KeyHash) -> DeletedSndChunkReplica
toReplica :: (UserId, ChunkReplicaId, C.APrivateAuthKey, FileDigest, Maybe Int64, Int, NonEmpty TransportHost, ServiceName, C.KeyHash) -> DeletedSndChunkReplica
toReplica (userId, replicaId, replicaKey, chunkDigest, delay, retries, host, port, keyHash) =
let server = XFTPServer host port keyHash
in DeletedSndChunkReplica {deletedSndChunkReplicaId, userId, server, replicaId, replicaKey, chunkDigest, delay, retries}
@@ -71,7 +71,8 @@ dbBusyLoop action = loop 500 3000000
loop :: Int -> Int -> IO a
loop t tLim =
action `E.catch` \(e :: SQLError) ->
if tLim > t && SQL.sqlError e == SQL.ErrorBusy
let se = SQL.sqlError e in
if tLim > t && (se == SQL.ErrorBusy || se == SQL.ErrorLocked)
then do
threadDelay t
loop (t * 9 `div` 8) (tLim - t)
@@ -67,6 +67,10 @@ 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.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
import Simplex.Messaging.Transport.Client (TransportHost)
@@ -102,7 +106,11 @@ schemaMigrations =
("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)
("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)
]
-- | The list of migrations in ascending order by date
@@ -128,9 +136,12 @@ run st = \case
where
runUp Migration {name, up, down} = withTransaction' st $ \db -> do
when (name == "m20220811_onion_hosts") $ updateServers db
insert db >> execSQL db up
insert db >> execSQL db up'
where
insert db = DB.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)
@@ -27,3 +27,24 @@ UPDATE connections SET user_id = 1;
PRAGMA ignore_check_constraints=OFF;
|]
-- This is executed in the new database
-- It does not create new user record
new_m20230110_users :: Query
new_m20230110_users =
[sql|
PRAGMA ignore_check_constraints=ON;
CREATE TABLE users (
user_id INTEGER PRIMARY KEY AUTOINCREMENT
);
ALTER TABLE connections ADD COLUMN user_id INTEGER CHECK (user_id NOT NULL)
REFERENCES users ON DELETE CASCADE;
CREATE INDEX idx_connections_user ON connections(user_id);
CREATE INDEX idx_commands_conn_id ON commands(conn_id);
PRAGMA ignore_check_constraints=OFF;
|]
@@ -0,0 +1,20 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240121_message_delivery_indexes where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20240121_message_delivery_indexes :: Query
m20240121_message_delivery_indexes =
[sql|
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);
|]
down_m20240121_message_delivery_indexes :: Query
down_m20240121_message_delivery_indexes =
[sql|
DROP INDEX idx_messages_snd_expired;
DROP INDEX idx_snd_message_deliveries_expired;
|]
@@ -0,0 +1,34 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240124_file_redirect where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20240124_file_redirect :: Query
m20240124_file_redirect =
[sql|
ALTER TABLE snd_files ADD COLUMN redirect_size INTEGER;
ALTER TABLE snd_files ADD COLUMN redirect_digest BLOB;
ALTER TABLE rcv_files ADD COLUMN redirect_id INTEGER REFERENCES rcv_files ON DELETE SET NULL;
ALTER TABLE rcv_files ADD COLUMN redirect_entity_id BLOB;
ALTER TABLE rcv_files ADD COLUMN redirect_size INTEGER;
ALTER TABLE rcv_files ADD COLUMN redirect_digest BLOB;
CREATE INDEX idx_rcv_files_redirect_id on rcv_files(redirect_id);
|]
down_m20240124_file_redirect :: Query
down_m20240124_file_redirect =
[sql|
DROP INDEX idx_rcv_files_redirect_id;
ALTER TABLE snd_files DROP COLUMN redirect_size;
ALTER TABLE snd_files DROP COLUMN redirect_digest;
ALTER TABLE rcv_files DROP COLUMN redirect_id;
ALTER TABLE rcv_files DROP COLUMN redirect_entity_id;
ALTER TABLE rcv_files DROP COLUMN redirect_size;
ALTER TABLE rcv_files DROP COLUMN redirect_digest;
|]
@@ -0,0 +1,18 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240223_connections_wait_delivery where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20240223_connections_wait_delivery :: Query
m20240223_connections_wait_delivery =
[sql|
ALTER TABLE connections ADD COLUMN deleted_at_wait_delivery TEXT;
|]
down_m20240223_connections_wait_delivery :: Query
down_m20240223_connections_wait_delivery =
[sql|
ALTER TABLE connections DROP COLUMN deleted_at_wait_delivery;
|]
@@ -0,0 +1,22 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20240225_ratchet_kem where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20240225_ratchet_kem :: Query
m20240225_ratchet_kem =
[sql|
ALTER TABLE ratchets ADD COLUMN pq_priv_kem BLOB;
ALTER TABLE connections ADD COLUMN pq_support INTEGER NOT NULL DEFAULT 0;
ALTER TABLE messages ADD COLUMN pq_encryption INTEGER NOT NULL DEFAULT 0;
|]
down_m20240225_ratchet_kem :: Query
down_m20240225_ratchet_kem =
[sql|
ALTER TABLE ratchets DROP COLUMN pq_priv_kem;
ALTER TABLE connections DROP COLUMN pq_support;
ALTER TABLE messages DROP COLUMN pq_encryption;
|]
@@ -26,7 +26,9 @@ CREATE TABLE connections(
deleted INTEGER DEFAULT 0 CHECK(deleted NOT NULL),
user_id INTEGER CHECK(user_id NOT NULL)
REFERENCES users ON DELETE CASCADE,
ratchet_sync_state TEXT NOT NULL DEFAULT 'ok'
ratchet_sync_state TEXT NOT NULL DEFAULT 'ok',
deleted_at_wait_delivery TEXT,
pq_support INTEGER NOT NULL DEFAULT 0
) WITHOUT ROWID;
CREATE TABLE rcv_queues(
host TEXT NOT NULL,
@@ -89,6 +91,7 @@ CREATE TABLE messages(
msg_type BLOB NOT NULL, --(H)ELLO,(R)EPLY,(D)ELETE. Should SMP confirmation be saved too?
msg_body BLOB NOT NULL DEFAULT x'',
msg_flags TEXT NULL,
pq_encryption INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY(conn_id, internal_id),
FOREIGN KEY(conn_id, internal_rcv_id) REFERENCES rcv_messages
ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
@@ -159,7 +162,8 @@ CREATE TABLE ratchets(
e2e_version INTEGER NOT NULL DEFAULT 1
,
x3dh_pub_key_1 BLOB,
x3dh_pub_key_2 BLOB
x3dh_pub_key_2 BLOB,
pq_priv_kem BLOB
) WITHOUT ROWID;
CREATE TABLE skipped_messages(
skipped_message_id INTEGER PRIMARY KEY,
@@ -279,6 +283,10 @@ CREATE TABLE rcv_files(
save_file_key BLOB,
save_file_nonce BLOB,
failed INTEGER DEFAULT 0,
redirect_id INTEGER REFERENCES rcv_files ON DELETE SET NULL,
redirect_entity_id BLOB,
redirect_size INTEGER,
redirect_digest BLOB,
UNIQUE(rcv_file_entity_id)
);
CREATE TABLE rcv_file_chunks(
@@ -322,7 +330,9 @@ CREATE TABLE snd_files(
,
src_file_key BLOB,
src_file_nonce BLOB,
failed INTEGER DEFAULT 0
failed INTEGER DEFAULT 0,
redirect_size INTEGER,
redirect_digest BLOB
);
CREATE TABLE snd_file_chunks(
snd_file_chunk_id INTEGER PRIMARY KEY,
@@ -497,3 +507,15 @@ CREATE INDEX idx_commands_server_commands ON commands(
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);
-24
View File
@@ -1,24 +0,0 @@
module Simplex.Messaging.Agent.TAsyncs where
import Control.Monad.IO.Unlift (MonadUnliftIO)
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import UnliftIO.Async (Async, async)
import UnliftIO.STM
data TAsyncs = TAsyncs
{ actionId :: TVar Int,
actions :: TMap Int (Async ())
}
newTAsyncs :: STM TAsyncs
newTAsyncs = TAsyncs <$> newTVar 0 <*> TM.empty
newAsyncAction :: MonadUnliftIO m => (Int -> m ()) -> TAsyncs -> m ()
newAsyncAction action as = do
aId <- atomically $ stateTVar (actionId as) $ \i -> (i + 1, i + 1)
a <- async $ action aId
atomically $ TM.insert aId a $ actions as
removeAsyncAction :: Int -> TAsyncs -> STM ()
removeAsyncAction aId = TM.delete aId . actions
+58 -20
View File
@@ -1,11 +1,13 @@
{-# LANGUAGE LambdaCase #-}
module Simplex.Messaging.Agent.TRcvQueues
( TRcvQueues (getRcvQueues),
( TRcvQueues (getRcvQueues, getConnections),
empty,
clear,
deleteConn,
hasConn,
getConns,
addQueue,
batchAddQueues,
deleteQueue,
getSessQueues,
getDelSessQueues,
@@ -14,49 +16,85 @@ module Simplex.Messaging.Agent.TRcvQueues
where
import Control.Concurrent.STM
import Data.Foldable (foldl')
import Data.List.NonEmpty (NonEmpty (..), (<|))
import qualified Data.List.NonEmpty as L
import qualified Data.Map.Strict as M
import Data.Set (Set)
import qualified Data.Set as S
import Simplex.Messaging.Agent.Protocol (ConnId, UserId)
import Simplex.Messaging.Agent.Store (RcvQueue, StoredRcvQueue (..))
import Simplex.Messaging.Protocol (RecipientId, SMPServer)
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
newtype TRcvQueues = TRcvQueues {getRcvQueues :: TMap (UserId, SMPServer, RecipientId) RcvQueue}
-- the fields in this record have the same data with swapped keys for lookup efficiency,
-- and all methods must maintain this invariant.
data TRcvQueues = TRcvQueues
{ getRcvQueues :: TMap (UserId, SMPServer, RecipientId) RcvQueue,
getConnections :: TMap ConnId (NonEmpty (UserId, SMPServer, RecipientId))
}
empty :: STM TRcvQueues
empty = TRcvQueues <$> TM.empty
empty = TRcvQueues <$> TM.empty <*> TM.empty
clear :: TRcvQueues -> STM ()
clear (TRcvQueues qs) = TM.clear qs
clear (TRcvQueues qs cs) = TM.clear qs >> TM.clear cs
deleteConn :: ConnId -> TRcvQueues -> STM ()
deleteConn cId (TRcvQueues qs) = modifyTVar' qs $ M.filter (\rq -> cId /= connId rq)
deleteConn cId (TRcvQueues qs cs) =
TM.lookupDelete cId cs >>= \case
Just ks -> modifyTVar' qs $ \qs' -> foldl' (flip M.delete) qs' ks
Nothing -> pure ()
hasConn :: ConnId -> TRcvQueues -> STM Bool
hasConn cId (TRcvQueues qs) = any (\rq -> cId == connId rq) <$> readTVar qs
getConns :: TRcvQueues -> STM (Set ConnId)
getConns (TRcvQueues qs) = M.foldr' (S.insert . connId) S.empty <$> readTVar qs
hasConn cId (TRcvQueues _ cs) = TM.member cId cs
addQueue :: RcvQueue -> TRcvQueues -> STM ()
addQueue rq (TRcvQueues qs) = TM.insert (qKey rq) rq qs
addQueue rq (TRcvQueues qs cs) = do
TM.insert k rq qs
TM.alter addQ (connId rq) cs
where
addQ = Just . maybe (k :| []) (k <|)
k = qKey rq
-- Save time by aggregating modifyTVar
batchAddQueues :: Foldable t => TRcvQueues -> t RcvQueue -> STM ()
batchAddQueues (TRcvQueues qs cs) rqs = do
modifyTVar' qs $ \now -> foldl' (\rqs' rq -> M.insert (qKey rq) rq rqs') now rqs
modifyTVar' cs $ \now -> foldl' (\cs' rq -> M.alter (addQ $ qKey rq) (connId rq) cs') now rqs
where
addQ k = Just . maybe (k :| []) (k <|)
deleteQueue :: RcvQueue -> TRcvQueues -> STM ()
deleteQueue rq (TRcvQueues qs) = TM.delete (qKey rq) qs
deleteQueue rq (TRcvQueues qs cs) = do
TM.delete k qs
TM.update delQ (connId rq) cs
where
delQ = L.nonEmpty . L.filter (/= k)
k = qKey rq
getSessQueues :: (UserId, SMPServer, Maybe ConnId) -> TRcvQueues -> STM [RcvQueue]
getSessQueues tSess (TRcvQueues qs) = M.foldl' addQ [] <$> readTVar qs
getSessQueues tSess (TRcvQueues qs _) = M.foldl' addQ [] <$> readTVar qs
where
addQ qs' rq = if rq `isSession` tSess then rq : qs' else qs'
getDelSessQueues :: (UserId, SMPServer, Maybe ConnId) -> TRcvQueues -> STM [RcvQueue]
getDelSessQueues tSess (TRcvQueues qs) = stateTVar qs $ M.foldl' addQ ([], M.empty)
getDelSessQueues :: (UserId, SMPServer, Maybe ConnId) -> TRcvQueues -> STM ([RcvQueue], [ConnId])
getDelSessQueues tSess (TRcvQueues qs cs) = do
(removedQs, qs'') <- (\qs' -> M.foldl' delQ ([], qs') qs') <$> readTVar qs
writeTVar qs $! qs''
removedConns <- stateTVar cs $ \cs' -> foldl' delConn ([], cs') removedQs
pure (removedQs, removedConns)
where
addQ (removed, qs') rq
| rq `isSession` tSess = (rq : removed, qs')
| otherwise = (removed, M.insert (qKey rq) rq qs')
delQ acc@(removed, qs') rq
| rq `isSession` tSess = (rq : removed, M.delete (qKey rq) qs')
| otherwise = acc
delConn (removed, cs') rq = M.alterF f cId cs'
where
cId = connId rq
f = \case
Just ks -> case L.nonEmpty $ L.filter (qKey rq /=) ks of
Just ks' -> (removed, Just ks')
Nothing -> (cId : removed, Nothing)
Nothing -> (removed, Nothing) -- "impossible" in invariant holds, because we get keys from the known queues
isSession :: RcvQueue -> (UserId, SMPServer, Maybe ConnId) -> Bool
isSession rq (uId, srv, connId_) =
+289 -172
View File
@@ -28,7 +28,7 @@
module Simplex.Messaging.Client
( -- * Connect (disconnect) client to (from) SMP server
TransportSession,
ProtocolClient (thVersion, sessionId, sessionTs),
ProtocolClient (thParams, sessionTs),
SMPClient,
getProtocolClient,
closeProtocolClient,
@@ -54,6 +54,9 @@ module Simplex.Messaging.Client
suspendSMPQueue,
deleteSMPQueue,
deleteSMPQueues,
createSMPProxySession,
proxySMPMessage,
forwardSMPMessage,
sendProtocolCommand,
-- * Supporting types and client configuration
@@ -63,20 +66,21 @@ module Simplex.Messaging.Client
NetworkConfig (..),
TransportSessionMode (..),
defaultClientConfig,
defaultSMPClientConfig,
defaultNetworkConfig,
transportClientConfig,
chooseTransportHost,
proxyUsername,
temporaryClientError,
smpProxyError,
ServerTransmission,
ClientCommand,
-- * For testing
ClientBatch (..),
PCTransmission,
batchClientTransmissions,
mkTransmission,
clientStub,
authTransmission,
smpClientStub,
)
where
@@ -84,8 +88,10 @@ import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Exception
import Control.Monad
import Control.Monad.Except
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Except
import Crypto.Random (ChaChaDRG)
import qualified Data.Aeson.TH as J
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
@@ -95,76 +101,77 @@ import Data.List (find)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as L
import Data.Maybe (fromMaybe)
import Data.Time.Clock (UTCTime, getCurrentTime)
import Data.Time.Clock (UTCTime (..), getCurrentTime)
import qualified Data.X509 as X
import qualified Data.X509.Validation as XV
import Network.Socket (ServiceName)
import Numeric.Natural
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON)
import Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.Protocol
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport
import Simplex.Messaging.Transport.Client (SocksProxy, TransportClientConfig (..), TransportHost (..), runTransportClient)
import Simplex.Messaging.Transport.KeepAlive
import Simplex.Messaging.Transport.WebSockets (WS)
import Simplex.Messaging.Util (bshow, raceAny_, threadDelay')
import Simplex.Messaging.Util (bshow, liftEitherWith, raceAny_, threadDelay')
import Simplex.Messaging.Version
import System.Timeout (timeout)
-- | 'SMPClient' is a handle used to send commands to a specific SMP server.
--
-- Use 'getSMPClient' to connect to an SMP server and create a client handle.
data ProtocolClient err msg = ProtocolClient
data ProtocolClient v err msg = ProtocolClient
{ action :: Maybe (Async ()),
sessionId :: SessionId,
thParams :: THandleParams v 'TClient,
sessionTs :: UTCTime,
thVersion :: Version,
timeoutPerBlock :: Int,
blockSize :: Int,
batch :: Bool,
client_ :: PClient err msg
client_ :: PClient v err msg
}
data PClient err msg = PClient
data PClient v err msg = PClient
{ connected :: TVar Bool,
transportSession :: TransportSession msg,
transportHost :: TransportHost,
tcpTimeout :: Int,
batchDelay :: Maybe Int,
pingErrorCount :: TVar Int,
clientCorrId :: TVar Natural,
clientCorrId :: TVar ChaChaDRG,
sentCommands :: TMap CorrId (Request err msg),
sndQ :: TBQueue ByteString,
rcvQ :: TBQueue (NonEmpty (SignedTransmission err msg)),
msgQ :: Maybe (TBQueue (ServerTransmission msg))
msgQ :: Maybe (TBQueue (ServerTransmission v msg))
}
clientStub :: ByteString -> STM (ProtocolClient err msg)
clientStub sessionId = do
smpClientStub :: TVar ChaChaDRG -> ByteString -> VersionSMP -> Maybe (THandleAuth 'TClient) -> STM SMPClient
smpClientStub g sessionId thVersion thAuth = do
connected <- newTVar False
clientCorrId <- newTVar 0
clientCorrId <- C.newRandomDRG g
sentCommands <- TM.empty
pingErrorCount <- newTVar 0
sndQ <- newTBQueue 100
rcvQ <- newTBQueue 100
return
ProtocolClient
{ action = Nothing,
sessionId,
sessionTs = undefined,
thVersion = 5,
timeoutPerBlock = undefined,
blockSize = smpBlockSize,
batch = undefined,
thParams =
THandleParams
{ sessionId,
thVersion,
thAuth,
blockSize = smpBlockSize,
implySessId = thVersion >= authCmdsSMPVersion,
batch = True
},
sessionTs = UTCTime (read "2024-03-31") 0,
client_ =
PClient
{ connected,
transportSession = undefined,
transportHost = undefined,
tcpTimeout = undefined,
batchDelay = Nothing,
pingErrorCount = undefined,
transportSession = (1, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001", Nothing),
transportHost = "localhost",
tcpTimeout = 15_000_000,
pingErrorCount,
clientCorrId,
sentCommands,
sndQ,
@@ -173,13 +180,13 @@ clientStub sessionId = do
}
}
type SMPClient = ProtocolClient ErrorType SMP.BrokerMsg
type SMPClient = ProtocolClient SMPVersion ErrorType BrokerMsg
-- | Type for client command data
type ClientCommand msg = (Maybe C.APrivateSignKey, EntityId, ProtoCommand msg)
type ClientCommand msg = (Maybe C.APrivateAuthKey, EntityId, ProtoCommand msg)
-- | Type synonym for transmission from some SPM server queue.
type ServerTransmission msg = (TransportSession msg, Version, SessionId, EntityId, msg)
type ServerTransmission v msg = (TransportSession msg, Version v, SessionId, EntityId, msg)
data HostMode
= -- | prefer (or require) onion hosts when connecting via SOCKS proxy
@@ -205,7 +212,7 @@ data NetworkConfig = NetworkConfig
-- | timeout of protocol commands (microseconds)
tcpTimeout :: Int,
-- | additional timeout per kilobyte (1024 bytes) to be sent
tcpTimeoutPerKb :: Int,
tcpTimeoutPerKb :: Int64,
-- | TCP keep-alive options, Nothing to skip enabling keep-alive
tcpKeepAlive :: Maybe KeepAliveOpts,
-- | period for SMP ping commands (microseconds, 0 to disable)
@@ -228,7 +235,7 @@ defaultNetworkConfig =
sessionMode = TSMUser,
tcpConnectTimeout = 20_000_000,
tcpTimeout = 15_000_000,
tcpTimeoutPerKb = 45_000, -- 45ms, should be less than 130ms to avoid Int overflow on 32 bit systems
tcpTimeoutPerKb = 5_000,
tcpKeepAlive = Just defaultKeepAliveOpts,
smpPingInterval = 600_000_000, -- 10min
smpPingCount = 3,
@@ -237,10 +244,11 @@ defaultNetworkConfig =
transportClientConfig :: NetworkConfig -> TransportClientConfig
transportClientConfig NetworkConfig {socksProxy, tcpKeepAlive, logTLSErrors} =
TransportClientConfig {socksProxy, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing}
TransportClientConfig {socksProxy, tcpKeepAlive, logTLSErrors, clientCredentials = Nothing, alpn = Nothing}
{-# INLINE transportClientConfig #-}
-- | protocol client configuration.
data ProtocolClientConfig = ProtocolClientConfig
data ProtocolClientConfig v = ProtocolClientConfig
{ -- | size of TBQueue to use for server commands and responses
qSize :: Natural,
-- | default server port if port is not specified in ProtocolServer
@@ -248,21 +256,26 @@ data ProtocolClientConfig = ProtocolClientConfig
-- | network configuration
networkConfig :: NetworkConfig,
-- | client-server protocol version range
serverVRange :: VersionRange,
-- | delay between sending batches of commands (microseconds)
batchDelay :: Maybe Int
serverVRange :: VersionRange v,
-- | agree shared session secret (used in SMP proxy)
agreeSecret :: Bool
}
-- | Default protocol client configuration.
defaultClientConfig :: ProtocolClientConfig
defaultClientConfig =
defaultClientConfig :: VersionRange v -> ProtocolClientConfig v
defaultClientConfig serverVRange =
ProtocolClientConfig
{ qSize = 64,
defaultTransport = ("443", transport @TLS),
networkConfig = defaultNetworkConfig,
serverVRange = supportedSMPServerVRange,
batchDelay = Nothing
serverVRange,
agreeSecret = False
}
{-# INLINE defaultClientConfig #-}
defaultSMPClientConfig :: ProtocolClientConfig SMPVersion
defaultSMPClientConfig = defaultClientConfig supportedClientSMPRelayVRange
{-# INLINE defaultSMPClientConfig #-}
data Request err msg = Request
{ entityId :: EntityId,
@@ -288,16 +301,19 @@ chooseTransportHost NetworkConfig {socksProxy, hostMode, requiredHostMode} hosts
onionHost = find isOnionHost hosts
publicHost = find (not . isOnionHost) hosts
protocolClientServer :: ProtocolTypeI (ProtoType msg) => ProtocolClient err msg -> String
protocolClientServer :: ProtocolTypeI (ProtoType msg) => ProtocolClient v err msg -> String
protocolClientServer = B.unpack . strEncode . snd3 . transportSession . client_
where
snd3 (_, s, _) = s
{-# INLINE protocolClientServer #-}
transportHost' :: ProtocolClient err msg -> TransportHost
transportHost' :: ProtocolClient v err msg -> TransportHost
transportHost' = transportHost . client_
{-# INLINE transportHost' #-}
transportSession' :: ProtocolClient err msg -> TransportSession msg
transportSession' :: ProtocolClient v err msg -> TransportSession msg
transportSession' = transportSession . client_
{-# INLINE transportSession' #-}
type UserId = Int64
@@ -309,20 +325,20 @@ type TransportSession msg = (UserId, ProtoServer msg, Maybe EntityId)
--
-- A single queue can be used for multiple 'SMPClient' instances,
-- as 'SMPServerTransmission' includes server information.
getProtocolClient :: forall err msg. Protocol err msg => TransportSession msg -> ProtocolClientConfig -> Maybe (TBQueue (ServerTransmission msg)) -> (ProtocolClient err msg -> IO ()) -> IO (Either (ProtocolClientError err) (ProtocolClient err msg))
getProtocolClient transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, serverVRange, batchDelay} msgQ disconnected = do
getProtocolClient :: forall v err msg. Protocol v err msg => TVar ChaChaDRG -> TransportSession msg -> ProtocolClientConfig v -> Maybe (TBQueue (ServerTransmission v msg)) -> (ProtocolClient v err msg -> IO ()) -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg))
getProtocolClient g transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, serverVRange, agreeSecret} msgQ disconnected = do
case chooseTransportHost networkConfig (host srv) of
Right useHost ->
(atomically (mkProtocolClient useHost) >>= runClient useTransport useHost)
`catch` \(e :: IOException) -> pure . Left $ PCEIOError e
Left e -> pure $ Left e
where
NetworkConfig {tcpConnectTimeout, tcpTimeout, tcpTimeoutPerKb, smpPingInterval} = networkConfig
mkProtocolClient :: TransportHost -> STM (PClient err msg)
NetworkConfig {tcpConnectTimeout, tcpTimeout, smpPingInterval} = networkConfig
mkProtocolClient :: TransportHost -> STM (PClient v err msg)
mkProtocolClient transportHost = do
connected <- newTVar False
pingErrorCount <- newTVar 0
clientCorrId <- newTVar 0
clientCorrId <- C.newRandomDRG g
sentCommands <- TM.empty
sndQ <- newTBQueue qSize
rcvQ <- newTBQueue qSize
@@ -332,7 +348,6 @@ getProtocolClient transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize,
transportSession,
transportHost,
tcpTimeout,
batchDelay,
pingErrorCount,
clientCorrId,
sentCommands,
@@ -341,7 +356,7 @@ getProtocolClient transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize,
msgQ
}
runClient :: (ServiceName, ATransport) -> TransportHost -> PClient err msg -> IO (Either (ProtocolClientError err) (ProtocolClient err msg))
runClient :: (ServiceName, ATransport) -> TransportHost -> PClient v err msg -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg))
runClient (port', ATransport t) useHost c = do
cVar <- newEmptyTMVarIO
let tcConfig = transportClientConfig networkConfig
@@ -349,12 +364,12 @@ getProtocolClient transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize,
action <-
async $
runTransportClient tcConfig (Just username) useHost port' (Just $ keyHash srv) (client t c cVar)
`finally` atomically (putTMVar cVar $ Left PCENetworkError)
`finally` atomically (tryPutTMVar cVar $ Left PCENetworkError)
c_ <- tcpConnectTimeout `timeout` atomically (takeTMVar cVar)
pure $ case c_ of
Just (Right c') -> Right c' {action = Just action}
Just (Left e) -> Left e
Nothing -> Left PCENetworkError
case c_ of
Just (Right c') -> pure $ Right c' {action = Just action}
Just (Left e) -> pure $ Left e
Nothing -> cancel action $> Left PCENetworkError
useTransport :: (ServiceName, ATransport)
useTransport = case port srv of
@@ -362,30 +377,30 @@ getProtocolClient transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize,
"80" -> ("80", transport @WS)
p -> (p, transport @TLS)
client :: forall c. Transport c => TProxy c -> PClient err msg -> TMVar (Either (ProtocolClientError err) (ProtocolClient err msg)) -> c -> IO ()
client _ c cVar h =
runExceptT (protocolClientHandshake @err @msg h (keyHash srv) serverVRange) >>= \case
client :: forall c. Transport c => TProxy c -> PClient v err msg -> TMVar (Either (ProtocolClientError err) (ProtocolClient v err msg)) -> c -> IO ()
client _ c cVar h = do
ks <- if agreeSecret then Just <$> atomically (C.generateKeyPair g) else pure Nothing
runExceptT (protocolClientHandshake @v @err @msg h ks (keyHash srv) serverVRange) >>= \case
Left e -> atomically . putTMVar cVar . Left $ PCETransportError e
Right th@THandle {sessionId, thVersion, blockSize, batch} -> do
Right th@THandle {params} -> do
sessionTs <- getCurrentTime
let timeoutPerBlock = (blockSize * tcpTimeoutPerKb) `div` 1024
c' = ProtocolClient {action = Nothing, client_ = c, sessionId, thVersion, sessionTs, timeoutPerBlock, blockSize, batch}
let c' = ProtocolClient {action = Nothing, client_ = c, thParams = params, sessionTs}
atomically $ do
writeTVar (connected c) True
putTMVar cVar $ Right c'
raceAny_ ([send c' th, process c', receive c' th] <> [ping c' | smpPingInterval > 0])
`finally` disconnected c'
send :: Transport c => ProtocolClient err msg -> THandle c -> IO ()
send :: Transport c => ProtocolClient v err msg -> THandle v c 'TClient -> IO ()
send ProtocolClient {client_ = PClient {sndQ}} h = forever $ atomically (readTBQueue sndQ) >>= tPutLog h
receive :: Transport c => ProtocolClient err msg -> THandle c -> IO ()
receive :: Transport c => ProtocolClient v err msg -> THandle v c 'TClient -> IO ()
receive ProtocolClient {client_ = PClient {rcvQ}} h = forever $ tGet h >>= atomically . writeTBQueue rcvQ
ping :: ProtocolClient err msg -> IO ()
ping :: ProtocolClient v err msg -> IO ()
ping c@ProtocolClient {client_ = PClient {pingErrorCount}} = do
threadDelay' smpPingInterval
runExceptT (sendProtocolCommand c Nothing "" $ protocolPing @err @msg) >>= \case
runExceptT (sendProtocolCommand c Nothing "" $ protocolPing @v @err @msg) >>= \case
Left PCEResponseTimeout -> do
cnt <- atomically $ stateTVar pingErrorCount $ \cnt -> (cnt + 1, cnt + 1)
when (maxCnt == 0 || cnt < maxCnt) $ ping c
@@ -393,10 +408,10 @@ getProtocolClient transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize,
where
maxCnt = smpPingCount networkConfig
process :: ProtocolClient err msg -> IO ()
process :: ProtocolClient v err msg -> IO ()
process c = forever $ atomically (readTBQueue $ rcvQ $ client_ c) >>= mapM_ (processMsg c)
processMsg :: ProtocolClient err msg -> SignedTransmission err msg -> IO ()
processMsg :: ProtocolClient v err msg -> SignedTransmission err msg -> IO ()
processMsg c@ProtocolClient {client_ = PClient {sentCommands}} (_, _, (corrId, entId, respOrErr)) =
if B.null $ bs corrId
then sendMsg respOrErr
@@ -422,10 +437,12 @@ getProtocolClient transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize,
proxyUsername :: TransportSession msg -> ByteString
proxyUsername (userId, _, entityId_) = C.sha256Hash $ bshow userId <> maybe "" (":" <>) entityId_
{-# INLINE proxyUsername #-}
-- | Disconnects client from the server and terminates client threads.
closeProtocolClient :: ProtocolClient err msg -> IO ()
closeProtocolClient :: ProtocolClient v err msg -> IO ()
closeProtocolClient = mapM_ uninterruptibleCancel . action
{-# INLINE closeProtocolClient #-}
-- | SMP client error type.
data ProtocolClientError err
@@ -465,19 +482,31 @@ temporaryClientError = \case
PCEResponseTimeout -> True
PCEIOError _ -> True
_ -> False
{-# INLINE temporaryClientError #-}
smpProxyError :: SMPClientError -> ErrorType
smpProxyError = \case
PCEProtocolError et -> PROXY (PROTOCOL et)
PCEResponseError et -> PROXY (RESPONSE et)
PCEUnexpectedResponse bs -> PROXY (UNEXPECTED $ B.unpack $ B.take 32 bs)
PCEResponseTimeout -> PROXY TIMEOUT
PCENetworkError -> PROXY NETWORK
PCEIncompatibleHost -> PROXY BAD_HOST
PCETransportError t -> PROXY (TRANSPORT t)
PCECryptoError _ -> INTERNAL
PCEIOError _ -> INTERNAL
-- | Create a new SMP queue.
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#create-queue-command
createSMPQueue ::
SMPClient ->
RcvPrivateSignKey ->
RcvPublicVerifyKey ->
C.AAuthKeyPair -> -- SMP v6 - signature key pair, SMP v7 - DH key pair
RcvPublicDhKey ->
Maybe BasicAuth ->
SubscriptionMode ->
ExceptT SMPClientError IO QueueIdsKeys
createSMPQueue c rpKey rKey dhKey auth subMode =
createSMPQueue c (rKey, rpKey) dhKey auth subMode =
sendSMPCommand c (Just rpKey) "" (NEW rKey dhKey auth subMode) >>= \case
IDS qik -> pure qik
r -> throwE . PCEUnexpectedResponse $ bshow r
@@ -485,7 +514,7 @@ createSMPQueue c rpKey rKey dhKey auth subMode =
-- | Subscribe to the SMP queue.
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#subscribe-to-queue
subscribeSMPQueue :: SMPClient -> RcvPrivateSignKey -> RecipientId -> ExceptT SMPClientError IO ()
subscribeSMPQueue :: SMPClient -> RcvPrivateAuthKey -> RecipientId -> ExceptT SMPClientError IO ()
subscribeSMPQueue c rpKey rId =
sendSMPCommand c (Just rpKey) rId SUB >>= \case
OK -> return ()
@@ -493,12 +522,12 @@ subscribeSMPQueue c rpKey rId =
r -> throwE . PCEUnexpectedResponse $ bshow r
-- | Subscribe to multiple SMP queues batching commands if supported.
subscribeSMPQueues :: SMPClient -> NonEmpty (RcvPrivateSignKey, RecipientId) -> IO (NonEmpty (Either SMPClientError ()))
subscribeSMPQueues :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId) -> IO (NonEmpty (Either SMPClientError ()))
subscribeSMPQueues c qs = sendProtocolCommands c cs >>= mapM (processSUBResponse c)
where
cs = L.map (\(rpKey, rId) -> (Just rpKey, rId, Cmd SRecipient SUB)) qs
streamSubscribeSMPQueues :: SMPClient -> NonEmpty (RcvPrivateSignKey, RecipientId) -> ([(RecipientId, Either SMPClientError ())] -> IO ()) -> IO ()
streamSubscribeSMPQueues :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId) -> ([(RecipientId, Either SMPClientError ())] -> IO ()) -> IO ()
streamSubscribeSMPQueues c qs cb = streamProtocolCommands c cs $ mapM process >=> cb
where
cs = L.map (\(rpKey, rId) -> (Just rpKey, rId, Cmd SRecipient SUB)) qs
@@ -514,14 +543,14 @@ processSUBResponse c (Response rId r) = case r of
writeSMPMessage :: SMPClient -> RecipientId -> BrokerMsg -> IO ()
writeSMPMessage c rId msg = atomically $ mapM_ (`writeTBQueue` serverTransmission c rId msg) (msgQ $ client_ c)
serverTransmission :: ProtocolClient err msg -> RecipientId -> msg -> ServerTransmission msg
serverTransmission ProtocolClient {thVersion, sessionId, client_ = PClient {transportSession}} entityId message =
serverTransmission :: ProtocolClient v err msg -> RecipientId -> msg -> ServerTransmission v msg
serverTransmission ProtocolClient {thParams = THandleParams {thVersion, sessionId}, client_ = PClient {transportSession}} entityId message =
(transportSession, thVersion, sessionId, entityId, message)
-- | Get message from SMP queue. The server returns ERR PROHIBITED if a client uses SUB and GET via the same transport connection for the same queue
--
-- https://github.covm/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#receive-a-message-from-the-queue
getSMPMessage :: SMPClient -> RcvPrivateSignKey -> RecipientId -> ExceptT SMPClientError IO (Maybe RcvMessage)
getSMPMessage :: SMPClient -> RcvPrivateAuthKey -> RecipientId -> ExceptT SMPClientError IO (Maybe RcvMessage)
getSMPMessage c rpKey rId =
sendSMPCommand c (Just rpKey) rId GET >>= \case
OK -> pure Nothing
@@ -531,30 +560,33 @@ getSMPMessage c rpKey rId =
-- | Subscribe to the SMP queue notifications.
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#subscribe-to-queue-notifications
subscribeSMPQueueNotifications :: SMPClient -> NtfPrivateSignKey -> NotifierId -> ExceptT SMPClientError IO ()
subscribeSMPQueueNotifications :: SMPClient -> NtfPrivateAuthKey -> NotifierId -> ExceptT SMPClientError IO ()
subscribeSMPQueueNotifications = okSMPCommand NSUB
{-# INLINE subscribeSMPQueueNotifications #-}
-- | Subscribe to multiple SMP queues notifications batching commands if supported.
subscribeSMPQueuesNtfs :: SMPClient -> NonEmpty (NtfPrivateSignKey, NotifierId) -> IO (NonEmpty (Either SMPClientError ()))
subscribeSMPQueuesNtfs :: SMPClient -> NonEmpty (NtfPrivateAuthKey, NotifierId) -> IO (NonEmpty (Either SMPClientError ()))
subscribeSMPQueuesNtfs = okSMPCommands NSUB
{-# INLINE subscribeSMPQueuesNtfs #-}
-- | Secure the SMP queue by adding a sender public key.
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#secure-queue-command
secureSMPQueue :: SMPClient -> RcvPrivateSignKey -> RecipientId -> SndPublicVerifyKey -> ExceptT SMPClientError IO ()
secureSMPQueue :: SMPClient -> RcvPrivateAuthKey -> RecipientId -> SndPublicAuthKey -> ExceptT SMPClientError IO ()
secureSMPQueue c rpKey rId senderKey = okSMPCommand (KEY senderKey) c rpKey rId
{-# INLINE secureSMPQueue #-}
-- | Enable notifications for the queue for push notifications server.
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#enable-notifications-command
enableSMPQueueNotifications :: SMPClient -> RcvPrivateSignKey -> RecipientId -> NtfPublicVerifyKey -> RcvNtfPublicDhKey -> ExceptT SMPClientError IO (NotifierId, RcvNtfPublicDhKey)
enableSMPQueueNotifications :: SMPClient -> RcvPrivateAuthKey -> RecipientId -> NtfPublicAuthKey -> RcvNtfPublicDhKey -> ExceptT SMPClientError IO (NotifierId, RcvNtfPublicDhKey)
enableSMPQueueNotifications c rpKey rId notifierKey rcvNtfPublicDhKey =
sendSMPCommand c (Just rpKey) rId (NKEY notifierKey rcvNtfPublicDhKey) >>= \case
NID nId rcvNtfSrvPublicDhKey -> pure (nId, rcvNtfSrvPublicDhKey)
r -> throwE . PCEUnexpectedResponse $ bshow r
-- | Enable notifications for the multiple queues for push notifications server.
enableSMPQueuesNtfs :: SMPClient -> NonEmpty (RcvPrivateSignKey, RecipientId, NtfPublicVerifyKey, RcvNtfPublicDhKey) -> IO (NonEmpty (Either SMPClientError (NotifierId, RcvNtfPublicDhKey)))
enableSMPQueuesNtfs :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId, NtfPublicAuthKey, RcvNtfPublicDhKey) -> IO (NonEmpty (Either SMPClientError (NotifierId, RcvNtfPublicDhKey)))
enableSMPQueuesNtfs c qs = L.map process <$> sendProtocolCommands c cs
where
cs = L.map (\(rpKey, rId, notifierKey, rcvNtfPublicDhKey) -> (Just rpKey, rId, Cmd SRecipient $ NKEY notifierKey rcvNtfPublicDhKey)) qs
@@ -566,17 +598,19 @@ enableSMPQueuesNtfs c qs = L.map process <$> sendProtocolCommands c cs
-- | Disable notifications for the queue for push notifications server.
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#disable-notifications-command
disableSMPQueueNotifications :: SMPClient -> RcvPrivateSignKey -> RecipientId -> ExceptT SMPClientError IO ()
disableSMPQueueNotifications :: SMPClient -> RcvPrivateAuthKey -> RecipientId -> ExceptT SMPClientError IO ()
disableSMPQueueNotifications = okSMPCommand NDEL
{-# INLINE disableSMPQueueNotifications #-}
-- | Disable notifications for multiple queues for push notifications server.
disableSMPQueuesNtfs :: SMPClient -> NonEmpty (RcvPrivateSignKey, RecipientId) -> IO (NonEmpty (Either SMPClientError ()))
disableSMPQueuesNtfs :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId) -> IO (NonEmpty (Either SMPClientError ()))
disableSMPQueuesNtfs = okSMPCommands NDEL
{-# INLINE disableSMPQueuesNtfs #-}
-- | Send SMP message.
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#send-message
sendSMPMessage :: SMPClient -> Maybe SndPrivateSignKey -> SenderId -> MsgFlags -> MsgBody -> ExceptT SMPClientError IO ()
sendSMPMessage :: SMPClient -> Maybe SndPrivateAuthKey -> SenderId -> MsgFlags -> MsgBody -> ExceptT SMPClientError IO ()
sendSMPMessage c spKey sId flags msg =
sendSMPCommand c spKey sId (SEND flags msg) >>= \case
OK -> pure ()
@@ -585,7 +619,7 @@ sendSMPMessage c spKey sId flags msg =
-- | Acknowledge message delivery (server deletes the message).
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#acknowledge-message-delivery
ackSMPMessage :: SMPClient -> RcvPrivateSignKey -> QueueId -> MsgId -> ExceptT SMPClientError IO ()
ackSMPMessage :: SMPClient -> RcvPrivateAuthKey -> QueueId -> MsgId -> ExceptT SMPClientError IO ()
ackSMPMessage c rpKey rId msgId =
sendSMPCommand c (Just rpKey) rId (ACK msgId) >>= \case
OK -> return ()
@@ -596,26 +630,125 @@ ackSMPMessage c rpKey rId msgId =
-- The existing messages from the queue will still be delivered.
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#suspend-queue
suspendSMPQueue :: SMPClient -> RcvPrivateSignKey -> QueueId -> ExceptT SMPClientError IO ()
suspendSMPQueue :: SMPClient -> RcvPrivateAuthKey -> QueueId -> ExceptT SMPClientError IO ()
suspendSMPQueue = okSMPCommand OFF
{-# INLINE suspendSMPQueue #-}
-- | Irreversibly delete SMP queue and all messages in it.
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#delete-queue
deleteSMPQueue :: SMPClient -> RcvPrivateSignKey -> RecipientId -> ExceptT SMPClientError IO ()
deleteSMPQueue :: SMPClient -> RcvPrivateAuthKey -> RecipientId -> ExceptT SMPClientError IO ()
deleteSMPQueue = okSMPCommand DEL
{-# INLINE deleteSMPQueue #-}
-- | Delete multiple SMP queues batching commands if supported.
deleteSMPQueues :: SMPClient -> NonEmpty (RcvPrivateSignKey, RecipientId) -> IO (NonEmpty (Either SMPClientError ()))
deleteSMPQueues :: SMPClient -> NonEmpty (RcvPrivateAuthKey, RecipientId) -> IO (NonEmpty (Either SMPClientError ()))
deleteSMPQueues = okSMPCommands DEL
{-# INLINE deleteSMPQueues #-}
okSMPCommand :: PartyI p => Command p -> SMPClient -> C.APrivateSignKey -> QueueId -> ExceptT SMPClientError IO ()
-- TODO picture
-- send PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender
-- receives PKEY :: SessionId -> X.CertificateChain -> X.SignedExact X.PubKey -> BrokerMsg
createSMPProxySession :: SMPClient -> SMPServer -> Maybe BasicAuth -> ExceptT SMPClientError IO (SessionId, VersionSMP, C.PublicKeyX25519)
createSMPProxySession c relayServ@ProtocolServer {keyHash = C.KeyHash kh} proxyAuth =
sendSMPCommand c Nothing "" (PRXY relayServ proxyAuth) >>= \case
-- XXX: rfc says sessionId should be in the entityId of response
PKEY sId vr (chain, key) -> do
case supportedClientSMPRelayVRange `compatibleVersion` vr of
Nothing -> throwE PCEIncompatibleHost -- TODO different error
Just (Compatible v) -> liftEitherWith x509Error $ (sId,v,) <$> validateRelay chain key
r -> throwE . PCEUnexpectedResponse $ bshow r
where
x509Error :: String -> SMPClientError
x509Error _msg = PCEResponseError $ error "TODO: x509 error" -- TODO different error
validateRelay :: X.CertificateChain -> X.SignedExact X.PubKey -> Either String C.PublicKeyX25519
validateRelay (X.CertificateChain cert) exact = do
serverKey <- case cert of
[leaf, ca]
| XV.Fingerprint kh == XV.getFingerprint ca X.HashSHA256 ->
C.x509ToPublic (X.certPubKey . X.signedObject $ X.getSigned leaf, []) >>= C.pubKey
_ -> throwError "bad certificate"
pubKey <- C.verifyX509 serverKey exact
C.x509ToPublic (pubKey, []) >>= C.pubKey
-- consider how to process slow responses - is it handled somehow locally or delegated to the caller
-- this method is used in the client
-- sends PFWD :: C.PublicKeyX25519 -> EncTransmission -> Command Sender
-- receives PRES :: EncResponse -> BrokerMsg -- proxy to client
proxySMPMessage ::
SMPClient ->
-- proxy session from PKEY
SessionId ->
VersionSMP ->
C.PublicKeyX25519 ->
-- message to deliver
Maybe SndPrivateAuthKey ->
SenderId ->
MsgFlags ->
MsgBody ->
ExceptT SMPClientError IO ()
-- TODO use version
proxySMPMessage c@ProtocolClient {thParams = proxyThParams, client_ = PClient {clientCorrId = g}} sessionId _v serverKey spKey sId flags msg = do
-- prepare params
let serverThAuth = (\ta -> ta {serverPeerPubKey = serverKey}) <$> thAuth proxyThParams
serverThParams = proxyThParams {sessionId, thAuth = serverThAuth}
(cmdPubKey, cmdPrivKey) <- liftIO . atomically $ C.generateKeyPair @'C.X25519 g
let cmdSecret = C.dh' serverKey cmdPrivKey
nonce@(C.CbNonce corrId) <- liftIO . atomically $ C.randomCbNonce g
-- encode
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth serverThParams (CorrId corrId, sId, Cmd SSender $ SEND flags msg)
auth <- liftEitherWith PCETransportError $ authTransmission serverThAuth spKey nonce tForAuth
b <- case batchTransmissions (batch serverThParams) (blockSize serverThParams) [Right (auth, tToSend)] of
[] -> throwE $ PCETransportError TELargeMsg -- some other error. Internal?
TBError e _ : _ -> throwE $ PCETransportError e -- large message error?
TBTransmission s _ : _ -> pure s
TBTransmissions s _ _ : _ -> pure s
et <- liftEitherWith PCECryptoError $ EncTransmission <$> C.cbEncrypt cmdSecret nonce b paddedProxiedMsgLength
sendProtocolCommand_ c (Just nonce) Nothing sessionId (Cmd SProxiedClient (PFWD cmdPubKey et)) >>= \case
-- TODO support PKEY + resend?
PRES (EncResponse er) -> do
t' <- liftEitherWith PCECryptoError $ C.cbDecrypt cmdSecret (C.reverseNonce nonce) er
case tParse proxyThParams t' of
t'' :| [] -> case tDecodeParseValidate proxyThParams t'' of
(_auth, _signed, (_c, _e, r)) -> case r of -- TODO: verify
Left e -> throwE $ PCEResponseError e
Right OK -> pure ()
Right (ERR e) -> throwE $ PCEProtocolError e
Right u -> throwE . PCEUnexpectedResponse $ bshow u -- possibly differentiate unexpected response from server/proxy
_ -> throwE $ PCETransportError TEBadBlock
r -> throwE . PCEUnexpectedResponse $ bshow r -- from proxy
-- this method is used in the proxy
-- sends RFWD :: EncFwdTransmission -> Command Sender
-- receives RRES :: EncFwdResponse -> BrokerMsg
-- proxy should send PRES to the client with EncResponse
forwardSMPMessage :: SMPClient -> CorrId -> C.PublicKeyX25519 -> EncTransmission -> ExceptT SMPClientError IO EncResponse
forwardSMPMessage c@ProtocolClient {thParams, client_ = PClient {clientCorrId = g}} fwdCorrId fwdKey fwdTransmission = do
-- prepare params
sessSecret <- case thAuth thParams of
Nothing -> throwError $ PCEProtocolError INTERNAL -- different error - proxy didn't pass key?
Just THAuthClient {sessSecret} -> maybe (throwError $ PCEProtocolError INTERNAL) pure sessSecret
nonce <- liftIO . atomically $ C.randomCbNonce g
-- wrap
let fwdT = FwdTransmission {fwdCorrId, fwdKey, fwdTransmission}
eft <- liftEitherWith PCECryptoError $ EncFwdTransmission <$> C.cbEncrypt sessSecret nonce (smpEncode fwdT) paddedForwardedMsgLength
-- send
sendProtocolCommand_ c (Just nonce) Nothing "" (Cmd SSender (RFWD eft)) >>= \case
RRES (EncFwdResponse efr) -> do
-- unwrap
r' <- liftEitherWith PCECryptoError $ C.cbDecrypt sessSecret (C.reverseNonce nonce) efr
FwdResponse {fwdCorrId = _, fwdResponse} <- liftEitherWith (const $ PCEResponseError BLOCK) $ smpDecode r'
pure fwdResponse
r -> throwE . PCEUnexpectedResponse $ bshow r
okSMPCommand :: PartyI p => Command p -> SMPClient -> C.APrivateAuthKey -> QueueId -> ExceptT SMPClientError IO ()
okSMPCommand cmd c pKey qId =
sendSMPCommand c (Just pKey) qId cmd >>= \case
OK -> return ()
r -> throwE . PCEUnexpectedResponse $ bshow r
okSMPCommands :: PartyI p => Command p -> SMPClient -> NonEmpty (C.APrivateSignKey, QueueId) -> IO (NonEmpty (Either SMPClientError ()))
okSMPCommands :: PartyI p => Command p -> SMPClient -> NonEmpty (C.APrivateAuthKey, QueueId) -> IO (NonEmpty (Either SMPClientError ()))
okSMPCommands cmd c qs = L.map process <$> sendProtocolCommands c cs
where
aCmd = Cmd sParty cmd
@@ -626,15 +759,16 @@ okSMPCommands cmd c qs = L.map process <$> sendProtocolCommands c cs
Left e -> Left e
-- | Send SMP command
sendSMPCommand :: PartyI p => SMPClient -> Maybe C.APrivateSignKey -> QueueId -> Command p -> ExceptT SMPClientError IO BrokerMsg
sendSMPCommand :: PartyI p => SMPClient -> Maybe C.APrivateAuthKey -> QueueId -> Command p -> ExceptT SMPClientError IO BrokerMsg
sendSMPCommand c pKey qId cmd = sendProtocolCommand c pKey qId (Cmd sParty cmd)
{-# INLINE sendSMPCommand #-}
type PCTransmission err msg = (SentRawTransmission, Request err msg)
type PCTransmission err msg = (Either TransportError SentRawTransmission, Request err msg)
-- | Send multiple commands with batching and collect responses
sendProtocolCommands :: forall err msg. ProtocolEncoding err (ProtoCommand msg) => ProtocolClient err msg -> NonEmpty (ClientCommand msg) -> IO (NonEmpty (Response err msg))
sendProtocolCommands c@ProtocolClient {batch, blockSize} cs = do
bs <- batchClientTransmissions batch blockSize <$> mapM (mkTransmission c) cs
sendProtocolCommands :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> NonEmpty (ClientCommand msg) -> IO (NonEmpty (Response err msg))
sendProtocolCommands c@ProtocolClient {thParams = THandleParams {batch, blockSize}} cs = do
bs <- batchTransmissions' batch blockSize <$> mapM (mkTransmission c) cs
validate . concat =<< mapM (sendBatch c) bs
where
validate :: [Response err msg] -> IO (NonEmpty (Response err msg))
@@ -649,103 +783,86 @@ sendProtocolCommands c@ProtocolClient {batch, blockSize} cs = do
where
diff = L.length cs - length rs
streamProtocolCommands :: forall err msg. ProtocolEncoding err (ProtoCommand msg) => ProtocolClient err msg -> NonEmpty (ClientCommand msg) -> ([Response err msg] -> IO ()) -> IO ()
streamProtocolCommands c@ProtocolClient {batch, blockSize} cs cb = do
bs <- batchClientTransmissions batch blockSize <$> mapM (mkTransmission c) cs
streamProtocolCommands :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> NonEmpty (ClientCommand msg) -> ([Response err msg] -> IO ()) -> IO ()
streamProtocolCommands c@ProtocolClient {thParams = THandleParams {batch, blockSize}} cs cb = do
bs <- batchTransmissions' batch blockSize <$> mapM (mkTransmission c) cs
mapM_ (cb <=< sendBatch c) bs
sendBatch :: ProtocolClient err msg -> ClientBatch err msg -> IO [Response err msg]
sendBatch :: ProtocolClient v err msg -> TransportBatch (Request err msg) -> IO [Response err msg]
sendBatch c@ProtocolClient {client_ = PClient {sndQ}} b = do
case b of
CBLargeTransmission Request {entityId} -> do
TBError e Request {entityId} -> do
putStrLn "send error: large message"
pure [Response entityId $ Left $ PCETransportError TELargeMsg]
CBTransmissions s n rs -> do
when (n > 0) $ atomically $ writeTBQueue sndQ $ tEncodeBatch n s
mapConcurrently (getResponse c) rs
CBTransmission s r -> do
pure [Response entityId $ Left $ PCETransportError e]
TBTransmissions s n rs
| n > 0 -> do
atomically $ writeTBQueue sndQ s
mapConcurrently (getResponse c) rs
| otherwise -> pure []
TBTransmission s r -> do
atomically $ writeTBQueue sndQ s
(: []) <$> getResponse c r
data ClientBatch err msg
= -- ByteString in CBTransmissions does not include count byte, it is added by tEncodeBatch
CBTransmissions ByteString Int [Request err msg]
| CBTransmission ByteString (Request err msg)
| CBLargeTransmission (Request err msg)
-- | encodes and batches transmissions into blocks
batchClientTransmissions :: forall err msg. Bool -> Int -> NonEmpty (PCTransmission err msg) -> [ClientBatch err msg]
batchClientTransmissions batch blkSize
| batch = reverse . mkBatch []
| otherwise = map mkBatch1 . L.toList
where
mkBatch :: [ClientBatch err msg] -> NonEmpty (PCTransmission err msg) -> [ClientBatch err msg]
mkBatch bs ts =
let (b, ts_) = encodeBatch "" 0 [] ts
bs' = b : bs
in maybe bs' (mkBatch bs') ts_
mkBatch1 :: PCTransmission err msg -> ClientBatch err msg
mkBatch1 (t, r)
| B.length s <= blkSize - 2 = CBTransmission s r
| otherwise = CBLargeTransmission r
where
s = tEncode t
encodeBatch :: ByteString -> Int -> [Request err msg] -> NonEmpty (PCTransmission err msg) -> (ClientBatch err msg, Maybe (NonEmpty (PCTransmission err msg)))
encodeBatch s n rs ts@((t, r) :| ts_)
| B.length s' <= blkSize - 3 && n < 255 =
case L.nonEmpty ts_ of
Just ts' -> encodeBatch s' n' rs' ts'
Nothing -> (CBTransmissions s' n' (reverse rs'), Nothing)
| n == 0 = (CBLargeTransmission r, L.nonEmpty ts_)
| otherwise = (CBTransmissions s n (reverse rs), Just ts)
where
s' = s <> smpEncode (Large $ tEncode t)
n' = n + 1
rs' = r : rs
-- | Send Protocol command
sendProtocolCommand :: forall err msg. ProtocolEncoding err (ProtoCommand msg) => ProtocolClient err msg -> Maybe C.APrivateSignKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg
sendProtocolCommand c@ProtocolClient {client_ = PClient {sndQ}, batch, blockSize} pKey entId cmd =
ExceptT $ uncurry sendRecv =<< mkTransmission c (pKey, entId, cmd)
sendProtocolCommand :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg
sendProtocolCommand c = sendProtocolCommand_ c Nothing
sendProtocolCommand_ :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> Maybe C.CbNonce -> Maybe C.APrivateAuthKey -> EntityId -> ProtoCommand msg -> ExceptT (ProtocolClientError err) IO msg
sendProtocolCommand_ c@ProtocolClient {client_ = PClient {sndQ}, thParams = THandleParams {batch, blockSize}} nonce_ pKey entId cmd =
ExceptT $ uncurry sendRecv =<< mkTransmission_ c nonce_ (pKey, entId, cmd)
where
-- two separate "atomically" needed to avoid blocking
sendRecv :: SentRawTransmission -> Request err msg -> IO (Either (ProtocolClientError err) msg)
sendRecv t r
| B.length s > blockSize - 2 = pure $ Left $ PCETransportError TELargeMsg
| otherwise = atomically (writeTBQueue sndQ s) >> response <$> getResponse c r
where
s
| batch = tEncodeBatch 1 . smpEncode . Large $ tEncode t
| otherwise = tEncode t
sendRecv :: Either TransportError SentRawTransmission -> Request err msg -> IO (Either (ProtocolClientError err) msg)
sendRecv t_ r = case t_ of
Left e -> pure . Left $ PCETransportError e
Right t
| B.length s > blockSize - 2 -> pure . Left $ PCETransportError TELargeMsg
| otherwise -> atomically (writeTBQueue sndQ s) >> response <$> getResponse c r
where
s
| batch = tEncodeBatch1 t
| otherwise = tEncode t
-- TODO switch to timeout or TimeManager that supports Int64
getResponse :: ProtocolClient err msg -> Request err msg -> IO (Response err msg)
getResponse :: ProtocolClient v err msg -> Request err msg -> IO (Response err msg)
getResponse ProtocolClient {client_ = PClient {tcpTimeout, pingErrorCount}} Request {entityId, responseVar} = do
response <-
timeout tcpTimeout (atomically (takeTMVar responseVar)) >>= \case
-- BTW: another registerDelay candidate. Also, crashes caller with BlockedIndef.
Just r -> atomically (writeTVar pingErrorCount 0) $> r
Nothing -> pure $ Left PCEResponseTimeout
pure Response {entityId, response}
mkTransmission :: forall err msg. ProtocolEncoding err (ProtoCommand msg) => ProtocolClient err msg -> ClientCommand msg -> IO (PCTransmission err msg)
mkTransmission ProtocolClient {sessionId, thVersion, client_ = PClient {clientCorrId, sentCommands}} (pKey, entId, cmd) = do
corrId <- atomically getNextCorrId
let t = signTransmission $ encodeTransmission thVersion sessionId (corrId, entId, cmd)
r <- atomically $ mkRequest corrId
pure (t, r)
mkTransmission :: ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> ClientCommand msg -> IO (PCTransmission err msg)
mkTransmission c = mkTransmission_ c Nothing
mkTransmission_ :: forall v err msg. ProtocolEncoding v err (ProtoCommand msg) => ProtocolClient v err msg -> Maybe C.CbNonce -> ClientCommand msg -> IO (PCTransmission err msg)
mkTransmission_ ProtocolClient {thParams, client_ = PClient {clientCorrId, sentCommands}} nonce_ (pKey_, entId, cmd) = do
nonce@(C.CbNonce corrId) <- maybe (atomically $ C.randomCbNonce clientCorrId) pure nonce_
let TransmissionForAuth {tForAuth, tToSend} = encodeTransmissionForAuth thParams (CorrId corrId, entId, cmd)
auth = authTransmission (thAuth thParams) pKey_ nonce tForAuth
r <- atomically $ mkRequest (CorrId corrId)
pure ((,tToSend) <$> auth, r)
where
getNextCorrId :: STM CorrId
getNextCorrId = do
i <- stateTVar clientCorrId $ \i -> (i, i + 1)
pure . CorrId $ bshow i
signTransmission :: ByteString -> SentRawTransmission
signTransmission t = ((`C.sign` t) <$> pKey, t)
mkRequest :: CorrId -> STM (Request err msg)
mkRequest corrId = do
r <- Request entId <$> newEmptyTMVar
TM.insert corrId r sentCommands
pure r
authTransmission :: Maybe (THandleAuth 'TClient) -> Maybe C.APrivateAuthKey -> C.CbNonce -> ByteString -> Either TransportError (Maybe TransmissionAuth)
authTransmission thAuth pKey_ nonce t = traverse authenticate pKey_
where
authenticate :: C.APrivateAuthKey -> Either TransportError TransmissionAuth
authenticate (C.APrivateAuthKey a pk) = case a of
C.SX25519 -> case thAuth of
Just THAuthClient {serverPeerPubKey = k} -> Right $ TAAuthenticator $ C.cbAuthenticate k pk nonce t
Nothing -> Left TENoServerAuth
C.SEd25519 -> sign pk
C.SEd448 -> sign pk
sign :: forall a. (C.AlgorithmI a, C.SignatureAlgorithm a) => C.PrivateKey a -> Either TransportError TransmissionAuth
sign pk = Right $ TASignature $ C.ASignature (C.sAlgorithm @a) (C.sign' pk t)
$(J.deriveJSON (enumJSON $ dropPrefix "HM") ''HostMode)
$(J.deriveJSON (enumJSON $ dropPrefix "TSM") ''TransportSessionMode)
+101 -66
View File
@@ -1,4 +1,5 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
@@ -18,6 +19,8 @@ import Control.Monad
import Control.Monad.Except
import Control.Monad.IO.Unlift
import Control.Monad.Trans.Except
import Control.Monad.Trans.Reader
import Crypto.Random (ChaChaDRG)
import Data.Bifunctor (bimap, first)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
@@ -36,7 +39,8 @@ import Simplex.Messaging.Agent.RetryInterval
import Simplex.Messaging.Client
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (BrokerMsg, NotifierId, NtfPrivateSignKey, ProtocolServer (..), QueueId, RcvPrivateSignKey, RecipientId, SMPServer)
import Simplex.Messaging.Protocol (BrokerMsg, NotifierId, NtfPrivateAuthKey, ProtocolServer (..), QueueId, RcvPrivateAuthKey, RecipientId, SMPServer)
import Simplex.Messaging.Session
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport
@@ -47,7 +51,7 @@ import UnliftIO.Exception (Exception)
import qualified UnliftIO.Exception as E
import UnliftIO.STM
type SMPClientVar = TMVar (Either SMPClientError SMPClient)
type SMPClientVar = SessionVar (Either SMPClientError SMPClient)
data SMPClientAgentEvent
= CAConnected SMPServer
@@ -64,7 +68,7 @@ type SMPSub = (SMPSubParty, QueueId)
-- type SMPServerSub = (SMPServer, SMPSub)
data SMPClientAgentConfig = SMPClientAgentConfig
{ smpCfg :: ProtocolClientConfig,
{ smpCfg :: ProtocolClientConfig SMPVersion,
reconnectInterval :: RetryInterval,
msgQSize :: Natural,
agentQSize :: Natural,
@@ -74,7 +78,7 @@ data SMPClientAgentConfig = SMPClientAgentConfig
defaultSMPClientAgentConfig :: SMPClientAgentConfig
defaultSMPClientAgentConfig =
SMPClientAgentConfig
{ smpCfg = defaultClientConfig {defaultTransport = ("5223", transport @TLS)},
{ smpCfg = defaultSMPClientConfig {defaultTransport = ("5223", transport @TLS)},
reconnectInterval =
RetryInterval
{ initialInterval = second,
@@ -90,13 +94,16 @@ defaultSMPClientAgentConfig =
data SMPClientAgent = SMPClientAgent
{ agentCfg :: SMPClientAgentConfig,
msgQ :: TBQueue (ServerTransmission BrokerMsg),
msgQ :: TBQueue (ServerTransmission SMPVersion BrokerMsg),
agentQ :: TBQueue SMPClientAgentEvent,
randomDrg :: TVar ChaChaDRG,
smpClients :: TMap SMPServer SMPClientVar,
srvSubs :: TMap SMPServer (TMap SMPSub C.APrivateSignKey),
pendingSrvSubs :: TMap SMPServer (TMap SMPSub C.APrivateSignKey),
smpSessions :: TMap SessionId SMPClient,
srvSubs :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey),
pendingSrvSubs :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey),
reconnections :: TVar [Async ()],
asyncClients :: TVar [Async ()]
asyncClients :: TVar [Async ()],
workerSeq :: TVar Int
}
newtype InternalException e = InternalException {unInternalException :: e}
@@ -104,83 +111,108 @@ newtype InternalException e = InternalException {unInternalException :: e}
instance Exception e => Exception (InternalException e)
instance (MonadUnliftIO m, Exception e) => MonadUnliftIO (ExceptT e m) where
withRunInIO :: ((forall a. ExceptT e m a -> IO a) -> IO b) -> ExceptT e m b
withRunInIO exceptToIO =
instance Exception e => MonadUnliftIO (ExceptT e IO) where
{-# INLINE withRunInIO #-}
withRunInIO :: ((forall a. ExceptT e IO a -> IO a) -> IO b) -> ExceptT e IO b
withRunInIO inner =
ExceptT . fmap (first unInternalException) . E.try $
withRunInIO $ \run ->
inner $ run . (either (E.throwIO . InternalException) pure <=< runExceptT)
-- as MonadUnliftIO instance for IO is `withRunInIO inner = inner id`,
-- the last two lines could be replaced with:
-- inner $ either (E.throwIO . InternalException) pure <=< runExceptT
instance Exception e => MonadUnliftIO (ExceptT e (ReaderT r IO)) where
{-# INLINE withRunInIO #-}
withRunInIO :: ((forall a. ExceptT e (ReaderT r IO) a -> IO a) -> IO b) -> ExceptT e (ReaderT r IO) b
withRunInIO inner =
withExceptT unInternalException . ExceptT . E.try $
withRunInIO $ \run ->
exceptToIO $ run . (either (E.throwIO . InternalException) return <=< runExceptT)
inner $ run . (either (E.throwIO . InternalException) pure <=< runExceptT)
newSMPClientAgent :: SMPClientAgentConfig -> STM SMPClientAgent
newSMPClientAgent agentCfg@SMPClientAgentConfig {msgQSize, agentQSize} = do
newSMPClientAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> STM SMPClientAgent
newSMPClientAgent agentCfg@SMPClientAgentConfig {msgQSize, agentQSize} randomDrg = do
msgQ <- newTBQueue msgQSize
agentQ <- newTBQueue agentQSize
smpClients <- TM.empty
smpSessions <- TM.empty
srvSubs <- TM.empty
pendingSrvSubs <- TM.empty
reconnections <- newTVar []
asyncClients <- newTVar []
pure SMPClientAgent {agentCfg, msgQ, agentQ, smpClients, srvSubs, pendingSrvSubs, reconnections, asyncClients}
workerSeq <- newTVar 0
pure
SMPClientAgent
{ agentCfg,
msgQ,
agentQ,
randomDrg,
smpClients,
smpSessions,
srvSubs,
pendingSrvSubs,
reconnections,
asyncClients,
workerSeq
}
getSMPServerClient' :: SMPClientAgent -> SMPServer -> ExceptT SMPClientError IO SMPClient
getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, msgQ} srv =
getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, smpSessions, msgQ, randomDrg, workerSeq} srv =
atomically getClientVar >>= either newSMPClient waitForSMPClient
where
getClientVar :: STM (Either SMPClientVar SMPClientVar)
getClientVar = maybe (Left <$> newClientVar) (pure . Right) =<< TM.lookup srv smpClients
newClientVar :: STM SMPClientVar
newClientVar = do
smpVar <- newEmptyTMVar
TM.insert srv smpVar smpClients
pure smpVar
getClientVar = getSessVar workerSeq srv smpClients
waitForSMPClient :: SMPClientVar -> ExceptT SMPClientError IO SMPClient
waitForSMPClient smpVar = do
waitForSMPClient v = do
let ProtocolClientConfig {networkConfig = NetworkConfig {tcpConnectTimeout}} = smpCfg agentCfg
smpClient_ <- liftIO $ tcpConnectTimeout `timeout` atomically (readTMVar smpVar)
smpClient_ <- liftIO $ tcpConnectTimeout `timeout` atomically (readTMVar $ sessionVar v)
liftEither $ case smpClient_ of
Just (Right smpClient) -> Right smpClient
Just (Left e) -> Left e
Nothing -> Left PCEResponseTimeout
newSMPClient :: SMPClientVar -> ExceptT SMPClientError IO SMPClient
newSMPClient smpVar = tryConnectClient pure tryConnectAsync
newSMPClient v = tryConnectClient pure (liftIO tryConnectAsync)
where
tryConnectClient :: (SMPClient -> ExceptT SMPClientError IO a) -> ExceptT SMPClientError IO () -> ExceptT SMPClientError IO a
tryConnectClient successAction retryAction =
tryE connectClient >>= \r -> case r of
tryE (connectClient v) >>= \r -> case r of
Right smp -> do
logInfo . decodeUtf8 $ "Agent connected to " <> showServer srv
atomically $ putTMVar smpVar r
atomically $ do
putTMVar (sessionVar v) r
TM.insert (sessionId $ thParams smp) smp smpSessions
successAction smp
Left e -> do
if e == PCENetworkError || e == PCEResponseTimeout
then retryAction
else atomically $ do
putTMVar smpVar (Left e)
TM.delete srv smpClients
putTMVar (sessionVar v) (Left e)
removeSessVar v srv smpClients
throwE e
tryConnectAsync :: ExceptT SMPClientError IO ()
tryConnectAsync :: IO ()
tryConnectAsync = do
a <- async connectAsync
a <- async $ void $ runExceptT connectAsync
atomically $ modifyTVar' (asyncClients ca) (a :)
connectAsync :: ExceptT SMPClientError IO ()
connectAsync =
withRetryInterval (reconnectInterval agentCfg) $ \_ loop ->
void $ tryConnectClient (const reconnectClient) loop
connectClient :: ExceptT SMPClientError IO SMPClient
connectClient = ExceptT $ getProtocolClient (1, srv, Nothing) (smpCfg agentCfg) (Just msgQ) clientDisconnected
connectClient :: SMPClientVar -> ExceptT SMPClientError IO SMPClient
connectClient v = ExceptT $ getProtocolClient randomDrg (1, srv, Nothing) (smpCfg agentCfg) (Just msgQ) (clientDisconnected v)
clientDisconnected :: SMPClient -> IO ()
clientDisconnected _ = do
removeClientAndSubs >>= (`forM_` serverDown)
clientDisconnected :: SMPClientVar -> SMPClient -> IO ()
clientDisconnected v smp = do
removeClientAndSubs v smp >>= (`forM_` serverDown)
logInfo . decodeUtf8 $ "Agent disconnected from " <> showServer srv
removeClientAndSubs :: IO (Maybe (Map SMPSub C.APrivateSignKey))
removeClientAndSubs = atomically $ do
TM.delete srv smpClients
removeClientAndSubs :: SMPClientVar -> SMPClient -> IO (Maybe (Map SMPSub C.APrivateAuthKey))
removeClientAndSubs v smp = atomically $ do
removeSessVar v srv smpClients
TM.delete (sessionId $ thParams smp) smpSessions
TM.lookupDelete srv (srvSubs ca) >>= mapM updateSubs
where
updateSubs sVar = do
@@ -191,17 +223,17 @@ getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, msgQ} srv =
addPendingSubs sVar ss = do
let ps = pendingSrvSubs ca
TM.lookup srv ps >>= \case
Just v -> TM.union ss v
Just ss' -> TM.union ss ss'
_ -> TM.insert srv sVar ps
serverDown :: Map SMPSub C.APrivateSignKey -> IO ()
serverDown :: Map SMPSub C.APrivateAuthKey -> IO ()
serverDown ss = unless (M.null ss) $ do
notify . CADisconnected srv $ M.keysSet ss
void $ runExceptT reconnectServer
reconnectServer
reconnectServer :: ExceptT SMPClientError IO ()
reconnectServer :: IO ()
reconnectServer = do
a <- async tryReconnectClient
a <- async $ void $ runExceptT tryReconnectClient
atomically $ modifyTVar' (reconnections ca) (a :)
tryReconnectClient :: ExceptT SMPClientError IO ()
@@ -224,15 +256,15 @@ getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, msgQ} srv =
SPNotifier -> True
SPRecipient -> False
subscribe_ :: SMPClient -> SMPSubParty -> [(SMPSub, C.APrivateSignKey)] -> ExceptT SMPClientError IO ()
subscribe_ :: SMPClient -> SMPSubParty -> [(SMPSub, C.APrivateAuthKey)] -> ExceptT SMPClientError IO ()
subscribe_ smp party = mapM_ subscribeBatch . toChunks (agentSubsBatchSize agentCfg)
where
subscribeBatch subs' = do
let subs'' :: (NonEmpty (QueueId, C.APrivateSignKey)) = L.map (first snd) subs'
let subs'' :: (NonEmpty (QueueId, C.APrivateAuthKey)) = L.map (first snd) subs'
rs <- liftIO $ smpSubscribeQueues party ca smp srv subs''
let rs' :: (NonEmpty ((SMPSub, C.APrivateSignKey), Either SMPClientError ())) =
let rs' :: (NonEmpty ((SMPSub, C.APrivateAuthKey), Either SMPClientError ())) =
L.zipWith (first . const) subs' rs
rs'' :: [Either (SMPSub, SMPClientError) (SMPSub, C.APrivateSignKey)] =
rs'' :: [Either (SMPSub, SMPClientError) (SMPSub, C.APrivateAuthKey)] =
map (\(sub, r) -> bimap (fst sub,) (const sub) r) $ L.toList rs'
(errs, oks) = partitionEithers rs''
(tempErrs, finalErrs) = partition (temporaryClientError . snd) errs
@@ -245,17 +277,20 @@ getSMPServerClient' ca@SMPClientAgent {agentCfg, smpClients, msgQ} srv =
notify :: SMPClientAgentEvent -> IO ()
notify evt = atomically $ writeTBQueue (agentQ ca) evt
closeSMPClientAgent :: MonadUnliftIO m => SMPClientAgent -> m ()
closeSMPClientAgent c = liftIO $ do
lookupSMPServerClient :: SMPClientAgent -> SessionId -> STM (Maybe SMPClient)
lookupSMPServerClient SMPClientAgent {smpSessions} sessId = TM.lookup sessId smpSessions
closeSMPClientAgent :: SMPClientAgent -> IO ()
closeSMPClientAgent c = do
closeSMPServerClients c
cancelActions $ reconnections c
cancelActions $ asyncClients c
closeSMPServerClients :: SMPClientAgent -> IO ()
closeSMPServerClients c = readTVarIO (smpClients c) >>= mapM_ (forkIO . closeClient)
closeSMPServerClients c = atomically (smpClients c `swapTVar` M.empty) >>= mapM_ (forkIO . closeClient)
where
closeClient smpVar =
atomically (readTMVar smpVar) >>= \case
closeClient v =
atomically (readTMVar $ sessionVar v) >>= \case
Right smp -> closeProtocolClient smp `catchAll_` pure ()
_ -> pure ()
@@ -270,7 +305,7 @@ withSMP ca srv action = (getSMPServerClient' ca srv >>= action) `catchE` logSMPE
liftIO $ putStrLn $ "SMP error (" <> show srv <> "): " <> show e
throwE e
subscribeQueue :: SMPClientAgent -> SMPServer -> (SMPSub, C.APrivateSignKey) -> ExceptT SMPClientError IO ()
subscribeQueue :: SMPClientAgent -> SMPServer -> (SMPSub, C.APrivateAuthKey) -> ExceptT SMPClientError IO ()
subscribeQueue ca srv sub = do
atomically $ addPendingSubscription ca srv sub
withSMP ca srv $ \smp -> subscribe_ smp `catchE` handleErr
@@ -284,20 +319,20 @@ subscribeQueue ca srv sub = do
removePendingSubscription ca srv (fst sub)
throwE e
subscribeQueuesSMP :: SMPClientAgent -> SMPServer -> NonEmpty (RecipientId, RcvPrivateSignKey) -> IO (NonEmpty (RecipientId, Either SMPClientError ()))
subscribeQueuesSMP :: SMPClientAgent -> SMPServer -> NonEmpty (RecipientId, RcvPrivateAuthKey) -> IO (NonEmpty (RecipientId, Either SMPClientError ()))
subscribeQueuesSMP = subscribeQueues_ SPRecipient
subscribeQueuesNtfs :: SMPClientAgent -> SMPServer -> NonEmpty (NotifierId, NtfPrivateSignKey) -> IO (NonEmpty (NotifierId, Either SMPClientError ()))
subscribeQueuesNtfs :: SMPClientAgent -> SMPServer -> NonEmpty (NotifierId, NtfPrivateAuthKey) -> IO (NonEmpty (NotifierId, Either SMPClientError ()))
subscribeQueuesNtfs = subscribeQueues_ SPNotifier
subscribeQueues_ :: SMPSubParty -> SMPClientAgent -> SMPServer -> NonEmpty (QueueId, C.APrivateSignKey) -> IO (NonEmpty (QueueId, Either SMPClientError ()))
subscribeQueues_ :: SMPSubParty -> SMPClientAgent -> SMPServer -> NonEmpty (QueueId, C.APrivateAuthKey) -> IO (NonEmpty (QueueId, Either SMPClientError ()))
subscribeQueues_ party ca srv subs = do
atomically $ forM_ subs $ addPendingSubscription ca srv . first (party,)
runExceptT (getSMPServerClient' ca srv) >>= \case
Left e -> pure $ L.map ((,Left e) . fst) subs
Right smp -> smpSubscribeQueues party ca smp srv subs
smpSubscribeQueues :: SMPSubParty -> SMPClientAgent -> SMPClient -> SMPServer -> NonEmpty (QueueId, C.APrivateSignKey) -> IO (NonEmpty (QueueId, Either SMPClientError ()))
smpSubscribeQueues :: SMPSubParty -> SMPClientAgent -> SMPClient -> SMPServer -> NonEmpty (QueueId, C.APrivateAuthKey) -> IO (NonEmpty (QueueId, Either SMPClientError ()))
smpSubscribeQueues party ca smp srv subs = do
rs <- L.zip subs <$> subscribe smp (L.map swap subs)
atomically $ forM rs $ \(sub, r) ->
@@ -318,22 +353,22 @@ showServer :: SMPServer -> ByteString
showServer ProtocolServer {host, port} =
strEncode host <> B.pack (if null port then "" else ':' : port)
smpSubscribe :: SMPClient -> (SMPSub, C.APrivateSignKey) -> ExceptT SMPClientError IO ()
smpSubscribe :: SMPClient -> (SMPSub, C.APrivateAuthKey) -> ExceptT SMPClientError IO ()
smpSubscribe smp ((party, queueId), privKey) = subscribe_ smp privKey queueId
where
subscribe_ = case party of
SPRecipient -> subscribeSMPQueue
SPNotifier -> subscribeSMPQueueNotifications
addSubscription :: SMPClientAgent -> SMPServer -> (SMPSub, C.APrivateSignKey) -> STM ()
addSubscription :: SMPClientAgent -> SMPServer -> (SMPSub, C.APrivateAuthKey) -> STM ()
addSubscription ca srv sub = do
addSub_ (srvSubs ca) srv sub
removePendingSubscription ca srv $ fst sub
addPendingSubscription :: SMPClientAgent -> SMPServer -> (SMPSub, C.APrivateSignKey) -> STM ()
addPendingSubscription :: SMPClientAgent -> SMPServer -> (SMPSub, C.APrivateAuthKey) -> STM ()
addPendingSubscription = addSub_ . pendingSrvSubs
addSub_ :: TMap SMPServer (TMap SMPSub C.APrivateSignKey) -> SMPServer -> (SMPSub, C.APrivateSignKey) -> STM ()
addSub_ :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey) -> SMPServer -> (SMPSub, C.APrivateAuthKey) -> STM ()
addSub_ subs srv (s, key) =
TM.lookup srv subs >>= \case
Just m -> TM.insert s key m
@@ -345,11 +380,11 @@ removeSubscription = removeSub_ . srvSubs
removePendingSubscription :: SMPClientAgent -> SMPServer -> SMPSub -> STM ()
removePendingSubscription = removeSub_ . pendingSrvSubs
removeSub_ :: TMap SMPServer (TMap SMPSub C.APrivateSignKey) -> SMPServer -> SMPSub -> STM ()
removeSub_ :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey) -> SMPServer -> SMPSub -> STM ()
removeSub_ subs srv s = TM.lookup srv subs >>= mapM_ (TM.delete s)
getSubKey :: TMap SMPServer (TMap SMPSub C.APrivateSignKey) -> SMPServer -> SMPSub -> STM (Maybe C.APrivateSignKey)
getSubKey :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey) -> SMPServer -> SMPSub -> STM (Maybe C.APrivateAuthKey)
getSubKey subs srv s = TM.lookup srv subs $>>= TM.lookup s
hasSub :: TMap SMPServer (TMap SMPSub C.APrivateSignKey) -> SMPServer -> SMPSub -> STM Bool
hasSub :: TMap SMPServer (TMap SMPSub C.APrivateAuthKey) -> SMPServer -> SMPSub -> STM Bool
hasSub subs srv s = maybe (pure False) (TM.member s) =<< TM.lookup srv subs
+90
View File
@@ -0,0 +1,90 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Simplex.Messaging.Compression where
import qualified Codec.Compression.Zstd as Z1
import qualified Codec.Compression.Zstd.FFI as Z
import Control.Monad (forM)
import Control.Monad.Except
import Control.Monad.IO.Class
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Unsafe as B
import Data.Either (fromRight)
import Data.List.NonEmpty (NonEmpty)
import Foreign
import Foreign.C.Types
import GHC.IO (unsafePerformIO)
import Simplex.Messaging.Encoding
import UnliftIO.Exception (bracket)
data Compressed
= -- | Short messages are left intact to skip copying and FFI festivities.
Passthrough ByteString
| -- | Generic compression using no extra context.
Compressed Large
-- | Messages below this length are not encoded to avoid compression overhead.
maxLengthPassthrough :: Int
maxLengthPassthrough = 180 -- Sampled from real client data. Messages with length > 180 rapidly gain compression ratio.
compressionLevel :: Num a => a
compressionLevel = 3
instance Encoding Compressed where
smpEncode = \case
Passthrough bytes -> "0" <> smpEncode bytes
Compressed bytes -> "1" <> smpEncode bytes
smpP =
smpP >>= \case
'0' -> Passthrough <$> smpP
'1' -> Compressed <$> smpP
x -> fail $ "unknown Compressed tag: " <> show x
-- | Compress as single chunk using stack-allocated context.
compress1 :: ByteString -> Compressed
compress1 bs
| B.length bs <= maxLengthPassthrough = Passthrough bs
| otherwise = Compressed . Large $ Z1.compress compressionLevel bs
type CompressCtx = (Ptr Z.CCtx, Ptr CChar, CSize)
withCompressCtx :: CSize -> (CompressCtx -> IO a) -> IO a
withCompressCtx scratchSize action =
bracket Z.createCCtx Z.freeCCtx $ \cctx ->
allocaBytes (fromIntegral scratchSize) $ \scratchPtr ->
action (cctx, scratchPtr, scratchSize)
-- | Compress bytes, falling back to Passthrough in case of some internal error.
compress :: CompressCtx -> ByteString -> IO Compressed
compress ctx bs = fromRight (Passthrough bs) <$> compress_ ctx bs
compress_ :: CompressCtx -> ByteString -> IO (Either String Compressed)
compress_ (cctx, scratchPtr, scratchSize) bs
| B.length bs <= maxLengthPassthrough = pure . Right $ Passthrough bs
| otherwise =
B.unsafeUseAsCStringLen bs $ \(sourcePtr, sourceSize) -> runExceptT $ do
-- should not fail, unless input buffer is too short
dstSize <- ExceptT $ Z.checkError $ Z.compressCCtx cctx scratchPtr scratchSize sourcePtr (fromIntegral sourceSize) compressionLevel
liftIO $ Compressed . Large <$> B.packCStringLen (scratchPtr, fromIntegral dstSize)
type DecompressCtx = (Ptr Z.DCtx, Ptr CChar, CSize)
withDecompressCtx :: Int -> (DecompressCtx -> IO a) -> IO a
withDecompressCtx maxUnpackedSize action =
bracket Z.createDCtx Z.freeDCtx $ \dctx ->
allocaBytes maxUnpackedSize $ \scratchPtr ->
action (dctx, scratchPtr, fromIntegral maxUnpackedSize)
decompress :: DecompressCtx -> Compressed -> IO (Either String ByteString)
decompress (dctx, scratchPtr, scratchSize) = \case
Passthrough bs -> pure $ Right bs
Compressed (Large bs) ->
B.unsafeUseAsCStringLen bs $ \(sourcePtr, sourceSize) -> do
res <- Z.checkError $ Z.decompressDCtx dctx scratchPtr scratchSize sourcePtr (fromIntegral sourceSize)
forM res $ \dstSize -> B.packCStringLen (scratchPtr, fromIntegral dstSize)
decompressBatch :: Int -> NonEmpty Compressed -> NonEmpty (Either String ByteString)
decompressBatch maxUnpackedSize items = unsafePerformIO $ withDecompressCtx maxUnpackedSize $ forM items . decompress
{-# NOINLINE decompressBatch #-} -- prevent double-evaluation under unsafePerformIO
+192 -46
View File
@@ -8,6 +8,7 @@
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RankNTypes #-}
@@ -36,7 +37,7 @@ module Simplex.Messaging.Crypto
Algorithm (..),
SAlgorithm (..),
Alg (..),
SignAlg (..),
AuthAlg (..),
DhAlg (..),
DhAlgorithm,
PrivateKey (..),
@@ -53,23 +54,32 @@ module Simplex.Messaging.Crypto
APublicVerifyKey (..),
APrivateDhKey (..),
APublicDhKey (..),
APrivateAuthKey (..),
APublicAuthKey (..),
CryptoPublicKey (..),
CryptoPrivateKey (..),
AAuthKeyPair,
KeyPair,
KeyPairX25519,
ASignatureKeyPair,
DhSecret (..),
DhSecretX25519,
ADhSecret (..),
KeyHash (..),
newRandom,
newRandomDRG,
generateAKeyPair,
generateKeyPair,
generateSignatureKeyPair,
generateAuthKeyPair,
generateDhKeyPair,
privateToX509,
x509ToPublic,
x509ToPrivate,
publicKey,
signatureKeyPair,
publicToX509,
encodeASNObj,
-- * key encoding/decoding
encodePubKey,
@@ -84,12 +94,20 @@ module Simplex.Messaging.Crypto
CryptoSignature (..),
SignatureSize (..),
SignatureAlgorithm,
AuthAlgorithm,
AlgorithmI (..),
sign,
sign',
verify,
verify',
validSignatureSize,
checkAlgorithm,
-- * crypto_box authenticator, as discussed in https://groups.google.com/g/sci.crypt/c/73yb5a9pz2Y/m/LNgRO7IYXOwJ
CbAuthenticator (..),
cbAuthenticatorSize,
cbAuthenticate,
cbVerify,
-- * DH derivation
dh',
@@ -115,12 +133,15 @@ module Simplex.Messaging.Crypto
CbNonce (unCbNonce),
pattern CbNonce,
cbEncrypt,
cbEncryptNoPad,
cbEncryptMaxLenBS,
cbDecrypt,
cbDecryptNoPad,
sbDecrypt_,
sbEncrypt_,
cbNonce,
randomCbNonce,
reverseNonce,
-- * NaCl crypto_secretbox
SbKey (unSbKey),
@@ -147,10 +168,13 @@ module Simplex.Messaging.Crypto
Certificate,
signCertificate,
signX509,
verifyX509,
certificateFingerprint,
signedFingerprint,
SignatureAlgorithmX509 (..),
SignedObject (..),
encodeCertChain,
certChainP,
-- * Cryptography error type
CryptoError (..),
@@ -173,7 +197,7 @@ import Crypto.Cipher.AES (AES256)
import qualified Crypto.Cipher.Types as AES
import qualified Crypto.Cipher.XSalsa as XSalsa
import qualified Crypto.Error as CE
import Crypto.Hash (Digest, SHA256 (..), SHA512, hash)
import Crypto.Hash (Digest, SHA256 (..), SHA512 (..), hash, hashDigestSize)
import qualified Crypto.MAC.Poly1305 as Poly1305
import qualified Crypto.PubKey.Curve25519 as X25519
import qualified Crypto.PubKey.Curve448 as X448
@@ -195,6 +219,7 @@ import qualified Data.ByteString.Char8 as B
import Data.ByteString.Lazy (fromStrict, toStrict)
import Data.Constraint (Dict (..))
import Data.Kind (Constraint, Type)
import qualified Data.List.NonEmpty as L
import Data.String
import Data.Type.Equality
import Data.Typeable (Proxy (Proxy), Typeable)
@@ -220,16 +245,14 @@ data SAlgorithm :: Algorithm -> Type where
SX25519 :: SAlgorithm X25519
SX448 :: SAlgorithm X448
deriving instance Eq (SAlgorithm a)
deriving instance Show (SAlgorithm a)
data Alg = forall a. AlgorithmI a => Alg (SAlgorithm a)
data SignAlg
data AuthAlg
= forall a.
(AlgorithmI a, SignatureAlgorithm a) =>
SignAlg (SAlgorithm a)
(AlgorithmI a, AuthAlgorithm a) =>
AuthAlg (SAlgorithm a)
data DhAlg
= forall a.
@@ -274,10 +297,11 @@ data APublicKey
AlgorithmI a =>
APublicKey (SAlgorithm a) (PublicKey a)
instance Eq APublicKey where
APublicKey a k == APublicKey a' k' = case testEquality a a' of
Just Refl -> k == k'
Nothing -> False
instance Encoding APublicKey where
smpEncode = smpEncode . encodePubKey
{-# INLINE smpEncode #-}
smpDecode = decodePubKey
{-# INLINE smpDecode #-}
deriving instance Show APublicKey
@@ -313,11 +337,6 @@ data APrivateKey
AlgorithmI a =>
APrivateKey (SAlgorithm a) (PrivateKey a)
instance Eq APrivateKey where
APrivateKey a k == APrivateKey a' k' = case testEquality a a' of
Just Refl -> k == k'
Nothing -> False
deriving instance Show APrivateKey
type PrivateKeyEd25519 = PrivateKey Ed25519
@@ -343,11 +362,6 @@ data APrivateSignKey
(AlgorithmI a, SignatureAlgorithm a) =>
APrivateSignKey (SAlgorithm a) (PrivateKey a)
instance Eq APrivateSignKey where
APrivateSignKey a k == APrivateSignKey a' k' = case testEquality a a' of
Just Refl -> k == k'
Nothing -> False
deriving instance Show APrivateSignKey
instance Encoding APrivateSignKey where
@@ -367,11 +381,6 @@ data APublicVerifyKey
(AlgorithmI a, SignatureAlgorithm a) =>
APublicVerifyKey (SAlgorithm a) (PublicKey a)
instance Eq APublicVerifyKey where
APublicVerifyKey a k == APublicVerifyKey a' k' = case testEquality a a' of
Just Refl -> k == k'
Nothing -> False
deriving instance Show APublicVerifyKey
data APrivateDhKey
@@ -379,11 +388,6 @@ data APrivateDhKey
(AlgorithmI a, DhAlgorithm a) =>
APrivateDhKey (SAlgorithm a) (PrivateKey a)
instance Eq APrivateDhKey where
APrivateDhKey a k == APrivateDhKey a' k' = case testEquality a a' of
Just Refl -> k == k'
Nothing -> False
deriving instance Show APrivateDhKey
data APublicDhKey
@@ -391,11 +395,6 @@ data APublicDhKey
(AlgorithmI a, DhAlgorithm a) =>
APublicDhKey (SAlgorithm a) (PublicKey a)
instance Eq APublicDhKey where
APublicDhKey a k == APublicDhKey a' k' = case testEquality a a' of
Just Refl -> k == k'
Nothing -> False
deriving instance Show APublicDhKey
data DhSecret (a :: Algorithm) where
@@ -425,6 +424,57 @@ dhAlgorithm = \case
SX448 -> Just Dict
_ -> Nothing
data APrivateAuthKey
= forall a.
(AlgorithmI a, AuthAlgorithm a) =>
APrivateAuthKey (SAlgorithm a) (PrivateKey a)
instance Eq APrivateAuthKey where
APrivateAuthKey a k == APrivateAuthKey a' k' = case testEquality a a' of
Just Refl -> k == k'
Nothing -> False
deriving instance Show APrivateAuthKey
instance Encoding APrivateAuthKey where
smpEncode = smpEncode . encodePrivKey
{-# INLINE smpEncode #-}
smpDecode = decodePrivKey
{-# INLINE smpDecode #-}
instance StrEncoding APrivateAuthKey where
strEncode = strEncode . encodePrivKey
{-# INLINE strEncode #-}
strDecode = decodePrivKey
{-# INLINE strDecode #-}
data APublicAuthKey
= forall a.
(AlgorithmI a, AuthAlgorithm a) =>
APublicAuthKey (SAlgorithm a) (PublicKey a)
instance Eq APublicAuthKey where
APublicAuthKey a k == APublicAuthKey a' k' = case testEquality a a' of
Just Refl -> k == k'
Nothing -> False
deriving instance Show APublicAuthKey
-- either X25519 or Ed algorithm that can be used to authorize commands to SMP server
type family AuthAlgorithm (a :: Algorithm) :: Constraint where
AuthAlgorithm Ed25519 = ()
AuthAlgorithm Ed448 = ()
AuthAlgorithm X25519 = ()
AuthAlgorithm a =
(Int ~ Bool, TypeError (Text "Algorithm " :<>: ShowType a :<>: Text " cannot be used for authorization"))
authAlgorithm :: SAlgorithm a -> Maybe (Dict (AuthAlgorithm a))
authAlgorithm = \case
SEd25519 -> Just Dict
SEd448 -> Just Dict
SX25519 -> Just Dict
_ -> Nothing
dhBytes' :: DhSecret a -> ByteString
dhBytes' = \case
DhSecretX25519 s -> BA.convert s
@@ -464,6 +514,12 @@ instance CryptoPublicKey APublicVerifyKey where
Just Dict -> Right $ APublicVerifyKey a k
_ -> Left "key does not support signature algorithms"
instance CryptoPublicKey APublicAuthKey where
toPubKey f (APublicAuthKey _ k) = f k
pubKey (APublicKey a k) = case authAlgorithm a of
Just Dict -> Right $ APublicAuthKey a k
_ -> Left "key does not support auth algorithms"
instance CryptoPublicKey APublicDhKey where
toPubKey f (APublicDhKey _ k) = f k
pubKey (APublicKey a k) = case dhAlgorithm a of
@@ -480,6 +536,12 @@ instance Encoding APublicVerifyKey where
smpDecode = decodePubKey
{-# INLINE smpDecode #-}
instance Encoding APublicAuthKey where
smpEncode = smpEncode . encodePubKey
{-# INLINE smpEncode #-}
smpDecode = decodePubKey
{-# INLINE smpDecode #-}
instance Encoding APublicDhKey where
smpEncode = smpEncode . encodePubKey
{-# INLINE smpEncode #-}
@@ -498,6 +560,12 @@ instance StrEncoding APublicVerifyKey where
strDecode = decodePubKey
{-# INLINE strDecode #-}
instance StrEncoding APublicAuthKey where
strEncode = strEncode . encodePubKey
{-# INLINE strEncode #-}
strDecode = decodePubKey
{-# INLINE strDecode #-}
instance StrEncoding APublicDhKey where
strEncode = strEncode . encodePubKey
{-# INLINE strEncode #-}
@@ -545,6 +613,13 @@ instance CryptoPrivateKey APrivateSignKey where
Just Dict -> Right $ APrivateSignKey a k
_ -> Left "key does not support signature algorithms"
instance CryptoPrivateKey APrivateAuthKey where
type PublicKeyType APrivateAuthKey = APublicAuthKey
toPrivKey f (APrivateAuthKey _ k) = f k
privKey (APrivateKey a k) = case authAlgorithm a of
Just Dict -> Right $ APrivateAuthKey a k
_ -> Left "key does not support auth algorithms"
instance CryptoPrivateKey APrivateDhKey where
type PublicKeyType APrivateDhKey = APublicDhKey
toPrivKey f (APrivateDhKey _ k) = f k
@@ -588,21 +663,32 @@ type KeyPairType pk = (PublicKeyType pk, pk)
type KeyPair a = KeyPairType (PrivateKey a)
type KeyPairX25519 = KeyPair X25519
-- TODO narrow key pair types to have the same algorithm in both keys
type AKeyPair = KeyPairType APrivateKey
type ASignatureKeyPair = KeyPairType APrivateSignKey
type ADhKeyPair = KeyPairType APrivateDhKey
type AAuthKeyPair = KeyPairType APrivateAuthKey
newRandom :: IO (TVar ChaChaDRG)
newRandom = newTVarIO =<< drgNew
newRandomDRG :: TVar ChaChaDRG -> STM (TVar ChaChaDRG)
newRandomDRG g = newTVar =<< stateTVar g (`withDRG` drgNew)
generateAKeyPair :: AlgorithmI a => SAlgorithm a -> TVar ChaChaDRG -> STM AKeyPair
generateAKeyPair a g = bimap (APublicKey a) (APrivateKey a) <$> generateKeyPair g
generateSignatureKeyPair :: (AlgorithmI a, SignatureAlgorithm a) => SAlgorithm a -> TVar ChaChaDRG -> STM ASignatureKeyPair
generateSignatureKeyPair a g = bimap (APublicVerifyKey a) (APrivateSignKey a) <$> generateKeyPair g
generateAuthKeyPair :: (AlgorithmI a, AuthAlgorithm a) => SAlgorithm a -> TVar ChaChaDRG -> STM AAuthKeyPair
generateAuthKeyPair a g = bimap (APublicAuthKey a) (APrivateAuthKey a) <$> generateKeyPair g
generateDhKeyPair :: (AlgorithmI a, DhAlgorithm a) => SAlgorithm a -> TVar ChaChaDRG -> STM ADhKeyPair
generateDhKeyPair a g = bimap (APublicDhKey a) (APrivateDhKey a) <$> generateKeyPair g
@@ -632,6 +718,10 @@ instance ToField APrivateSignKey where toField = toField . encodePrivKey
instance ToField APublicVerifyKey where toField = toField . encodePubKey
instance ToField APrivateAuthKey where toField = toField . encodePrivKey
instance ToField APublicAuthKey where toField = toField . encodePubKey
instance ToField APrivateDhKey where toField = toField . encodePrivKey
instance ToField APublicDhKey where toField = toField . encodePubKey
@@ -646,6 +736,10 @@ instance FromField APrivateSignKey where fromField = blobFieldDecoder decodePriv
instance FromField APublicVerifyKey where fromField = blobFieldDecoder decodePubKey
instance FromField APrivateAuthKey where fromField = blobFieldDecoder decodePrivKey
instance FromField APublicAuthKey where fromField = blobFieldDecoder decodePubKey
instance FromField APrivateDhKey where fromField = blobFieldDecoder decodePrivKey
instance FromField APublicDhKey where fromField = blobFieldDecoder decodePubKey
@@ -656,7 +750,7 @@ instance (Typeable a, AlgorithmI a) => FromField (PublicKey a) where fromField =
instance (Typeable a, AlgorithmI a) => FromField (DhSecret a) where fromField = blobFieldDecoder strDecode
instance IsString (Maybe ASignature) where
instance IsString ASignature where
fromString = parseString $ decode >=> decodeSignature
data Signature (a :: Algorithm) where
@@ -672,11 +766,6 @@ data ASignature
(AlgorithmI a, SignatureAlgorithm a) =>
ASignature (SAlgorithm a) (Signature a)
instance Eq ASignature where
ASignature a s == ASignature a' s' = case testEquality a a' of
Just Refl -> s == s'
_ -> False
deriving instance Show ASignature
class CryptoSignature s where
@@ -761,6 +850,8 @@ data CryptoError
CryptoHeaderError String
| -- | no sending chain key in ratchet state
CERatchetState
| -- | no decapsulation key in ratchet state
CERatchetKEMState
| -- | header decryption error (could indicate that another key should be tried)
CERatchetHeader
| -- | too many skipped messages
@@ -1021,6 +1112,18 @@ signX509 key = fst . objectToSignedExact f
signatureAlgorithmX509 key,
()
)
{-# INLINE signX509 #-}
verifyX509 :: (ASN1Object o, Eq o, Show o) => APublicVerifyKey -> SignedExact o -> Either String o
verifyX509 key exact = do
signature <- case signedAlg of
SignatureALG_IntrinsicHash PubKeyALG_Ed25519 -> ASignature SEd25519 <$> decodeSignature signedSignature
SignatureALG_IntrinsicHash PubKeyALG_Ed448 -> ASignature SEd448 <$> decodeSignature signedSignature
_ -> Left "unknown x509 signature algorithm"
if verify key signature $ getSignedData exact then Right signedObject else Left "bad signature"
where
Signed {signedObject, signedAlg, signedSignature} = getSigned exact
{-# INLINE verifyX509 #-}
certificateFingerprint :: SignedCertificate -> KeyHash
certificateFingerprint = signedFingerprint
@@ -1049,7 +1152,7 @@ instance SignatureAlgorithmX509 pk => SignatureAlgorithmX509 (a, pk) where
signatureAlgorithmX509 = signatureAlgorithmX509 . snd
-- | A wrapper to marshall signed ASN1 objects, like certificates.
newtype SignedObject a = SignedObject (SignedExact a)
newtype SignedObject a = SignedObject {getSignedExact :: SignedExact a}
instance (Typeable a, Eq a, Show a, ASN1Object a) => FromField (SignedObject a) where
fromField = fmap SignedObject . blobFieldDecoder decodeSignedObject
@@ -1057,6 +1160,20 @@ instance (Typeable a, Eq a, Show a, ASN1Object a) => FromField (SignedObject a)
instance (Eq a, Show a, ASN1Object a) => ToField (SignedObject a) where
toField (SignedObject s) = toField $ encodeSignedObject s
instance (Eq a, Show a, ASN1Object a) => Encoding (SignedObject a) where
smpEncode (SignedObject exact) = smpEncode . Large $ encodeSignedObject exact
smpP = fmap SignedObject . decodeSignedObject . unLarge <$?> smpP
encodeCertChain :: CertificateChain -> L.NonEmpty Large
encodeCertChain cc = L.fromList $ map Large blobs
where
CertificateChainRaw blobs = encodeCertificateChain cc
certChainP :: A.Parser CertificateChain
certChainP = do
rawChain <- CertificateChainRaw . map unLarge . L.toList <$> smpP
either (fail . show) pure $ decodeCertificateChain rawChain
-- | Signature verification.
--
-- Used by SMP servers to authorize SMP commands and by SMP agents to verify messages.
@@ -1073,10 +1190,14 @@ dh' :: DhAlgorithm a => PublicKey a -> PrivateKey a -> DhSecret a
dh' (PublicKeyX25519 k) (PrivateKeyX25519 pk _) = DhSecretX25519 $ X25519.dh k pk
dh' (PublicKeyX448 k) (PrivateKeyX448 pk _) = DhSecretX448 $ X448.dh k pk
-- | NaCl @crypto_box@ encrypt with a shared DH secret and 192-bit nonce.
-- | NaCl @crypto_box@ encrypt with padding with a shared DH secret and 192-bit nonce.
cbEncrypt :: DhSecret X25519 -> CbNonce -> ByteString -> Int -> Either CryptoError ByteString
cbEncrypt (DhSecretX25519 secret) = sbEncrypt_ secret
-- | NaCl @crypto_box@ encrypt with a shared DH secret and 192-bit nonce (without padding).
cbEncryptNoPad :: DhSecret X25519 -> CbNonce -> ByteString -> ByteString
cbEncryptNoPad (DhSecretX25519 secret) (CbNonce nonce) = cryptoBox secret nonce
-- | NaCl @secret_box@ encrypt with a symmetric 256-bit key and 192-bit nonce.
sbEncrypt :: SbKey -> CbNonce -> ByteString -> Int -> Either CryptoError ByteString
sbEncrypt (SbKey key) = sbEncrypt_ key
@@ -1098,21 +1219,43 @@ cryptoBox secret nonce s = BA.convert tag <> c
cbDecrypt :: DhSecret X25519 -> CbNonce -> ByteString -> Either CryptoError ByteString
cbDecrypt (DhSecretX25519 secret) = sbDecrypt_ secret
-- | NaCl @crypto_box@ decrypt with a shared DH secret and 192-bit nonce (without unpadding).
cbDecryptNoPad :: DhSecret X25519 -> CbNonce -> ByteString -> Either CryptoError ByteString
cbDecryptNoPad (DhSecretX25519 secret) = sbDecryptNoPad_ secret
-- | NaCl @secret_box@ decrypt with a symmetric 256-bit key and 192-bit nonce.
sbDecrypt :: SbKey -> CbNonce -> ByteString -> Either CryptoError ByteString
sbDecrypt (SbKey key) = sbDecrypt_ key
-- | NaCl @crypto_box@ decrypt with a shared DH secret and 192-bit nonce.
sbDecrypt_ :: ByteArrayAccess key => key -> CbNonce -> ByteString -> Either CryptoError ByteString
sbDecrypt_ secret (CbNonce nonce) packet
sbDecrypt_ secret nonce = unPad <=< sbDecryptNoPad_ secret nonce
-- | NaCl @crypto_box@ decrypt with a shared DH secret and 192-bit nonce (without unpadding).
sbDecryptNoPad_ :: ByteArrayAccess key => key -> CbNonce -> ByteString -> Either CryptoError ByteString
sbDecryptNoPad_ secret (CbNonce nonce) packet
| B.length packet < 16 = Left CBDecryptError
| BA.constEq tag' tag = unPad msg
| BA.constEq tag' tag = Right msg
| otherwise = Left CBDecryptError
where
(tag', c) = B.splitAt 16 packet
(rs, msg) = xSalsa20 secret nonce c
tag = Poly1305.auth rs c
-- type for authentication scheme using NaCl @crypto_box@ over the sha512 digest of the message.
newtype CbAuthenticator = CbAuthenticator ByteString deriving (Eq, Show)
cbAuthenticatorSize :: Int
cbAuthenticatorSize = hashDigestSize SHA512 + authTagSize -- 64 + 16 = 80 bytes
-- create crypto_box authenticator for a message.
cbAuthenticate :: PublicKeyX25519 -> PrivateKeyX25519 -> CbNonce -> ByteString -> CbAuthenticator
cbAuthenticate k pk nonce msg = CbAuthenticator $ cbEncryptNoPad (dh' k pk) nonce (sha512Hash msg)
-- verify crypto_box authenticator for a message.
cbVerify :: PublicKeyX25519 -> PrivateKeyX25519 -> CbNonce -> CbAuthenticator -> ByteString -> Bool
cbVerify k pk nonce (CbAuthenticator s) authorized = cbDecryptNoPad (dh' k pk) nonce s == Right (sha512Hash authorized)
newtype CbNonce = CryptoBoxNonce {unCbNonce :: ByteString}
deriving (Eq, Show)
@@ -1150,6 +1293,9 @@ randomCbNonce = fmap CryptoBoxNonce . randomBytes 24
randomBytes :: Int -> TVar ChaChaDRG -> STM ByteString
randomBytes n gVar = stateTVar gVar $ randomBytesGenerate n
reverseNonce :: CbNonce -> CbNonce
reverseNonce (CryptoBoxNonce s) = CryptoBoxNonce (B.reverse s)
instance Encoding CbNonce where
smpEncode = unCbNonce
smpP = CryptoBoxNonce <$> A.take 24
+1 -1
View File
@@ -76,7 +76,7 @@ withFile :: CryptoFile -> IOMode -> (CryptoFileHandle -> ExceptT FTCryptoError I
withFile (CryptoFile path cfArgs) mode action = do
sb <- forM cfArgs $ \(CFArgs key nonce) ->
liftEitherWith FTCECryptoError (LC.sbInit key nonce) >>= newTVarIO
IO.withFile path mode $ \h -> action $ CFHandle h sb
ExceptT . IO.withFile path mode $ \h -> runExceptT $ action $ CFHandle h sb
hPut :: CryptoFileHandle -> LazyByteString -> IO ()
hPut (CFHandle h sb_) s = LB.hPut h =<< maybe (pure s) encrypt sb_
+1 -9
View File
@@ -41,7 +41,6 @@ import qualified Crypto.MAC.Poly1305 as Poly1305
import Data.Bifunctor (first)
import Data.ByteArray (ByteArrayAccess)
import qualified Data.ByteArray as BA
import qualified Data.ByteString as S
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
@@ -175,7 +174,7 @@ secretBoxTailTag sbProcess secret nonce msg = run <$> sbInit_ secret nonce
-- passes lazy bytestring via initialized secret box returning the reversed list of chunks
secretBoxLazy_ :: (SbState -> ByteString -> (ByteString, SbState)) -> SbState -> LazyByteString -> ([ByteString], SbState)
secretBoxLazy_ sbProcess state = foldlChunks update ([], state)
secretBoxLazy_ sbProcess state = LB.foldlChunks update ([], state)
where
update (cs, st) chunk = let (!c, !st') = sbProcess st chunk in (c : cs, st')
@@ -231,10 +230,3 @@ cryptoPassed :: CE.CryptoFailable b -> Either CryptoError b
cryptoPassed = \case
CE.CryptoPassed a -> Right a
CE.CryptoFailed e -> Left $ CryptoPoly1305Error e
foldlChunks :: (a -> S.ByteString -> a) -> a -> LazyByteString -> a
foldlChunks f = go
where
go !a LB.Empty = a
go !a (LB.Chunk c cs) = go (f a c) cs
{-# INLINE foldlChunks #-}
File diff suppressed because it is too large Load Diff
@@ -19,16 +19,20 @@ import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
newtype KEMPublicKey = KEMPublicKey ByteString
deriving (Show)
deriving (Eq, Show)
newtype KEMSecretKey = KEMSecretKey ScrubbedBytes
deriving (Show)
deriving (Eq, Show)
newtype KEMCiphertext = KEMCiphertext ByteString
deriving (Show)
deriving (Eq, Show)
newtype KEMSharedKey = KEMSharedKey ScrubbedBytes
deriving (Show)
deriving (Eq, Show)
unsafeRevealKEMSharedKey :: KEMSharedKey -> String
unsafeRevealKEMSharedKey (KEMSharedKey scrubbed) = show (BA.convert scrubbed :: ByteString)
{-# DEPRECATED unsafeRevealKEMSharedKey "unsafeRevealKEMSharedKey left in code" #-}
type KEMKeyPair = (KEMPublicKey, KEMSecretKey)
@@ -60,6 +64,18 @@ sntrup761Dec (KEMCiphertext c) (KEMSecretKey sk) =
KEMSharedKey
<$> BA.alloc c_SNTRUP761_SIZE (\kPtr -> c_sntrup761_dec kPtr cPtr skPtr)
instance Encoding KEMSecretKey where
smpEncode (KEMSecretKey c) = smpEncode . Large $ BA.convert c
smpP = KEMSecretKey . BA.convert . unLarge <$> smpP
instance StrEncoding KEMSecretKey where
strEncode (KEMSecretKey pk) = strEncode (BA.convert pk :: ByteString)
strP = KEMSecretKey . BA.convert <$> strP @ByteString
instance Encoding KEMPublicKey where
smpEncode (KEMPublicKey pk) = smpEncode . Large $ BA.convert pk
smpP = KEMPublicKey . BA.convert . unLarge <$> smpP
instance StrEncoding KEMPublicKey where
strEncode (KEMPublicKey pk) = strEncode (BA.convert pk :: ByteString)
strP = KEMPublicKey . BA.convert <$> strP @ByteString
@@ -68,6 +84,25 @@ instance Encoding KEMCiphertext where
smpEncode (KEMCiphertext c) = smpEncode . Large $ BA.convert c
smpP = KEMCiphertext . BA.convert . unLarge <$> smpP
instance Encoding KEMSharedKey where
smpEncode (KEMSharedKey c) = smpEncode (BA.convert c :: ByteString)
smpP = KEMSharedKey . BA.convert <$> smpP @ByteString
instance StrEncoding KEMCiphertext where
strEncode (KEMCiphertext pk) = strEncode (BA.convert pk :: ByteString)
strP = KEMCiphertext . BA.convert <$> strP @ByteString
instance StrEncoding KEMSharedKey where
strEncode (KEMSharedKey pk) = strEncode (BA.convert pk :: ByteString)
strP = KEMSharedKey . BA.convert <$> strP @ByteString
instance ToJSON KEMSecretKey where
toJSON = strToJSON
toEncoding = strToJEncoding
instance FromJSON KEMSecretKey where
parseJSON = strParseJSON "KEMSecretKey"
instance ToJSON KEMPublicKey where
toJSON = strToJSON
toEncoding = strToJEncoding
@@ -75,8 +110,22 @@ instance ToJSON KEMPublicKey where
instance FromJSON KEMPublicKey where
parseJSON = strParseJSON "KEMPublicKey"
instance ToJSON KEMCiphertext where
toJSON = strToJSON
toEncoding = strToJEncoding
instance FromJSON KEMCiphertext where
parseJSON = strParseJSON "KEMCiphertext"
instance ToField KEMSharedKey where
toField (KEMSharedKey k) = toField (BA.convert k :: ByteString)
instance FromField KEMSharedKey where
fromField f = KEMSharedKey . BA.convert @ByteString <$> fromField f
instance ToJSON KEMSharedKey where
toJSON = strToJSON
toEncoding = strToJEncoding
instance FromJSON KEMSharedKey where
parseJSON = strParseJSON "KEMSharedKey"
+7 -7
View File
@@ -110,7 +110,7 @@ lenP = fromIntegral . c2w <$> A.anyChar
{-# INLINE lenP #-}
instance Encoding a => Encoding (Maybe a) where
smpEncode = maybe "0" (("1" <>) . smpEncode)
smpEncode = maybe "0" (('1' `B.cons`) . smpEncode)
{-# INLINE smpEncode #-}
smpP =
smpP >>= \case
@@ -174,37 +174,37 @@ instance (Encoding a, Encoding b) => Encoding (a, b) where
{-# INLINE smpP #-}
instance (Encoding a, Encoding b, Encoding c) => Encoding (a, b, c) where
smpEncode (a, b, c) = smpEncode a <> smpEncode b <> smpEncode c
smpEncode (a, b, c) = B.concat [smpEncode a, smpEncode b, smpEncode c]
{-# INLINE smpEncode #-}
smpP = (,,) <$> smpP <*> smpP <*> smpP
{-# INLINE smpP #-}
instance (Encoding a, Encoding b, Encoding c, Encoding d) => Encoding (a, b, c, d) where
smpEncode (a, b, c, d) = smpEncode a <> smpEncode b <> smpEncode c <> smpEncode d
smpEncode (a, b, c, d) = B.concat [smpEncode a, smpEncode b, smpEncode c, smpEncode d]
{-# INLINE smpEncode #-}
smpP = (,,,) <$> smpP <*> smpP <*> smpP <*> smpP
{-# INLINE smpP #-}
instance (Encoding a, Encoding b, Encoding c, Encoding d, Encoding e) => Encoding (a, b, c, d, e) where
smpEncode (a, b, c, d, e) = smpEncode a <> smpEncode b <> smpEncode c <> smpEncode d <> smpEncode e
smpEncode (a, b, c, d, e) = B.concat [smpEncode a, smpEncode b, smpEncode c, smpEncode d, smpEncode e]
{-# INLINE smpEncode #-}
smpP = (,,,,) <$> smpP <*> smpP <*> smpP <*> smpP <*> smpP
{-# INLINE smpP #-}
instance (Encoding a, Encoding b, Encoding c, Encoding d, Encoding e, Encoding f) => Encoding (a, b, c, d, e, f) where
smpEncode (a, b, c, d, e, f) = smpEncode a <> smpEncode b <> smpEncode c <> smpEncode d <> smpEncode e <> smpEncode f
smpEncode (a, b, c, d, e, f) = B.concat [smpEncode a, smpEncode b, smpEncode c, smpEncode d, smpEncode e, smpEncode f]
{-# INLINE smpEncode #-}
smpP = (,,,,,) <$> smpP <*> smpP <*> smpP <*> smpP <*> smpP <*> smpP
{-# INLINE smpP #-}
instance (Encoding a, Encoding b, Encoding c, Encoding d, Encoding e, Encoding f, Encoding g) => Encoding (a, b, c, d, e, f, g) where
smpEncode (a, b, c, d, e, f, g) = smpEncode a <> smpEncode b <> smpEncode c <> smpEncode d <> smpEncode e <> smpEncode f <> smpEncode g
smpEncode (a, b, c, d, e, f, g) = B.concat [smpEncode a, smpEncode b, smpEncode c, smpEncode d, smpEncode e, smpEncode f, smpEncode g]
{-# INLINE smpEncode #-}
smpP = (,,,,,,) <$> smpP <*> smpP <*> smpP <*> smpP <*> smpP <*> smpP <*> smpP
{-# INLINE smpP #-}
instance (Encoding a, Encoding b, Encoding c, Encoding d, Encoding e, Encoding f, Encoding g, Encoding h) => Encoding (a, b, c, d, e, f, g, h) where
smpEncode (a, b, c, d, e, f, g, h) = smpEncode a <> smpEncode b <> smpEncode c <> smpEncode d <> smpEncode e <> smpEncode f <> smpEncode g <> smpEncode h
smpEncode (a, b, c, d, e, f, g, h) = B.concat [smpEncode a, smpEncode b, smpEncode c, smpEncode d, smpEncode e, smpEncode f, smpEncode g, smpEncode h]
{-# INLINE smpEncode #-}
smpP = (,,,,,,,) <$> smpP <*> smpP <*> smpP <*> smpP <*> smpP <*> smpP <*> smpP <*> smpP
{-# INLINE smpP #-}
+8
View File
@@ -75,6 +75,8 @@ instance StrEncoding Str where
strEncode = unStr
strP = Str <$> A.takeTill (== ' ') <* optional A.space
-- inherited from ByteString, the parser only allows non-empty strings
-- only Char8 elements may round-trip as B.pack truncates unicode
instance StrEncoding String where
strEncode = strEncode . B.pack
strP = B.unpack <$> strP
@@ -179,6 +181,12 @@ instance (StrEncoding a, StrEncoding b, StrEncoding c, StrEncoding d, StrEncodin
strP = (,,,,) <$> strP_ <*> strP_ <*> strP_ <*> strP_ <*> strP
{-# INLINE strP #-}
instance (StrEncoding a, StrEncoding b, StrEncoding c, StrEncoding d, StrEncoding e, StrEncoding f) => StrEncoding (a, b, c, d, e, f) where
strEncode (a, b, c, d, e, f) = B.unwords [strEncode a, strEncode b, strEncode c, strEncode d, strEncode e, strEncode f]
{-# INLINE strEncode #-}
strP = (,,,,,) <$> strP_ <*> strP_ <*> strP_ <*> strP_ <*> strP_ <*> strP
{-# INLINE strP #-}
strP_ :: StrEncoding a => Parser a
strP_ = strP <* A.space
+16 -12
View File
@@ -10,57 +10,61 @@ import Data.Word (Word16)
import Simplex.Messaging.Client
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Notifications.Protocol
import Simplex.Messaging.Notifications.Transport (NTFVersion, supportedClientNTFVRange)
import Simplex.Messaging.Protocol (ErrorType)
import Simplex.Messaging.Util (bshow)
type NtfClient = ProtocolClient ErrorType NtfResponse
type NtfClient = ProtocolClient NTFVersion ErrorType NtfResponse
type NtfClientError = ProtocolClientError ErrorType
ntfRegisterToken :: NtfClient -> C.APrivateSignKey -> NewNtfEntity 'Token -> ExceptT NtfClientError IO (NtfTokenId, C.PublicKeyX25519)
defaultNTFClientConfig :: ProtocolClientConfig NTFVersion
defaultNTFClientConfig = defaultClientConfig supportedClientNTFVRange
ntfRegisterToken :: NtfClient -> C.APrivateAuthKey -> NewNtfEntity 'Token -> ExceptT NtfClientError IO (NtfTokenId, C.PublicKeyX25519)
ntfRegisterToken c pKey newTkn =
sendNtfCommand c (Just pKey) "" (TNEW newTkn) >>= \case
NRTknId tknId dhKey -> pure (tknId, dhKey)
r -> throwE . PCEUnexpectedResponse $ bshow r
ntfVerifyToken :: NtfClient -> C.APrivateSignKey -> NtfTokenId -> NtfRegCode -> ExceptT NtfClientError IO ()
ntfVerifyToken :: NtfClient -> C.APrivateAuthKey -> NtfTokenId -> NtfRegCode -> ExceptT NtfClientError IO ()
ntfVerifyToken c pKey tknId code = okNtfCommand (TVFY code) c pKey tknId
ntfCheckToken :: NtfClient -> C.APrivateSignKey -> NtfTokenId -> ExceptT NtfClientError IO NtfTknStatus
ntfCheckToken :: NtfClient -> C.APrivateAuthKey -> NtfTokenId -> ExceptT NtfClientError IO NtfTknStatus
ntfCheckToken c pKey tknId =
sendNtfCommand c (Just pKey) tknId TCHK >>= \case
NRTkn stat -> pure stat
r -> throwE . PCEUnexpectedResponse $ bshow r
ntfReplaceToken :: NtfClient -> C.APrivateSignKey -> NtfTokenId -> DeviceToken -> ExceptT NtfClientError IO ()
ntfReplaceToken :: NtfClient -> C.APrivateAuthKey -> NtfTokenId -> DeviceToken -> ExceptT NtfClientError IO ()
ntfReplaceToken c pKey tknId token = okNtfCommand (TRPL token) c pKey tknId
ntfDeleteToken :: NtfClient -> C.APrivateSignKey -> NtfTokenId -> ExceptT NtfClientError IO ()
ntfDeleteToken :: NtfClient -> C.APrivateAuthKey -> NtfTokenId -> ExceptT NtfClientError IO ()
ntfDeleteToken = okNtfCommand TDEL
ntfEnableCron :: NtfClient -> C.APrivateSignKey -> NtfTokenId -> Word16 -> ExceptT NtfClientError IO ()
ntfEnableCron :: NtfClient -> C.APrivateAuthKey -> NtfTokenId -> Word16 -> ExceptT NtfClientError IO ()
ntfEnableCron c pKey tknId int = okNtfCommand (TCRN int) c pKey tknId
ntfCreateSubscription :: NtfClient -> C.APrivateSignKey -> NewNtfEntity 'Subscription -> ExceptT NtfClientError IO NtfSubscriptionId
ntfCreateSubscription :: NtfClient -> C.APrivateAuthKey -> NewNtfEntity 'Subscription -> ExceptT NtfClientError IO NtfSubscriptionId
ntfCreateSubscription c pKey newSub =
sendNtfCommand c (Just pKey) "" (SNEW newSub) >>= \case
NRSubId subId -> pure subId
r -> throwE . PCEUnexpectedResponse $ bshow r
ntfCheckSubscription :: NtfClient -> C.APrivateSignKey -> NtfSubscriptionId -> ExceptT NtfClientError IO NtfSubStatus
ntfCheckSubscription :: NtfClient -> C.APrivateAuthKey -> NtfSubscriptionId -> ExceptT NtfClientError IO NtfSubStatus
ntfCheckSubscription c pKey subId =
sendNtfCommand c (Just pKey) subId SCHK >>= \case
NRSub stat -> pure stat
r -> throwE . PCEUnexpectedResponse $ bshow r
ntfDeleteSubscription :: NtfClient -> C.APrivateSignKey -> NtfSubscriptionId -> ExceptT NtfClientError IO ()
ntfDeleteSubscription :: NtfClient -> C.APrivateAuthKey -> NtfSubscriptionId -> ExceptT NtfClientError IO ()
ntfDeleteSubscription = okNtfCommand SDEL
-- | Send notification server command
sendNtfCommand :: NtfEntityI e => NtfClient -> Maybe C.APrivateSignKey -> NtfEntityId -> NtfCommand e -> ExceptT NtfClientError IO NtfResponse
sendNtfCommand :: NtfEntityI e => NtfClient -> Maybe C.APrivateAuthKey -> NtfEntityId -> NtfCommand e -> ExceptT NtfClientError IO NtfResponse
sendNtfCommand c pKey entId cmd = sendProtocolCommand c pKey entId (NtfCmd sNtfEntity cmd)
okNtfCommand :: NtfEntityI e => NtfCommand e -> NtfClient -> C.APrivateSignKey -> NtfEntityId -> ExceptT NtfClientError IO ()
okNtfCommand :: NtfEntityI e => NtfCommand e -> NtfClient -> C.APrivateAuthKey -> NtfEntityId -> ExceptT NtfClientError IO ()
okNtfCommand cmd c pKey entId =
sendNtfCommand c (Just pKey) entId cmd >>= \case
NROk -> return ()
+29 -17
View File
@@ -11,12 +11,14 @@
module Simplex.Messaging.Notifications.Protocol where
import Control.Applicative ((<|>))
import Data.Aeson (FromJSON (..), ToJSON (..), (.:), (.=))
import qualified Data.Aeson as J
import qualified Data.Aeson.Encoding as JE
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Functor (($>))
import Data.Kind
import Data.Maybe (isNothing)
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
@@ -28,7 +30,7 @@ import Simplex.Messaging.Agent.Protocol (updateSMPServerHosts)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Notifications.Transport (ntfClientHandshake)
import Simplex.Messaging.Notifications.Transport (NTFVersion, ntfClientHandshake)
import Simplex.Messaging.Parsers (fromTextField_)
import Simplex.Messaging.Protocol hiding (Command (..), CommandTag (..))
import Simplex.Messaging.Util (eitherToMaybe, (<$?>))
@@ -124,8 +126,8 @@ instance ToJSON NtfRegCode where
toEncoding = strToJEncoding
data NewNtfEntity (e :: NtfEntity) where
NewNtfTkn :: DeviceToken -> C.APublicVerifyKey -> C.PublicKeyX25519 -> NewNtfEntity 'Token
NewNtfSub :: NtfTokenId -> SMPQueueNtf -> NtfPrivateSignKey -> NewNtfEntity 'Subscription
NewNtfTkn :: DeviceToken -> NtfPublicAuthKey -> C.PublicKeyX25519 -> NewNtfEntity 'Token
NewNtfSub :: NtfTokenId -> SMPQueueNtf -> NtfPrivateAuthKey -> NewNtfEntity 'Subscription
deriving instance Show (NewNtfEntity e)
@@ -147,10 +149,10 @@ instance Encoding ANewNtfEntity where
'S' -> ANE SSubscription <$> (NewNtfSub <$> smpP <*> smpP <*> smpP)
_ -> fail "bad ANewNtfEntity"
instance Protocol ErrorType NtfResponse where
instance Protocol NTFVersion ErrorType NtfResponse where
type ProtoCommand NtfResponse = NtfCmd
type ProtoType NtfResponse = 'PNTF
protocolClientHandshake = ntfClientHandshake
protocolClientHandshake c _ks = ntfClientHandshake c
protocolPing = NtfCmd SSubscription PING
protocolError = \case
NRErr e -> Just e
@@ -184,7 +186,7 @@ data NtfCmd = forall e. NtfEntityI e => NtfCmd (SNtfEntity e) (NtfCommand e)
deriving instance Show NtfCmd
instance NtfEntityI e => ProtocolEncoding ErrorType (NtfCommand e) where
instance NtfEntityI e => ProtocolEncoding NTFVersion ErrorType (NtfCommand e) where
type Tag (NtfCommand e) = NtfCommandTag e
encodeProtocol _v = \case
TNEW newTkn -> e (TNEW_, ' ', newTkn)
@@ -203,27 +205,27 @@ instance NtfEntityI e => ProtocolEncoding ErrorType (NtfCommand e) where
protocolP _v tag = (\(NtfCmd _ c) -> checkEntity c) <$?> protocolP _v (NCT (sNtfEntity @e) tag)
fromProtocolError = fromProtocolError @ErrorType @NtfResponse
fromProtocolError = fromProtocolError @NTFVersion @ErrorType @NtfResponse
{-# INLINE fromProtocolError #-}
checkCredentials (sig, _, entityId, _) cmd = case cmd of
checkCredentials (auth, _, entityId, _) cmd = case cmd of
-- TNEW and SNEW must have signature but NOT token/subscription IDs
TNEW {} -> sigNoEntity
SNEW {} -> sigNoEntity
PING
| isNothing sig && B.null entityId -> Right cmd
| isNothing auth && B.null entityId -> Right cmd
| otherwise -> Left $ CMD HAS_AUTH
-- other client commands must have both signature and entity ID
_
| isNothing sig || B.null entityId -> Left $ CMD NO_AUTH
| isNothing auth || B.null entityId -> Left $ CMD NO_AUTH
| otherwise -> Right cmd
where
sigNoEntity
| isNothing sig = Left $ CMD NO_AUTH
| isNothing auth = Left $ CMD NO_AUTH
| not (B.null entityId) = Left $ CMD HAS_AUTH
| otherwise = Right cmd
instance ProtocolEncoding ErrorType NtfCmd where
instance ProtocolEncoding NTFVersion ErrorType NtfCmd where
type Tag NtfCmd = NtfCmdTag
encodeProtocol _v (NtfCmd _ c) = encodeProtocol _v c
@@ -243,7 +245,7 @@ instance ProtocolEncoding ErrorType NtfCmd where
SDEL_ -> pure SDEL
PING_ -> pure PING
fromProtocolError = fromProtocolError @ErrorType @NtfResponse
fromProtocolError = fromProtocolError @NTFVersion @ErrorType @NtfResponse
{-# INLINE fromProtocolError #-}
checkCredentials t (NtfCmd e c) = NtfCmd e <$> checkCredentials t c
@@ -290,7 +292,7 @@ data NtfResponse
| NRPong
deriving (Show)
instance ProtocolEncoding ErrorType NtfResponse where
instance ProtocolEncoding NTFVersion ErrorType NtfResponse where
type Tag NtfResponse = NtfResponseTag
encodeProtocol _v = \case
NRTknId entId dhKey -> e (NRTknId_, ' ', entId, dhKey)
@@ -358,7 +360,11 @@ instance StrEncoding SMPQueueNtf where
notifierId <- A.char '/' *> strP
pure SMPQueueNtf {smpServer, notifierId}
data PushProvider = PPApnsDev | PPApnsProd | PPApnsTest
data PushProvider
= PPApnsDev -- provider for Apple development environment
| PPApnsProd -- production environment, including TestFlight
| PPApnsTest -- used for tests, to use APNS mock server
| PPApnsNull -- used to test servers from the client - does not communicate with APNS
deriving (Eq, Ord, Show)
instance Encoding PushProvider where
@@ -366,11 +372,13 @@ instance Encoding PushProvider where
PPApnsDev -> "AD"
PPApnsProd -> "AP"
PPApnsTest -> "AT"
PPApnsNull -> "AN"
smpP =
A.take 2 >>= \case
"AD" -> pure PPApnsDev
"AP" -> pure PPApnsProd
"AT" -> pure PPApnsTest
"AN" -> pure PPApnsNull
_ -> fail "bad PushProvider"
instance StrEncoding PushProvider where
@@ -378,11 +386,13 @@ instance StrEncoding PushProvider where
PPApnsDev -> "apns_dev"
PPApnsProd -> "apns_prod"
PPApnsTest -> "apns_test"
PPApnsNull -> "apns_null"
strP =
A.takeTill (== ' ') >>= \case
"apns_dev" -> pure PPApnsDev
"apns_prod" -> pure PPApnsProd
"apns_test" -> pure PPApnsTest
"apns_null" -> pure PPApnsNull
_ -> fail "bad PushProvider"
instance FromField PushProvider where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8
@@ -398,10 +408,12 @@ instance Encoding DeviceToken where
instance StrEncoding DeviceToken where
strEncode (DeviceToken p t) = strEncode p <> " " <> t
strP = DeviceToken <$> strP <* A.space <*> hexStringP
strP = nullToken <|> hexToken
where
nullToken = "apns_null test_ntf_token" $> DeviceToken PPApnsNull "test_ntf_token"
hexToken = DeviceToken <$> strP <* A.space <*> hexStringP
hexStringP =
A.takeWhile (\c -> A.isDigit c || (c >= 'a' && c <= 'f')) >>= \s ->
A.takeWhile (`B.elem` "0123456789abcdef") >>= \s ->
if even (B.length s) then pure s else fail "odd number of hex characters"
instance ToJSON DeviceToken where
+50 -41
View File
@@ -23,7 +23,6 @@ import Data.Int (Int64)
import Data.List (intercalate, sort)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (catMaybes)
import qualified Data.Text as T
@@ -48,8 +47,8 @@ import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.Server
import Simplex.Messaging.Server.Stats
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport (ATransport (..), THandle (..), TProxy, Transport (..))
import Simplex.Messaging.Transport.Server (runTransportServer)
import Simplex.Messaging.Transport (ATransport (..), THandle (..), THandleAuth (..), THandleParams (..), TProxy, Transport (..), TransportPeer (..))
import Simplex.Messaging.Transport.Server (runTransportServer, tlsServerCredentials)
import Simplex.Messaging.Util
import System.Exit (exitFailure)
import System.IO (BufferMode (..), hPutStrLn, hSetBuffering)
@@ -75,19 +74,23 @@ ntfServer cfg@NtfServerConfig {transports, transportConfig = tCfg} started = do
restoreServerStats
s <- asks subscriber
ps <- asks pushServer
subs <- readTVarIO =<< asks (subscriptions . store)
void . forkIO $ resubscribe s subs
resubscribe s
raceAny_ (ntfSubscriber s : ntfPush ps : map runServer transports <> serverStatsThread_ cfg) `finally` stopServer
where
runServer :: (ServiceName, ATransport) -> M ()
runServer (tcpPort, ATransport t) = do
serverParams <- asks tlsServerParams
runTransportServer started tcpPort serverParams tCfg (runClient t)
serverSignKey <- either fail pure . fromTLSCredentials $ tlsServerCredentials serverParams
env <- ask
liftIO $ runTransportServer started tcpPort serverParams tCfg $ \h -> runClient serverSignKey t h `runReaderT` env
fromTLSCredentials (_, pk) = C.x509ToPrivate (pk, []) >>= C.privKey
runClient :: Transport c => TProxy c -> c -> M ()
runClient _ h = do
runClient :: Transport c => C.APrivateSignKey -> TProxy c -> c -> M ()
runClient signKey _ h = do
kh <- asks serverIdentity
liftIO (runExceptT $ ntfServerHandshake h kh supportedNTFServerVRange) >>= \case
ks <- atomically . C.generateKeyPair =<< asks random
NtfServerConfig {ntfServerVRange} <- asks config
liftIO (runExceptT $ ntfServerHandshake signKey h ks kh ntfServerVRange) >>= \case
Right th -> runNtfClientTransport th
Left _ -> pure ()
@@ -105,13 +108,13 @@ ntfServer cfg@NtfServerConfig {transports, transportConfig = tCfg} started = do
logServerStats :: Int64 -> Int64 -> FilePath -> M ()
logServerStats startAt logInterval statsFilePath = do
initialDelay <- (startAt -) . fromIntegral . (`div` 1000000_000000) . diffTimeToPicoseconds . utctDayTime <$> liftIO getCurrentTime
liftIO $ putStrLn $ "server stats log enabled: " <> statsFilePath
logInfo $ "server stats log enabled: " <> T.pack statsFilePath
liftIO $ threadDelay' $ 1000000 * (initialDelay + if initialDelay < 0 then 86400 else 0)
NtfServerStats {fromTime, tknCreated, tknVerified, tknDeleted, subCreated, subDeleted, ntfReceived, ntfDelivered, activeTokens, activeSubs} <- asks serverStats
let interval = 1000000 * logInterval
withFile statsFilePath AppendMode $ \h -> liftIO $ do
hSetBuffering h LineBuffering
forever $ do
forever $ do
withFile statsFilePath AppendMode $ \h -> liftIO $ do
hSetBuffering h LineBuffering
ts <- getCurrentTime
fromTime' <- atomically $ swapTVar fromTime ts
tknCreated' <- atomically $ swapTVar tknCreated 0
@@ -141,13 +144,15 @@ ntfServer cfg@NtfServerConfig {transports, transportConfig = tCfg} started = do
weekCount sub,
monthCount sub
]
threadDelay' interval
liftIO $ threadDelay' interval
resubscribe :: NtfSubscriber -> Map NtfSubscriptionId NtfSubData -> M ()
resubscribe NtfSubscriber {newSubQ} subs = do
subs' <- atomically $ filterM (fmap ntfShouldSubscribe . readTVar . subStatus) $ M.elems subs
resubscribe :: NtfSubscriber -> M ()
resubscribe NtfSubscriber {newSubQ} = do
logInfo "Preparing SMP resubscriptions..."
subs <- readTVarIO =<< asks (subscriptions . store)
subs' <- filterM (fmap ntfShouldSubscribe . readTVarIO . subStatus) $ M.elems subs
atomically . writeTBQueue newSubQ $ map NtfSub subs'
liftIO $ logInfo $ "SMP resubscriptions queued (" <> tshow (length subs') <> " subscriptions)"
logInfo $ "SMP resubscriptions queued (" <> tshow (length subs') <> " subscriptions)"
ntfSubscriber :: NtfSubscriber -> M ()
ntfSubscriber NtfSubscriber {smpSubscribers, newSubQ, smpAgent = ca@SMPClientAgent {msgQ, agentQ}} = do
@@ -334,11 +339,11 @@ updateTknStatus NtfTknData {ntfTknId, tknStatus} status = do
old <- atomically $ stateTVar tknStatus (,status)
when (old /= status) $ withNtfLog $ \sl -> logTokenStatus sl ntfTknId status
runNtfClientTransport :: Transport c => THandle c -> M ()
runNtfClientTransport th@THandle {sessionId} = do
runNtfClientTransport :: Transport c => THandleNTF c 'TServer -> M ()
runNtfClientTransport th@THandle {params} = do
qSize <- asks $ clientQSize . config
ts <- liftIO getSystemTime
c <- atomically $ newNtfServerClient qSize sessionId ts
c <- atomically $ newNtfServerClient qSize params ts
s <- asks subscriber
ps <- asks pushServer
expCfg <- asks $ inactiveClientExpiration . config
@@ -351,8 +356,8 @@ runNtfClientTransport th@THandle {sessionId} = do
clientDisconnected :: NtfServerClient -> IO ()
clientDisconnected NtfServerClient {connected} = atomically $ writeTVar connected False
receive :: Transport c => THandle c -> NtfServerClient -> M ()
receive th NtfServerClient {rcvQ, sndQ, rcvActiveAt} = forever $ do
receive :: Transport c => THandleNTF c 'TServer -> NtfServerClient -> M ()
receive th@THandle {params = THandleParams {thAuth}} NtfServerClient {rcvQ, sndQ, rcvActiveAt} = forever $ do
ts <- liftIO $ tGet th
forM_ ts $ \t@(_, _, (corrId, entId, cmdOrError)) -> do
atomically . writeTVar rcvActiveAt =<< liftIO getSystemTime
@@ -360,16 +365,16 @@ receive th NtfServerClient {rcvQ, sndQ, rcvActiveAt} = forever $ do
case cmdOrError of
Left e -> write sndQ (corrId, entId, NRErr e)
Right cmd ->
verifyNtfTransmission t cmd >>= \case
verifyNtfTransmission ((,C.cbNonce (SMP.bs corrId)) <$> thAuth) t cmd >>= \case
VRVerified req -> write rcvQ req
VRFailed -> write sndQ (corrId, entId, NRErr AUTH)
where
write q t = atomically $ writeTBQueue q t
send :: Transport c => THandle c -> NtfServerClient -> IO ()
send h@THandle {thVersion = v} NtfServerClient {sndQ, sessionId, sndActiveAt} = forever $ do
send :: Transport c => THandleNTF c 'TServer -> NtfServerClient -> IO ()
send h@THandle {params} NtfServerClient {sndQ, sndActiveAt} = forever $ do
t <- atomically $ readTBQueue sndQ
void . liftIO $ tPut h Nothing [(Nothing, encodeTransmission v sessionId t)]
void . liftIO $ tPut h [Right (Nothing, encodeTransmission params t)]
atomically . writeTVar sndActiveAt =<< liftIO getSystemTime
-- instance Show a => Show (TVar a) where
@@ -377,14 +382,14 @@ send h@THandle {thVersion = v} NtfServerClient {sndQ, sessionId, sndActiveAt} =
data VerificationResult = VRVerified NtfRequest | VRFailed
verifyNtfTransmission :: SignedTransmission ErrorType NtfCmd -> NtfCmd -> M VerificationResult
verifyNtfTransmission (sig_, signed, (corrId, entId, _)) cmd = do
verifyNtfTransmission :: Maybe (THandleAuth 'TServer, C.CbNonce) -> SignedTransmission ErrorType NtfCmd -> NtfCmd -> M VerificationResult
verifyNtfTransmission auth_ (tAuth, authorized, (corrId, entId, _)) cmd = do
st <- asks store
case cmd of
NtfCmd SToken c@(TNEW tkn@(NewNtfTkn _ k _)) -> do
r_ <- atomically $ getNtfTokenRegistration st tkn
pure $
if verifyCmdSignature sig_ signed k
if verifyCmdAuthorization auth_ tAuth authorized k
then case r_ of
Just t@NtfTknData {tknVerifyKey}
| k == tknVerifyKey -> verifiedTknCmd t c
@@ -405,7 +410,7 @@ verifyNtfTransmission (sig_, signed, (corrId, entId, _)) cmd = do
then do
t_ <- atomically $ getActiveNtfToken st subTknId
verifyToken' t_ $ verifiedSubCmd s c
else pure $ maybe False (dummyVerifyCmd signed) sig_ `seq` VRFailed
else pure $ maybe False (dummyVerifyCmd auth_ authorized) tAuth `seq` VRFailed
NtfCmd SSubscription PING -> pure $ VRVerified $ NtfReqPing corrId entId
NtfCmd SSubscription c -> do
s_ <- atomically $ getNtfSubscription st entId
@@ -413,7 +418,7 @@ verifyNtfTransmission (sig_, signed, (corrId, entId, _)) cmd = do
Just s@NtfSubData {tokenId = subTknId} -> do
t_ <- atomically $ getActiveNtfToken st subTknId
verifyToken' t_ $ verifiedSubCmd s c
_ -> pure $ maybe False (dummyVerifyCmd signed) sig_ `seq` VRFailed
_ -> pure $ maybe False (dummyVerifyCmd auth_ authorized) tAuth `seq` VRFailed
where
verifiedTknCmd t c = VRVerified (NtfReqCmd SToken (NtfTkn t) (corrId, entId, c))
verifiedSubCmd s c = VRVerified (NtfReqCmd SSubscription (NtfSub s) (corrId, entId, c))
@@ -421,10 +426,10 @@ verifyNtfTransmission (sig_, signed, (corrId, entId, _)) cmd = do
verifyToken t_ positiveVerificationResult =
pure $ case t_ of
Just t@NtfTknData {tknVerifyKey} ->
if verifyCmdSignature sig_ signed tknVerifyKey
if verifyCmdAuthorization auth_ tAuth authorized tknVerifyKey
then positiveVerificationResult t
else VRFailed
_ -> maybe False (dummyVerifyCmd signed) sig_ `seq` VRFailed
_ -> maybe False (dummyVerifyCmd auth_ authorized) tAuth `seq` VRFailed
verifyToken' :: Maybe NtfTknData -> VerificationResult -> M VerificationResult
verifyToken' t_ = verifyToken t_ . const
@@ -437,7 +442,7 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
where
processCommand :: NtfRequest -> M (Transmission NtfResponse)
processCommand = \case
NtfReqNew corrId (ANE SToken newTkn@(NewNtfTkn _ _ dhPubKey)) -> do
NtfReqNew corrId (ANE SToken newTkn@(NewNtfTkn token _ dhPubKey)) -> do
logDebug "TNEW - new token"
st <- asks store
ks@(srvDhPubKey, srvDhPrivKey) <- atomically . C.generateKeyPair =<< asks random
@@ -448,9 +453,9 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
atomically $ addNtfToken st tknId tkn
atomically $ writeTBQueue pushQ (tkn, PNVerification regCode)
withNtfLog (`logCreateToken` tkn)
incNtfStat tknCreated
incNtfStatT token tknCreated
pure (corrId, "", NRTknId tknId srvDhPubKey)
NtfReqCmd SToken (NtfTkn tkn@NtfTknData {ntfTknId, tknStatus, tknRegCode, tknDhSecret, tknDhKeys = (srvDhPubKey, srvDhPrivKey), tknCronInterval}) (corrId, tknId, cmd) -> do
NtfReqCmd SToken (NtfTkn tkn@NtfTknData {token, ntfTknId, tknStatus, tknRegCode, tknDhSecret, tknDhKeys = (srvDhPubKey, srvDhPrivKey), tknCronInterval}) (corrId, tknId, cmd) -> do
status <- readTVarIO tknStatus
(corrId,tknId,) <$> case cmd of
TNEW (NewNtfTkn _ _ dhPubKey) -> do
@@ -469,7 +474,7 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
updateTknStatus tkn NTActive
tIds <- atomically $ removeInactiveTokenRegistrations st tkn
forM_ tIds cancelInvervalNotifications
incNtfStat tknVerified
incNtfStatT token tknVerified
pure NROk
| otherwise -> do
logDebug "TVFY - incorrect code or token status"
@@ -488,8 +493,8 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
addNtfToken st tknId tkn'
writeTBQueue pushQ (tkn', PNVerification regCode)
withNtfLog $ \s -> logUpdateToken s tknId token' regCode
incNtfStat tknDeleted
incNtfStat tknCreated
incNtfStatT token tknDeleted
incNtfStatT token tknCreated
pure NROk
TDEL -> do
logDebug "TDEL"
@@ -499,7 +504,7 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
atomically $ removeSubscription ca smpServer (SPNotifier, notifierId)
cancelInvervalNotifications tknId
withNtfLog (`logDeleteToken` tknId)
incNtfStat tknDeleted
incNtfStatT token tknDeleted
pure NROk
TCRN 0 -> do
logDebug "TCRN 0"
@@ -578,6 +583,10 @@ client NtfServerClient {rcvQ, sndQ} NtfSubscriber {newSubQ, smpAgent = ca} NtfPu
withNtfLog :: (StoreLog 'WriteMode -> IO a) -> M ()
withNtfLog action = liftIO . mapM_ action =<< asks storeLog
incNtfStatT :: DeviceToken -> (NtfServerStats -> TVar Int) -> M ()
incNtfStatT (DeviceToken PPApnsNull _) _ = pure ()
incNtfStatT _ statSel = incNtfStat statSel
incNtfStat :: (NtfServerStats -> TVar Int) -> M ()
incNtfStat statSel = do
stats <- asks serverStats
@@ -12,7 +12,6 @@ import Control.Concurrent.Async (Async)
import Control.Logger.Simple
import Control.Monad.IO.Unlift
import Crypto.Random
import Data.ByteString.Char8 (ByteString)
import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty)
import Data.Time.Clock (getCurrentTime)
@@ -29,11 +28,12 @@ import Simplex.Messaging.Notifications.Server.Push.APNS
import Simplex.Messaging.Notifications.Server.Stats
import Simplex.Messaging.Notifications.Server.Store
import Simplex.Messaging.Notifications.Server.StoreLog
import Simplex.Messaging.Notifications.Transport (NTFVersion, VersionRangeNTF)
import Simplex.Messaging.Protocol (CorrId, SMPServer, Transmission)
import Simplex.Messaging.Server.Expiration
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport (ATransport)
import Simplex.Messaging.Transport (ATransport, THandleParams, TransportPeer (..))
import Simplex.Messaging.Transport.Server (TransportServerConfig, loadFingerprint, loadTLSServerParams)
import System.IO (IOMode (..))
import System.Mem.Weak (Weak)
@@ -60,6 +60,7 @@ data NtfServerConfig = NtfServerConfig
logStatsStartTime :: Int64,
serverStatsLogFile :: FilePath,
serverStatsBackupFile :: Maybe FilePath,
ntfServerVRange :: VersionRangeNTF,
transportConfig :: TransportServerConfig
}
@@ -82,14 +83,14 @@ data NtfEnv = NtfEnv
serverStats :: NtfServerStats
}
newNtfServerEnv :: (MonadUnliftIO m, MonadRandom m) => NtfServerConfig -> m NtfEnv
newNtfServerEnv :: NtfServerConfig -> IO NtfEnv
newNtfServerEnv config@NtfServerConfig {subQSize, pushQSize, smpAgentCfg, apnsConfig, storeLogFile, caCertificateFile, certificateFile, privateKeyFile} = do
random <- liftIO C.newRandom
store <- atomically newNtfStore
logInfo "restoring subscriptions..."
storeLog <- liftIO $ mapM (`readWriteNtfStore` store) storeLogFile
logInfo "restored subscriptions"
subscriber <- atomically $ newNtfSubscriber subQSize smpAgentCfg
subscriber <- atomically $ newNtfSubscriber subQSize smpAgentCfg random
pushServer <- atomically $ newNtfPushServer pushQSize apnsConfig
tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile
Fingerprint fp <- liftIO $ loadFingerprint caCertificateFile
@@ -102,11 +103,11 @@ data NtfSubscriber = NtfSubscriber
smpAgent :: SMPClientAgent
}
newNtfSubscriber :: Natural -> SMPClientAgentConfig -> STM NtfSubscriber
newNtfSubscriber qSize smpAgentCfg = do
newNtfSubscriber :: Natural -> SMPClientAgentConfig -> TVar ChaChaDRG -> STM NtfSubscriber
newNtfSubscriber qSize smpAgentCfg random = do
smpSubscribers <- TM.empty
newSubQ <- newTBQueue qSize
smpAgent <- newSMPClientAgent smpAgentCfg
smpAgent <- newSMPClientAgent smpAgentCfg random
pure NtfSubscriber {smpSubscribers, newSubQ, smpAgent}
data SMPSubscriber = SMPSubscriber
@@ -142,7 +143,9 @@ newNtfPushServer qSize apnsConfig = do
newPushClient :: NtfPushServer -> PushProvider -> IO PushProviderClient
newPushClient NtfPushServer {apnsConfig, pushClients} pp = do
c <- apnsPushProviderClient <$> createAPNSPushClient (apnsProviderHost pp) apnsConfig
c <- case apnsProviderHost pp of
Nothing -> pure $ \_ _ -> pure ()
Just host -> apnsPushProviderClient <$> createAPNSPushClient host apnsConfig
atomically $ TM.insert pp c pushClients
pure c
@@ -158,17 +161,17 @@ data NtfRequest
data NtfServerClient = NtfServerClient
{ rcvQ :: TBQueue NtfRequest,
sndQ :: TBQueue (Transmission NtfResponse),
sessionId :: ByteString,
ntfThParams :: THandleParams NTFVersion 'TServer,
connected :: TVar Bool,
rcvActiveAt :: TVar SystemTime,
sndActiveAt :: TVar SystemTime
}
newNtfServerClient :: Natural -> ByteString -> SystemTime -> STM NtfServerClient
newNtfServerClient qSize sessionId ts = do
newNtfServerClient :: Natural -> THandleParams NTFVersion 'TServer -> SystemTime -> STM NtfServerClient
newNtfServerClient qSize ntfThParams ts = do
rcvQ <- newTBQueue qSize
sndQ <- newTBQueue qSize
connected <- newTVar True
rcvActiveAt <- newTVar ts
sndActiveAt <- newTVar ts
return NtfServerClient {rcvQ, sndQ, sessionId, connected, rcvActiveAt, sndActiveAt}
return NtfServerClient {rcvQ, sndQ, ntfThParams, connected, rcvActiveAt, sndActiveAt}
@@ -13,15 +13,16 @@ import Data.Maybe (fromMaybe)
import qualified Data.Text as T
import Network.Socket (HostName)
import Options.Applicative
import Simplex.Messaging.Client (ProtocolClientConfig (..))
import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig)
import Simplex.Messaging.Client.Agent (defaultSMPClientAgentConfig)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Notifications.Server (runNtfServer)
import Simplex.Messaging.Notifications.Server.Env (NtfServerConfig (..), defaultInactiveClientExpiration)
import Simplex.Messaging.Notifications.Server.Push.APNS (defaultAPNSPushClientConfig)
import Simplex.Messaging.Notifications.Transport (supportedServerNTFVRange)
import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), pattern NtfServer)
import Simplex.Messaging.Server.CLI
import Simplex.Messaging.Server.Expiration
import Simplex.Messaging.Transport (simplexMQVersion)
import Simplex.Messaging.Transport.Client (TransportHost (..))
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig)
import System.Directory (createDirectoryIfMissing, doesFileExist)
@@ -29,12 +30,6 @@ import System.FilePath (combine)
import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
import Text.Read (readMaybe)
ntfServerVersion :: String
ntfServerVersion = "1.7.0.2"
defaultSMPBatchDelay :: Int
defaultSMPBatchDelay = 10000
ntfServerCLI :: FilePath -> FilePath -> IO ()
ntfServerCLI cfgPath logPath =
getCliCommand' (cliCommandP cfgPath logPath iniFile) serverVersion >>= \case
@@ -42,6 +37,10 @@ ntfServerCLI cfgPath logPath =
doesFileExist iniFile >>= \case
True -> exitError $ "Error: server is already initialized (" <> iniFile <> " exists).\nRun `" <> executableName <> " start`."
_ -> initializeServer opts
OnlineCert certOpts ->
doesFileExist iniFile >>= \case
True -> genOnline cfgPath certOpts
_ -> exitError $ "Error: server is not initialized (" <> iniFile <> " does not exist).\nRun `" <> executableName <> " init`."
Start ->
doesFileExist iniFile >>= \case
True -> readIniFile iniFile >>= either exitError runServer
@@ -53,7 +52,7 @@ ntfServerCLI cfgPath logPath =
putStrLn "Deleted configuration and log files"
where
iniFile = combine cfgPath "ntf-server.ini"
serverVersion = "SMP notifications server v" <> ntfServerVersion
serverVersion = "SMP notifications server v" <> simplexMQVersion
defaultServerPort = "443"
executableName = "ntf-server"
storeLogFilePath = combine logPath "ntf-server-store.log"
@@ -84,9 +83,7 @@ ntfServerCLI cfgPath logPath =
\# host is only used to print server address on start\n"
<> ("host: " <> host <> "\n")
<> ("port: " <> defaultServerPort <> "\n")
<> "log_tls_errors: off\n\
\# delay between command batches sent to SMP relays (microseconds), 0 to disable\n"
<> ("smp_batch_delay: " <> show defaultSMPBatchDelay <> "\n")
<> "log_tls_errors: off\n"
<> "websockets: off\n\n\
\[INACTIVE_CLIENTS]\n\
\# TTL and interval to check inactive clients\n\
@@ -108,8 +105,6 @@ ntfServerCLI cfgPath logPath =
enableStoreLog = settingIsOn "STORE_LOG" "enable" ini
logStats = settingIsOn "STORE_LOG" "log_stats" ini
c = combine cfgPath . ($ defaultX509Config)
smpBatchDelay = readIniDefault defaultSMPBatchDelay "TRANSPORT" "smp_batch_delay" ini
batchDelay = if smpBatchDelay <= 0 then Nothing else Just smpBatchDelay
serverConfig =
NtfServerConfig
{ transports = iniTransports ini,
@@ -118,7 +113,7 @@ ntfServerCLI cfgPath logPath =
clientQSize = 64,
subQSize = 512,
pushQSize = 1048,
smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {batchDelay}},
smpAgentCfg = defaultSMPClientAgentConfig,
apnsConfig = defaultAPNSPushClientConfig,
subsBatchSize = 900,
inactiveClientExpiration =
@@ -135,6 +130,7 @@ ntfServerCLI cfgPath logPath =
logStatsStartTime = 0, -- seconds from 00:00 UTC
serverStatsLogFile = combine logPath "ntf-server-stats.daily.log",
serverStatsBackupFile = logStats $> combine logPath "ntf-server-stats.log",
ntfServerVRange = supportedServerNTFVRange,
transportConfig =
defaultTransportServerConfig
{ logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini
@@ -143,6 +139,7 @@ ntfServerCLI cfgPath logPath =
data CliCommand
= Init InitOptions
| OnlineCert CertOptions
| Start
| Delete
@@ -158,6 +155,7 @@ cliCommandP :: FilePath -> FilePath -> FilePath -> Parser CliCommand
cliCommandP cfgPath logPath iniFile =
hsubparser
( command "init" (info (Init <$> initP) (progDesc $ "Initialize server - creates " <> cfgPath <> " and " <> logPath <> " directories and configuration files"))
<> command "cert" (info (OnlineCert <$> certOptionsP) (progDesc $ "Generate new online TLS server credentials (configuration: " <> iniFile <> ")"))
<> command "start" (info (pure Start) (progDesc $ "Start server (configuration: " <> iniFile <> ")"))
<> command "delete" (info (pure Delete) (progDesc "Delete configuration and log files"))
)
@@ -54,7 +54,7 @@ import Simplex.Messaging.Parsers (defaultJSON)
import Simplex.Messaging.Protocol (EncNMsgMeta)
import Simplex.Messaging.Transport.HTTP2 (HTTP2Body (..))
import Simplex.Messaging.Transport.HTTP2.Client
import Simplex.Messaging.Util (safeDecodeUtf8)
import Simplex.Messaging.Util (safeDecodeUtf8, tshow)
import System.Environment (getEnv)
import UnliftIO.STM
@@ -193,11 +193,12 @@ data APNSPushClientConfig = APNSPushClientConfig
caStoreFile :: FilePath
}
apnsProviderHost :: PushProvider -> HostName
apnsProviderHost :: PushProvider -> Maybe HostName
apnsProviderHost = \case
PPApnsTest -> "localhost"
PPApnsDev -> "api.sandbox.push.apple.com"
PPApnsProd -> "api.push.apple.com"
PPApnsNull -> Nothing
PPApnsTest -> Just "localhost"
PPApnsDev -> Just "api.sandbox.push.apple.com"
PPApnsProd -> Just "api.push.apple.com"
defaultAPNSPushClientConfig :: APNSPushClientConfig
defaultAPNSPushClientConfig =
@@ -257,11 +258,11 @@ mkApnsJWTToken appTeamId jwtHeader privateKey = do
connectHTTPS2 :: HostName -> APNSPushClientConfig -> TVar (Maybe HTTP2Client) -> IO (Either HTTP2ClientError HTTP2Client)
connectHTTPS2 apnsHost APNSPushClientConfig {apnsPort, http2cfg, caStoreFile} https2Client = do
caStore_ <- XS.readCertificateStore caStoreFile
when (isNothing caStore_) $ putStrLn $ "Error loading CertificateStore from " <> caStoreFile
when (isNothing caStore_) $ logError $ "Error loading CertificateStore from " <> T.pack caStoreFile
r <- getHTTP2Client apnsHost apnsPort caStore_ http2cfg disconnected
case r of
Right client -> atomically . writeTVar https2Client $ Just client
Left e -> putStrLn $ "Error connecting to APNS: " <> show e
Left e -> logError $ "Error connecting to APNS: " <> tshow e
pure r
where
disconnected = atomically $ writeTVar https2Client Nothing
@@ -19,7 +19,7 @@ import qualified Data.Set as S
import Data.Word (Word16)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Notifications.Protocol
import Simplex.Messaging.Protocol (NtfPrivateSignKey, SMPServer)
import Simplex.Messaging.Protocol (NtfPrivateAuthKey, NtfPublicAuthKey, SMPServer)
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Util (whenM, ($>>=))
@@ -46,7 +46,7 @@ data NtfTknData = NtfTknData
{ ntfTknId :: NtfTokenId,
token :: DeviceToken,
tknStatus :: TVar NtfTknStatus,
tknVerifyKey :: C.APublicVerifyKey,
tknVerifyKey :: NtfPublicAuthKey,
tknDhKeys :: C.KeyPair 'C.X25519,
tknDhSecret :: C.DhSecretX25519,
tknRegCode :: NtfRegCode,
@@ -62,7 +62,7 @@ mkNtfTknData ntfTknId (NewNtfTkn token tknVerifyKey _) tknDhKeys tknDhSecret tkn
data NtfSubData = NtfSubData
{ ntfSubId :: NtfSubscriptionId,
smpQueue :: SMPQueueNtf,
notifierKey :: NtfPrivateSignKey,
notifierKey :: NtfPrivateAuthKey,
tokenId :: NtfTokenId,
subStatus :: TVar NtfSubStatus
}
@@ -24,17 +24,20 @@ module Simplex.Messaging.Notifications.Server.StoreLog
where
import Control.Concurrent.STM
import Control.Logger.Simple
import Control.Monad
import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import qualified Data.Text as T
import Data.Word (Word16)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Notifications.Protocol
import Simplex.Messaging.Notifications.Server.Store
import Simplex.Messaging.Protocol (NtfPrivateSignKey)
import Simplex.Messaging.Protocol (NtfPrivateAuthKey)
import Simplex.Messaging.Server.StoreLog
import Simplex.Messaging.Util (whenM)
import Simplex.Messaging.Util (safeDecodeUtf8, whenM)
import System.Directory (doesFileExist, renameFile)
import System.IO
@@ -52,7 +55,7 @@ data NtfTknRec = NtfTknRec
{ ntfTknId :: NtfTokenId,
token :: DeviceToken,
tknStatus :: NtfTknStatus,
tknVerifyKey :: C.APublicVerifyKey,
tknVerifyKey :: C.APublicAuthKey,
tknDhKeys :: C.KeyPair 'C.X25519,
tknDhSecret :: C.DhSecretX25519,
tknRegCode :: NtfRegCode,
@@ -74,7 +77,7 @@ mkTknRec NtfTknData {ntfTknId, token, tknStatus = status, tknVerifyKey, tknDhKey
data NtfSubRec = NtfSubRec
{ ntfSubId :: NtfSubscriptionId,
smpQueue :: SMPQueueNtf,
notifierKey :: NtfPrivateSignKey,
notifierKey :: NtfPrivateAuthKey,
tokenId :: NtfTokenId,
subStatus :: NtfSubStatus
}
@@ -189,10 +192,10 @@ readWriteNtfStore f st = do
pure s
readNtfStore :: FilePath -> NtfStore -> IO ()
readNtfStore f st = mapM_ addNtfLogRecord . B.lines =<< B.readFile f
readNtfStore f st = mapM_ (addNtfLogRecord . LB.toStrict) . LB.lines =<< LB.readFile f
where
addNtfLogRecord s = case strDecode s of
Left e -> B.putStrLn $ "Log parsing error (" <> B.pack e <> "): " <> B.take 100 s
Left e -> logError $ "Log parsing error (" <> T.pack e <> "): " <> safeDecodeUtf8 (B.take 100 s)
Right lr -> atomically $ case lr of
CreateToken r@NtfTknRec {ntfTknId} -> do
tkn <- mkTknData r
+105 -24
View File
@@ -1,72 +1,153 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
module Simplex.Messaging.Notifications.Transport where
import Control.Monad (forM)
import Control.Monad.Except
import Data.Attoparsec.ByteString.Char8 (Parser)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Word (Word16)
import qualified Data.X509 as X
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Transport
import Simplex.Messaging.Util (liftEitherWith)
import Simplex.Messaging.Version
import Simplex.Messaging.Version.Internal
ntfBlockSize :: Int
ntfBlockSize = 512
supportedNTFServerVRange :: VersionRange
supportedNTFServerVRange = mkVersionRange 1 1
data NTFVersion
instance VersionScope NTFVersion
type VersionNTF = Version NTFVersion
type VersionRangeNTF = VersionRange NTFVersion
pattern VersionNTF :: Word16 -> VersionNTF
pattern VersionNTF v = Version v
initialNTFVersion :: VersionNTF
initialNTFVersion = VersionNTF 1
authBatchCmdsNTFVersion :: VersionNTF
authBatchCmdsNTFVersion = VersionNTF 2
currentClientNTFVersion :: VersionNTF
currentClientNTFVersion = VersionNTF 1
currentServerNTFVersion :: VersionNTF
currentServerNTFVersion = VersionNTF 1
supportedClientNTFVRange :: VersionRangeNTF
supportedClientNTFVRange = mkVersionRange initialNTFVersion currentClientNTFVersion
supportedServerNTFVRange :: VersionRangeNTF
supportedServerNTFVRange = mkVersionRange initialNTFVersion currentServerNTFVersion
type THandleNTF c p = THandle NTFVersion c p
data NtfServerHandshake = NtfServerHandshake
{ ntfVersionRange :: VersionRange,
sessionId :: SessionId
{ ntfVersionRange :: VersionRangeNTF,
sessionId :: SessionId,
-- pub key to agree shared secrets for command authorization and entity ID encryption.
authPubKey :: Maybe (X.SignedExact X.PubKey)
}
data NtfClientHandshake = NtfClientHandshake
{ -- | agreed SMP notifications server protocol version
ntfVersion :: Version,
ntfVersion :: VersionNTF,
-- | server identity - CA certificate fingerprint
keyHash :: C.KeyHash
}
instance Encoding NtfServerHandshake where
smpEncode NtfServerHandshake {ntfVersionRange, sessionId} =
smpEncode (ntfVersionRange, sessionId)
smpEncode NtfServerHandshake {ntfVersionRange, sessionId, authPubKey} =
B.concat
[ smpEncode (ntfVersionRange, sessionId),
encodeAuthEncryptCmds (maxVersion ntfVersionRange) $ C.SignedObject <$> authPubKey
]
smpP = do
(ntfVersionRange, sessionId) <- smpP
pure NtfServerHandshake {ntfVersionRange, sessionId}
-- TODO drop SMP v6: remove special parser and make key non-optional
authPubKey <- authEncryptCmdsP (maxVersion ntfVersionRange) $ C.getSignedExact <$> smpP
pure NtfServerHandshake {ntfVersionRange, sessionId, authPubKey}
encodeAuthEncryptCmds :: Encoding a => VersionNTF -> Maybe a -> ByteString
encodeAuthEncryptCmds v k
| v >= authBatchCmdsNTFVersion = maybe "" smpEncode k
| otherwise = ""
authEncryptCmdsP :: VersionNTF -> Parser a -> Parser (Maybe a)
authEncryptCmdsP v p = if v >= authBatchCmdsNTFVersion then Just <$> p else pure Nothing
instance Encoding NtfClientHandshake where
smpEncode NtfClientHandshake {ntfVersion, keyHash} = smpEncode (ntfVersion, keyHash)
smpEncode NtfClientHandshake {ntfVersion, keyHash} =
smpEncode (ntfVersion, keyHash)
smpP = do
(ntfVersion, keyHash) <- smpP
pure NtfClientHandshake {ntfVersion, keyHash}
-- | Notifcations server transport handshake.
ntfServerHandshake :: forall c. Transport c => c -> C.KeyHash -> VersionRange -> ExceptT TransportError IO (THandle c)
ntfServerHandshake c kh ntfVRange = do
let th@THandle {sessionId} = ntfTHandle c
sendHandshake th $ NtfServerHandshake {sessionId, ntfVersionRange = ntfVRange}
ntfServerHandshake :: forall c. Transport c => C.APrivateSignKey -> c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c 'TServer)
ntfServerHandshake serverSignKey c (k, pk) kh ntfVRange = do
let th@THandle {params = THandleParams {sessionId}} = ntfTHandle c
let sk = C.signX509 serverSignKey $ C.publicToX509 k
sendHandshake th $ NtfServerHandshake {sessionId, ntfVersionRange = ntfVRange, authPubKey = Just sk}
getHandshake th >>= \case
NtfClientHandshake {ntfVersion, keyHash}
NtfClientHandshake {ntfVersion = v, keyHash}
| keyHash /= kh ->
throwError $ TEHandshake IDENTITY
| ntfVersion `isCompatible` ntfVRange ->
pure (th :: THandle c) {thVersion = ntfVersion}
| v `isCompatible` ntfVRange ->
pure $ ntfThHandleServer th v pk
| otherwise -> throwError $ TEHandshake VERSION
-- | Notifcations server client transport handshake.
ntfClientHandshake :: forall c. Transport c => c -> C.KeyHash -> VersionRange -> ExceptT TransportError IO (THandle c)
ntfClientHandshake :: forall c. Transport c => c -> C.KeyHash -> VersionRangeNTF -> ExceptT TransportError IO (THandleNTF c 'TClient)
ntfClientHandshake c keyHash ntfVRange = do
let th@THandle {sessionId} = ntfTHandle c
NtfServerHandshake {sessionId = sessId, ntfVersionRange} <- getHandshake th
let th@THandle {params = THandleParams {sessionId}} = ntfTHandle c
NtfServerHandshake {sessionId = sessId, ntfVersionRange, authPubKey = sk'} <- getHandshake th
if sessionId /= sessId
then throwError TEBadSession
else case ntfVersionRange `compatibleVersion` ntfVRange of
Just (Compatible ntfVersion) -> do
sendHandshake th $ NtfClientHandshake {ntfVersion, keyHash}
pure (th :: THandle c) {thVersion = ntfVersion}
Just (Compatible v) -> do
ck_ <- forM sk' $ \signedKey -> liftEitherWith (const $ TEHandshake BAD_AUTH) $ do
serverKey <- getServerVerifyKey c
pubKey <- C.verifyX509 serverKey signedKey
(,(getServerCerts c, signedKey)) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey)
sendHandshake th $ NtfClientHandshake {ntfVersion = v, keyHash}
pure $ ntfThHandleClient th v ck_
Nothing -> throwError $ TEHandshake VERSION
ntfTHandle :: Transport c => c -> THandle c
ntfTHandle c = THandle {connection = c, sessionId = tlsUnique c, blockSize = ntfBlockSize, thVersion = 0, batch = False}
ntfThHandleServer :: forall c. THandleNTF c 'TServer -> VersionNTF -> C.PrivateKeyX25519 -> THandleNTF c 'TServer
ntfThHandleServer th v pk =
let thAuth = THAuthServer {serverPrivKey = pk, sessSecret' = Nothing}
in ntfThHandle_ th v (Just thAuth)
ntfThHandleClient :: forall c. THandleNTF c 'TClient -> VersionNTF -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleNTF c 'TClient
ntfThHandleClient th v ck_ =
let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = ck, sessSecret = Nothing}) <$> ck_
in ntfThHandle_ th v thAuth
ntfThHandle_ :: forall c p. THandleNTF c p -> VersionNTF -> Maybe (THandleAuth p) -> THandleNTF c p
ntfThHandle_ th@THandle {params} v thAuth =
-- TODO drop SMP v6: make thAuth non-optional
let v3 = v >= authBatchCmdsNTFVersion
params' = params {thVersion = v, thAuth, implySessId = v3, batch = v3}
in (th :: THandleNTF c p) {params = params'}
ntfTHandle :: Transport c => c -> THandleNTF c p
ntfTHandle c = THandle {connection = c, params}
where
params = THandleParams {sessionId = tlsUnique c, blockSize = ntfBlockSize, thVersion = VersionNTF 0, thAuth = Nothing, implySessId = False, batch = False}
+4 -3
View File
@@ -47,10 +47,11 @@ data NtfToken = NtfToken
{ deviceToken :: DeviceToken,
ntfServer :: NtfServer,
ntfTokenId :: Maybe NtfTokenId,
-- TODO combine keys to key pair as the types should match
-- | key used by the ntf server to verify transmissions
ntfPubKey :: C.APublicVerifyKey,
ntfPubKey :: C.APublicAuthKey,
-- | key used by the ntf client to sign transmissions
ntfPrivKey :: C.APrivateSignKey,
ntfPrivKey :: C.APrivateAuthKey,
-- | client's DH keys (to repeat registration if necessary)
ntfDhKeys :: C.KeyPair 'C.X25519,
-- | shared DH secret used to encrypt/decrypt notifications e2e
@@ -63,7 +64,7 @@ data NtfToken = NtfToken
}
deriving (Show)
newNtfToken :: DeviceToken -> NtfServer -> C.ASignatureKeyPair -> C.KeyPair 'C.X25519 -> NotificationsMode -> NtfToken
newNtfToken :: DeviceToken -> NtfServer -> C.AAuthKeyPair -> C.KeyPair 'C.X25519 -> NotificationsMode -> NtfToken
newNtfToken deviceToken ntfServer (ntfPubKey, ntfPrivKey) ntfDhKeys ntfMode =
NtfToken
{ deviceToken,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+72 -6
View File
@@ -1,3 +1,4 @@
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
@@ -10,6 +11,7 @@
module Simplex.Messaging.Server.CLI where
import Control.Monad
import Data.ASN1.Types (asn1CharacterToString)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Either (fromRight)
@@ -17,6 +19,8 @@ import Data.Ini (Ini, lookupValue)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import qualified Data.X509 as X
import qualified Data.X509.File as XF
import Data.X509.Validation (Fingerprint (..))
import Network.Socket (HostName, ServiceName)
import Options.Applicative
@@ -27,12 +31,14 @@ import Simplex.Messaging.Transport.Server (loadFingerprint)
import Simplex.Messaging.Transport.WebSockets (WS)
import Simplex.Messaging.Util (eitherToMaybe, whenM)
import System.Directory (doesDirectoryExist, listDirectory, removeDirectoryRecursive, removePathForcibly)
import System.Environment (lookupEnv)
import System.Exit (exitFailure)
import System.FilePath (combine)
import System.IO (IOMode (..), hFlush, hGetLine, stdout, withFile)
import System.Process (readCreateProcess, shell)
import Text.Read (readMaybe)
exitError :: String -> IO ()
exitError :: String -> IO a
exitError msg = putStrLn msg >> exitFailure
confirmOrExit :: String -> IO ()
@@ -84,14 +90,18 @@ getCliCommand' cmdP version =
versionOption = infoOption version (long "version" <> short 'v' <> help "Show version")
createServerX509 :: FilePath -> X509Config -> IO ByteString
createServerX509 cfgPath x509cfg = do
createOpensslCaConf
createOpensslServerConf
createServerX509 = createServerX509_ True
createServerX509_ :: Bool -> FilePath -> X509Config -> IO ByteString
createServerX509_ createCA cfgPath x509cfg = do
let alg = show $ signAlgorithm (x509cfg :: X509Config)
-- CA certificate (identity/offline)
run $ "openssl genpkey -algorithm " <> alg <> " -out " <> c caKeyFile
run $ "openssl req -new -x509 -days 999999 -config " <> c opensslCaConfFile <> " -extensions v3 -key " <> c caKeyFile <> " -out " <> c caCrtFile
when createCA $ do
createOpensslCaConf
run $ "openssl genpkey -algorithm " <> alg <> " -out " <> c caKeyFile
run $ "openssl req -new -x509 -days 999999 -config " <> c opensslCaConfFile <> " -extensions v3 -key " <> c caKeyFile <> " -out " <> c caCrtFile
-- server certificate (online)
createOpensslServerConf
run $ "openssl genpkey -algorithm " <> alg <> " -out " <> c serverKeyFile
run $ "openssl req -new -config " <> c opensslServerConfFile <> " -reqexts v3 -key " <> c serverKeyFile <> " -out " <> c serverCsrFile
run $ "openssl x509 -req -days 999999 -extfile " <> c opensslServerConfFile <> " -extensions v3 -in " <> c serverCsrFile <> " -CA " <> c caCrtFile <> " -CAkey " <> c caKeyFile <> " -CAcreateserial -out " <> c serverCrtFile
@@ -131,6 +141,59 @@ createServerX509 cfgPath x509cfg = do
withFile (c fingerprintFile) WriteMode (`B.hPutStrLn` strEncode fp)
pure fp
data CertOptions = CertOptions
{ signAlgorithm_ :: Maybe SignAlgorithm,
commonName_ :: Maybe HostName
}
deriving (Show)
certOptionsP :: Parser CertOptions
certOptionsP = do
signAlgorithm_ <-
optional $
option
(maybeReader readMaybe)
( long "sign-algorithm"
<> short 'a'
<> help "Set new signature algorithm used for TLS certificates: ED25519, ED448"
<> metavar "ALG"
)
commonName_ <-
optional $
strOption
( long "cn"
<> help
"Set new Common Name for TLS online certificate"
<> metavar "FQDN"
)
pure CertOptions {signAlgorithm_, commonName_}
genOnline :: FilePath -> CertOptions -> IO ()
genOnline cfgPath CertOptions {signAlgorithm_, commonName_} = do
(signAlgorithm, commonName) <-
case (signAlgorithm_, commonName_) of
(Just alg, Just cn) -> pure (alg, cn)
_ ->
XF.readSignedObject certPath >>= \case
[old] -> either exitError pure . fromX509 . X.signedObject $ X.getSigned old
[] -> exitError $ "No certificate found at " <> certPath
_ -> exitError $ "Too many certificates at " <> certPath
let x509cfg = defaultX509Config {signAlgorithm, commonName}
void $ createServerX509_ False cfgPath x509cfg
putStrLn "Generated new server credentials"
warnCAPrivateKeyFile cfgPath x509cfg
where
certPath = combine cfgPath $ serverCrtFile defaultX509Config
fromX509 X.Certificate {certSignatureAlg, certSubjectDN} = (,) <$> maybe oldAlg Right signAlgorithm_ <*> maybe oldCN Right commonName_
where
oldAlg = case certSignatureAlg of
X.SignatureALG_IntrinsicHash X.PubKeyALG_Ed448 -> Right ED448
X.SignatureALG_IntrinsicHash X.PubKeyALG_Ed25519 -> Right ED25519
alg -> Left $ "Unexpected signature algorithm " <> show alg
oldCN = case X.getDnElement X.DnCommonName certSubjectDN of
Nothing -> Left "Certificate subject has no CN element"
Just cn -> maybe (Left "Certificate subject CN decoding failed") Right $ asn1CharacterToString cn
warnCAPrivateKeyFile :: FilePath -> X509Config -> IO ()
warnCAPrivateKeyFile cfgPath X509Config {caKeyFile} =
putStrLn $
@@ -235,3 +298,6 @@ printServiceInfo serverVersion srv@(ProtoServerWithAuth ProtocolServer {keyHash}
clearDirIfExists :: FilePath -> IO ()
clearDirIfExists path = whenM (doesDirectoryExist path) $ listDirectory path >>= mapM_ (removePathForcibly . combine path)
getEnvPath :: String -> FilePath -> IO FilePath
getEnvPath name def = maybe def (\case "" -> def; f -> f) <$> lookupEnv name
+7 -1
View File
@@ -6,9 +6,13 @@ module Simplex.Messaging.Server.Control where
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.ByteString (ByteString)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (BasicAuth)
data CPClientRole = CPRNone | CPRUser | CPRAdmin
data ControlProtocol
= CPSuspend
= CPAuth BasicAuth
| CPSuspend
| CPResume
| CPClients
| CPStats
@@ -24,6 +28,7 @@ data ControlProtocol
instance StrEncoding ControlProtocol where
strEncode = \case
CPAuth bs -> "auth " <> strEncode bs
CPSuspend -> "suspend"
CPResume -> "resume"
CPClients -> "clients"
@@ -39,6 +44,7 @@ instance StrEncoding ControlProtocol where
CPSkip -> ""
strP =
A.takeTill (== ' ') >>= \case
"auth" -> CPAuth <$> (A.space *> strP)
"suspend" -> pure CPSuspend
"resume" -> pure CPResume
"clients" -> pure CPClients
+46 -20
View File
@@ -10,6 +10,8 @@ import Control.Monad.IO.Unlift
import Crypto.Random
import Data.ByteString.Char8 (ByteString)
import Data.Int (Int64)
import Data.IntMap.Strict (IntMap)
import qualified Data.IntMap.Strict as IM
import Data.List.NonEmpty (NonEmpty)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
@@ -20,6 +22,7 @@ import Network.Socket (ServiceName)
import qualified Network.TLS as T
import Numeric.Natural
import Simplex.Messaging.Agent.Lock
import Simplex.Messaging.Client.Agent (SMPClientAgent, SMPClientAgentConfig, newSMPClientAgent)
import Simplex.Messaging.Crypto (KeyHash (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Protocol
@@ -31,9 +34,8 @@ import Simplex.Messaging.Server.Stats
import Simplex.Messaging.Server.StoreLog
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport (ATransport)
import Simplex.Messaging.Transport (ATransport, VersionRangeSMP, VersionSMP)
import Simplex.Messaging.Transport.Server (SocketState, TransportServerConfig, loadFingerprint, loadTLSServerParams, newSocketState)
import Simplex.Messaging.Version
import System.IO (IOMode (..))
import System.Mem.Weak (Weak)
import UnliftIO.STM
@@ -52,6 +54,9 @@ data ServerConfig = ServerConfig
allowNewQueues :: Bool,
-- | simple password that the clients need to pass in handshake to be able to create new queues
newQueueBasicAuth :: Maybe BasicAuth,
-- | control port passwords,
controlPortUserAuth :: Maybe BasicAuth,
controlPortAdminAuth :: Maybe BasicAuth,
-- | time after which the messages can be removed from the queues and check interval, seconds
messageExpiration :: Maybe ExpirationConfig,
-- | time after which the socket with inactive client can be disconnected (without any messages or commands, incl. PING),
@@ -71,11 +76,13 @@ data ServerConfig = ServerConfig
privateKeyFile :: FilePath,
certificateFile :: FilePath,
-- | SMP client-server protocol version range
smpServerVRange :: VersionRange,
smpServerVRange :: VersionRangeSMP,
-- | TCP transport config
transportConfig :: TransportServerConfig,
-- | run listener on control port
controlPort :: Maybe ServiceName
controlPort :: Maybe ServiceName,
smpAgentCfg :: SMPClientAgentConfig,
allowSMPProxy :: Bool -- auth is the same with `newQueueBasicAuth`
}
defMsgExpirationDays :: Int64
@@ -106,8 +113,9 @@ data Env = Env
tlsServerParams :: T.ServerParams,
serverStats :: ServerStats,
sockets :: SocketState,
clientSeq :: TVar Int,
clients :: TMap Int Client
clientSeq :: TVar ClientId,
clients :: TVar (IntMap Client),
proxyAgent :: ProxyAgent -- senders served on this proxy
}
data Server = Server
@@ -118,18 +126,27 @@ data Server = Server
savingLock :: Lock
}
data ProxyAgent = ProxyAgent
{ smpAgent :: SMPClientAgent
}
type ClientId = Int
data Client = Client
{ clientId :: Int,
{ clientId :: ClientId,
subscriptions :: TMap RecipientId (TVar Sub),
ntfSubscriptions :: TMap NotifierId (),
rcvQ :: TBQueue (NonEmpty (Maybe QueueRec, Transmission Cmd)),
sndQ :: TBQueue (NonEmpty (Transmission BrokerMsg)),
thVersion :: Version,
endThreads :: TVar (IntMap (Weak ThreadId)),
endThreadSeq :: TVar Int,
thVersion :: VersionSMP,
sessionId :: ByteString,
connected :: TVar Bool,
createdAt :: SystemTime,
rcvActiveAt :: TVar SystemTime,
sndActiveAt :: TVar SystemTime
sndActiveAt :: TVar SystemTime,
proxyClient_ :: TVar (Maybe C.DhSecretX25519) -- this client is actually an SMP proxy
}
data SubscriptionThread = NoSub | SubPending | SubThread (Weak ThreadId) | ProhibitSub
@@ -148,42 +165,46 @@ newServer = do
savingLock <- createLock
return Server {subscribedQ, subscribers, ntfSubscribedQ, notifiers, savingLock}
newClient :: TVar Int -> Natural -> Version -> ByteString -> SystemTime -> STM Client
newClient :: TVar ClientId -> Natural -> VersionSMP -> ByteString -> SystemTime -> STM Client
newClient nextClientId qSize thVersion sessionId createdAt = do
clientId <- stateTVar nextClientId $ \next -> (next, next + 1)
subscriptions <- TM.empty
ntfSubscriptions <- TM.empty
rcvQ <- newTBQueue qSize
sndQ <- newTBQueue qSize
endThreads <- newTVar IM.empty
endThreadSeq <- newTVar 0
connected <- newTVar True
rcvActiveAt <- newTVar createdAt
sndActiveAt <- newTVar createdAt
return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt}
proxyClient_ <- newTVar Nothing
return Client {clientId, subscriptions, ntfSubscriptions, rcvQ, sndQ, endThreads, endThreadSeq, thVersion, sessionId, connected, createdAt, rcvActiveAt, sndActiveAt, proxyClient_}
newSubscription :: SubscriptionThread -> STM Sub
newSubscription subThread = do
delivered <- newEmptyTMVar
return Sub {subThread, delivered}
newEnv :: forall m. (MonadUnliftIO m, MonadRandom m) => ServerConfig -> m Env
newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, storeLogFile} = do
newEnv :: ServerConfig -> IO Env
newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile, storeLogFile, smpAgentCfg} = do
server <- atomically newServer
queueStore <- atomically newQueueStore
msgStore <- atomically newMsgStore
random <- liftIO C.newRandom
storeLog <- restoreQueues queueStore `mapM` storeLogFile
tlsServerParams <- liftIO $ loadTLSServerParams caCertificateFile certificateFile privateKeyFile
Fingerprint fp <- liftIO $ loadFingerprint caCertificateFile
tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile
Fingerprint fp <- loadFingerprint caCertificateFile
let serverIdentity = KeyHash fp
serverStats <- atomically . newServerStats =<< liftIO getCurrentTime
serverStats <- atomically . newServerStats =<< getCurrentTime
sockets <- atomically newSocketState
clientSeq <- newTVarIO 0
clients <- atomically TM.empty
return Env {config, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerParams, serverStats, sockets, clientSeq, clients}
clients <- newTVarIO mempty
proxyAgent <- atomically $ newSMPProxyAgent smpAgentCfg random
return Env {config, server, serverIdentity, queueStore, msgStore, random, storeLog, tlsServerParams, serverStats, sockets, clientSeq, clients, proxyAgent}
where
restoreQueues :: QueueStore -> FilePath -> m (StoreLog 'WriteMode)
restoreQueues :: QueueStore -> FilePath -> IO (StoreLog 'WriteMode)
restoreQueues QueueStore {queues, senders, notifiers} f = do
(qs, s) <- liftIO $ readWriteStoreLog f
(qs, s) <- readWriteStoreLog f
atomically $ do
writeTVar queues =<< mapM newTVar qs
writeTVar senders $! M.foldr' addSender M.empty qs
@@ -195,3 +216,8 @@ newEnv config@ServerConfig {caCertificateFile, certificateFile, privateKeyFile,
addNotifier q = case notifier q of
Nothing -> id
Just NtfCreds {notifierId} -> M.insert notifierId (recipientId q)
newSMPProxyAgent :: SMPClientAgentConfig -> TVar ChaChaDRG -> STM ProxyAgent
newSMPProxyAgent smpAgentCfg random = do
smpAgent <- newSMPClientAgent smpAgentCfg random
pure ProxyAgent {smpAgent}
+24 -9
View File
@@ -18,6 +18,8 @@ import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Network.Socket (HostName)
import Options.Applicative
import Simplex.Messaging.Client (ProtocolClientConfig (..))
import Simplex.Messaging.Client.Agent (SMPClientAgentConfig (..), defaultSMPClientAgentConfig)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (ProtoServerWithAuth), pattern SMPServer)
@@ -25,10 +27,11 @@ import Simplex.Messaging.Server (runSMPServer)
import Simplex.Messaging.Server.CLI
import Simplex.Messaging.Server.Env.STM (ServerConfig (..), defMsgExpirationDays, defaultInactiveClientExpiration, defaultMessageExpiration)
import Simplex.Messaging.Server.Expiration
import Simplex.Messaging.Transport (simplexMQVersion, supportedSMPServerVRange)
import Simplex.Messaging.Transport (simplexMQVersion, supportedServerSMPRelayVRange, batchCmdsSMPVersion, sendingProxySMPVersion)
import Simplex.Messaging.Transport.Client (TransportHost (..))
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig)
import Simplex.Messaging.Util (safeDecodeUtf8)
import Simplex.Messaging.Version (mkVersionRange)
import System.Directory (createDirectoryIfMissing, doesFileExist)
import System.FilePath (combine)
import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
@@ -41,6 +44,10 @@ smpServerCLI cfgPath logPath =
doesFileExist iniFile >>= \case
True -> exitError $ "Error: server is already initialized (" <> iniFile <> " exists).\nRun `" <> executableName <> " start`."
_ -> initializeServer opts
OnlineCert certOpts ->
doesFileExist iniFile >>= \case
True -> genOnline cfgPath certOpts
_ -> exitError $ "Error: server is not initialized (" <> iniFile <> " does not exist).\nRun `" <> executableName <> " init`."
Start ->
doesFileExist iniFile >>= \case
True -> readIniFile iniFile >>= either exitError runServer
@@ -56,8 +63,8 @@ smpServerCLI cfgPath logPath =
defaultServerPort = "5223"
executableName = "smp-server"
storeLogFilePath = combine logPath "smp-server-store.log"
initializeServer opts
| scripted opts = initialize opts
initializeServer opts@InitOptions {ip, fqdn, scripted}
| scripted = initialize opts
| otherwise = do
putStrLn "Use `smp-server init -h` for available options."
void $ withPrompt "SMP server will be initialized (press Enter)" getLine
@@ -65,9 +72,9 @@ smpServerCLI cfgPath logPath =
logStats <- onOffPrompt "Enable logging daily statistics" False
putStrLn "Require a password to create new messaging queues?"
password <- withPrompt "'r' for random (default), 'n' - no password, or enter password: " serverPassword
let host = fromMaybe (ip opts) (fqdn opts)
let host = fromMaybe ip fqdn
host' <- withPrompt ("Enter server FQDN or IP address for certificate (" <> host <> "): ") getLine
initialize opts {enableStoreLog, logStats, fqdn = if null host' then fqdn opts else Just host', password}
initialize opts {enableStoreLog, logStats, fqdn = if null host' then fqdn else Just host', password}
where
serverPassword =
getLine >>= \case
@@ -78,7 +85,7 @@ smpServerCLI cfgPath logPath =
case strDecode $ encodeUtf8 $ T.pack s of
Right auth -> pure . Just $ ServerPassword auth
_ -> putStrLn "Invalid password. Only latin letters, digits and symbols other than '@' and ':' are allowed" >> serverPassword
initialize InitOptions {enableStoreLog, logStats, signAlgorithm, ip, fqdn, password} = do
initialize InitOptions {enableStoreLog, logStats, signAlgorithm, password} = do
clearDirIfExists cfgPath
clearDirIfExists logPath
createDirectoryIfMissing True cfgPath
@@ -124,6 +131,8 @@ smpServerCLI cfgPath logPath =
_ -> "# create_password: password to create new queues (any printable ASCII characters without whitespace, '@', ':' and '/')"
)
<> "\n\n\
\# control_port_admin_password:\n\
\# control_port_user_password:\n\
\[TRANSPORT]\n\
\# host is only used to print server address on start\n"
<> ("host: " <> host <> "\n")
@@ -185,6 +194,8 @@ smpServerCLI cfgPath logPath =
-- allow creating new queues by default
allowNewQueues = fromMaybe True $ iniOnOff "AUTH" "new_queues" ini,
newQueueBasicAuth = either error id <$> strDecodeIni "AUTH" "create_password" ini,
controlPortAdminAuth = either error id <$> strDecodeIni "AUTH" "control_port_admin_password" ini,
controlPortUserAuth = either error id <$> strDecodeIni "AUTH" "control_port_user_password" ini,
messageExpiration =
Just
defaultMessageExpiration
@@ -200,16 +211,19 @@ smpServerCLI cfgPath logPath =
logStatsStartTime = 0, -- seconds from 00:00 UTC
serverStatsLogFile = combine logPath "smp-server-stats.daily.log",
serverStatsBackupFile = logStats $> combine logPath "smp-server-stats.log",
smpServerVRange = supportedSMPServerVRange,
smpServerVRange = supportedServerSMPRelayVRange,
transportConfig =
defaultTransportServerConfig
{ logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini
},
controlPort = either (const Nothing) (Just . T.unpack) $ lookupValue "TRANSPORT" "control_port" ini
controlPort = either (const Nothing) (Just . T.unpack) $ lookupValue "TRANSPORT" "control_port" ini,
smpAgentCfg = defaultSMPClientAgentConfig {smpCfg = (smpCfg defaultSMPClientAgentConfig) {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion, agreeSecret = True}},
allowSMPProxy = True -- TODO: "get from INI"
}
data CliCommand
= Init InitOptions
| OnlineCert CertOptions
| Start
| Delete
@@ -231,6 +245,7 @@ cliCommandP :: FilePath -> FilePath -> FilePath -> Parser CliCommand
cliCommandP cfgPath logPath iniFile =
hsubparser
( command "init" (info (Init <$> initP) (progDesc $ "Initialize server - creates " <> cfgPath <> " and " <> logPath <> " directories and configuration files"))
<> command "cert" (info (OnlineCert <$> certOptionsP) (progDesc $ "Generate new online TLS server credentials (configuration: " <> iniFile <> ")"))
<> command "start" (info (pure Start) (progDesc $ "Start server (configuration: " <> iniFile <> ")"))
<> command "delete" (info (pure Delete) (progDesc "Delete configuration and log files"))
)
@@ -255,7 +270,7 @@ cliCommandP cfgPath logPath iniFile =
( long "sign-algorithm"
<> short 'a'
<> help "Signature algorithm used for TLS certificates: ED25519, ED448"
<> value ED448
<> value ED25519
<> showDefault
<> metavar "ALG"
)
+4 -7
View File
@@ -3,14 +3,11 @@
module Simplex.Messaging.Server.MsgStore where
import Control.Applicative ((<|>))
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (Message (..), RcvMessage (..), RecipientId)
import Simplex.Messaging.Protocol (Message (..), RecipientId)
data MsgLogRecord = MLRv3 RecipientId Message | MLRv1 RecipientId RcvMessage
data MsgLogRecord = MLRv3 RecipientId Message
instance StrEncoding MsgLogRecord where
strEncode = \case
MLRv3 rId msg -> strEncode (Str "v3", rId, msg)
MLRv1 rId msg -> strEncode (rId, msg)
strP = "v3 " *> (MLRv3 <$> strP_ <*> strP) <|> MLRv1 <$> strP_ <*> strP
strEncode (MLRv3 rId msg) = strEncode (Str "v3", rId, msg)
strP = "v3 " *> (MLRv3 <$> strP_ <*> strP)
+8 -9
View File
@@ -25,7 +25,6 @@ module Simplex.Messaging.Server.MsgStore.STM
where
import Control.Concurrent.STM.TQueue (flushTQueue)
import Control.Monad (when)
import qualified Data.ByteString.Char8 as B
import Data.Functor (($>))
import Data.Int (Int64)
@@ -116,15 +115,15 @@ tryDelPeekMsg mq msgId' =
| otherwise -> pure (Nothing, msg_)
_ -> pure (Nothing, Nothing)
deleteExpiredMsgs :: MsgQueue -> Int64 -> STM ()
deleteExpiredMsgs mq old = loop
deleteExpiredMsgs :: MsgQueue -> Int64 -> STM Int
deleteExpiredMsgs mq old = loop 0
where
loop = tryPeekMsg mq >>= mapM_ delOldMsg
delOldMsg = \case
Message {msgTs} ->
when (systemSeconds msgTs < old) $
tryDeleteMsg mq >> loop
_ -> pure ()
loop dc =
tryPeekMsg mq >>= \case
Just Message {msgTs}
| systemSeconds msgTs < old ->
tryDeleteMsg mq >> loop (dc + 1)
_ -> pure dc
tryDeleteMsg :: MsgQueue -> STM ()
tryDeleteMsg MsgQueue {msgQueue = q, size} =
+5 -5
View File
@@ -10,21 +10,21 @@ import Simplex.Messaging.Protocol
data QueueRec = QueueRec
{ recipientId :: !RecipientId,
recipientKey :: !RcvPublicVerifyKey,
recipientKey :: !RcvPublicAuthKey,
rcvDhSecret :: !RcvDhSecret,
senderId :: !SenderId,
senderKey :: !(Maybe SndPublicVerifyKey),
senderKey :: !(Maybe SndPublicAuthKey),
notifier :: !(Maybe NtfCreds),
status :: !ServerQueueStatus
}
deriving (Eq, Show)
deriving (Show)
data NtfCreds = NtfCreds
{ notifierId :: !NotifierId,
notifierKey :: !NtfPublicVerifyKey,
notifierKey :: !NtfPublicAuthKey,
rcvNtfDhSecret :: !RcvNtfDhSecret
}
deriving (Eq, Show)
deriving (Show)
instance StrEncoding NtfCreds where
strEncode NtfCreds {notifierId, notifierKey, rcvNtfDhSecret} = strEncode (notifierId, notifierKey, rcvNtfDhSecret)
@@ -54,7 +54,7 @@ addQueue QueueStore {queues, senders} q@QueueRec {recipientId = rId, senderId =
where
hasId = (||) <$> TM.member rId queues <*> TM.member sId senders
getQueue :: QueueStore -> SParty p -> QueueId -> STM (Either ErrorType QueueRec)
getQueue :: DirectParty p => QueueStore -> SParty p -> QueueId -> STM (Either ErrorType QueueRec)
getQueue QueueStore {queues, senders, notifiers} party qId =
toResult <$> (mapM readTVar =<< getVar)
where
@@ -63,7 +63,7 @@ getQueue QueueStore {queues, senders, notifiers} party qId =
SSender -> TM.lookup qId senders $>>= (`TM.lookup` queues)
SNotifier -> TM.lookup qId notifiers $>>= (`TM.lookup` queues)
secureQueue :: QueueStore -> RecipientId -> SndPublicVerifyKey -> STM (Either ErrorType QueueRec)
secureQueue :: QueueStore -> RecipientId -> SndPublicAuthKey -> STM (Either ErrorType QueueRec)
secureQueue QueueStore {queues} rId sKey =
withQueue rId queues $ \qVar ->
readTVar qVar >>= \q -> case senderKey q of
@@ -94,14 +94,14 @@ suspendQueue :: QueueStore -> RecipientId -> STM (Either ErrorType ())
suspendQueue QueueStore {queues} rId =
withQueue rId queues $ \qVar -> modifyTVar' qVar (\q -> q {status = QueueOff}) $> Just ()
deleteQueue :: QueueStore -> RecipientId -> STM (Either ErrorType ())
deleteQueue :: QueueStore -> RecipientId -> STM (Either ErrorType QueueRec)
deleteQueue QueueStore {queues, senders, notifiers} rId = do
TM.lookupDelete rId queues >>= \case
Just qVar ->
readTVar qVar >>= \q -> do
TM.delete (senderId q) senders
forM_ (notifier q) $ \NtfCreds {notifierId} -> TM.delete notifierId notifiers
pure $ Right ()
pure $ Right q
_ -> pure $ Left AUTH
toResult :: Maybe a -> Either ErrorType a
+35 -11
View File
@@ -3,6 +3,7 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
module Simplex.Messaging.Server.Stats where
@@ -22,9 +23,12 @@ data ServerStats = ServerStats
{ fromTime :: TVar UTCTime,
qCreated :: TVar Int,
qSecured :: TVar Int,
qDeleted :: TVar Int,
qDeletedAll :: TVar Int,
qDeletedNew :: TVar Int,
qDeletedSecured :: TVar Int,
msgSent :: TVar Int,
msgRecv :: TVar Int,
msgExpired :: TVar Int,
activeQueues :: PeriodStats RecipientId,
msgSentNtf :: TVar Int,
msgRecvNtf :: TVar Int,
@@ -37,9 +41,12 @@ data ServerStatsData = ServerStatsData
{ _fromTime :: UTCTime,
_qCreated :: Int,
_qSecured :: Int,
_qDeleted :: Int,
_qDeletedAll :: Int,
_qDeletedNew :: Int,
_qDeletedSecured :: Int,
_msgSent :: Int,
_msgRecv :: Int,
_msgExpired :: Int,
_activeQueues :: PeriodStatsData RecipientId,
_msgSentNtf :: Int,
_msgRecvNtf :: Int,
@@ -54,41 +61,50 @@ newServerStats ts = do
fromTime <- newTVar ts
qCreated <- newTVar 0
qSecured <- newTVar 0
qDeleted <- newTVar 0
qDeletedAll <- newTVar 0
qDeletedNew <- newTVar 0
qDeletedSecured <- newTVar 0
msgSent <- newTVar 0
msgRecv <- newTVar 0
msgExpired <- newTVar 0
activeQueues <- newPeriodStats
msgSentNtf <- newTVar 0
msgRecvNtf <- newTVar 0
activeQueuesNtf <- newPeriodStats
qCount <- newTVar 0
msgCount <- newTVar 0
pure ServerStats {fromTime, qCreated, qSecured, qDeleted, msgSent, msgRecv, activeQueues, msgSentNtf, msgRecvNtf, activeQueuesNtf, qCount, msgCount}
pure ServerStats {fromTime, qCreated, qSecured, qDeletedAll, qDeletedNew, qDeletedSecured, msgSent, msgRecv, msgExpired, activeQueues, msgSentNtf, msgRecvNtf, activeQueuesNtf, qCount, msgCount}
getServerStatsData :: ServerStats -> STM ServerStatsData
getServerStatsData s = do
_fromTime <- readTVar $ fromTime s
_qCreated <- readTVar $ qCreated s
_qSecured <- readTVar $ qSecured s
_qDeleted <- readTVar $ qDeleted s
_qDeletedAll <- readTVar $ qDeletedAll s
_qDeletedNew <- readTVar $ qDeletedNew s
_qDeletedSecured <- readTVar $ qDeletedSecured s
_msgSent <- readTVar $ msgSent s
_msgRecv <- readTVar $ msgRecv s
_msgExpired <- readTVar $ msgExpired s
_activeQueues <- getPeriodStatsData $ activeQueues s
_msgSentNtf <- readTVar $ msgSentNtf s
_msgRecvNtf <- readTVar $ msgRecvNtf s
_activeQueuesNtf <- getPeriodStatsData $ activeQueuesNtf s
_qCount <- readTVar $ qCount s
_msgCount <- readTVar $ msgCount s
pure ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeleted, _msgSent, _msgRecv, _activeQueues, _msgSentNtf, _msgRecvNtf, _activeQueuesNtf, _qCount, _msgCount}
pure ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _msgSent, _msgRecv, _msgExpired, _activeQueues, _msgSentNtf, _msgRecvNtf, _activeQueuesNtf, _qCount, _msgCount}
setServerStats :: ServerStats -> ServerStatsData -> STM ()
setServerStats s d = do
writeTVar (fromTime s) $! _fromTime d
writeTVar (qCreated s) $! _qCreated d
writeTVar (qSecured s) $! _qSecured d
writeTVar (qDeleted s) $! _qDeleted d
writeTVar (qDeletedAll s) $! _qDeletedAll d
writeTVar (qDeletedNew s) $! _qDeletedNew d
writeTVar (qDeletedSecured s) $! _qDeletedSecured d
writeTVar (msgSent s) $! _msgSent d
writeTVar (msgRecv s) $! _msgRecv d
writeTVar (msgExpired s) $! _msgExpired d
setPeriodStats (activeQueues s) (_activeQueues d)
writeTVar (msgSentNtf s) $! _msgSentNtf d
writeTVar (msgRecvNtf s) $! _msgRecvNtf d
@@ -97,14 +113,18 @@ setServerStats s d = do
writeTVar (msgCount s) $! _msgCount d
instance StrEncoding ServerStatsData where
strEncode ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeleted, _msgSent, _msgRecv, _msgSentNtf, _msgRecvNtf, _activeQueues, _activeQueuesNtf} =
strEncode ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _msgSent, _msgRecv, _msgExpired, _msgSentNtf, _msgRecvNtf, _activeQueues, _activeQueuesNtf, _qCount, _msgCount} =
B.unlines
[ "fromTime=" <> strEncode _fromTime,
"qCreated=" <> strEncode _qCreated,
"qSecured=" <> strEncode _qSecured,
"qDeleted=" <> strEncode _qDeleted,
"qDeletedAll=" <> strEncode _qDeletedAll,
"qDeletedNew=" <> strEncode _qDeletedNew,
"qDeletedSecured=" <> strEncode _qDeletedSecured,
"qCount=" <> strEncode _qCount,
"msgSent=" <> strEncode _msgSent,
"msgRecv=" <> strEncode _msgRecv,
"msgExpired=" <> strEncode _msgExpired,
"msgSentNtf=" <> strEncode _msgSentNtf,
"msgRecvNtf=" <> strEncode _msgRecvNtf,
"activeQueues:",
@@ -116,9 +136,13 @@ instance StrEncoding ServerStatsData where
_fromTime <- "fromTime=" *> strP <* A.endOfLine
_qCreated <- "qCreated=" *> strP <* A.endOfLine
_qSecured <- "qSecured=" *> strP <* A.endOfLine
_qDeleted <- "qDeleted=" *> strP <* A.endOfLine
(_qDeletedAll, _qDeletedNew, _qDeletedSecured) <-
(,0,0) <$> ("qDeleted=" *> strP <* A.endOfLine)
<|> ((,,) <$> ("qDeletedAll=" *> strP <* A.endOfLine) <*> ("qDeletedNew=" *> strP <* A.endOfLine) <*> ("qDeletedSecured=" *> strP <* A.endOfLine))
_qCount <- "qCount=" *> strP <* A.endOfLine <|> pure 0
_msgSent <- "msgSent=" *> strP <* A.endOfLine
_msgRecv <- "msgRecv=" *> strP <* A.endOfLine
_msgExpired <- "msgExpired=" *> strP <* A.endOfLine <|> pure 0
_msgSentNtf <- "msgSentNtf=" *> strP <* A.endOfLine <|> pure 0
_msgRecvNtf <- "msgRecvNtf=" *> strP <* A.endOfLine <|> pure 0
_activeQueues <-
@@ -133,7 +157,7 @@ instance StrEncoding ServerStatsData where
optional ("activeQueuesNtf:" <* A.endOfLine) >>= \case
Just _ -> strP <* optional A.endOfLine
_ -> pure newPeriodStatsData
pure ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeleted, _msgSent, _msgRecv, _msgSentNtf, _msgRecvNtf, _activeQueues, _activeQueuesNtf, _qCount = 0, _msgCount = 0}
pure ServerStatsData {_fromTime, _qCreated, _qSecured, _qDeletedAll, _qDeletedNew, _qDeletedSecured, _msgSent, _msgRecv, _msgExpired, _msgSentNtf, _msgRecvNtf, _activeQueues, _activeQueuesNtf, _qCount, _msgCount = 0}
data PeriodStats a = PeriodStats
{ day :: TVar (Set a),
+8 -7
View File
@@ -25,8 +25,8 @@ where
import Control.Applicative (optional, (<|>))
import Control.Monad (foldM, unless, when)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Functor (($>))
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
@@ -46,7 +46,7 @@ data StoreLog (a :: IOMode) where
data StoreLogRecord
= CreateQueue QueueRec
| SecureQueue QueueId SndPublicVerifyKey
| SecureQueue QueueId SndPublicAuthKey
| AddNotifier QueueId NtfCreds
| SuspendQueue QueueId
| DeleteQueue QueueId
@@ -114,13 +114,13 @@ closeStoreLog = \case
writeStoreLogRecord :: StrEncoding r => StoreLog 'WriteMode -> r -> IO ()
writeStoreLogRecord (WriteStoreLog _ h) r = do
B.hPutStrLn h $ strEncode r
B.hPut h $ strEncode r `B.snoc` '\n' -- hPutStrLn makes write non-atomic for length > 1024
hFlush h
logCreateQueue :: StoreLog 'WriteMode -> QueueRec -> IO ()
logCreateQueue s = writeStoreLogRecord s . CreateQueue
logSecureQueue :: StoreLog 'WriteMode -> QueueId -> SndPublicVerifyKey -> IO ()
logSecureQueue :: StoreLog 'WriteMode -> QueueId -> SndPublicAuthKey -> IO ()
logSecureQueue s qId sKey = writeStoreLogRecord s $ SecureQueue qId sKey
logAddNotifier :: StoreLog 'WriteMode -> QueueId -> NtfCreds -> IO ()
@@ -148,13 +148,14 @@ writeQueues s = mapM_ $ \q -> when (active q) $ logCreateQueue s q
active QueueRec {status} = status == QueueActive
readQueues :: FilePath -> IO (Map RecipientId QueueRec)
readQueues f = foldM processLine M.empty . B.lines =<< B.readFile f
readQueues f = foldM processLine M.empty . LB.lines =<< LB.readFile f
where
processLine :: Map RecipientId QueueRec -> ByteString -> IO (Map RecipientId QueueRec)
processLine m s = case strDecode $ trimCR s of
processLine :: Map RecipientId QueueRec -> LB.ByteString -> IO (Map RecipientId QueueRec)
processLine m s' = case strDecode $ trimCR s of
Right r -> pure $ procLogRecord r
Left e -> printError e $> m
where
s = LB.toStrict s'
procLogRecord :: StoreLogRecord -> Map RecipientId QueueRec
procLogRecord = \case
CreateQueue q -> M.insert (recipientId q) q m
+35
View File
@@ -0,0 +1,35 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Simplex.Messaging.ServiceScheme where
import Control.Applicative ((<|>))
import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Char8 as B
import Data.Functor (($>))
import Network.Socket (HostName, ServiceName)
import Simplex.Messaging.Encoding.String (StrEncoding (..))
data ServiceScheme = SSSimplex | SSAppServer SrvLoc
deriving (Eq, Show)
instance StrEncoding ServiceScheme where
strEncode = \case
SSSimplex -> "simplex:"
SSAppServer srv -> "https://" <> strEncode srv
strP =
"simplex:" $> SSSimplex
<|> "https://" *> (SSAppServer <$> strP)
data SrvLoc = SrvLoc HostName ServiceName
deriving (Eq, Ord, Show)
instance StrEncoding SrvLoc where
strEncode (SrvLoc host port) = B.pack $ host <> if null port then "" else ':' : port
strP = SrvLoc <$> host <*> (port <|> pure "")
where
host = B.unpack <$> A.takeWhile1 (A.notInClass ":#,;/ ")
port = show <$> (A.char ':' *> (A.decimal :: A.Parser Int))
simplexChat :: ServiceScheme
simplexChat = SSAppServer $ SrvLoc "simplex.chat" ""
+38
View File
@@ -0,0 +1,38 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Simplex.Messaging.Session where
import Control.Concurrent.STM
import Control.Monad
import Data.Composition ((.:.))
import Data.Functor (($>))
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
data SessionVar a = SessionVar
{ sessionVar :: TMVar a,
sessionVarId :: Int
}
getSessVar :: forall k a. Ord k => TVar Int -> k -> TMap k (SessionVar a) -> STM (Either (SessionVar a) (SessionVar a))
getSessVar sessSeq sessKey vs = maybe (Left <$> newSessionVar) (pure . Right) =<< TM.lookup sessKey vs
where
newSessionVar :: STM (SessionVar a)
newSessionVar = do
sessionVar <- newEmptyTMVar
sessionVarId <- stateTVar sessSeq $ \next -> (next, next + 1)
let v = SessionVar {sessionVar, sessionVarId}
TM.insert sessKey v vs
pure v
removeSessVar :: Ord k => SessionVar a -> k -> TMap k (SessionVar a) -> STM ()
removeSessVar = void .:. removeSessVar'
{-# INLINE removeSessVar #-}
removeSessVar' :: Ord k => SessionVar a -> k -> TMap k (SessionVar a) -> STM Bool
removeSessVar' v sessKey vs =
TM.lookup sessKey vs >>= \case
Just v' | sessionVarId v == sessionVarId v' -> TM.delete sessKey vs $> True
_ -> pure False
+228 -54
View File
@@ -5,11 +5,14 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
-- |
@@ -26,7 +29,19 @@
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a
module Simplex.Messaging.Transport
( -- * SMP transport parameters
supportedSMPServerVRange,
SMPVersion,
VersionSMP,
VersionRangeSMP,
THandleSMP,
supportedClientSMPRelayVRange,
supportedServerSMPRelayVRange,
currentClientSMPRelayVersion,
currentServerSMPRelayVersion,
batchCmdsSMPVersion,
basicAuthSMPVersion,
subModeSMPVersion,
authCmdsSMPVersion,
sendingProxySMPVersion,
simplexMQVersion,
smpBlockSize,
TransportConfig (..),
@@ -36,10 +51,12 @@ module Simplex.Messaging.Transport
TProxy (..),
ATransport (..),
TransportPeer (..),
getServerVerifyKey,
-- * TLS Transport
TLS (..),
SessionId,
ALPN,
connectTLS,
closeTLS,
supportedParameters,
@@ -47,6 +64,8 @@ module Simplex.Messaging.Transport
-- * SMP transport
THandle (..),
THandleParams (..),
THandleAuth (..),
TransportError (..),
HandshakeError (..),
smpServerHandshake,
@@ -60,19 +79,24 @@ module Simplex.Messaging.Transport
)
where
import Control.Applicative ((<|>))
import Control.Applicative (optional, (<|>))
import Control.Monad (forM)
import Control.Monad.Except
import Control.Monad.Trans.Except (throwE)
import qualified Data.Aeson.TH as J
import Data.Attoparsec.ByteString.Char8 (Parser)
import Data.Bifunctor (first)
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bifunctor (bimap, first)
import Data.Bitraversable (bimapM)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Default (def)
import Data.Functor (($>))
import Data.Version (showVersion)
import Data.Word (Word16)
import qualified Data.X509 as X
import qualified Data.X509.Validation as XV
import GHC.IO.Handle.Internals (ioe_EOF)
import Network.Socket
import qualified Network.TLS as T
@@ -80,21 +104,73 @@ import qualified Network.TLS.Extra as TE
import qualified Paths_simplexmq as SMQ
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Parsers (dropPrefix, parse, parseRead1, sumTypeJSON)
import Simplex.Messaging.Parsers (dropPrefix, parseRead1, sumTypeJSON)
import Simplex.Messaging.Transport.Buffer
import Simplex.Messaging.Util (bshow, catchAll, catchAll_)
import Simplex.Messaging.Util (bshow, catchAll, catchAll_, liftEitherWith)
import Simplex.Messaging.Version
import Simplex.Messaging.Version.Internal
import UnliftIO.Exception (Exception)
import qualified UnliftIO.Exception as E
import UnliftIO.STM
-- * Transport parameters
-- min size it works with:
-- unsigned message: 16292 (paddedProxiedMsgLength = 16151, paddedForwardedMsgLength = 16239)
-- Ed448: 16406 (16384 + 22, fails with 21)
-- Ed25519: 16356
-- X25519: 16381
smpBlockSize :: Int
smpBlockSize = 16384
supportedSMPServerVRange :: VersionRange
supportedSMPServerVRange = mkVersionRange 1 6
-- SMP protocol version history:
-- 1 - binary protocol encoding (1/1/2022)
-- 2 - message flags (used to control notifications, 6/6/2022)
-- 3 - encrypt message timestamp and flags together with the body when delivered to the recipient (7/5/2022)
-- 4 - support command batching (7/17/2022)
-- 5 - basic auth for SMP servers (11/12/2022)
-- 6 - allow creating queues without subscribing (9/10/2023)
-- 7 - support authenticated encryption to verify senders' commands, imply but do NOT send session ID in signed part (2/3/2024)
data SMPVersion
instance VersionScope SMPVersion
type VersionSMP = Version SMPVersion
type VersionRangeSMP = VersionRange SMPVersion
pattern VersionSMP :: Word16 -> VersionSMP
pattern VersionSMP v = Version v
batchCmdsSMPVersion :: VersionSMP
batchCmdsSMPVersion = VersionSMP 4
basicAuthSMPVersion :: VersionSMP
basicAuthSMPVersion = VersionSMP 5
subModeSMPVersion :: VersionSMP
subModeSMPVersion = VersionSMP 6
authCmdsSMPVersion :: VersionSMP
authCmdsSMPVersion = VersionSMP 7
sendingProxySMPVersion :: VersionSMP
sendingProxySMPVersion = VersionSMP 8
currentClientSMPRelayVersion :: VersionSMP
currentClientSMPRelayVersion = VersionSMP 6
currentServerSMPRelayVersion :: VersionSMP
currentServerSMPRelayVersion = VersionSMP 6
-- minimal supported protocol version is 4
-- TODO remove code that supports sending commands without batching
supportedClientSMPRelayVRange :: VersionRangeSMP
supportedClientSMPRelayVRange = mkVersionRange batchCmdsSMPVersion currentClientSMPRelayVersion
supportedServerSMPRelayVRange :: VersionRangeSMP
supportedServerSMPRelayVRange = mkVersionRange batchCmdsSMPVersion currentServerSMPRelayVersion
simplexMQVersion :: String
simplexMQVersion = showVersion SMQ.version
@@ -117,10 +193,12 @@ class Transport c where
transportConfig :: c -> TransportConfig
-- | Upgrade server TLS context to connection (used in the server)
getServerConnection :: TransportConfig -> T.Context -> IO c
getServerConnection :: TransportConfig -> X.CertificateChain -> T.Context -> IO c
-- | Upgrade client TLS context to connection (used in the client)
getClientConnection :: TransportConfig -> T.Context -> IO c
getClientConnection :: TransportConfig -> X.CertificateChain -> T.Context -> IO c
getServerCerts :: c -> X.CertificateChain
-- | tls-unique channel binding per RFC5929
tlsUnique :: c -> SessionId
@@ -148,6 +226,12 @@ data TProxy c = TProxy
data ATransport = forall c. Transport c => ATransport (TProxy c)
getServerVerifyKey :: Transport c => c -> Either String C.APublicVerifyKey
getServerVerifyKey c =
case getServerCerts c of
X.CertificateChain (server : _ca) -> C.x509ToPublic (X.certPubKey . X.signedObject $ X.getSigned server, []) >>= C.pubKey
_ -> Left "no certificate chain"
-- * TLS Transport
data TLS = TLS
@@ -155,9 +239,13 @@ data TLS = TLS
tlsPeer :: TransportPeer,
tlsUniq :: ByteString,
tlsBuffer :: TBuffer,
tlsALPN :: Maybe ALPN,
tlsServerCerts :: X.CertificateChain,
tlsTransportConfig :: TransportConfig
}
type ALPN = ByteString
connectTLS :: T.TLSParams p => Maybe HostName -> TransportConfig -> p -> Socket -> IO T.Context
connectTLS host_ TransportConfig {logTLSErrors} params sock =
E.bracketOnError (T.contextNew sock params) closeTLS $ \ctx ->
@@ -167,12 +255,13 @@ connectTLS host_ TransportConfig {logTLSErrors} params sock =
logThrow e = putStrLn ("TLS error" <> host <> ": " <> show e) >> E.throwIO e
host = maybe "" (\h -> " (" <> h <> ")") host_
getTLS :: TransportPeer -> TransportConfig -> T.Context -> IO TLS
getTLS tlsPeer cfg cxt = withTlsUnique tlsPeer cxt newTLS
getTLS :: TransportPeer -> TransportConfig -> X.CertificateChain -> T.Context -> IO TLS
getTLS tlsPeer cfg tlsServerCerts cxt = withTlsUnique tlsPeer cxt newTLS
where
newTLS tlsUniq = do
tlsBuffer <- atomically newTBuffer
pure TLS {tlsContext = cxt, tlsTransportConfig = cfg, tlsPeer, tlsUniq, tlsBuffer}
tlsALPN <- T.getNegotiatedProtocol cxt
pure TLS {tlsContext = cxt, tlsALPN, tlsTransportConfig = cfg, tlsServerCerts, tlsPeer, tlsUniq, tlsBuffer}
withTlsUnique :: TransportPeer -> T.Context -> (ByteString -> IO c) -> IO c
withTlsUnique peer cxt f =
@@ -207,6 +296,7 @@ instance Transport TLS where
transportConfig = tlsTransportConfig
getServerConnection = getTLS TServer
getClientConnection = getTLS TClient
getServerCerts = tlsServerCerts
tlsUnique = tlsUniq
closeConnection tls = closeTLS $ tlsContext tls
@@ -217,8 +307,8 @@ instance Transport TLS where
getBuffered tlsBuffer n t_ (T.recvData tlsContext)
cPut :: TLS -> ByteString -> IO ()
cPut TLS {tlsContext, tlsTransportConfig = TransportConfig {transportTimeout = t_}} s =
withTimedErr t_ . T.sendData tlsContext $ BL.fromStrict s
cPut TLS {tlsContext, tlsTransportConfig = TransportConfig {transportTimeout = t_}} =
withTimedErr t_ . T.sendData tlsContext . LB.fromStrict
getLn :: TLS -> IO ByteString
getLn TLS {tlsContext, tlsBuffer} = do
@@ -230,45 +320,96 @@ instance Transport TLS where
-- * SMP transport
-- | The handle for SMP encrypted transport connection over Transport .
data THandle c = THandle
-- | The handle for SMP encrypted transport connection over Transport.
data THandle v c p = THandle
{ connection :: c,
sessionId :: SessionId,
params :: THandleParams v p
}
type THandleSMP c p = THandle SMPVersion c p
data THandleParams v p = THandleParams
{ sessionId :: SessionId,
blockSize :: Int,
-- | agreed server protocol version
thVersion :: Version,
thVersion :: Version v,
-- | peer public key for command authorization and shared secrets for entity ID encryption
thAuth :: Maybe (THandleAuth p),
-- | do NOT send session ID in transmission, but include it into signed message
-- based on protocol version
implySessId :: Bool,
-- | send multiple transmissions in a single block
-- based on protocol and protocol version
-- based on protocol version
batch :: Bool
}
data THandleAuth (p :: TransportPeer) where
THAuthClient ::
{ serverPeerPubKey :: C.PublicKeyX25519, -- used by the client to combine with client's private per-queue key
serverCertKey :: (X.CertificateChain, X.SignedExact X.PubKey), -- the key here is serverPeerPubKey signed with server certificate
sessSecret :: Maybe C.DhSecretX25519 -- session secret (will be used in SMP proxy only)
} ->
THandleAuth 'TClient
THAuthServer ::
{ serverPrivKey :: C.PrivateKeyX25519, -- used by the server to combine with client's public per-queue key
sessSecret' :: Maybe C.DhSecretX25519 -- session secret (will be used in SMP proxy only)
} ->
THandleAuth 'TServer
-- | TLS-unique channel binding
type SessionId = ByteString
data ServerHandshake = ServerHandshake
{ smpVersionRange :: VersionRange,
sessionId :: SessionId
{ smpVersionRange :: VersionRangeSMP,
sessionId :: SessionId,
-- pub key to agree shared secrets for command authorization and entity ID encryption.
-- todo C.PublicKeyX25519
authPubKey :: Maybe (X.CertificateChain, X.SignedExact X.PubKey)
}
data ClientHandshake = ClientHandshake
{ -- | agreed SMP server protocol version
smpVersion :: Version,
smpVersion :: VersionSMP,
-- | server identity - CA certificate fingerprint
keyHash :: C.KeyHash
keyHash :: C.KeyHash,
-- pub key to agree shared secret for entity ID encryption, shared secret for command authorization is agreed using per-queue keys.
authPubKey :: Maybe C.PublicKeyX25519
}
instance Encoding ClientHandshake where
smpEncode ClientHandshake {smpVersion, keyHash} = smpEncode (smpVersion, keyHash)
smpEncode ClientHandshake {smpVersion, keyHash, authPubKey} =
smpEncode (smpVersion, keyHash) <> encodeAuthEncryptCmds smpVersion authPubKey
smpP = do
(smpVersion, keyHash) <- smpP
pure ClientHandshake {smpVersion, keyHash}
-- TODO drop SMP v6: remove special parser and make key non-optional
authPubKey <- authEncryptCmdsP smpVersion smpP
pure ClientHandshake {smpVersion, keyHash, authPubKey}
instance Encoding ServerHandshake where
smpEncode ServerHandshake {smpVersionRange, sessionId} =
smpEncode (smpVersionRange, sessionId)
smpEncode ServerHandshake {smpVersionRange, sessionId, authPubKey} =
smpEncode (smpVersionRange, sessionId) <> auth
where
auth =
encodeAuthEncryptCmds (maxVersion smpVersionRange) $
bimap C.encodeCertChain C.SignedObject <$> authPubKey
smpP = do
(smpVersionRange, sessionId) <- smpP
pure ServerHandshake {smpVersionRange, sessionId}
-- TODO drop SMP v6: remove special parser and make key non-optional
authPubKey <- authEncryptCmdsP (maxVersion smpVersionRange) authP
pure ServerHandshake {smpVersionRange, sessionId, authPubKey}
where
authP = do
cert <- C.certChainP
C.SignedObject key <- smpP
pure (cert, key)
encodeAuthEncryptCmds :: Encoding a => VersionSMP -> Maybe a -> ByteString
encodeAuthEncryptCmds v k
| v >= authCmdsSMPVersion = maybe "" smpEncode k
| otherwise = ""
authEncryptCmdsP :: VersionSMP -> Parser a -> Parser (Maybe a)
authEncryptCmdsP v p = if v >= authCmdsSMPVersion then optional p else pure Nothing
-- | Error of SMP encrypted transport over TCP.
data TransportError
@@ -278,6 +419,9 @@ data TransportError
TELargeMsg
| -- | incorrect session ID
TEBadSession
| -- | absent server key for v7 entity
-- This error happens when the server did not provide a DH key to authorize commands for the queue that should be authorized with a DH key.
TENoServerAuth
| -- | transport handshake error
TEHandshake {handshakeErr :: HandshakeError}
deriving (Eq, Read, Show, Exception)
@@ -290,6 +434,8 @@ data HandshakeError
VERSION
| -- | incorrect server identity
IDENTITY
| -- | v7 authentication failed
BAD_AUTH
deriving (Eq, Read, Show, Exception)
-- | SMP encrypted transport error parser.
@@ -298,6 +444,7 @@ transportErrorP =
"BLOCK" $> TEBadBlock
<|> "LARGE_MSG" $> TELargeMsg
<|> "SESSION" $> TEBadSession
<|> "NO_AUTH" $> TENoServerAuth
<|> "HANDSHAKE " *> (TEHandshake <$> parseRead1)
-- | Serialize SMP encrypted transport error.
@@ -306,17 +453,18 @@ serializeTransportError = \case
TEBadBlock -> "BLOCK"
TELargeMsg -> "LARGE_MSG"
TEBadSession -> "SESSION"
TENoServerAuth -> "NO_AUTH"
TEHandshake e -> "HANDSHAKE " <> bshow e
-- | Pad and send block to SMP transport.
tPutBlock :: Transport c => THandle c -> ByteString -> IO (Either TransportError ())
tPutBlock THandle {connection = c, blockSize} block =
tPutBlock :: Transport c => THandle v c p -> ByteString -> IO (Either TransportError ())
tPutBlock THandle {connection = c, params = THandleParams {blockSize}} block =
bimapM (const $ pure TELargeMsg) (cPut c) $
C.pad block blockSize
-- | Receive block from SMP transport.
tGetBlock :: Transport c => THandle c -> IO (Either TransportError ByteString)
tGetBlock THandle {connection = c, blockSize} = do
tGetBlock :: Transport c => THandle v c p -> IO (Either TransportError ByteString)
tGetBlock THandle {connection = c, params = THandleParams {blockSize}} = do
msg <- cGet c blockSize
if B.length msg == blockSize
then pure . first (const TELargeMsg) $ C.unPad msg
@@ -325,44 +473,70 @@ tGetBlock THandle {connection = c, blockSize} = do
-- | Server SMP transport handshake.
--
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a
smpServerHandshake :: forall c. Transport c => c -> C.KeyHash -> VersionRange -> ExceptT TransportError IO (THandle c)
smpServerHandshake c kh smpVRange = do
let th@THandle {sessionId} = smpTHandle c
sendHandshake th $ ServerHandshake {sessionId, smpVersionRange = smpVRange}
smpServerHandshake :: forall c. Transport c => C.APrivateSignKey -> c -> C.KeyPairX25519 -> C.KeyHash -> VersionRangeSMP -> ExceptT TransportError IO (THandleSMP c 'TServer)
smpServerHandshake serverSignKey c (k, pk) kh smpVRange = do
let th@THandle {params = THandleParams {sessionId}} = smpTHandle c
sk = C.signX509 serverSignKey $ C.publicToX509 k
certChain = getServerCerts c
sendHandshake th $ ServerHandshake {sessionId, smpVersionRange = smpVRange, authPubKey = Just (certChain, sk)}
getHandshake th >>= \case
ClientHandshake {smpVersion, keyHash}
ClientHandshake {smpVersion = v, keyHash, authPubKey = k'}
| keyHash /= kh ->
throwE $ TEHandshake IDENTITY
| smpVersion `isCompatible` smpVRange -> do
pure $ smpThHandle th smpVersion
| v `isCompatible` smpVRange ->
pure $ smpThHandleServer th v pk k'
| otherwise -> throwE $ TEHandshake VERSION
-- | Client SMP transport handshake.
--
-- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a
smpClientHandshake :: forall c. Transport c => c -> C.KeyHash -> VersionRange -> ExceptT TransportError IO (THandle c)
smpClientHandshake c keyHash smpVRange = do
let th@THandle {sessionId} = smpTHandle c
ServerHandshake {sessionId = sessId, smpVersionRange} <- getHandshake th
smpClientHandshake :: forall c. Transport c => c -> Maybe C.KeyPairX25519 -> C.KeyHash -> VersionRangeSMP -> ExceptT TransportError IO (THandleSMP c 'TClient)
smpClientHandshake c ks_ keyHash@(C.KeyHash kh) smpVRange = do
let th@THandle {params = THandleParams {sessionId}} = smpTHandle c
ServerHandshake {sessionId = sessId, smpVersionRange, authPubKey} <- getHandshake th
if sessionId /= sessId
then throwE TEBadSession
else case smpVersionRange `compatibleVersion` smpVRange of
Just (Compatible smpVersion) -> do
sendHandshake th $ ClientHandshake {smpVersion, keyHash}
pure $ smpThHandle th smpVersion
Just (Compatible v) -> do
ck_ <- forM authPubKey $ \certKey@(X.CertificateChain cert, exact) ->
liftEitherWith (const $ TEHandshake BAD_AUTH) $ do
case cert of
[_leaf, ca] | XV.Fingerprint kh == XV.getFingerprint ca X.HashSHA256 -> pure ()
_ -> throwError "bad certificate"
serverKey <- getServerVerifyKey c
pubKey <- C.verifyX509 serverKey exact
(,certKey) <$> (C.x509ToPublic (pubKey, []) >>= C.pubKey)
sendHandshake th $ ClientHandshake {smpVersion = v, keyHash, authPubKey = fst <$> ks_}
pure $ smpThHandleClient th v (snd <$> ks_) ck_
Nothing -> throwE $ TEHandshake VERSION
smpThHandle :: forall c. THandle c -> Version -> THandle c
smpThHandle th v = (th :: THandle c) {thVersion = v, batch = v >= 4}
smpThHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> C.PrivateKeyX25519 -> Maybe C.PublicKeyX25519 -> THandleSMP c 'TServer
smpThHandleServer th v pk k_ =
let thAuth = THAuthServer {serverPrivKey = pk, sessSecret' = (`C.dh'` pk) <$> k_}
in smpThHandle_ th v (Just thAuth)
sendHandshake :: (Transport c, Encoding smp) => THandle c -> smp -> ExceptT TransportError IO ()
smpThHandleClient :: forall c. THandleSMP c 'TClient -> VersionSMP -> Maybe C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, (X.CertificateChain, X.SignedExact X.PubKey)) -> THandleSMP c 'TClient
smpThHandleClient th v pk_ ck_ =
let thAuth = (\(k, ck) -> THAuthClient {serverPeerPubKey = k, serverCertKey = ck, sessSecret = C.dh' k <$> pk_}) <$> ck_
in smpThHandle_ th v thAuth
smpThHandle_ :: forall c p. THandleSMP c p -> VersionSMP -> Maybe (THandleAuth p) -> THandleSMP c p
smpThHandle_ th@THandle {params} v thAuth =
-- TODO drop SMP v6: make thAuth non-optional
let params' = params {thVersion = v, thAuth, implySessId = v >= authCmdsSMPVersion}
in (th :: THandleSMP c p) {params = params'}
sendHandshake :: (Transport c, Encoding smp) => THandle v c p -> smp -> ExceptT TransportError IO ()
sendHandshake th = ExceptT . tPutBlock th . smpEncode
getHandshake :: (Transport c, Encoding smp) => THandle c -> ExceptT TransportError IO smp
getHandshake th = ExceptT $ (parse smpP (TEHandshake PARSE) =<<) <$> tGetBlock th
-- ignores tail bytes to allow future extensions
getHandshake :: (Transport c, Encoding smp) => THandle v c p -> ExceptT TransportError IO smp
getHandshake th = ExceptT $ (first (\_ -> TEHandshake PARSE) . A.parseOnly smpP =<<) <$> tGetBlock th
smpTHandle :: Transport c => c -> THandle c
smpTHandle c = THandle {connection = c, sessionId = tlsUnique c, blockSize = smpBlockSize, thVersion = 0, batch = False}
smpTHandle :: Transport c => c -> THandleSMP c p
smpTHandle c = THandle {connection = c, params}
where
params = THandleParams {sessionId = tlsUnique c, blockSize = smpBlockSize, thVersion = VersionSMP 0, thAuth = Nothing, implySessId = False, batch = True}
$(J.deriveJSON (sumTypeJSON id) ''HandshakeError)
+35 -18
View File
@@ -17,11 +17,13 @@ module Simplex.Messaging.Transport.Client
TransportHost (..),
TransportHosts (..),
TransportHosts_ (..),
validateCertificateChain
)
where
import Control.Applicative (optional)
import Control.Monad.IO.Unlift
import Control.Logger.Simple (logError)
import Control.Monad (when)
import Data.Aeson (FromJSON (..), ToJSON (..))
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.ByteString.Char8 (ByteString)
@@ -48,11 +50,12 @@ import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (parseAll, parseString)
import Simplex.Messaging.Transport
import Simplex.Messaging.Transport.KeepAlive
import Simplex.Messaging.Util (bshow, (<$?>))
import Simplex.Messaging.Util (bshow, (<$?>), catchAll, tshow)
import System.IO.Error
import Text.Read (readMaybe)
import UnliftIO.Exception (IOException)
import qualified UnliftIO.Exception as E
import UnliftIO.STM
data TransportHost
= THIPv4 (Word8, Word8, Word8, Word8)
@@ -111,34 +114,42 @@ data TransportClientConfig = TransportClientConfig
{ socksProxy :: Maybe SocksProxy,
tcpKeepAlive :: Maybe KeepAliveOpts,
logTLSErrors :: Bool,
clientCredentials :: Maybe (X.CertificateChain, T.PrivKey)
clientCredentials :: Maybe (X.CertificateChain, T.PrivKey),
alpn :: Maybe [ALPN]
}
deriving (Eq, Show)
defaultTransportClientConfig :: TransportClientConfig
defaultTransportClientConfig = TransportClientConfig Nothing (Just defaultKeepAliveOpts) True Nothing
defaultTransportClientConfig = TransportClientConfig Nothing (Just defaultKeepAliveOpts) True Nothing Nothing
clientTransportConfig :: TransportClientConfig -> TransportConfig
clientTransportConfig TransportClientConfig {logTLSErrors} =
TransportConfig {logTLSErrors, transportTimeout = Nothing}
-- | Connect to passed TCP host:port and pass handle to the client.
runTransportClient :: (Transport c, MonadUnliftIO m) => TransportClientConfig -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> m a) -> m a
runTransportClient :: Transport c => TransportClientConfig -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> IO a) -> IO a
runTransportClient = runTLSTransportClient supportedParameters Nothing
runTLSTransportClient :: (Transport c, MonadUnliftIO m) => T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> m a) -> m a
runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy, tcpKeepAlive, clientCredentials} proxyUsername host port keyHash client = do
runTLSTransportClient :: Transport c => T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (c -> IO a) -> IO a
runTLSTransportClient tlsParams caStore_ cfg@TransportClientConfig {socksProxy, tcpKeepAlive, clientCredentials, alpn} proxyUsername host port keyHash client = do
serverCert <- newEmptyTMVarIO
let hostName = B.unpack $ strEncode host
clientParams = mkTLSClientParams tlsParams caStore_ hostName port keyHash clientCredentials
clientParams = mkTLSClientParams tlsParams caStore_ hostName port keyHash clientCredentials alpn serverCert
connectTCP = case socksProxy of
Just proxy -> connectSocksClient proxy proxyUsername $ hostAddr host
_ -> connectTCPClient hostName
c <- liftIO $ do
c <- do
sock <- connectTCP port
mapM_ (setSocketKeepAlive sock) tcpKeepAlive
mapM_ (setSocketKeepAlive sock) tcpKeepAlive `catchAll` \e -> logError ("Error setting TCP keep-alive" <> tshow e)
let tCfg = clientTransportConfig cfg
connectTLS (Just hostName) tCfg clientParams sock >>= getClientConnection tCfg
client c `E.finally` liftIO (closeConnection c)
connectTLS (Just hostName) tCfg clientParams sock >>= \tls -> do
chain <- atomically (tryTakeTMVar serverCert) >>= \case
Nothing -> do
logError "onServerCertificate didn't fire or failed to get cert chain"
closeTLS tls >> error "onServerCertificate failed"
Just c -> pure c
getClientConnection tCfg chain tls
client c `E.finally` closeConnection c
where
hostAddr = \case
THIPv4 addr -> SocksAddrIPV4 $ tupleToHostAddress addr
@@ -206,24 +217,30 @@ instance ToJSON SocksProxy where
instance FromJSON SocksProxy where
parseJSON = strParseJSON "SocksProxy"
mkTLSClientParams :: T.Supported -> Maybe XS.CertificateStore -> HostName -> ServiceName -> Maybe C.KeyHash -> Maybe (X.CertificateChain, T.PrivKey) -> T.ClientParams
mkTLSClientParams supported caStore_ host port cafp_ clientCreds_ =
mkTLSClientParams :: T.Supported -> Maybe XS.CertificateStore -> HostName -> ServiceName -> Maybe C.KeyHash -> Maybe (X.CertificateChain, T.PrivKey) -> Maybe [ALPN] -> TMVar X.CertificateChain -> T.ClientParams
mkTLSClientParams supported caStore_ host port cafp_ clientCreds_ alpn_ serverCerts =
(T.defaultParamsClient host p)
{ T.clientShared = def {T.sharedCAStore = fromMaybe (T.sharedCAStore def) caStore_},
T.clientHooks =
def
{ T.onServerCertificate = maybe def (\cafp _ _ _ -> validateCertificateChain cafp host p) cafp_,
T.onCertificateRequest = maybe def (const . pure . Just) clientCreds_
{ T.onServerCertificate = onServerCert,
T.onCertificateRequest = maybe def (const . pure . Just) clientCreds_,
T.onSuggestALPN = pure alpn_
},
T.clientSupported = supported
}
where
p = B.pack port
onServerCert _ _ _ c = do
errs <- maybe def (\ca -> validateCertificateChain ca host p c) cafp_
when (null errs) $
atomically (putTMVar serverCerts c)
pure errs
validateCertificateChain :: C.KeyHash -> HostName -> ByteString -> X.CertificateChain -> IO [XV.FailedReason]
validateCertificateChain _ _ _ (X.CertificateChain []) = pure [XV.EmptyChain]
validateCertificateChain _ _ _ (X.CertificateChain [_]) = pure [XV.EmptyChain]
validateCertificateChain (C.KeyHash kh) host port cc@(X.CertificateChain sc@[_, caCert]) =
validateCertificateChain (C.KeyHash kh) host port cc@(X.CertificateChain [_, caCert]) =
if Fingerprint kh == XV.getFingerprint caCert X.HashSHA256
then x509validate
else pure [XV.UnknownCA]
@@ -233,7 +250,7 @@ validateCertificateChain (C.KeyHash kh) host port cc@(X.CertificateChain sc@[_,
where
hooks = XV.defaultHooks
checks = XV.defaultChecks {XV.checkFQHN = False}
certStore = XS.makeCertificateStore sc
certStore = XS.makeCertificateStore [caCert]
cache = XV.exceptionValidationCache [] -- we manually check fingerprint only of the identity certificate (ca.crt)
serviceID = (host, port)
validateCertificateChain _ _ _ _ = pure [XV.AuthorityTooDeep]
+3 -3
View File
@@ -16,15 +16,15 @@ import qualified Network.HTTP2.Server as HS
import Network.Socket (SockAddr (..))
import qualified Network.TLS as T
import qualified Network.TLS.Extra as TE
import Simplex.Messaging.Transport (SessionId, TLS (tlsUniq), Transport (cGet, cPut))
import Simplex.Messaging.Transport (TLS, Transport (cGet, cPut))
import Simplex.Messaging.Transport.Buffer
import qualified System.TimeManager as TI
defaultHTTP2BufferSize :: BufferSize
defaultHTTP2BufferSize = 32768
withHTTP2 :: BufferSize -> (Config -> SessionId -> IO a) -> TLS -> IO a
withHTTP2 sz run c = E.bracket (allocHTTP2Config c sz) freeSimpleConfig (`run` tlsUniq c)
withHTTP2 :: BufferSize -> (Config -> IO a) -> IO () -> TLS -> IO a
withHTTP2 sz run fin c = E.bracket (allocHTTP2Config c sz) (\cfg -> freeSimpleConfig cfg `E.finally` fin) run
allocHTTP2Config :: TLS -> BufferSize -> IO Config
allocHTTP2Config c sz = do
+32 -15
View File
@@ -23,15 +23,20 @@ import qualified Network.TLS as T
import Numeric.Natural (Natural)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Transport (SessionId, TLS)
import Simplex.Messaging.Transport (ALPN, SessionId, TLS (tlsALPN), getServerCerts, getServerVerifyKey, tlsUniq)
import Simplex.Messaging.Transport.Client (TransportClientConfig (..), TransportHost (..), runTLSTransportClient)
import Simplex.Messaging.Transport.HTTP2
import Simplex.Messaging.Util (eitherToMaybe)
import UnliftIO.STM
import UnliftIO.Timeout
import qualified Data.X509 as X
data HTTP2Client = HTTP2Client
{ action :: Maybe (Async HTTP2Response),
sessionId :: SessionId,
sessionALPN :: Maybe ALPN,
serverKey :: Maybe C.APublicVerifyKey, -- may not always be a key we control (i.e. APNS with apple-mandated key types)
serverCerts :: X.CertificateChain,
sessionTs :: UTCTime,
sendReq :: Request -> (Response -> IO HTTP2Response) -> IO HTTP2Response,
client_ :: HClient
@@ -66,7 +71,7 @@ defaultHTTP2ClientConfig =
HTTP2ClientConfig
{ qSize = 64,
connTimeout = 10000000,
transportConfig = TransportClientConfig Nothing Nothing True Nothing,
transportConfig = TransportClientConfig Nothing Nothing True Nothing Nothing,
bufferSize = defaultHTTP2BufferSize,
bodyHeadSize = 16384,
suportedTLSParams = http2TLSParams
@@ -86,9 +91,10 @@ getVerifiedHTTP2Client proxyUsername host port keyHash caStore config disconnect
attachHTTP2Client :: HTTP2ClientConfig -> TransportHost -> ServiceName -> IO () -> Int -> TLS -> IO (Either HTTP2ClientError HTTP2Client)
attachHTTP2Client config host port disconnected bufferSize tls = getVerifiedHTTP2ClientWith config host port disconnected setup
where
setup :: (TLS -> H.Client HTTP2Response) -> IO HTTP2Response
setup = runHTTP2ClientWith bufferSize host ($ tls)
getVerifiedHTTP2ClientWith :: HTTP2ClientConfig -> TransportHost -> ServiceName -> IO () -> ((SessionId -> H.Client HTTP2Response) -> IO HTTP2Response) -> IO (Either HTTP2ClientError HTTP2Client)
getVerifiedHTTP2ClientWith :: HTTP2ClientConfig -> TransportHost -> ServiceName -> IO () -> ((TLS -> H.Client HTTP2Response) -> IO HTTP2Response) -> IO (Either HTTP2ClientError HTTP2Client)
getVerifiedHTTP2ClientWith config host port disconnected setup =
(atomically mkHTTPS2Client >>= runClient)
`E.catch` \(e :: IOException) -> pure . Left $ HCIOError e
@@ -104,15 +110,25 @@ getVerifiedHTTP2ClientWith config host port disconnected setup =
cVar <- newEmptyTMVarIO
action <- async $ setup (client c cVar) `E.finally` atomically (putTMVar cVar $ Left HCNetworkError)
c_ <- connTimeout config `timeout` atomically (takeTMVar cVar)
pure $ case c_ of
Just (Right c') -> Right c' {action = Just action}
Just (Left e) -> Left e
Nothing -> Left HCNetworkError
case c_ of
Just (Right c') -> pure $ Right c' {action = Just action}
Just (Left e) -> pure $ Left e
Nothing -> cancel action $> Left HCNetworkError
client :: HClient -> TMVar (Either HTTP2ClientError HTTP2Client) -> SessionId -> H.Client HTTP2Response
client c cVar sessionId sendReq = do
client :: HClient -> TMVar (Either HTTP2ClientError HTTP2Client) -> TLS -> H.Client HTTP2Response
client c cVar tls sendReq = do
sessionTs <- getCurrentTime
let c' = HTTP2Client {action = Nothing, client_ = c, sendReq, sessionId, sessionTs}
let c' =
HTTP2Client
{ action = Nothing,
client_ = c,
serverKey = eitherToMaybe $ getServerVerifyKey tls,
serverCerts = getServerCerts tls,
sendReq,
sessionTs,
sessionId = tlsUniq tls,
sessionALPN = tlsALPN tls
}
atomically $ do
writeTVar (connected c) True
putTMVar cVar (Right c')
@@ -154,13 +170,14 @@ sendRequestDirect HTTP2Client {client_ = HClient {config, disconnected}, sendReq
http2RequestTimeout :: HTTP2ClientConfig -> Maybe Int -> Int
http2RequestTimeout HTTP2ClientConfig {connTimeout} = maybe connTimeout (connTimeout +)
runHTTP2Client :: forall a. T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> BufferSize -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (SessionId -> H.Client a) -> IO a
runHTTP2Client :: forall a. T.Supported -> Maybe XS.CertificateStore -> TransportClientConfig -> BufferSize -> Maybe ByteString -> TransportHost -> ServiceName -> Maybe C.KeyHash -> (TLS -> H.Client a) -> IO a
runHTTP2Client tlsParams caStore tcConfig bufferSize proxyUsername host port keyHash = runHTTP2ClientWith bufferSize host setup
where
setup :: (TLS -> IO a) -> IO a
setup = runTLSTransportClient tlsParams caStore tcConfig proxyUsername host port keyHash
runHTTP2ClientWith :: forall a. BufferSize -> TransportHost -> ((TLS -> IO a) -> IO a) -> (SessionId -> H.Client a) -> IO a
runHTTP2ClientWith bufferSize host setup client = setup $ withHTTP2 bufferSize run
runHTTP2ClientWith :: forall a. BufferSize -> TransportHost -> ((TLS -> IO a) -> IO a) -> (TLS -> H.Client a) -> IO a
runHTTP2ClientWith bufferSize host setup client = setup $ \tls -> withHTTP2 bufferSize (run tls) (pure ()) tls
where
run :: H.Config -> SessionId -> IO a
run cfg sessId = H.run (ClientConfig "https" (strEncode host) 20) cfg $ client sessId
run :: TLS -> H.Config -> IO a
run tls cfg = H.run (ClientConfig "https" (strEncode host) 20) cfg $ client tls
+13 -12
View File
@@ -13,14 +13,14 @@ import Network.Socket
import qualified Network.TLS as T
import Numeric.Natural (Natural)
import Simplex.Messaging.Server.Expiration
import Simplex.Messaging.Transport (SessionId, TLS, closeConnection)
import Simplex.Messaging.Transport (ALPN, SessionId, TLS, closeConnection, tlsALPN, tlsUniq)
import Simplex.Messaging.Transport.HTTP2
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), loadSupportedTLSServerParams, runTransportServer)
import Simplex.Messaging.Util (threadDelay')
import UnliftIO (finally)
import UnliftIO.Concurrent (forkIO, killThread)
type HTTP2ServerFunc = SessionId -> Request -> (Response -> IO ()) -> IO ()
type HTTP2ServerFunc = SessionId -> Maybe ALPN -> Request -> (Response -> IO ()) -> IO ()
data HTTP2ServerConfig = HTTP2ServerConfig
{ qSize :: Natural,
@@ -37,6 +37,7 @@ data HTTP2ServerConfig = HTTP2ServerConfig
data HTTP2Request = HTTP2Request
{ sessionId :: SessionId,
sessionALPN :: Maybe ALPN,
request :: Request,
reqBody :: HTTP2Body,
sendResponse :: Response -> IO ()
@@ -54,32 +55,32 @@ getHTTP2Server HTTP2ServerConfig {qSize, http2Port, bufferSize, bodyHeadSize, se
started <- newEmptyTMVarIO
reqQ <- newTBQueueIO qSize
action <- async $
runHTTP2Server started http2Port bufferSize tlsServerParams transportConfig Nothing $ \sessionId r sendResponse -> do
runHTTP2Server started http2Port bufferSize tlsServerParams transportConfig Nothing (const $ pure ()) $ \sessionId sessionALPN r sendResponse -> do
reqBody <- getHTTP2Body r bodyHeadSize
atomically $ writeTBQueue reqQ HTTP2Request {sessionId, request = r, reqBody, sendResponse}
atomically $ writeTBQueue reqQ HTTP2Request {sessionId, sessionALPN, request = r, reqBody, sendResponse}
void . atomically $ takeTMVar started
pure HTTP2Server {action, reqQ}
closeHTTP2Server :: HTTP2Server -> IO ()
closeHTTP2Server = uninterruptibleCancel . action
runHTTP2Server :: TMVar Bool -> ServiceName -> BufferSize -> T.ServerParams -> TransportServerConfig -> Maybe ExpirationConfig -> HTTP2ServerFunc -> IO ()
runHTTP2Server started port bufferSize serverParams transportConfig expCfg_ = runHTTP2ServerWith_ expCfg_ bufferSize setup
runHTTP2Server :: TMVar Bool -> ServiceName -> BufferSize -> T.ServerParams -> TransportServerConfig -> Maybe ExpirationConfig -> (SessionId -> IO ()) -> HTTP2ServerFunc -> IO ()
runHTTP2Server started port bufferSize serverParams transportConfig expCfg_ clientFinished = runHTTP2ServerWith_ expCfg_ clientFinished bufferSize setup
where
setup = runTransportServer started port serverParams transportConfig
runHTTP2ServerWith :: BufferSize -> ((TLS -> IO ()) -> a) -> HTTP2ServerFunc -> a
runHTTP2ServerWith = runHTTP2ServerWith_ Nothing
runHTTP2ServerWith = runHTTP2ServerWith_ Nothing (\_sessId -> pure ())
runHTTP2ServerWith_ :: Maybe ExpirationConfig -> BufferSize -> ((TLS -> IO ()) -> a) -> HTTP2ServerFunc -> a
runHTTP2ServerWith_ expCfg_ bufferSize setup http2Server = setup $ \tls -> do
runHTTP2ServerWith_ :: Maybe ExpirationConfig -> (SessionId -> IO ()) -> BufferSize -> ((TLS -> IO ()) -> a) -> HTTP2ServerFunc -> a
runHTTP2ServerWith_ expCfg_ clientFinished bufferSize setup http2Server = setup $ \tls -> do
activeAt <- newTVarIO =<< getSystemTime
tid_ <- mapM (forkIO . expireInactiveClient tls activeAt) expCfg_
withHTTP2 bufferSize (run activeAt) tls `finally` mapM_ killThread tid_
withHTTP2 bufferSize (run tls activeAt) (clientFinished $ tlsUniq tls) tls `finally` mapM_ killThread tid_
where
run activeAt cfg sessId = H.run cfg $ \req _aux sendResp -> do
run tls activeAt cfg = H.run cfg $ \req _aux sendResp -> do
getSystemTime >>= atomically . writeTVar activeAt
http2Server sessId req (`sendResp` [])
http2Server (tlsUniq tls) (tlsALPN tls) req (`sendResp` [])
expireInactiveClient tls activeAt expCfg = loop
where
loop = do
+23 -17
View File
@@ -19,24 +19,24 @@ module Simplex.Messaging.Transport.Server
loadTLSServerParams,
loadFingerprint,
smpServerHandshake,
tlsServerCredentials
)
where
import Control.Applicative ((<|>))
import Control.Logger.Simple
import Control.Monad
import Control.Monad.IO.Unlift
import qualified Crypto.Store.X509 as SX
import Data.Default (def)
import Data.List (find)
import Data.IntMap.Strict (IntMap)
import qualified Data.IntMap.Strict as IM
import Data.Maybe (fromJust)
import qualified Data.X509 as X
import Data.X509.Validation (Fingerprint (..))
import qualified Data.X509.Validation as XV
import Network.Socket
import qualified Network.TLS as T
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport
import Simplex.Messaging.Util (catchAll_, labelMyThread, tshow)
import System.Exit (exitFailure)
@@ -49,7 +49,8 @@ import UnliftIO.STM
data TransportServerConfig = TransportServerConfig
{ logTLSErrors :: Bool,
tlsSetupTimeout :: Int,
transportTimeout :: Int
transportTimeout :: Int,
alpn :: Maybe [ALPN]
}
deriving (Eq, Show)
@@ -58,7 +59,8 @@ defaultTransportServerConfig =
TransportServerConfig
{ logTLSErrors = True,
tlsSetupTimeout = 60000000,
transportTimeout = 40000000
transportTimeout = 40000000,
alpn = Nothing
}
serverTransportConfig :: TransportServerConfig -> TransportConfig
@@ -69,33 +71,37 @@ serverTransportConfig TransportServerConfig {logTLSErrors} =
-- | Run transport server (plain TCP or WebSockets) on passed TCP port and signal when server started and stopped via passed TMVar.
--
-- All accepted connections are passed to the passed function.
runTransportServer :: forall c m. (Transport c, MonadUnliftIO m) => TMVar Bool -> ServiceName -> T.ServerParams -> TransportServerConfig -> (c -> m ()) -> m ()
runTransportServer :: forall c. Transport c => TMVar Bool -> ServiceName -> T.ServerParams -> TransportServerConfig -> (c -> IO ()) -> IO ()
runTransportServer started port params cfg server = do
ss <- atomically newSocketState
runTransportServerState ss started port params cfg server
runTransportServerState :: forall c m. (Transport c, MonadUnliftIO m) => SocketState -> TMVar Bool -> ServiceName -> T.ServerParams -> TransportServerConfig -> (c -> m ()) -> m ()
runTransportServerState :: forall c . Transport c => SocketState -> TMVar Bool -> ServiceName -> T.ServerParams -> TransportServerConfig -> (c -> IO ()) -> IO ()
runTransportServerState ss started port = runTransportServerSocketState ss started (startTCPServer started port) (transportName (TProxy :: TProxy c))
-- | Run a transport server with provided connection setup and handler.
runTransportServerSocket :: (MonadUnliftIO m, T.TLSParams p, Transport a) => TMVar Bool -> IO Socket -> String -> p -> TransportServerConfig -> (a -> m ()) -> m ()
runTransportServerSocket :: Transport a => TMVar Bool -> IO Socket -> String -> T.ServerParams -> TransportServerConfig -> (a -> IO ()) -> IO ()
runTransportServerSocket started getSocket threadLabel serverParams cfg server = do
ss <- atomically newSocketState
runTransportServerSocketState ss started getSocket threadLabel serverParams cfg server
-- | Run a transport server with provided connection setup and handler.
runTransportServerSocketState :: (MonadUnliftIO m, T.TLSParams p, Transport a) => SocketState -> TMVar Bool -> IO Socket -> String -> p -> TransportServerConfig -> (a -> m ()) -> m ()
runTransportServerSocketState :: Transport a => SocketState -> TMVar Bool -> IO Socket -> String -> T.ServerParams -> TransportServerConfig -> (a -> IO ()) -> IO ()
runTransportServerSocketState ss started getSocket threadLabel serverParams cfg server = do
u <- askUnliftIO
labelMyThread $ "transport server for " <> threadLabel
liftIO . runTCPServerSocket ss started getSocket $ \conn ->
E.bracket (setup conn >>= maybe (fail "tls setup timeout") pure) closeConnection (unliftIO u . server)
runTCPServerSocket ss started getSocket $ \conn ->
E.bracket (setup conn >>= maybe (fail "tls setup timeout") pure) closeConnection server
where
tCfg = serverTransportConfig cfg
setup conn = timeout (tlsSetupTimeout cfg) $ do
labelMyThread $ threadLabel <> "/setup"
tls <- connectTLS Nothing tCfg serverParams conn
getServerConnection tCfg tls
getServerConnection tCfg (fst $ tlsServerCredentials serverParams) tls
tlsServerCredentials :: T.ServerParams -> (X.CertificateChain, X.PrivKey)
tlsServerCredentials serverParams = case T.sharedCredentials $ T.serverShared serverParams of
T.Credentials [creds] -> creds
_ -> error "server has more than one key"
-- | Run TCP server without TLS
runTCPServer :: TMVar Bool -> ServiceName -> (Socket -> IO ()) -> IO ()
@@ -110,18 +116,18 @@ runTCPServerSocket (accepted, gracefullyClosed, clients) started getSocket serve
forever . E.bracketOnError (accept sock) (close . fst) $ \(conn, _peer) -> do
cId <- atomically $ stateTVar accepted $ \cId -> let cId' = cId + 1 in cId `seq` (cId', cId')
let closeConn _ = do
atomically $ TM.delete cId clients
atomically $ modifyTVar' clients $ IM.delete cId
gracefulClose conn 5000 `catchAll_` pure () -- catchAll_ is needed here in case the connection was closed earlier
atomically $ modifyTVar' gracefullyClosed (+1)
tId <- mkWeakThreadId =<< server conn `forkFinally` closeConn
atomically $ TM.insert cId tId clients
atomically $ modifyTVar' clients $ IM.insert cId tId
type SocketState = (TVar Int, TVar Int, TMap Int (Weak ThreadId))
type SocketState = (TVar Int, TVar Int, TVar (IntMap (Weak ThreadId)))
newSocketState :: STM SocketState
newSocketState = (,,) <$> newTVar 0 <*> newTVar 0 <*> newTVar mempty
closeServer :: TMVar Bool -> TMap Int (Weak ThreadId) -> Socket -> IO ()
closeServer :: TMVar Bool -> TVar (IntMap (Weak ThreadId)) -> Socket -> IO ()
closeServer started clients sock = do
readTVarIO clients >>= mapM_ (deRefWeak >=> mapM_ killThread)
close sock
+13 -8
View File
@@ -7,7 +7,8 @@ module Simplex.Messaging.Transport.WebSockets (WS (..)) where
import qualified Control.Exception as E
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy as LB
import qualified Data.X509 as X
import qualified Network.TLS as T
import Network.WebSockets
import Network.WebSockets.Stream (Stream)
@@ -29,7 +30,8 @@ data WS = WS
tlsUniq :: ByteString,
wsStream :: Stream,
wsConnection :: Connection,
wsTransportConfig :: TransportConfig
wsTransportConfig :: TransportConfig,
wsServerCerts :: X.CertificateChain
}
websocketsOpts :: ConnectionOptions
@@ -50,12 +52,15 @@ instance Transport WS where
transportConfig :: WS -> TransportConfig
transportConfig = wsTransportConfig
getServerConnection :: TransportConfig -> T.Context -> IO WS
getServerConnection :: TransportConfig -> X.CertificateChain -> T.Context -> IO WS
getServerConnection = getWS TServer
getClientConnection :: TransportConfig -> T.Context -> IO WS
getClientConnection :: TransportConfig -> X.CertificateChain -> T.Context -> IO WS
getClientConnection = getWS TClient
getServerCerts :: WS -> X.CertificateChain
getServerCerts = wsServerCerts
tlsUnique :: WS -> ByteString
tlsUnique = tlsUniq
@@ -79,13 +84,13 @@ instance Transport WS where
then E.throwIO TEBadBlock
else pure $ B.init s
getWS :: TransportPeer -> TransportConfig -> T.Context -> IO WS
getWS wsPeer cfg cxt = withTlsUnique wsPeer cxt connectWS
getWS :: TransportPeer -> TransportConfig -> X.CertificateChain -> T.Context -> IO WS
getWS wsPeer cfg wsServerCerts cxt = withTlsUnique wsPeer cxt connectWS
where
connectWS tlsUniq = do
s <- makeTLSContextStream cxt
wsConnection <- connectPeer wsPeer s
pure $ WS {wsPeer, tlsUniq, wsStream = s, wsConnection, wsTransportConfig = cfg}
pure $ WS {wsPeer, tlsUniq, wsStream = s, wsConnection, wsTransportConfig = cfg, wsServerCerts}
connectPeer :: TransportPeer -> Stream -> IO Connection
connectPeer TServer = acceptClientRequest
connectPeer TClient = sendClientRequest
@@ -101,5 +106,5 @@ makeTLSContextStream cxt =
(Just <$> T.recvData cxt) `E.catch` \case
T.Error_EOF -> pure Nothing
e -> E.throwIO e
writeStream :: Maybe BL.ByteString -> IO ()
writeStream :: Maybe LB.ByteString -> IO ()
writeStream = maybe (closeTLS cxt) (T.sendData cxt)
+16 -16
View File
@@ -50,26 +50,18 @@ maybeWord :: (a -> ByteString) -> Maybe a -> ByteString
maybeWord f = maybe "" $ B.cons ' ' . f
{-# INLINE maybeWord #-}
liftIOEither :: (MonadIO m, MonadError e m) => IO (Either e a) -> m a
liftIOEither a = liftIO a >>= liftEither
{-# INLINE liftIOEither #-}
liftError :: (MonadIO m, MonadError e' m) => (e -> e') -> ExceptT e IO a -> m a
liftError f = liftEitherError f . runExceptT
liftError :: MonadIO m => (e -> e') -> ExceptT e IO a -> ExceptT e' m a
liftError f = liftError' f . runExceptT
{-# INLINE liftError #-}
liftEitherError :: (MonadIO m, MonadError e' m) => (e -> e') -> IO (Either e a) -> m a
liftEitherError f a = liftIOEither (first f <$> a)
{-# INLINE liftEitherError #-}
liftError' :: MonadIO m => (e -> e') -> IO (Either e a) -> ExceptT e' m a
liftError' f = ExceptT . fmap (first f) . liftIO
{-# INLINE liftError' #-}
liftEitherWith :: MonadError e' m => (e -> e') -> Either e a -> m a
liftEitherWith :: MonadIO m => (e -> e') -> Either e a -> ExceptT e' m a
liftEitherWith f = liftEither . first f
{-# INLINE liftEitherWith #-}
liftE :: (e -> e') -> ExceptT e IO a -> ExceptT e' IO a
liftE f a = ExceptT $ first f <$> runExceptT a
{-# INLINE liftE #-}
ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM ba t f = ba >>= \b -> if b then t else f
{-# INLINE ifM #-}
@@ -109,10 +101,18 @@ tryAllErrors :: (MonadUnliftIO m, MonadError e m) => (E.SomeException -> e) -> m
tryAllErrors err action = tryError action `UE.catch` (pure . Left . err)
{-# INLINE tryAllErrors #-}
tryAllErrors' :: MonadUnliftIO m => (E.SomeException -> e) -> ExceptT e m a -> m (Either e a)
tryAllErrors' err action = runExceptT action `UE.catch` (pure . Left . err)
{-# INLINE tryAllErrors' #-}
catchAllErrors :: (MonadUnliftIO m, MonadError e m) => (E.SomeException -> e) -> m a -> (e -> m a) -> m a
catchAllErrors err action handler = tryAllErrors err action >>= either handler pure
{-# INLINE catchAllErrors #-}
catchAllErrors' :: MonadUnliftIO m => (E.SomeException -> e) -> ExceptT e m a -> (e -> m a) -> m a
catchAllErrors' err action handler = tryAllErrors' err action >>= either handler pure
{-# INLINE catchAllErrors' #-}
catchThrow :: (MonadUnliftIO m, MonadError e m) => m a -> (E.SomeException -> e) -> m a
catchThrow action err = catchAllErrors err action throwError
{-# INLINE catchThrow #-}
@@ -148,8 +148,8 @@ safeDecodeUtf8 = decodeUtf8With onError
where
onError _ _ = Just '?'
timeoutThrow :: (MonadUnliftIO m, MonadError e m) => e -> Int -> m a -> m a
timeoutThrow e ms action = timeout ms action >>= maybe (throwError e) pure
timeoutThrow :: MonadUnliftIO m => e -> Int -> ExceptT e m a -> ExceptT e m a
timeoutThrow e ms action = ExceptT (sequence <$> (ms `timeout` runExceptT action)) >>= maybe (throwError e) pure
threadDelay' :: Int64 -> IO ()
threadDelay' time
+48 -40
View File
@@ -1,13 +1,16 @@
{-# LANGUAGE ConstrainedClassMethods #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeSynonymInstances #-}
module Simplex.Messaging.Version
( Version,
VersionRange (minVersion, maxVersion),
VersionScope,
pattern VersionRange,
VersionI (..),
VersionRangeI (..),
@@ -24,47 +27,61 @@ module Simplex.Messaging.Version
where
import Control.Applicative (optional)
import Data.Aeson (FromJSON (..), ToJSON (..))
import qualified Data.Aeson as J
import qualified Data.Aeson.Encoding as JE
import Data.Aeson.Types ((.:), (.=))
import qualified Data.Aeson.Types as JT
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Word (Word16)
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Util ((<$?>))
import Simplex.Messaging.Version.Internal (Version (..))
pattern VersionRange :: Word16 -> Word16 -> VersionRange
pattern VersionRange :: Version v -> Version v -> VersionRange v
pattern VersionRange v1 v2 <- VRange v1 v2
{-# COMPLETE VersionRange #-}
type Version = Word16
data VersionRange = VRange
{ minVersion :: Version,
maxVersion :: Version
data VersionRange v = VRange
{ minVersion :: Version v,
maxVersion :: Version v
}
deriving (Eq, Show)
instance J.FromJSON (VersionRange v) where
parseJSON (J.Object v) = do
minVersion <- v .: "minVersion"
maxVersion <- v .: "maxVersion"
pure VRange {minVersion, maxVersion}
parseJSON invalid =
JT.prependFailure "bad VersionRange, " (JT.typeMismatch "Object" invalid)
instance J.ToJSON (VersionRange v) where
toEncoding VRange {minVersion, maxVersion} = JE.pairs $ ("minVersion" .= minVersion) <> ("maxVersion" .= maxVersion)
toJSON VRange {minVersion, maxVersion} = J.object ["minVersion" .= minVersion, "maxVersion" .= maxVersion]
class VersionScope v
-- | construct valid version range, to be used in constants
mkVersionRange :: Version -> Version -> VersionRange
mkVersionRange :: Version v -> Version v -> VersionRange v
mkVersionRange v1 v2
| v1 <= v2 = VRange v1 v2
| otherwise = error "invalid version range"
safeVersionRange :: Version -> Version -> Maybe VersionRange
safeVersionRange :: Version v -> Version v -> Maybe (VersionRange v)
safeVersionRange v1 v2
| v1 <= v2 = Just $ VRange v1 v2
| otherwise = Nothing
versionToRange :: Version -> VersionRange
versionToRange :: Version v -> VersionRange v
versionToRange v = VRange v v
instance Encoding VersionRange where
instance VersionScope v => Encoding (VersionRange v) where
smpEncode (VRange v1 v2) = smpEncode (v1, v2)
smpP =
maybe (fail "invalid version range") pure
=<< safeVersionRange <$> smpP <*> smpP
instance StrEncoding VersionRange where
instance VersionScope v => StrEncoding (VersionRange v) where
strEncode (VRange v1 v2)
| v1 == v2 = strEncode v1
| otherwise = strEncode v1 <> "-" <> strEncode v2
@@ -73,32 +90,23 @@ instance StrEncoding VersionRange where
v2 <- maybe (pure v1) (const strP) =<< optional (A.char '-')
maybe (fail "invalid version range") pure $ safeVersionRange v1 v2
instance ToJSON VersionRange where
toJSON (VRange v1 v2) = toJSON (v1, v2)
toEncoding (VRange v1 v2) = toEncoding (v1, v2)
class VersionScope v => VersionI v a | a -> v where
type VersionRangeT v a
version :: a -> Version v
toVersionRangeT :: a -> VersionRange v -> VersionRangeT v a
instance FromJSON VersionRange where
parseJSON v =
(\(v1, v2) -> maybe (Left "bad VersionRange") Right $ safeVersionRange v1 v2)
<$?> parseJSON v
class VersionScope v => VersionRangeI v a | a -> v where
type VersionT v a
versionRange :: a -> VersionRange v
toVersionT :: a -> Version v -> VersionT v a
class VersionI a where
type VersionRangeT a
version :: a -> Version
toVersionRangeT :: a -> VersionRange -> VersionRangeT a
class VersionRangeI a where
type VersionT a
versionRange :: a -> VersionRange
toVersionT :: a -> Version -> VersionT a
instance VersionI Version where
type VersionRangeT Version = VersionRange
instance VersionScope v => VersionI v (Version v) where
type VersionRangeT v (Version v) = VersionRange v
version = id
toVersionRangeT _ vr = vr
instance VersionRangeI VersionRange where
type VersionT VersionRange = Version
instance VersionScope v => VersionRangeI v (VersionRange v) where
type VersionT v (VersionRange v) = Version v
versionRange = id
toVersionT _ v = v
@@ -109,18 +117,18 @@ pattern Compatible a <- Compatible_ a
{-# COMPLETE Compatible #-}
isCompatible :: VersionI a => a -> VersionRange -> Bool
isCompatible :: VersionI v a => a -> VersionRange v -> Bool
isCompatible x (VRange v1 v2) = let v = version x in v1 <= v && v <= v2
isCompatibleRange :: VersionRangeI a => a -> VersionRange -> Bool
isCompatibleRange :: VersionRangeI v a => a -> VersionRange v -> Bool
isCompatibleRange x (VRange min2 max2) = min1 <= max2 && min2 <= max1
where
VRange min1 max1 = versionRange x
proveCompatible :: VersionI a => a -> VersionRange -> Maybe (Compatible a)
proveCompatible :: VersionI v a => a -> VersionRange v -> Maybe (Compatible a)
proveCompatible x vr = x `mkCompatibleIf` (x `isCompatible` vr)
compatibleVersion :: VersionRangeI a => a -> VersionRange -> Maybe (Compatible (VersionT a))
compatibleVersion :: VersionRangeI v a => a -> VersionRange v -> Maybe (Compatible (VersionT v a))
compatibleVersion x vr =
toVersionT x (min max1 max2) `mkCompatibleIf` isCompatibleRange x vr
where
+25
View File
@@ -0,0 +1,25 @@
module Simplex.Messaging.Version.Internal where
import Data.Aeson (FromJSON (..), ToJSON (..))
import Data.Word (Word16)
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
-- Do not use constructor of this type directry
newtype Version v = Version Word16
deriving (Eq, Ord, Show)
instance Encoding (Version v) where
smpEncode (Version v) = smpEncode v
smpP = Version <$> smpP
instance StrEncoding (Version v) where
strEncode (Version v) = strEncode v
strP = Version <$> strP
instance ToJSON (Version v) where
toEncoding (Version v) = toEncoding v
toJSON (Version v) = toJSON v
instance FromJSON (Version v) where
parseJSON v = Version <$> parseJSON v

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