mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-08-30 09:48:23 +00:00
Compare commits
818
Commits
legacy
...
master-ghc8107
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
257982ccce | ||
|
|
6bffcc8503 | ||
|
|
3f2e50eebd | ||
|
|
cc8b8cd10d | ||
|
|
febf9019e2 | ||
|
|
2a6be894e1 | ||
|
|
8fa369f9d3 | ||
|
|
31f520af05 | ||
|
|
7c29e2a189 | ||
|
|
f49607e522 | ||
|
|
281bdebcb8 | ||
|
|
757b7eec81 | ||
|
|
88a4cf7d90 | ||
|
|
8ac97ed700 | ||
|
|
79d9f41fd8 | ||
|
|
2fe408666e | ||
|
|
317c68a860 | ||
|
|
6926b45703 | ||
|
|
2991644e9f | ||
|
|
c7a7323f54 | ||
|
|
044eca5ac4 | ||
|
|
0841067132 | ||
|
|
40ba94ce72 | ||
|
|
c501f4f9cc | ||
|
|
20d9767c54 | ||
|
|
3b348a463c | ||
|
|
7aae6f3cbe | ||
|
|
d60911c780 | ||
|
|
e0b7942e45 | ||
|
|
4f5d52ada4 | ||
|
|
897001efbf | ||
|
|
c051ebab74 | ||
|
|
9460551a04 | ||
|
|
6a2e6b040e | ||
|
|
bd06b47a9d | ||
|
|
102487bc4f | ||
|
|
248144f3de | ||
|
|
ecb23c66e0 | ||
|
|
c0566d37a0 | ||
|
|
1a8dfb4cbe | ||
|
|
1a0c4b73de | ||
|
|
f3045563ed | ||
|
|
e9b5a849ab | ||
|
|
a5fed340e2 | ||
|
|
ce796ec225 | ||
|
|
4660ee9cff | ||
|
|
70bf665938 | ||
|
|
b5f733d2db | ||
|
|
c22c15a2e8 | ||
|
|
db1b2f77cd | ||
|
|
8e2ffcb297 | ||
|
|
3a2969c44a | ||
|
|
0410948b56 | ||
|
|
26b551132e | ||
|
|
511d793b92 | ||
|
|
5f750a6783 | ||
|
|
bba7ad3494 | ||
|
|
d920a2504b | ||
|
|
6e2bb08026 | ||
|
|
55a6157880 | ||
|
|
05a794b5fd | ||
|
|
73d7f84ee3 | ||
|
|
9dfc0e0e92 | ||
|
|
cf8b9c12ff | ||
|
|
9994375c16 | ||
|
|
9d12d76078 | ||
|
|
deb3fc7359 | ||
|
|
80905c0822 | ||
|
|
decf25d7dc | ||
|
|
5e047f3199 | ||
|
|
2a43717738 | ||
|
|
1ad69cf74f | ||
|
|
696fa652a5 | ||
|
|
9f0f53d2ec | ||
|
|
6b0da8ac50 | ||
|
|
9195509485 | ||
|
|
cf7fbf8a54 | ||
|
|
96a38505d6 | ||
|
|
6087628c59 | ||
|
|
753a6c7542 | ||
|
|
899d26e8c8 | ||
|
|
ec1b72cb80 | ||
|
|
e7bd0fb31a | ||
|
|
fda1284ae4 | ||
|
|
53c793d559 | ||
|
|
8d47f69083 | ||
|
|
3828a9591c | ||
|
|
81385e39bf | ||
|
|
3438655532 | ||
|
|
fee897f367 | ||
|
|
f2d832fe20 | ||
|
|
57d08c0e36 | ||
|
|
00b48babf3 | ||
|
|
0b488ceb66 | ||
|
|
887ccbcf6c | ||
|
|
0cabe0690b | ||
|
|
84ce037867 | ||
|
|
504465f8e2 | ||
|
|
351f42650c | ||
|
|
980e5c4d1e | ||
|
|
17a1a911d8 | ||
|
|
5dc3d739b2 | ||
|
|
dc65197cfd | ||
|
|
4c0b8a31d2 | ||
|
|
cffbd39f4e | ||
|
|
a67669287a | ||
|
|
7bdae793cb | ||
|
|
4eb3b8e113 | ||
|
|
ed05428227 | ||
|
|
066d91b0f5 | ||
|
|
414b019ad4 | ||
|
|
002f36dde0 | ||
|
|
b001b748db | ||
|
|
ec268e2006 | ||
|
|
a97c7da48a | ||
|
|
fcce544488 | ||
|
|
a302b601ea | ||
|
|
44abb90c63 | ||
|
|
d09adb5cd6 | ||
|
|
b329eb9a86 | ||
|
|
40e6d16e48 | ||
|
|
f3111f4559 | ||
|
|
cf2a17b80c | ||
|
|
e586bef57a | ||
|
|
e2065ab352 | ||
|
|
43f283471c | ||
|
|
a1e56979b7 | ||
|
|
6446edd1a6 | ||
|
|
82aec2cd8f | ||
|
|
fdbfe0e8d1 | ||
|
|
e81f3b5529 | ||
|
|
6314bb1706 | ||
|
|
c54be32135 | ||
|
|
7e2b309450 | ||
|
|
4ed40fa5d5 | ||
|
|
de95119ca6 | ||
|
|
4fae7dcaee | ||
|
|
d989d11478 | ||
|
|
1901e96ecc | ||
|
|
3fee468051 | ||
|
|
58cb2855d2 | ||
|
|
745a144e0c | ||
|
|
4c6c436e7f | ||
|
|
b61e3b5f95 | ||
|
|
58dbc197ce | ||
|
|
1afcefa5e7 | ||
|
|
532cd2f39c | ||
|
|
2f5c646e55 | ||
|
|
f76a5ca5b6 | ||
|
|
f2657f9c0b | ||
|
|
fe22d9b299 | ||
|
|
75fe28a8a6 | ||
|
|
54dc8d42e7 | ||
|
|
0e1562deae | ||
|
|
94540a2c71 | ||
|
|
16367fcb3b | ||
|
|
8be2505fa0 | ||
|
|
a000419bd7 | ||
|
|
c8a8e2c297 | ||
|
|
f7d038ef20 | ||
|
|
4a927d1ae2 | ||
|
|
3a74558e84 | ||
|
|
e3fff98b1b | ||
|
|
acea477aab | ||
|
|
c31668ce28 | ||
|
|
2efe1496d2 | ||
|
|
011e1109e9 | ||
|
|
fbf1afc9f5 | ||
|
|
20dc00d96d | ||
|
|
862aa29a6f | ||
|
|
ec6e44f034 | ||
|
|
76e3863cbb | ||
|
|
1c2fbdb2c5 | ||
|
|
1822296481 | ||
|
|
b747080db3 | ||
|
|
20f7b538e5 | ||
|
|
89caf55729 | ||
|
|
c08cfb460b | ||
|
|
03eca19d38 | ||
|
|
a5a3a2cbad | ||
|
|
4a3c5abf32 | ||
|
|
6c6f22051d | ||
|
|
d693868bc0 | ||
|
|
32a64b994e | ||
|
|
a83f85dc58 | ||
|
|
0f7ede5eed | ||
|
|
ce64c91d5a | ||
|
|
66177fd550 | ||
|
|
9d83a9c017 | ||
|
|
019db0ab91 | ||
|
|
8954f39425 | ||
|
|
4d96e3700c | ||
|
|
eaa8221b95 | ||
|
|
af3f70829d | ||
|
|
ff8197b87b | ||
|
|
fd96ee2840 | ||
|
|
605970f6b6 | ||
|
|
dc40c3461a | ||
|
|
4da637a565 | ||
|
|
23cecc7b68 | ||
|
|
9692e636ac | ||
|
|
2b93e0b17d | ||
|
|
9f0b9a83d6 | ||
|
|
27cfc4c62b | ||
|
|
5e39c47975 | ||
|
|
ed0f8c79a2 | ||
|
|
29eb9e4e27 | ||
|
|
e346a81e46 | ||
|
|
e6e17ec637 | ||
|
|
d35bd8a954 | ||
|
|
d1774e5b56 | ||
|
|
9c460966b2 | ||
|
|
d0cd34989d | ||
|
|
d838be773b | ||
|
|
2edc7529f9 | ||
|
|
6d9af2ec17 | ||
|
|
3533929905 | ||
|
|
d8e60ecfdb | ||
|
|
215d2414b7 | ||
|
|
b1489138ea | ||
|
|
9f8db13553 | ||
|
|
511a97c5d0 | ||
|
|
44f0dd39f3 | ||
|
|
bdff274f56 | ||
|
|
83364052b1 | ||
|
|
c6033e51a0 | ||
|
|
0f23b4ab5c | ||
|
|
e27c4f7b81 | ||
|
|
b5869cf169 | ||
|
|
6b03876ef1 | ||
|
|
6a665a0833 | ||
|
|
c5eb65fec8 | ||
|
|
5743069021 | ||
|
|
0df7733125 | ||
|
|
d41c2bec2a | ||
|
|
7e265e5c59 | ||
|
|
2bec353eae | ||
|
|
40da7e76dd | ||
|
|
2e02ba86fd | ||
|
|
02eb1e9822 | ||
|
|
cd48c33c0d | ||
|
|
5258fe9b7e | ||
|
|
e33d5b5184 | ||
|
|
2b1a02d7d4 | ||
|
|
9255d48b69 | ||
|
|
bab689099f | ||
|
|
a0eb53b891 | ||
|
|
db120b6d2e | ||
|
|
64f5d63d2e | ||
|
|
ddc2da8fe4 | ||
|
|
3fe1f3c822 | ||
|
|
448b674fb1 | ||
|
|
cf1cd75a15 | ||
|
|
40164ff21f | ||
|
|
c1266d55a4 | ||
|
|
cf8f78f518 | ||
|
|
accfee5a52 | ||
|
|
cc2066473c | ||
|
|
8fde8e1344 | ||
|
|
deec963de8 | ||
|
|
552759018e | ||
|
|
6d52150672 | ||
|
|
e9f949f117 | ||
|
|
a0c86158d9 | ||
|
|
cc38deb65d | ||
|
|
cf147397a4 | ||
|
|
2f7c115207 | ||
|
|
6ed4dd1515 | ||
|
|
914240108d | ||
|
|
e764a51fc0 | ||
|
|
94de01a06f | ||
|
|
faec32dc5e | ||
|
|
6227215c3f | ||
|
|
3d3eb335e8 | ||
|
|
453e18c4ad | ||
|
|
249bcc7bb3 | ||
|
|
4afb72070a | ||
|
|
524c1de4de | ||
|
|
875a4bac5e | ||
|
|
f02cb2da4c | ||
|
|
2f15ce2662 | ||
|
|
781f8e0000 | ||
|
|
c3872f0a55 | ||
|
|
3e9e144145 | ||
|
|
50c9c95287 | ||
|
|
1b4b184517 | ||
|
|
e4aad7583f | ||
|
|
f20a996797 | ||
|
|
8c8a984c1c | ||
|
|
202922bceb | ||
|
|
3ab5e9d110 | ||
|
|
4ce4fa3423 | ||
|
|
fbf0b8b175 | ||
|
|
1f394b76a6 | ||
|
|
97b8b37325 | ||
|
|
bbf9e28696 | ||
|
|
50664930cc | ||
|
|
cd1adc8307 | ||
|
|
733c9374bf | ||
|
|
a8121fc8ad | ||
|
|
c970674452 | ||
|
|
4a9b5412db | ||
|
|
1ecef81b4d | ||
|
|
2bf8cba4fd | ||
|
|
628169adb4 | ||
|
|
fcef7deec0 | ||
|
|
44535628a5 | ||
|
|
6a48b735d1 | ||
|
|
bd0cc25083 | ||
|
|
15637e545b | ||
|
|
c0dcf283eb | ||
|
|
2ddfb044fc | ||
|
|
acdf4f41c5 | ||
|
|
a3d9ee213d | ||
|
|
cf7346d48b | ||
|
|
4e8657097c | ||
|
|
d8f81b5cdb | ||
|
|
2ae3100bed | ||
|
|
88c6b0d663 | ||
|
|
8659d4de05 | ||
|
|
bccef0ba47 | ||
|
|
d7a008985b | ||
|
|
9775861b6f | ||
|
|
4d333a90fa | ||
|
|
eb1723acce | ||
|
|
eff501ddcc | ||
|
|
b342b1dc59 | ||
|
|
1523c00bcc | ||
|
|
3637d0c610 | ||
|
|
360e55a48f | ||
|
|
ee2a764f93 | ||
|
|
cfe995325a | ||
|
|
3be0008c5a | ||
|
|
5d8febc353 | ||
|
|
c77e7f7e43 | ||
|
|
6858f74897 | ||
|
|
d1b4fa8911 | ||
|
|
7fb48930f6 | ||
|
|
ff038b492c | ||
|
|
b59669a65e | ||
|
|
2ccef1690b | ||
|
|
d4fc638478 | ||
|
|
f66e8239f4 | ||
|
|
56eea29ec3 | ||
|
|
a854b5f2b2 | ||
|
|
63b7956083 | ||
|
|
4f4b4f7eee | ||
|
|
f9c613ffb8 | ||
|
|
14cb88e725 | ||
|
|
6ccbe5e66e | ||
|
|
f921dc3adb | ||
|
|
c4f377a85b | ||
|
|
ca5c4b75eb | ||
|
|
0af6533510 | ||
|
|
42df6a421d | ||
|
|
bc8acf2abf | ||
|
|
19feee881b | ||
|
|
324e01300d | ||
|
|
b549910c17 | ||
|
|
a1eda12475 | ||
|
|
abee1fa70b | ||
|
|
701d06ba01 | ||
|
|
cac3432b03 | ||
|
|
56cc2bc71f | ||
|
|
9c9ba8c25c | ||
|
|
8e024590bc | ||
|
|
f47e7bf3c5 | ||
|
|
5e31c60c35 | ||
|
|
a0a092747a | ||
|
|
36bf92e54f | ||
|
|
1f12697279 | ||
|
|
92a379e75c | ||
|
|
0dddd59b36 | ||
|
|
f4ad3a983e | ||
|
|
b328492dc9 | ||
|
|
698b5c4146 | ||
|
|
acfa65200a | ||
|
|
61e0c346df | ||
|
|
11c235a8a3 | ||
|
|
058e3ac55e | ||
|
|
470b512a88 | ||
|
|
8dd728e834 | ||
|
|
7adcbf839f | ||
|
|
69a5896dac | ||
|
|
4b29855430 | ||
|
|
1d78e35688 | ||
|
|
af903bd405 | ||
|
|
51b0833f98 | ||
|
|
874f9f5cd6 | ||
|
|
66d3465c19 | ||
|
|
d5bf99c32c | ||
|
|
1d3a604988 | ||
|
|
267236a4f8 | ||
|
|
aa17cc55c1 | ||
|
|
fb21d9836e | ||
|
|
e4842f4f47 | ||
|
|
dc920d90d9 | ||
|
|
f53c1f5559 | ||
|
|
0942bf9f1e | ||
|
|
57fc190a91 | ||
|
|
43fb513ef7 | ||
|
|
fbccca9947 | ||
|
|
40000047af | ||
|
|
c2342cba05 | ||
|
|
f3b6ed4db0 | ||
|
|
95db734b2d | ||
|
|
e281efdcb8 | ||
|
|
d2b88a1baa | ||
|
|
8f9e6c2112 | ||
|
|
f917c15b5e | ||
|
|
374526a11b | ||
|
|
29b99d6716 | ||
|
|
d33cf644f5 | ||
|
|
6fc3b26970 | ||
|
|
029fc6e781 | ||
|
|
d9a0e78b04 | ||
|
|
4beaafa35a | ||
|
|
561649a498 | ||
|
|
8d9816809f | ||
|
|
eb5c1c78cb | ||
|
|
19aef52135 | ||
|
|
d06bb2b9c5 | ||
|
|
f43e5d6833 | ||
|
|
7f81396b09 | ||
|
|
f97c1a7712 | ||
|
|
10e0e58ec3 | ||
|
|
0fa40fbfb0 | ||
|
|
f4baa5d672 | ||
|
|
b686bc0c17 | ||
|
|
29332a5e9f | ||
|
|
9cd5f96fe4 | ||
|
|
a2eea4f18f | ||
|
|
413aad5139 | ||
|
|
0a9b6e4ab4 | ||
|
|
f8505d4add | ||
|
|
41d3c14157 | ||
|
|
d4962daf11 | ||
|
|
085b785ee8 | ||
|
|
84ce7a90d0 | ||
|
|
abb3f20ed5 | ||
|
|
a3f58fdc6b | ||
|
|
e3e05d474d | ||
|
|
13a9eee0cf | ||
|
|
959ae34f16 | ||
|
|
8c1a9c529d | ||
|
|
f08d81252d | ||
|
|
afecefc3ad | ||
|
|
e328ae5d06 | ||
|
|
605e6c64c6 | ||
|
|
4bf9f2b535 | ||
|
|
bab6a1577b | ||
|
|
ca6164167e | ||
|
|
42a96d6d00 | ||
|
|
6adbc56021 | ||
|
|
882d2b123b | ||
|
|
da5058a0c4 | ||
|
|
50c210c5c0 | ||
|
|
7d53504bb3 | ||
|
|
e4b47825b5 | ||
|
|
b215bd954d | ||
|
|
26d149d17c | ||
|
|
c66a7e371f | ||
|
|
f2c1455a27 | ||
|
|
f872c25f09 | ||
|
|
e4b77ed9e6 | ||
|
|
5894f91927 | ||
|
|
1459a0613a | ||
|
|
a7b39b710c | ||
|
|
212a9bc4ab | ||
|
|
3fc9afbb35 | ||
|
|
b2d4bfd0a8 | ||
|
|
cfab5a18d3 | ||
|
|
180b4b9dcb | ||
|
|
0437796232 | ||
|
|
02bba01c16 | ||
|
|
68138c08d2 | ||
|
|
6bfaa4985e | ||
|
|
7d99c4b35c | ||
|
|
e9db0a1162 | ||
|
|
b8c23ea8d5 | ||
|
|
b76ef03dbe | ||
|
|
fcaddb7848 | ||
|
|
d788c3ca95 | ||
|
|
e07121266a | ||
|
|
2f39f055c1 | ||
|
|
5d06dde757 | ||
|
|
eb1f9370c1 | ||
|
|
d810db4eed | ||
|
|
d8f07e8dde | ||
|
|
0ab90cb204 | ||
|
|
1d40bb97c2 | ||
|
|
1ce63bee44 | ||
|
|
aafe2d43f5 | ||
|
|
0b259af9cb | ||
|
|
6bbe1dfc66 | ||
|
|
a6f401041a | ||
|
|
1670c9c05e | ||
|
|
cde8a11693 | ||
|
|
3a4f8cb6eb | ||
|
|
e75846aa38 | ||
|
|
23496f1a34 | ||
|
|
9f5e6bfd19 | ||
|
|
cb5ad1619c | ||
|
|
7a238812b7 | ||
|
|
991548b64d | ||
|
|
cc798145d2 | ||
|
|
4339218c57 | ||
|
|
abd694bd5a | ||
|
|
9c1b43791c | ||
|
|
e3d2d6fc91 | ||
|
|
36b4179b8b | ||
|
|
bc26dc1d68 | ||
|
|
4c0164c49e | ||
|
|
40ae809033 | ||
|
|
2c52c514c0 | ||
|
|
57d83ae42d | ||
|
|
f9cd7e5416 | ||
|
|
77f1d45021 | ||
|
|
5ff80f0688 | ||
|
|
e622e12a7f | ||
|
|
6b6ea78eac | ||
|
|
238a2e7fe9 | ||
|
|
82eff6c757 | ||
|
|
0392f849aa | ||
|
|
16f5fdeeb0 | ||
|
|
c3012173e3 | ||
|
|
f6a321e198 | ||
|
|
c82fae72f2 | ||
|
|
9695786aa5 | ||
|
|
56ef95e8ae | ||
|
|
79d67694ed | ||
|
|
8dc051179a | ||
|
|
0b25b57e3c | ||
|
|
07bf2bca3b | ||
|
|
6c14372666 | ||
|
|
2c7cad5378 | ||
|
|
c9501a23a0 | ||
|
|
926e515bc0 | ||
|
|
09b51dc44f | ||
|
|
85d507d5d3 | ||
|
|
b0ac0744e2 | ||
|
|
ba40d75886 | ||
|
|
0c7c0d066d | ||
|
|
3d6ad64d62 | ||
|
|
51d0b48ce1 | ||
|
|
a8260290e7 | ||
|
|
2809963a2e | ||
|
|
d2321b750d | ||
|
|
c2f1dabcd0 | ||
|
|
9e0e6b1afc | ||
|
|
58622c6aaf | ||
|
|
b82452fe03 | ||
|
|
70e7c71762 | ||
|
|
a92827c986 | ||
|
|
f10e3f697c | ||
|
|
ef4d4c9e16 | ||
|
|
ffb4b4763c | ||
|
|
0d9d549cea | ||
|
|
a7c3133c35 | ||
|
|
5b61c6bccd | ||
|
|
d78fb8761e | ||
|
|
cd9cb19765 | ||
|
|
30b36cd756 | ||
|
|
08a4b41691 | ||
|
|
756bd1eca7 | ||
|
|
7a72488275 | ||
|
|
7fa4862a83 | ||
|
|
0ab44b1836 | ||
|
|
d1db7d6f79 | ||
|
|
ec70429106 | ||
|
|
31c78e516d | ||
|
|
b58523d4a2 | ||
|
|
9f981042b9 | ||
|
|
8c2227ccce | ||
|
|
bca44ae54a | ||
|
|
50ddcd3a13 | ||
|
|
3336f987c3 | ||
|
|
1d56ef3a60 | ||
|
|
0c45e0bf76 | ||
|
|
5ccebaeb21 | ||
|
|
5f94240ed4 | ||
|
|
331918bac7 | ||
|
|
f7bd8a3982 | ||
|
|
68c2682e70 | ||
|
|
62485b9367 | ||
|
|
91802fe94f | ||
|
|
0a71822dd0 | ||
|
|
9c12b4edc0 | ||
|
|
c1348aa54f | ||
|
|
4220c3bdaf | ||
|
|
7736ef8576 | ||
|
|
80309a0089 | ||
|
|
bfb556c860 | ||
|
|
3f985e8fd7 | ||
|
|
a3d1f5540d | ||
|
|
3f69636f1a | ||
|
|
60294521f4 | ||
|
|
4b3d04bd27 | ||
|
|
0dc34cd287 | ||
|
|
628930df1f | ||
|
|
46ba047077 | ||
|
|
facb6f4561 | ||
|
|
6a27b82759 | ||
|
|
b5ccce0f6a | ||
|
|
b60408837a | ||
|
|
c2ec691a46 | ||
|
|
ef7c66762e | ||
|
|
9f6316fa6d | ||
|
|
95a2f7560d | ||
|
|
b67c3e5935 | ||
|
|
ce3ca08199 | ||
|
|
fd410280b5 | ||
|
|
c74f4d729b | ||
|
|
49070fffe0 | ||
|
|
8dda99d77f | ||
|
|
4e4eea34f9 | ||
|
|
964daf5442 | ||
|
|
6a1cea93b5 | ||
|
|
ace94d7c69 | ||
|
|
1064e9c315 | ||
|
|
2733a09a40 | ||
|
|
7249cb0f0e | ||
|
|
f8ec838912 | ||
|
|
c6dde772b4 | ||
|
|
633cd675b5 | ||
|
|
83c18272cf | ||
|
|
7652ece045 | ||
|
|
7774fc3271 | ||
|
|
43d24d07dd | ||
|
|
a2204c9b84 | ||
|
|
f718a84335 | ||
|
|
e6fbaf5e50 | ||
|
|
7541ef4c13 | ||
|
|
5eb2822d25 | ||
|
|
0a2cad2745 | ||
|
|
4dc7d9bc77 | ||
|
|
12caa328a8 | ||
|
|
b5c5a8ad2a | ||
|
|
60d0f19121 | ||
|
|
68f52c47dd | ||
|
|
6f3e9dc603 | ||
|
|
17888f89a9 | ||
|
|
f84439f79d | ||
|
|
9d8a9c4fe4 | ||
|
|
45ddecc4b8 | ||
|
|
327f7112d0 | ||
|
|
d38303d5f1 | ||
|
|
65c77c78f3 | ||
|
|
6382367ec0 | ||
|
|
a89e019bb0 | ||
|
|
a638486bb3 | ||
|
|
a211e67829 | ||
|
|
390e99ca95 | ||
|
|
f577fcdacf | ||
|
|
ac7ffc413e | ||
|
|
fb26916eea | ||
|
|
2eff717248 | ||
|
|
d31958855f | ||
|
|
3ba1926b1e | ||
|
|
c62730fe4c | ||
|
|
bb99fdaaa2 | ||
|
|
a6ec93c38e | ||
|
|
33f822d72c | ||
|
|
b6e87e4a3e | ||
|
|
62acbc4ad4 | ||
|
|
4e1184d9eb | ||
|
|
cd22e06b3a | ||
|
|
6ef6bedc03 | ||
|
|
c380431b94 | ||
|
|
f466fa76e5 | ||
|
|
f2409e7280 | ||
|
|
337d5bdfe7 | ||
|
|
800581b2bf | ||
|
|
f060914ab8 | ||
|
|
14d76a1582 | ||
|
|
a37b24a8c2 | ||
|
|
5c6ec96d64 | ||
|
|
7a19ab224b | ||
|
|
7a611bed5a | ||
|
|
a406159ed4 | ||
|
|
14780a47d6 | ||
|
|
4a66f68c55 | ||
|
|
d1e6147adf | ||
|
|
7d1fdadef0 | ||
|
|
09e2e75c42 | ||
|
|
dff5cad1be | ||
|
|
c1f5f9d846 | ||
|
|
9c4778b129 | ||
|
|
137afb68fe | ||
|
|
229e2607d7 | ||
|
|
c380c79560 | ||
|
|
c9994c3a2c | ||
|
|
137ff7043d | ||
|
|
6fe3bfa980 | ||
|
|
2b857876b4 | ||
|
|
b777a4fd93 | ||
|
|
e15a25d92e | ||
|
|
1cd68f4159 | ||
|
|
495439adf5 | ||
|
|
98fac579c0 | ||
|
|
670b3b7974 | ||
|
|
305ae94cce | ||
|
|
a9a6917056 | ||
|
|
502ee39eb3 | ||
|
|
ac899a67c4 | ||
|
|
6e7089284e | ||
|
|
40efdf97de | ||
|
|
7e0bcc7aa0 | ||
|
|
56fea79097 | ||
|
|
26a01dfc40 | ||
|
|
9c3962bbe3 | ||
|
|
1882e37bf0 | ||
|
|
45e264c398 | ||
|
|
41047b5db8 | ||
|
|
15f01af6cf | ||
|
|
bfa4911217 | ||
|
|
d1eba1ef2f | ||
|
|
5f20f0124a | ||
|
|
38f4cbe74f | ||
|
|
c071adb041 | ||
|
|
657c9b1b37 | ||
|
|
4c0414d2d6 | ||
|
|
2f342d3914 | ||
|
|
70823b899f | ||
|
|
b6aa0c886a | ||
|
|
3909c7b10e | ||
|
|
5306489d72 | ||
|
|
f62cc8bda5 | ||
|
|
d8a61de290 | ||
|
|
083d39be22 | ||
|
|
d48da40f7e | ||
|
|
1cf68ae448 | ||
|
|
c81e667e01 | ||
|
|
c95854ee3f | ||
|
|
b5cb5618c1 | ||
|
|
14afa24a0a | ||
|
|
0bfac998a5 | ||
|
|
0775bceb5e | ||
|
|
db5ae65f3c | ||
|
|
92b3137b79 | ||
|
|
77b1d05da2 | ||
|
|
2a89394174 | ||
|
|
4d68042884 | ||
|
|
5e57c5dfd7 | ||
|
|
d87ff42cf5 | ||
|
|
c4f7020aae | ||
|
|
624360bf39 | ||
|
|
ee5f144ea5 | ||
|
|
7ca0ead6a2 | ||
|
|
b415537ba5 | ||
|
|
a5a00f3d02 | ||
|
|
02c023f939 | ||
|
|
6f1d9db8ec | ||
|
|
3c923a3dc0 | ||
|
|
ec0881f50f | ||
|
|
488398df9f | ||
|
|
e452c6ebff | ||
|
|
19edf09ae2 | ||
|
|
4727e568d5 | ||
|
|
4a73a7ecd4 | ||
|
|
83d085cadc | ||
|
|
f3523bbba9 | ||
|
|
33bb38299b | ||
|
|
f314ff1bb6 | ||
|
|
1737686c1e | ||
|
|
5e29e3698e | ||
|
|
5e3f66a4cb | ||
|
|
c8178e7f1f | ||
|
|
c66f9efa55 | ||
|
|
73cad5a6c4 | ||
|
|
51a9750891 | ||
|
|
7bf97fac18 | ||
|
|
14954df6b9 | ||
|
|
388e4d2bcc | ||
|
|
0c866105d2 | ||
|
|
96cf4d288c | ||
|
|
e2cd370513 | ||
|
|
f9f1b8f355 | ||
|
|
f154f7ee3a | ||
|
|
1873e82804 | ||
|
|
400e057dab | ||
|
|
129246c9e6 | ||
|
|
8ca2d64bc9 | ||
|
|
e0acb42a28 | ||
|
|
1df146c702 | ||
|
|
323fb1f03c | ||
|
|
de01692ffd | ||
|
|
bcf5e25cab | ||
|
|
5aa0e97cd9 | ||
|
|
cf3d0dfdc3 | ||
|
|
83d352cfbe | ||
|
|
7dba734ab8 | ||
|
|
a3101edbb9 | ||
|
|
95fbd70346 | ||
|
|
acf5c15a05 | ||
|
|
560d9ceb71 | ||
|
|
bfa05c9432 | ||
|
|
c45454d9e5 | ||
|
|
f6991539a2 | ||
|
|
5f7366b7bd | ||
|
|
f640e71f82 | ||
|
|
52eef2d7c7 | ||
|
|
6bdf4f3ff3 | ||
|
|
82ae5c623f | ||
|
|
58758e0572 | ||
|
|
b116408ec3 | ||
|
|
6126e72d4f | ||
|
|
bebd1e5fb9 | ||
|
|
db509d0311 | ||
|
|
e1002d5ac0 | ||
|
|
99b3749890 | ||
|
|
ab875198ed | ||
|
|
f3d79ebcda | ||
|
|
01e8c232f0 | ||
|
|
eb7fcae31b | ||
|
|
eb941f7435 | ||
|
|
227d83d0e7 | ||
|
|
057d5eeb24 |
@@ -0,0 +1,4 @@
|
||||
* @epoberezkin @spaced4ndy
|
||||
/Dockerfile @shumvgolove
|
||||
/scripts/docker/ @shumvgolove
|
||||
/scripts/main/ @shumvgolove
|
||||
@@ -1 +1,2 @@
|
||||
github: simplex-chat
|
||||
open_collective: simplex-chat
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"template": "${{UNCATEGORIZED}}",
|
||||
"pr_template": "- ${{TITLE}}\n"
|
||||
"pr_template": "- ${{TITLE}}"
|
||||
}
|
||||
|
||||
+50
-19
@@ -11,33 +11,59 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
name: build-${{ matrix.os }}-${{ matrix.ghc }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-20.04
|
||||
platform_name: 20_04-x86-64
|
||||
ghc: "8.10.7"
|
||||
- os: ubuntu-20.04
|
||||
platform_name: 20_04-x86-64
|
||||
ghc: "9.6.3"
|
||||
- os: ubuntu-22.04
|
||||
platform_name: 22_04-x86-64
|
||||
ghc: "9.6.3"
|
||||
steps:
|
||||
- name: Clone project
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Stack
|
||||
- name: Setup Haskell
|
||||
uses: haskell/actions/setup@v1
|
||||
with:
|
||||
ghc-version: '8.8.4'
|
||||
enable-stack: true
|
||||
stack-version: 'latest'
|
||||
ghc-version: ${{ matrix.ghc }}
|
||||
cabal-version: "3.10.1.0"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.stack
|
||||
key: ${{ hashFiles('stack.yaml') }}
|
||||
path: |
|
||||
~/.cabal/store
|
||||
dist-newstyle
|
||||
key: ${{ matrix.os }}-${{ hashFiles('cabal.project', 'simplexmq.cabal') }}
|
||||
|
||||
- name: Build & test
|
||||
id: build_test
|
||||
run: |
|
||||
stack build --test --force-dirty
|
||||
install_root=$(stack path --local-install-root)
|
||||
mv ${install_root}/bin/smp-server smp-server-ubuntu-20_04-x86-64
|
||||
|
||||
- name: Build changelog
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: cabal build --enable-tests
|
||||
|
||||
- name: Test
|
||||
timeout-minutes: 30
|
||||
shell: bash
|
||||
run: cabal test --test-show-details=direct
|
||||
|
||||
- name: Prepare binaries
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
shell: bash
|
||||
run: |
|
||||
mv $(cabal list-bin smp-server) smp-server-ubuntu-${{ matrix.platform_name}}
|
||||
mv $(cabal list-bin ntf-server) ntf-server-ubuntu-${{ matrix.platform_name}}
|
||||
mv $(cabal list-bin xftp-server) xftp-server-ubuntu-${{ matrix.platform_name}}
|
||||
mv $(cabal list-bin xftp) xftp-ubuntu-${{ matrix.platform_name}}
|
||||
|
||||
- name: Build changelog
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
|
||||
id: build_changelog
|
||||
uses: mikepenz/release-changelog-builder-action@v1
|
||||
with:
|
||||
@@ -47,18 +73,23 @@ jobs:
|
||||
commitMode: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
- name: Create release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body: |
|
||||
See full changelog [here](https://github.com/simplex-chat/simplexmq/blob/master/CHANGELOG.md).
|
||||
Commits, chronological:
|
||||
|
||||
Commits:
|
||||
${{ steps.build_changelog.outputs.changelog }}
|
||||
prerelease: true
|
||||
files: |
|
||||
LICENSE
|
||||
smp-server-ubuntu-20_04-x86-64
|
||||
smp-server-ubuntu-${{ matrix.platform_name}}
|
||||
ntf-server-ubuntu-${{ matrix.platform_name}}
|
||||
xftp-server-ubuntu-${{ matrix.platform_name}}
|
||||
xftp-ubuntu-${{ matrix.platform_name}}
|
||||
fail_on_unmatched_files: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Build and push Docker image to Docker Hub
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and push Docker image
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- app: smp-server
|
||||
app_port: 5223
|
||||
- app: xftp-server
|
||||
app_port: 443
|
||||
steps:
|
||||
- name: Clone project
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Extract metadata for Docker image
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.app }}
|
||||
flavor: |
|
||||
latest=auto
|
||||
tags: |
|
||||
type=semver,pattern=v{{version}}
|
||||
type=semver,pattern=v{{major}}.{{minor}}
|
||||
type=semver,pattern=v{{major}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
push: true
|
||||
build-args: |
|
||||
APP=${{ matrix.app }}
|
||||
APP_PORT=${{ matrix.app_port }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -2,3 +2,12 @@
|
||||
*.db
|
||||
*.db.bak
|
||||
*.session.sql
|
||||
tests/tmp
|
||||
dist-newstyle/
|
||||
|
||||
cabal.project.local
|
||||
cabal.project.local~
|
||||
|
||||
.hpc/
|
||||
*.tix
|
||||
.coverage
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- ignore: {name: "Use underscore"}
|
||||
+375
@@ -1,3 +1,378 @@
|
||||
# 5.4.0
|
||||
|
||||
Migrate to GHC 9.6.3
|
||||
|
||||
Agent:
|
||||
- database improvements:
|
||||
- track slow queries.
|
||||
- better performance.
|
||||
- "busy" error handling.
|
||||
- create parent folder when needed.
|
||||
- support closing and re-opening database.
|
||||
- SMP agent improvements
|
||||
- streaming for batched SMP commands.
|
||||
- fix asynchronous JOINing connection.
|
||||
- handle repeating JOINs without failure.
|
||||
- api to get subscribed connections.
|
||||
- return simplex:/ links as invitations.
|
||||
- fix memory leak.
|
||||
- XFTP improvements:
|
||||
- suspend when agent is suspended.
|
||||
- support locally encrypted files.
|
||||
- fixes - create empty file, prevent permanent error treated as temporary.
|
||||
- upgrade HTTP2 library (fixes error handling and flow control).
|
||||
- Remote control protocol (XRCP)
|
||||
|
||||
SMP server:
|
||||
- control port commands for GHC threads introspection.
|
||||
- allow creating new queues without subscriptions (required for iOS).
|
||||
|
||||
XFTP server:
|
||||
- allow 64kb file chunks.
|
||||
|
||||
NTF server:
|
||||
- faster startup.
|
||||
|
||||
# 5.3.0
|
||||
|
||||
Agent:
|
||||
- improve performance, track slow database queries.
|
||||
- support delivery receipts.
|
||||
|
||||
SMP server:
|
||||
- control port
|
||||
|
||||
# 5.2.0 (NTF server 1.5.0)
|
||||
|
||||
Agent:
|
||||
- treat agent INACTIVE error as temporary - fixes failed message delivery in some race conditions.
|
||||
- restore connection confirmations after client restart - fixes failed connections.
|
||||
- ratchet resynchronization protocol and API.
|
||||
- increase connection version to mutually supported by both peers on each received message.
|
||||
|
||||
Client:
|
||||
- make timeout for batched functions dependent on the number of batches - fixes expiry on large batches.
|
||||
|
||||
Servers:
|
||||
- add timeout in case of sending TCP traffic and in case of partial delivery of requested blocks to avoid resource leaks.
|
||||
|
||||
# 5.1.2, 5.1.3 (NTF server 1.4.1, 1.4.2)
|
||||
|
||||
Agent:
|
||||
- ACK message on decryption error (fixes stuck message delivery bug)
|
||||
- more robust connection switching logic, API to abort switching the address
|
||||
|
||||
Notification server:
|
||||
- batch subscriptions to SMP servers
|
||||
|
||||
# 5.1.1 (NTF server 1.4.0)
|
||||
|
||||
Agent:
|
||||
- store and check hashes of previous encrypted messages to differentiate between duplicates and decryption errors
|
||||
|
||||
Server:
|
||||
- larger processing queues
|
||||
- expire messages when restoring them
|
||||
|
||||
# 5.1.0
|
||||
|
||||
XFTP client:
|
||||
- check encrypted file exists when uploading
|
||||
- remove user ID from deletion API
|
||||
|
||||
Agent:
|
||||
- vacuum database on migrations
|
||||
|
||||
SMP server:
|
||||
- configure message expiration time in INI file
|
||||
|
||||
# 5.0.0
|
||||
|
||||
SimpleX File Transfer Protocol (XFTP):
|
||||
- XFTP server
|
||||
- XFTP CLI
|
||||
- support XFTP in the agent
|
||||
|
||||
Other changes:
|
||||
- preserve retry interval for sending messages on agent restarts
|
||||
- support IPv6 in the servers and in the agent
|
||||
- support for 32-bit platforms
|
||||
|
||||
Read more about XFTP in this post: https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html
|
||||
|
||||
# 4.4.1
|
||||
|
||||
SMP agent:
|
||||
|
||||
- fix handling of server addresses.
|
||||
- prevent message delivery getting stuck on IO error in SMP client.
|
||||
|
||||
# 4.4.0
|
||||
|
||||
SMP agent:
|
||||
|
||||
- support multiple user profiles with transport session isolation.
|
||||
- support optional transport isolation per connection.
|
||||
- batch connection deletion
|
||||
- improve asynchronous connection deletion – it may now be completed after the client is restarted as well.
|
||||
- improve subscription logic to retry if initial attempt fails.
|
||||
- end SMP client connection after a number of failed PINGs (default is 3).
|
||||
|
||||
# 4.3.0
|
||||
|
||||
SMP server:
|
||||
|
||||
- additional server usage statistics.
|
||||
|
||||
SMP agent:
|
||||
|
||||
- increase retry interval when sending messages after ERR QUOTA.
|
||||
|
||||
# 4.2.0
|
||||
|
||||
SMP agent and server:
|
||||
|
||||
- reduce sender traffic in cases when queue quota is exceeded:
|
||||
- server sends quota exceeded message to the recipient when sender receives ERR QUOTA.
|
||||
- recipient sends QCONT message to the send once the queue is drained (via reply queue).
|
||||
- sender retry delays are increased, reducing traffic, but sender instantly resumes delivery where QCONT is received.
|
||||
|
||||
SMP server:
|
||||
|
||||
- increase internal queue sizes.
|
||||
|
||||
SMP agent:
|
||||
|
||||
- deduplicate connection IDs in connect/disconnect responses.
|
||||
- unit tests for Crypto.hs.
|
||||
- fix connection switch to another queue: correctly set primary send queue.
|
||||
|
||||
Notification server (v1.3.0):
|
||||
|
||||
- check token status when sending verification notification.
|
||||
|
||||
# 4.1.0
|
||||
|
||||
SMP agent and server:
|
||||
|
||||
- option to toggle TLS handshake error logs (disabled by default).
|
||||
|
||||
SMP agent:
|
||||
|
||||
- include server address in BROKER error.
|
||||
- api to get hash of double ratchet associated data (for connection verification).
|
||||
- api to get agent statistics.
|
||||
|
||||
# 4.0.0
|
||||
|
||||
SMP server:
|
||||
|
||||
- Basic authentication. The server address can now include an optional password that is required to create messaging queues, so the contacts who message you will not be able to receive messages via your server, unless you share with them the address with the password. It is recommended to enable basic authentication on all private servers by adding `create_password` parameter into AUTH section of server INI file (the previously deployed servers do not have this section, you need to add it).
|
||||
- Disable creating new queues completely with `new_queues: off` parameter in AUTH section of INI file - it can be used to simplify migrating the existing connections to another server.
|
||||
- Updated server CLI with changed defaults:
|
||||
- interactive server initialization, use -y flag to initialize the server non-interactively.
|
||||
- store log is now enabled by default, so messaging queues and messages are restored when the server is restarted (to restore undelivered messages the server needs to be stopped with SIGINT signal).
|
||||
- a random password is now generated by default during the server initialization.
|
||||
|
||||
SMP agent:
|
||||
|
||||
- API to test SMP servers. It connects to the server, creates and deletes a messaging queue. The new SimpleX Chat client uses this API to allow you to test that you have the correct server address, with the valid certificate fingerprint and password, before enabling the new server.
|
||||
|
||||
# 3.4.0
|
||||
|
||||
SMP agent:
|
||||
|
||||
- increase concurrency with connection-level locks
|
||||
- fix issues identified in security assessment (see the announcement: https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md)
|
||||
- manual connection queue rotation
|
||||
- optional client data in connection requests links
|
||||
|
||||
SMP server:
|
||||
|
||||
- specialize monad stack to improve performance
|
||||
- log slow commands
|
||||
|
||||
# 3.3.0
|
||||
|
||||
SMP server:
|
||||
|
||||
- allow repeated KEY command with the same key (to avoid failures on retries)
|
||||
|
||||
SMP agent:
|
||||
|
||||
- enable/disable connection notifications
|
||||
- asynchronous commands that retry on network error
|
||||
- use SQLCipher
|
||||
|
||||
# 3.2.0
|
||||
|
||||
SMP agent:
|
||||
|
||||
- Support multiple server hostnames (including onion hostnames) in server addresses.
|
||||
- Network configuration options.
|
||||
- Options to define rules to choose server hostname.
|
||||
|
||||
# 3.1.0
|
||||
|
||||
SMP server and agent:
|
||||
|
||||
- SMP protocol v4: batching multiple server commands/responses in a transport block.
|
||||
|
||||
# 3.0.0
|
||||
|
||||
SMP server:
|
||||
|
||||
- restore undelivered messages when the server is restarted.
|
||||
- SMP protocol v3 to support push notification:
|
||||
- updated SEND and MSG to add message flags (for notification flag that confirm whether the notification is sent and for any future extensions) and to move message meta-data sent to the recipient into the encrypted envelope.
|
||||
- update NKEY and NID to add e2e encryption keys (for the notification meta-data encryption between SMP server and the client), and update NMSG to include this meta-data.
|
||||
- update ACK command to include message ID (to avoid acknowledging unprocessed message).
|
||||
- add NDEL commands to remove notification subscription credentials from SMP queue.
|
||||
- add GET command to receive messages without subscription - to be used in iOS notification service extension to receive messages without terminating app subscriptions.
|
||||
|
||||
SMP agent:
|
||||
|
||||
- new protocol for duplex connection handshake reducing traffic and connection time.
|
||||
- support for SMP notifications server and managing device token.
|
||||
- remove redundant FQDN validation from TLS handshake to prepare for access via Tor.
|
||||
- support for fully stopping agent and for temporary suspending agent operations.
|
||||
- improve management of duplicate message delivery.
|
||||
|
||||
SMP notifications server v1.0:
|
||||
|
||||
- SMP notifications protocol with version negotiation during handshake.
|
||||
- device token registration and verification (via background notification).
|
||||
- SMP notification subscriptions and push notifications via APNS.
|
||||
- restoring notification subscriptions when the server is restarted.
|
||||
|
||||
# 2.3.0
|
||||
|
||||
SMP server:
|
||||
|
||||
- Save and restore undelivered messages, to avoid losing them. To save messages the server has to be stopped with SIGINT signal, if it is stopped with SIGTERM undelivered messages would not be saved.
|
||||
|
||||
# 2.2.0
|
||||
|
||||
SMP server:
|
||||
|
||||
- Fix sockets/threads/memory leak
|
||||
|
||||
SMP agent:
|
||||
|
||||
- Support stopping and resuming agent with `disconnectAgentClient` / `resumeAgentClient`
|
||||
|
||||
# 2.1.1
|
||||
|
||||
SMP server:
|
||||
|
||||
- gracefully close sockets on client disconnection
|
||||
- CLI warning when deleting server configuration
|
||||
|
||||
# 2.1.0
|
||||
|
||||
SMP server:
|
||||
|
||||
- configuration to expire inactive clients in ini file, increased TTL and check interval for client expiration
|
||||
|
||||
# 2.0.0
|
||||
|
||||
Push notifications server (beta):
|
||||
|
||||
- supports APNS
|
||||
- manage device tokens verification via notification delivery
|
||||
- sending periodic background notification to check messages (not more frequent than every 20 min)
|
||||
|
||||
SMP server:
|
||||
|
||||
- disconnect inactive clients after some period
|
||||
- remove undelivered messages after 30 days
|
||||
- log aggregate usage daily stats: only the number of queues created/secured/deleted/used and messages sent/delivered is logged, as one line per day, so we can plan server capacity and diagnose any problems.
|
||||
|
||||
SMP agent:
|
||||
|
||||
- manage device tokens and notification server connection
|
||||
- DOWN/UP events to the agent user about server disconnections/reconnections are now sent once per server
|
||||
|
||||
# 1.1.0
|
||||
|
||||
SMP server:
|
||||
|
||||
- message TTL and periodic deletion of old messages
|
||||
- configuration to prevent creation of the new queues
|
||||
|
||||
SMP agent:
|
||||
|
||||
- asynchronous connection handshake
|
||||
- configurable SMP servers at run-time
|
||||
- use TCP keep-alive for connection stability
|
||||
- improve stability of connection subscriptions
|
||||
- auto-vacuum DB to remove deleted records
|
||||
|
||||
# 1.0.3
|
||||
|
||||
SMP server:
|
||||
|
||||
- Reduce server message queue quota to 128 messages.
|
||||
|
||||
SMP agent:
|
||||
|
||||
- Add "yes to migrations" option.
|
||||
- Make new SMP client attempt to reconnect on network error.
|
||||
- Reduce connection handshake expiration to 2 days.
|
||||
|
||||
JSON encoding of types used in simplex-chat, some other minor adjustments.
|
||||
|
||||
# 1.0.2
|
||||
|
||||
General:
|
||||
|
||||
- Enable TLS 1.3 parameters for TLS handshake (server and client).
|
||||
- Switch from hs-tls fork to original repo now that it supports getFinished and getPeerFinished APIs for both TLS 1.2 and TLS 1.3.
|
||||
|
||||
SMP server:
|
||||
|
||||
- Perform TLS handshake in a separate thread per-connection.
|
||||
|
||||
SMP agent:
|
||||
|
||||
- Cease attempts to send HELLO after one week timeout.
|
||||
- Coalesce requests to connect to SMP servers, to have 1 connection per server.
|
||||
|
||||
# 1.0.1
|
||||
|
||||
SMP server:
|
||||
|
||||
- Explicitly set line buffering in stdout/stderr to log each line when output is redirected to files.
|
||||
|
||||
# 1.0.0
|
||||
|
||||
Security and privacy improvements:
|
||||
|
||||
- Faster and more secure 2-layer E2E encryption with additional encryption layer between servers and recipients:
|
||||
- application messages in each duplex connection (managed by SMP agents - see [overview](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md)) are encrypted using [double-ratchet algorithm](https://www.signal.org/docs/specifications/doubleratchet/), providing forward secrecy and break-in recovery. This layer uses two Curve448 keys per client for [X3DH key agreement](https://www.signal.org/docs/specifications/x3dh/), SHA512 based HKDFs and AES-GCM AEAD encryption.
|
||||
- SMP client messages are additionally E2E encrypted in each SMP queue to avoid cipher-text correlation of messages sent via multiple redundant queues (that will be supported soon). This and the next layer use [NaCl crypto_box algorithm](https://nacl.cr.yp.to/index.html) with XSalsa20Poly1305 cipher and Curve25519 keys for DH key agreement.
|
||||
- Messages delivered from the servers to the recipients are additionally encrypted to avoid cipher-text correlation between sent and received messages.
|
||||
- To prevent any traffic correlation by content size, SimpleX uses fixed transport block size of 16kb (16384 bytes) with padding on all encryption layers:
|
||||
- application messages are padded to 15788 bytes before E2E double-ratchet encryption.
|
||||
- messages between SMP clients are padded to 16032 bytes before E2E encryption in each SMP queue.
|
||||
- messages from the server to the recipient are padded to padded to 16088 bytes before the additional encryption layer (see above).
|
||||
- TLS 1.2+ with tls-unique channel binding in each command to prevent replay attacks.
|
||||
- Server identity verification via server offline certificate fingerprints included in SMP server addresses.
|
||||
|
||||
New functionality:
|
||||
|
||||
- Support for notification servers with new SMP commands: `NKEY`/`NID`, `NSUB`/`NMSG`.
|
||||
|
||||
Efficiency improvements:
|
||||
|
||||
- Binary protocol encodings to reduce overhead from circa 15% to approximately 3.7% of transmitted application message size, with only 2.2% overhead for SMP protocol messages.
|
||||
- More performant cryptographic algorithms.
|
||||
|
||||
For more information about SimpleX:
|
||||
|
||||
- [SimpleX overview](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md).
|
||||
- [SimpleX chat v1 announcement](https://github.com/simplex-chat/simplex-chat/blob/master/blog/20220112-simplex-chat-v1-released.md).
|
||||
|
||||
# 0.5.2
|
||||
|
||||
- Fix message delivery logic that blocked delivery of all server messages when server per-queue quota exceeded, making it concurrent per SMP queue, not per server.
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
ARG TAG=22.04
|
||||
|
||||
FROM ubuntu:${TAG} AS build
|
||||
|
||||
### Build stage
|
||||
|
||||
# Install curl and git and simplexmq dependencies
|
||||
RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev llvm-12 llvm-12-dev libnuma-dev
|
||||
|
||||
# Specify bootstrap Haskell versions
|
||||
ENV BOOTSTRAP_HASKELL_GHC_VERSION=8.10.7
|
||||
ENV BOOTSTRAP_HASKELL_CABAL_VERSION=3.6.2.0
|
||||
|
||||
# Install ghcup
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 sh
|
||||
|
||||
# Adjust PATH
|
||||
ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH"
|
||||
|
||||
# Set both as default
|
||||
RUN ghcup set ghc "${BOOTSTRAP_HASKELL_GHC_VERSION}" && \
|
||||
ghcup set cabal
|
||||
|
||||
COPY . /project
|
||||
WORKDIR /project
|
||||
|
||||
ARG APP
|
||||
ARG APP_PORT
|
||||
RUN if [ -z "$APP" ] || [ -z "$APP_PORT" ]; then printf "Please spcify \$APP and \$APP_PORT build-arg.\n"; exit 1; fi
|
||||
|
||||
# Compile app
|
||||
RUN cabal update
|
||||
RUN cabal build exe:$APP
|
||||
|
||||
# Create new path containing all files needed
|
||||
RUN mkdir /final
|
||||
WORKDIR /final
|
||||
|
||||
# Strip the binary from debug symbols to reduce size
|
||||
RUN bin=$(find /project/dist-newstyle -name "$APP" -type f -executable) && \
|
||||
mv "$bin" ./ && \
|
||||
strip ./"$APP" &&\
|
||||
mv /project/scripts/docker/entrypoint-"$APP" ./entrypoint
|
||||
|
||||
### Final stage
|
||||
FROM ubuntu:${TAG}
|
||||
|
||||
# Install OpenSSL dependency
|
||||
RUN apt-get update && apt-get install -y openssl libnuma-dev
|
||||
|
||||
# Copy compiled app from build stage
|
||||
COPY --from=build /final /usr/local/bin/
|
||||
|
||||
# Open app listening port
|
||||
ARG APP_PORT
|
||||
EXPOSE $APP_PORT
|
||||
|
||||
# simplexmq requires using SIGINT to correctly preserve undelivered messages and restore them on restart
|
||||
STOPSIGNAL SIGINT
|
||||
|
||||
# Finally, execute helper script
|
||||
ENTRYPOINT [ "/usr/local/bin/entrypoint" ]
|
||||
@@ -3,111 +3,299 @@
|
||||
[](https://github.com/simplex-chat/simplexmq/actions?query=workflow%3Abuild)
|
||||
[](https://github.com/simplex-chat/simplexmq/releases)
|
||||
|
||||
📢 **v0.5.1 brings a hotfix to the server's subscription management logic, to apply it log in to your server via SSH and run the following command. If you have store log enabled for your server, information about already established queues will be preserved.** If you're doing a custom installation instead of Linode or DigitalOcean you may have to change the path for binary download.
|
||||
📢 SimpleXMQ v1 is released - with many security, privacy and efficiency improvements, new functionality - see [release notes](https://github.com/simplex-chat/simplexmq/releases/tag/v1.0.0).
|
||||
|
||||
```sh
|
||||
systemctl stop smp-server
|
||||
curl -L -o /opt/simplex/bin/smp-server https://github.com/simplex-chat/simplexmq/releases/download/v0.5.1/smp-server-ubuntu-20_04-x86-64
|
||||
systemctl start smp-server
|
||||
```
|
||||
**Please note**: v1 is not backwards compatible, but it has the version negotiation built into all protocol layers for forwards compatibility of this version and backwards compatibility of the future versions, that will be backwards compatible for at least two versions back.
|
||||
|
||||
If you have a server deployed please deploy a new server to a new host and retire the previous version once it is no longer used.
|
||||
|
||||
## Message broker for unidirectional (simplex) queues
|
||||
|
||||
SimpleXMQ is a message broker for managing message queues and sending messages over public network. It consists of SMP server, SMP client library and SMP agent that implement [SMP protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md) for client-server communication and [SMP agent protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md) to manage duplex connections via simplex queues on multiple SMP servers.
|
||||
SimpleXMQ is a message broker for managing message queues and sending messages over public network. It consists of SMP server, SMP client library and SMP agent that implement [SMP protocol](./protocol/simplex-messaging.md) for client-server communication and [SMP agent protocol](./protocol/agent-protocol.md) to manage duplex connections via simplex queues on multiple SMP servers.
|
||||
|
||||
SMP protocol is inspired by [Redis serialization protocol](https://redis.io/topics/protocol), but it is much simpler - it currently has only 8 client commands and 6 server responses.
|
||||
SMP protocol is inspired by [Redis serialization protocol](https://redis.io/topics/protocol), but it is much simpler - it currently has only 10 client commands and 8 server responses.
|
||||
|
||||
SimpleXMQ is implemented in Haskell - it benefits from robust software transactional memory (STM) and concurrency primitives that Haskell provides.
|
||||
|
||||
## SimpleXMQ roadmap
|
||||
|
||||
- SMP queue redundancy and rotation in SMP agent duplex connections.
|
||||
- SimpleX service protocol and application template - to enable users building services and chat bots that work over SimpleX protocol stack. The first such service will be a notification service for a mobile app.
|
||||
- SMP queue redundancy and rotation in SMP agent connections.
|
||||
- SMP agents synchronization to share connections and messages between multiple agents (it would allow using multiple devices for [simplex-chat](https://github.com/simplex-chat/simplex-chat)).
|
||||
- Streams - high performance message queues. See [Streams RFC](https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2021-02-28-streams.md) for details.
|
||||
|
||||
## Components
|
||||
|
||||
### SMP server
|
||||
|
||||
[SMP server](https://github.com/simplex-chat/simplexmq/blob/master/apps/smp-server/Main.hs) can be run on any Linux distribution without any dependencies. It uses in-memory persistence with an optional append-only log of created queues that allows to re-start the server without losing the connections. This log is compacted on every server restart, permanently removing suspended and removed queues.
|
||||
[SMP server](./apps/smp-server/Main.hs) can be run on any Linux distribution, including low power/low memory devices. OpenSSL library is required for initialization.
|
||||
|
||||
To enable the queue logging, uncomment `enable: on` option in `smp-server.ini` configuration file that is created the first time the server is started.
|
||||
To initialize the server use `smp-server init -n <fqdn>` (or `smp-server init --ip <ip>` for IP based address) command - it will generate keys and certificates for TLS transport. The fingerprint of offline certificate is used as part of the server address to protect client/server connection against man-in-the-middle attacks: `smp://<fingerprint>@<hostname>[:5223]`.
|
||||
|
||||
On the first start the server generates an RSA key pair for encrypted transport handshake and outputs hash of the public key every time it runs - this hash should be used as part of the server address: `<hostname>:5223#<key hash>`.
|
||||
SMP server uses in-memory persistence with an optional append-only log of created queues that allows to re-start the server without losing the connections. This log is compacted on every server restart, permanently removing suspended and removed queues.
|
||||
|
||||
SMP server implements [SMP protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md).
|
||||
To enable store log, initialize server using `smp-server -l` command, or modify `smp-server.ini` created during initialization (uncomment `enable: on` option in the store log section). Use `smp-server --help` for other usage tips.
|
||||
|
||||
Starting from version 2.3.0, when store log is enabled, the server would also enable saving undelivered messages on exit and restoring them on start. This can be disabled via a separate setting `restore_messages` in `smp-server.ini` file. Saving messages would only work if the server is stopped with SIGINT signal (keyboard interrupt), if it is stopped with SIGTERM signal the messages would not be saved.
|
||||
|
||||
> **Please note:** On initialization SMP server creates a chain of two certificates: a self-signed CA certificate ("offline") and a server certificate used for TLS handshake ("online"). **You should store CA certificate private key securely and delete it from the server. If server TLS credential is compromised this key can be used to sign a new one, keeping the same server identity and established connections.** CA private key location by default is `/etc/opt/simplex/ca.key`.
|
||||
|
||||
SMP server implements [SMP protocol](./protocol/simplex-messaging.md).
|
||||
|
||||
#### Running SMP server on MacOS
|
||||
|
||||
SMP server requires OpenSSL library for initialization. On MacOS OpenSSL library may be replaced with LibreSSL, which doesn't support required algorithms. Before initializing SMP server verify you have OpenSSL installed:
|
||||
|
||||
```sh
|
||||
openssl version
|
||||
```
|
||||
|
||||
If it says "LibreSSL", please install original OpenSSL:
|
||||
|
||||
```sh
|
||||
brew update
|
||||
brew install openssl
|
||||
echo 'PATH="/opt/homebrew/opt/openssl@3/bin:$PATH"' >> ~/.zprofile # or follow whatever instructions brew suggests
|
||||
. ~/.zprofile # or restart your terminal to start a new session
|
||||
```
|
||||
|
||||
Now `openssl version` should be saying "OpenSSL". You can now run `smp-server init` to initialize your SMP server.
|
||||
|
||||
### SMP client library
|
||||
|
||||
[SMP client](https://github.com/simplex-chat/simplexmq/blob/master/src/Simplex/Messaging/Client.hs) is a Haskell library to connect to SMP servers that allows to:
|
||||
[SMP client](./src/Simplex/Messaging/Client.hs) is a Haskell library to connect to SMP servers that allows to:
|
||||
|
||||
- execute commands with a functional API.
|
||||
- receive messages and other notifications via STM queue.
|
||||
- automatically send keep-alive commands.
|
||||
|
||||
### SMP agent
|
||||
|
||||
[SMP agent library](https://github.com/simplex-chat/simplexmq/blob/master/src/Simplex/Messaging/Agent.hs) can be used to run SMP agent as part of another application and to communicate with the agent via STM queues, without serializing and parsing commands and responses.
|
||||
[SMP agent library](./src/Simplex/Messaging/Agent.hs) can be used to run SMP agent as part of another application and to communicate with the agent via STM queues, without serializing and parsing commands and responses.
|
||||
|
||||
Haskell type [ACommand](https://github.com/simplex-chat/simplexmq/blob/master/src/Simplex/Messaging/Agent/Protocol.hs) represents SMP agent protocol to communicate via STM queues.
|
||||
Haskell type [ACommand](./src/Simplex/Messaging/Agent/Protocol.hs) represents SMP agent protocol to communicate via STM queues.
|
||||
|
||||
See [simplex-chat](https://github.com/simplex-chat/simplex-chat) terminal UI for the example of integrating SMP agent into another application.
|
||||
|
||||
[SMP agent executable](https://github.com/simplex-chat/simplexmq/blob/master/apps/smp-agent/Main.hs) can be used to run a standalone SMP agent process that implements plaintext [SMP agent protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md) via TCP port 5224, so it can be used via telnet. It can be deployed in private networks to share access to the connections between multiple applications and services.
|
||||
[SMP agent executable](./apps/smp-agent/Main.hs) can be used to run a standalone SMP agent process that implements plaintext [SMP agent protocol](./protocol/agent-protocol.md) via TCP port 5224, so it can be used via telnet. It can be deployed in private networks to share access to the connections between multiple applications and services.
|
||||
|
||||
## Using SMP server and SMP agent
|
||||
|
||||
You can either run your own SMP server locally or deploy using [Linode StackScript](#deploy-smp-server-on-linode), or try local SMP agent with the deployed servers:
|
||||
You can either run your own SMP server locally or deploy using [Linode StackScript](https://cloud.linode.com/stackscripts/748014), or try local SMP agent with the deployed servers:
|
||||
|
||||
`smp2.simplex.im#z5W2QLQ1Br3Yd6CoWg7bIq1bHdwK7Y8bEiEXBs/WfAg=` (London, UK)
|
||||
`smp3.simplex.im#nxc7HnrnM8dOKgkMp008ub/9o9LXJlxlMrMpR+mfMQw=` (Fremont, CA)
|
||||
`smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im`
|
||||
|
||||
`smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im`
|
||||
|
||||
`smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im`
|
||||
|
||||
It's the easiest to try SMP agent via a prototype [simplex-chat](https://github.com/simplex-chat/simplex-chat) terminal UI.
|
||||
|
||||
[<img alt="Linode" src="https://raw.githubusercontent.com/simplex-chat/simplexmq/master/img/linode.svg" align="right" width="200">](https://cloud.linode.com/stackscripts/748014)
|
||||
## Deploy SMP/XFTP servers on Linux
|
||||
|
||||
You can run your SMP/XFTP server as a Linux process, optionally using a service manager for booting and restarts.
|
||||
|
||||
Notice that `smp-server` and `xftp-server` requires `openssl` as run-time dependency (it is used to generate server certificates during initialization). Install it with your packet manager:
|
||||
|
||||
```sh
|
||||
# For Ubuntu
|
||||
apt update && apt install openssl
|
||||
```
|
||||
|
||||
### Install binaries
|
||||
|
||||
#### Using Docker
|
||||
|
||||
On Linux, you can deploy smp and xftp server using Docker. This will download image from [Docker Hub](https://hub.docker.com/r/simplexchat).
|
||||
|
||||
1. Create directories for persistent Docker configuration:
|
||||
|
||||
```sh
|
||||
mkdir -p $HOME/simplex/{xftp,smp}/{config,logs} && mkdir -p $HOME/simplex/xftp/files
|
||||
```
|
||||
|
||||
2. Run your Docker container.
|
||||
|
||||
- `smp-server`
|
||||
|
||||
You must change **your_ip_or_domain**. `-e "pass=password"` is optional variable to password-protect your `smp` server:
|
||||
```sh
|
||||
docker run -d \
|
||||
-e "ADDR=your_ip_or_domain" \
|
||||
-e "PASS=password" \
|
||||
-p 5223:5223 \
|
||||
-v $HOME/simplex/smp/config:/etc/opt/simplex:z \
|
||||
-v $HOME/simplex/smp/logs:/var/opt/simplex:z \
|
||||
simplexchat/smp-server:latest
|
||||
```
|
||||
|
||||
- `xftp-server`
|
||||
|
||||
You must change **your_ip_or_domain** and **maximum_storage**.
|
||||
```sh
|
||||
docker run -d \
|
||||
-e "ADDR=your_ip_or_domain" \
|
||||
-e "QUOTA=maximum_storage" \
|
||||
-p 443:443 \
|
||||
-v $HOME/simplex/xftp/config:/etc/opt/simplex-xftp:z \
|
||||
-v $HOME/simplex/xftp/logs:/var/opt/simplex-xftp:z \
|
||||
-v $HOME/simplex/xftp/files:/srv/xftp:z \
|
||||
simplexchat/xftp-server:latest
|
||||
```
|
||||
|
||||
#### Using installation script
|
||||
|
||||
**Please note** that currently, only Ubuntu distribution is supported.
|
||||
|
||||
You can install and setup servers automatically using our script:
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/install.sh -o simplex-server-install.sh \
|
||||
&& if echo 'b8cf2be103f21f9461d9a500bcd3db06ab7d01d68871b07f4bd245195cbead1d simplex-server-install.sh' | sha256sum -c; then chmod +x ./simplex-server-install.sh && ./simplex-server-install.sh; rm ./simplex-server-install.sh; else echo "SHA-256 checksum is incorrect!" && rm ./simplex-server-install.sh; fi
|
||||
```
|
||||
|
||||
### Build from source
|
||||
|
||||
#### Using Docker
|
||||
|
||||
> **Please note:** to build the app use source code from [stable branch](https://github.com/simplex-chat/simplexmq/tree/stable).
|
||||
|
||||
On Linux, you can build smp server using Docker.
|
||||
|
||||
1. Build your images:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/simplex-chat/simplexmq
|
||||
cd simplexmq
|
||||
git checkout stable
|
||||
DOCKER_BUILDKIT=1 docker build -t local/smp-server --build-arg APP="smp-server" --build-arg APP_PORT="5223" . # For xmp-server
|
||||
DOCKER_BUILDKIT=1 docker build -t local/xftp-server --build-arg APP="xftp-server" --build-arg APP_PORT="443" . # For xftp-server
|
||||
```
|
||||
|
||||
2. Create directories for persistent Docker configuration:
|
||||
|
||||
```sh
|
||||
mkdir -p $HOME/simplex/{xftp,smp}/{config,logs} && mkdir -p $HOME/simplex/xftp/files
|
||||
```
|
||||
|
||||
3. Run your Docker container.
|
||||
|
||||
- `smp-server`
|
||||
|
||||
You must change **your_ip_or_domain**. `-e "pass=password"` is optional variable to password-protect your `smp` server:
|
||||
```sh
|
||||
docker run -d \
|
||||
-e "ADDR=your_ip_or_domain" \
|
||||
-e "PASS=password" \
|
||||
-p 5223:5223 \
|
||||
-v $HOME/simplex/smp/config:/etc/opt/simplex:z \
|
||||
-v $HOME/simplex/smp/logs:/var/opt/simplex:z \
|
||||
simplexchat/smp-server:latest
|
||||
```
|
||||
|
||||
- `xftp-server`
|
||||
|
||||
You must change **your_ip_or_domain** and **maximum_storage**.
|
||||
```sh
|
||||
docker run -d \
|
||||
-e "ADDR=your_ip_or_domain" \
|
||||
-e "QUOTA=maximum_storage" \
|
||||
-p 443:443 \
|
||||
-v $HOME/simplex/xftp/config:/etc/opt/simplex-xftp:z \
|
||||
-v $HOME/simplex/xftp/logs:/var/opt/simplex-xftp:z \
|
||||
-v $HOME/simplex/xftp/files:/srv/xftp:z \
|
||||
simplexchat/xftp-server:latest
|
||||
```
|
||||
|
||||
#### Using your distribution
|
||||
|
||||
1. Install [Haskell GHCup](https://www.haskell.org/ghcup/), GHC 8.10.7 and cabal:
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
|
||||
ghcup install ghc 8.10.7
|
||||
ghcup install cabal
|
||||
ghcup set ghc 8.10.7
|
||||
ghcup set cabal
|
||||
```
|
||||
|
||||
2. Build the project:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/simplex-chat/simplexmq
|
||||
cd simplexmq
|
||||
git checkout stable
|
||||
# On Ubuntu. Depending on your distribution, use your package manager to determine package names.
|
||||
apt-get update && apt-get install -y build-essential libgmp3-dev zlib1g-dev
|
||||
cabal update
|
||||
cabal install
|
||||
```
|
||||
|
||||
- Initialize SMP server with `smp-server init [-l] -n <fqdn>` or `smp-server init [-l] --ip <ip>` - depending on how you initialize it, either FQDN or IP will be used for server's address.
|
||||
|
||||
- Run `smp-server start` to start SMP server, or you can configure a service manager to run it as a service.
|
||||
|
||||
- Optionally, `smp-server` can be setup for having an onion address in `tor` network. See: [`scripts/tor`](./scripts/tor/). In this case, the server address can have both public and onion hostname pointing to the same server, to allow two people connect when only one of them is using Tor. The server address would be: `smp://<fingerprint>@<public_hostname>,<onion_hostname>`
|
||||
|
||||
See [this section](#smp-server) for more information. Run `smp-server -h` and `smp-server init -h` for explanation of commands and options.
|
||||
|
||||
[<img alt="Linode" src="./img/linode.svg" align="right" width="200">](https://cloud.linode.com/stackscripts/748014)
|
||||
|
||||
## Deploy SMP server on Linode
|
||||
|
||||
You can get Linode [free credits](https://www.linode.com/lp/affiliate-referral/?irclickid=02-QkdTEpxyLW0W0EOSREQreUkB2DtzGE2lGTE0&irgwc=1&utm_source=impact) to deploy SMP server.
|
||||
\* You can use free credit Linode offers when [creating a new account](https://www.linode.com/) to deploy an SMP server.
|
||||
|
||||
Deployment on [Linode](https://www.linode.com/) is performed via StackScripts, which serve as recipes for Linode instances, also called Linodes. To deploy SMP server on Linode:
|
||||
Deployment on Linode is performed via StackScripts, which serve as recipes for Linode instances, also called Linodes. To deploy SMP server on Linode:
|
||||
|
||||
- Create a Linode account or login with an already existing one.
|
||||
- Open [SMP server StackScript](https://cloud.linode.com/stackscripts/748014) and click "Deploy New Linode".
|
||||
- You can optionally configure the following parameters:
|
||||
- [SMP Server store log](#SMP-server) flag for queue persistence on server restart (recommended).
|
||||
- [Linode API token](https://www.linode.com/docs/guides/getting-started-with-the-linode-api#get-an-access-token) for attaching server info as tags to Linode (server address, public key hash, version) and adding A record to your 2nd level domain (Note: 2nd level e.g. `example.com` domain should be [created](https://cloud.linode.com/domains/create) in your account prior to deployment). The API token access scope should be read/write access to "linodes" (to update linode tags - you need them), and "domains" (to add A record for the 3rd level domain, e.g. `smp`).
|
||||
- Domain name to use instead of Linode ip address, e.g. `smp.example.com`.
|
||||
- Choose the region and plan according to your requirements (for regular use Shared CPU Nanode should be sufficient).
|
||||
- Provide ssh key to be able to connect to your Linode via ssh. This step is required if you haven't provided a Linode API token, because you will need to login to your Linode and get a public key hash either from the welcome message or from the file `/etc/opt/simplex/pub_key_hash` on your Linode after SMP server starts.
|
||||
- SMP Server store log flag for queue persistence on server restart, recommended.
|
||||
- [Linode API token](https://www.linode.com/docs/guides/getting-started-with-the-linode-api#get-an-access-token) to attach server address etc. as tags to Linode and to add A record to your 2nd level domain (e.g. `example.com` [domain should be created](https://cloud.linode.com/domains/create) in your account prior to deployment). The API token access scopes:
|
||||
- read/write for "linodes"
|
||||
- read/write for "domains"
|
||||
- Domain name to use instead of Linode IP address, e.g. `smp1.example.com`.
|
||||
- Choose the region and plan, Shared CPU Nanode with 1Gb is sufficient.
|
||||
- Provide ssh key to be able to connect to your Linode via ssh. If you haven't provided a Linode API token this step is required to login to your Linode and get the server's fingerprint either from the welcome message or from the file `/etc/opt/simplex/fingerprint` after server starts. See [Linode's guide on ssh](https://www.linode.com/docs/guides/use-public-key-authentication-with-ssh/) .
|
||||
- Deploy your Linode. After it starts wait for SMP server to start and for tags to appear (if a Linode API token was provided). It may take up to 5 minutes depending on the connection speed on the Linode. Connecting Linode IP address to provided domain name may take some additional time.
|
||||
- Get `hostname` and `hash` either from Linode tags (click on a tag and copy it's value from the browser search panel) or via ssh. Linode has a good [guide](https://www.linode.com/docs/guides/use-public-key-authentication-with-ssh/) about ssh.
|
||||
- Great, your own SMP server is ready! Use `address#hash` as SMP server address in the client.
|
||||
- Get `address` and `fingerprint` either from Linode tags (click on a tag and copy it's value from the browser search panel) or via ssh.
|
||||
- Great, your own SMP server is ready! If you provided FQDN use `smp://<fingerprint>@<fqdn>` as SMP server address in the client, otherwise use `smp://<fingerprint>@<ip_address>`.
|
||||
|
||||
Please submit an [issue](https://github.com/simplex-chat/simplexmq/issues) if any problems occur.
|
||||
|
||||
[<img alt="DigitalOcean" src="https://raw.githubusercontent.com/simplex-chat/simplexmq/master/img/digitalocean.png" align="right" width="300">](https://marketplace.digitalocean.com/apps/simplex-server)
|
||||
[<img alt="DigitalOcean" src="/img/digitalocean.png" align="right" width="300">](https://marketplace.digitalocean.com/apps/simplex-server)
|
||||
|
||||
## Deploy SMP server on DigitalOcean
|
||||
|
||||
You can deploy SMP server using [SimpleX Server 1-click app](https://marketplace.digitalocean.com/apps/simplex-server) from DigitalOcean marketplace:
|
||||
> 🚧 DigitalOcean snapshot is currently not up to date, it will soon be updated 🏗️
|
||||
|
||||
\* When creating a DigitalOcean account you can use [this link](https://try.digitalocean.com/freetrialoffer/) to get free credit. (You would still be required either to provide your credit card details or make a confirmation pre-payment with PayPal)
|
||||
|
||||
To deploy SMP server use [SimpleX Server 1-click app](https://marketplace.digitalocean.com/apps/simplex-server) from DigitalOcean marketplace:
|
||||
|
||||
- Create a DigitalOcean account or login with an already existing one.
|
||||
- Click 'Create SimpleX server Droplet' button.
|
||||
- Choose the region and plan according to your requirements (cheapest Regular plan should be sufficient).
|
||||
- Provide ssh key and confirm Droplet creation.
|
||||
- SSH to created Droplet (`ssh root@<droplet_ip_address>`) to get SMP server public key hash - either from the welcome message or from `/etc/opt/simplex/pub_key_hash`. DigitalOcean has a good guide on [how to login to Droplet via ssh](https://docs.digitalocean.com/products/droplets/how-to/connect-with-ssh/).
|
||||
- Great, your own SMP server is ready! Use `ip_address#hash` as SMP server address in the client.
|
||||
- Choose the region and plan according to your requirements (Basic plan should be sufficient).
|
||||
- Finalize Droplet creation.
|
||||
- Open "Console" on your Droplet management page to get SMP server fingerprint - either from the welcome message or from `/etc/opt/simplex/fingerprint`. Alternatively you can manually SSH to created Droplet, see [DigitalOcean instruction](https://docs.digitalocean.com/products/droplets/how-to/connect-with-ssh/).
|
||||
- Great, your own SMP server is ready! Use `smp://<fingerprint>@<ip_address>` as SMP server address in the client.
|
||||
|
||||
Please submit an [issue](https://github.com/simplex-chat/simplexmq/issues) if any problems occur.
|
||||
|
||||
> **Please note:** SMP server uses server address as a Common Name for server certificate generated during initialization. If you would like your server address to be FQDN instead of IP address, you can log in to your Droplet and run the commands below to re-initialize the server. Alternatively you can use [Linode StackScript](https://cloud.linode.com/stackscripts/748014) which allows this parameterization.
|
||||
|
||||
```sh
|
||||
smp-server delete
|
||||
smp-server init [-l] -n <fqdn>
|
||||
```
|
||||
|
||||
## SMP server design
|
||||
|
||||

|
||||

|
||||
|
||||
## SMP agent design
|
||||
|
||||

|
||||

|
||||
|
||||
## License
|
||||
|
||||
[AGPL v3](https://github.com/simplex-chat/simplexmq/blob/master/LICENSE)
|
||||
[AGPL v3](./LICENSE)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
module Main where
|
||||
|
||||
import Control.Logger.Simple
|
||||
import Simplex.Messaging.Notifications.Server.Main
|
||||
|
||||
cfgPath :: FilePath
|
||||
cfgPath = "/etc/opt/simplex-notifications"
|
||||
|
||||
logPath :: FilePath
|
||||
logPath = "/var/opt/simplex-notifications"
|
||||
|
||||
logCfg :: LogConfig
|
||||
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
setLogLevel LogDebug -- change to LogError in production
|
||||
withGlobalLogging logCfg $ ntfServerCLI cfgPath logPath
|
||||
+23
-4
@@ -6,12 +6,30 @@ module Main where
|
||||
|
||||
import Control.Logger.Simple
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Simplex.Messaging.Agent (runSMPAgent)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Transport (TCP, Transport (..))
|
||||
import Simplex.Messaging.Agent.Server (runSMPAgent)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..))
|
||||
import Simplex.Messaging.Client (defaultNetworkConfig)
|
||||
import Simplex.Messaging.Transport (TLS, Transport (..))
|
||||
|
||||
cfg :: AgentConfig
|
||||
cfg = defaultAgentConfig {smpServers = L.fromList ["localhost:5223#bU0K+bRg24xWW//lS0umO1Zdw/SXqpJNtm1/RrPLViE="]}
|
||||
cfg = defaultAgentConfig
|
||||
|
||||
agentDbFile :: String
|
||||
agentDbFile = "smp-agent.db"
|
||||
|
||||
agentDbKey :: String
|
||||
agentDbKey = ""
|
||||
|
||||
servers :: InitialAgentServers
|
||||
servers =
|
||||
InitialAgentServers
|
||||
{ smp = M.fromList [(1, L.fromList ["smp://bU0K-bRg24xWW__lS0umO1Zdw_SXqpJNtm1_RrPLViE=@localhost:5223"])],
|
||||
ntf = [],
|
||||
xftp = M.empty,
|
||||
netCfg = defaultNetworkConfig
|
||||
}
|
||||
|
||||
logCfg :: LogConfig
|
||||
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
@@ -20,4 +38,5 @@ main :: IO ()
|
||||
main = do
|
||||
putStrLn $ "SMP agent listening on port " ++ tcpPort (cfg :: AgentConfig)
|
||||
setLogLevel LogInfo -- LogError
|
||||
withGlobalLogging logCfg $ runSMPAgent (transport @TCP) cfg
|
||||
Right st <- createAgentStore agentDbFile agentDbKey MCConsole
|
||||
withGlobalLogging logCfg $ runSMPAgent (transport @TLS) cfg servers st
|
||||
|
||||
+16
-267
@@ -1,277 +1,26 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Main where
|
||||
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Trans.Except
|
||||
import qualified Crypto.Store.PKCS8 as S
|
||||
import Data.ByteString.Base64 (encode)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Char (toLower)
|
||||
import Data.Ini (Ini, lookupValue, readIniFile)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.X509 (PrivKey (PrivKeyRSA))
|
||||
import Network.Socket (ServiceName)
|
||||
import Options.Applicative
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Server (runSMPServer)
|
||||
import Simplex.Messaging.Server.Env.STM
|
||||
import Simplex.Messaging.Server.StoreLog (StoreLog, openReadStoreLog, storeLogFilePath)
|
||||
import Simplex.Messaging.Transport (ATransport (..), TCP, Transport (..))
|
||||
import Simplex.Messaging.Transport.WebSockets (WS)
|
||||
import System.Directory (createDirectoryIfMissing, doesFileExist, removeFile)
|
||||
import System.Exit (exitFailure)
|
||||
import System.FilePath (combine)
|
||||
import System.IO (IOMode (..), hFlush, stdout)
|
||||
import Text.Read (readEither)
|
||||
import Control.Logger.Simple
|
||||
import Simplex.Messaging.Server.Main
|
||||
import System.Environment
|
||||
|
||||
defaultServerPort :: ServiceName
|
||||
defaultServerPort = "5223"
|
||||
defaultCfgPath :: FilePath
|
||||
defaultCfgPath = "/etc/opt/simplex"
|
||||
|
||||
defaultBlockSize :: Int
|
||||
defaultBlockSize = 4096
|
||||
defaultLogPath :: FilePath
|
||||
defaultLogPath = "/var/opt/simplex"
|
||||
|
||||
serverConfig :: ServerConfig
|
||||
serverConfig =
|
||||
ServerConfig
|
||||
{ tbqSize = 16,
|
||||
serverTbqSize = 128,
|
||||
msgQueueQuota = 256,
|
||||
queueIdBytes = 12,
|
||||
msgIdBytes = 6,
|
||||
-- below parameters are set based on ini file /etc/opt/simplex/smp-server.ini
|
||||
transports = undefined,
|
||||
storeLog = undefined,
|
||||
blockSize = undefined,
|
||||
serverPrivateKey = undefined
|
||||
}
|
||||
|
||||
newKeySize :: Int
|
||||
newKeySize = 2048 `div` 8
|
||||
|
||||
cfgDir :: FilePath
|
||||
cfgDir = "/etc/opt/simplex"
|
||||
|
||||
logDir :: FilePath
|
||||
logDir = "/var/opt/simplex"
|
||||
|
||||
defaultStoreLogFile :: FilePath
|
||||
defaultStoreLogFile = combine logDir "smp-server-store.log"
|
||||
|
||||
iniFile :: FilePath
|
||||
iniFile = combine cfgDir "smp-server.ini"
|
||||
|
||||
defaultKeyFile :: FilePath
|
||||
defaultKeyFile = combine cfgDir "server_key"
|
||||
logCfg :: LogConfig
|
||||
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
opts <- getServerOpts
|
||||
case serverCommand opts of
|
||||
ServerInit ->
|
||||
runExceptT (getConfig opts) >>= \case
|
||||
Right cfg -> do
|
||||
putStrLn "Error: server is already initialized. Start it with `smp-server start` command"
|
||||
printConfig cfg
|
||||
exitFailure
|
||||
Left _ -> do
|
||||
cfg <- initializeServer opts
|
||||
putStrLn "Server was initialized. Start it with `smp-server start` command"
|
||||
printConfig cfg
|
||||
ServerStart ->
|
||||
runExceptT (getConfig opts) >>= \case
|
||||
Right cfg -> runServer cfg
|
||||
Left e -> do
|
||||
putStrLn $ "Server is not initialized: " <> e
|
||||
putStrLn "Initialize server with `smp-server init` command"
|
||||
exitFailure
|
||||
ServerDelete -> do
|
||||
deleteServer
|
||||
putStrLn "Server key, config file and store log deleted"
|
||||
|
||||
getConfig :: ServerOpts -> ExceptT String IO ServerConfig
|
||||
getConfig opts = do
|
||||
ini <- readIni
|
||||
pk <- readKey ini
|
||||
storeLog <- liftIO $ openStoreLog opts ini
|
||||
pure $ makeConfig ini pk storeLog
|
||||
|
||||
makeConfig :: IniOpts -> C.FullPrivateKey -> Maybe (StoreLog 'ReadMode) -> ServerConfig
|
||||
makeConfig IniOpts {serverPort, blockSize, enableWebsockets} pk storeLog =
|
||||
let transports = (serverPort, transport @TCP) : [("80", transport @WS) | enableWebsockets]
|
||||
in serverConfig {serverPrivateKey = pk, storeLog, blockSize, transports}
|
||||
|
||||
printConfig :: ServerConfig -> IO ()
|
||||
printConfig ServerConfig {serverPrivateKey, storeLog} = do
|
||||
B.putStrLn $ "transport key hash: " <> serverKeyHash serverPrivateKey
|
||||
putStrLn $ case storeLog of
|
||||
Just s -> "store log: " <> storeLogFilePath s
|
||||
Nothing -> "store log disabled"
|
||||
|
||||
initializeServer :: ServerOpts -> IO ServerConfig
|
||||
initializeServer opts = do
|
||||
createDirectoryIfMissing False cfgDir
|
||||
ini <- createIni opts
|
||||
pk <- createKey ini
|
||||
storeLog <- openStoreLog opts ini
|
||||
pure $ makeConfig ini pk storeLog
|
||||
|
||||
runServer :: ServerConfig -> IO ()
|
||||
runServer cfg = do
|
||||
printConfig cfg
|
||||
forM_ (transports cfg) $ \(port, ATransport t) ->
|
||||
putStrLn $ "listening on port " <> port <> " (" <> transportName t <> ")"
|
||||
runSMPServer cfg
|
||||
|
||||
deleteServer :: IO ()
|
||||
deleteServer = do
|
||||
ini <- runExceptT readIni
|
||||
deleteIfExists iniFile
|
||||
case ini of
|
||||
Right IniOpts {storeLogFile, serverKeyFile} -> do
|
||||
deleteIfExists storeLogFile
|
||||
deleteIfExists serverKeyFile
|
||||
Left _ -> do
|
||||
deleteIfExists defaultKeyFile
|
||||
deleteIfExists defaultStoreLogFile
|
||||
|
||||
data IniOpts = IniOpts
|
||||
{ enableStoreLog :: Bool,
|
||||
storeLogFile :: FilePath,
|
||||
serverKeyFile :: FilePath,
|
||||
serverPort :: ServiceName,
|
||||
blockSize :: Int,
|
||||
enableWebsockets :: Bool
|
||||
}
|
||||
|
||||
readIni :: ExceptT String IO IniOpts
|
||||
readIni = do
|
||||
fileExists iniFile
|
||||
ini <- ExceptT $ readIniFile iniFile
|
||||
let enableStoreLog = (== Right "on") $ lookupValue "STORE_LOG" "enable" ini
|
||||
storeLogFile = opt defaultStoreLogFile "STORE_LOG" "file" ini
|
||||
serverKeyFile = opt defaultKeyFile "TRANSPORT" "key_file" ini
|
||||
serverPort = opt defaultServerPort "TRANSPORT" "port" ini
|
||||
enableWebsockets = (== Right "on") $ lookupValue "TRANSPORT" "websockets" ini
|
||||
blockSize <- liftEither . readEither $ opt (show defaultBlockSize) "TRANSPORT" "block_size" ini
|
||||
pure IniOpts {enableStoreLog, storeLogFile, serverKeyFile, serverPort, blockSize, enableWebsockets}
|
||||
where
|
||||
opt :: String -> Text -> Text -> Ini -> String
|
||||
opt def section key ini = either (const def) T.unpack $ lookupValue section key ini
|
||||
|
||||
createIni :: ServerOpts -> IO IniOpts
|
||||
createIni ServerOpts {enableStoreLog} = do
|
||||
writeFile iniFile $
|
||||
"[STORE_LOG]\n\
|
||||
\# The server uses STM memory to store SMP queues and messages,\n\
|
||||
\# that will be lost on restart (e.g., as with redis).\n\
|
||||
\# This option enables saving SMP queues to append only log,\n\
|
||||
\# and restoring them when the server is started.\n\
|
||||
\# Log is compacted on start (deleted queues are removed).\n\
|
||||
\# The messages in the queues are not logged.\n\n"
|
||||
<> (if enableStoreLog then "" else "# ")
|
||||
<> "enable: on\n\
|
||||
\# file: "
|
||||
<> defaultStoreLogFile
|
||||
<> "\n\n\
|
||||
\[TRANSPORT]\n\n\
|
||||
\# key_file: "
|
||||
<> defaultKeyFile
|
||||
<> "\n\
|
||||
\# port: "
|
||||
<> defaultServerPort
|
||||
<> "\n\
|
||||
\# block_size: "
|
||||
<> show defaultBlockSize
|
||||
<> "\n\
|
||||
\websockets: on\n"
|
||||
pure
|
||||
IniOpts
|
||||
{ enableStoreLog,
|
||||
storeLogFile = defaultStoreLogFile,
|
||||
serverKeyFile = defaultKeyFile,
|
||||
serverPort = defaultServerPort,
|
||||
blockSize = defaultBlockSize,
|
||||
enableWebsockets = True
|
||||
}
|
||||
|
||||
readKey :: IniOpts -> ExceptT String IO C.FullPrivateKey
|
||||
readKey IniOpts {serverKeyFile} = do
|
||||
fileExists serverKeyFile
|
||||
liftIO (S.readKeyFile serverKeyFile) >>= \case
|
||||
[S.Unprotected (PrivKeyRSA pk)] -> pure $ C.FullPrivateKey pk
|
||||
[_] -> err "not RSA key"
|
||||
[] -> err "invalid key file format"
|
||||
_ -> err "more than one key"
|
||||
where
|
||||
err :: String -> ExceptT String IO b
|
||||
err e = throwE $ e <> ": " <> serverKeyFile
|
||||
|
||||
createKey :: IniOpts -> IO C.FullPrivateKey
|
||||
createKey IniOpts {serverKeyFile} = do
|
||||
(_, pk) <- C.generateKeyPair newKeySize
|
||||
S.writeKeyFile S.TraditionalFormat serverKeyFile [PrivKeyRSA $ C.rsaPrivateKey pk]
|
||||
pure pk
|
||||
|
||||
fileExists :: FilePath -> ExceptT String IO ()
|
||||
fileExists path = do
|
||||
exists <- liftIO $ doesFileExist path
|
||||
unless exists . throwE $ "file " <> path <> " not found"
|
||||
|
||||
deleteIfExists :: FilePath -> IO ()
|
||||
deleteIfExists path = doesFileExist path >>= (`when` removeFile path)
|
||||
|
||||
confirm :: String -> IO ()
|
||||
confirm msg = do
|
||||
putStr $ msg <> " (y/N): "
|
||||
hFlush stdout
|
||||
ok <- getLine
|
||||
when (map toLower ok /= "y") exitFailure
|
||||
|
||||
serverKeyHash :: C.FullPrivateKey -> B.ByteString
|
||||
serverKeyHash = encode . C.unKeyHash . C.publicKeyHash . C.publicKey'
|
||||
|
||||
openStoreLog :: ServerOpts -> IniOpts -> IO (Maybe (StoreLog 'ReadMode))
|
||||
openStoreLog ServerOpts {enableStoreLog = l} IniOpts {enableStoreLog = l', storeLogFile = f}
|
||||
| l || l' = do
|
||||
createDirectoryIfMissing True logDir
|
||||
Just <$> openReadStoreLog f
|
||||
| otherwise = pure Nothing
|
||||
|
||||
data ServerOpts = ServerOpts
|
||||
{ serverCommand :: ServerCommand,
|
||||
enableStoreLog :: Bool
|
||||
}
|
||||
|
||||
data ServerCommand = ServerInit | ServerStart | ServerDelete
|
||||
|
||||
serverOpts :: Parser ServerOpts
|
||||
serverOpts =
|
||||
ServerOpts
|
||||
<$> subparser
|
||||
( command "init" (info (pure ServerInit) (progDesc "Initialize server: generate server key and ini file"))
|
||||
<> command "start" (info (pure ServerStart) (progDesc "Start server (ini: /etc/opt/simplex/smp-server.ini)"))
|
||||
<> command "delete" (info (pure ServerDelete) (progDesc "Delete server key, ini file and store log"))
|
||||
)
|
||||
<*> switch
|
||||
( long "store-log"
|
||||
<> short 'l'
|
||||
<> help "enable store log for SMP queues persistence"
|
||||
)
|
||||
|
||||
getServerOpts :: IO ServerOpts
|
||||
getServerOpts = customExecParser p opts
|
||||
where
|
||||
p = prefs showHelpOnEmpty
|
||||
opts =
|
||||
info
|
||||
(serverOpts <**> helper)
|
||||
( fullDesc
|
||||
<> header "Simplex Messaging Protocol (SMP) Server"
|
||||
)
|
||||
setLogLevel LogDebug
|
||||
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
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
module Main where
|
||||
|
||||
import Control.Logger.Simple
|
||||
import Simplex.FileTransfer.Server.Main
|
||||
|
||||
cfgPath :: FilePath
|
||||
cfgPath = "/etc/opt/simplex-xftp"
|
||||
|
||||
logPath :: FilePath
|
||||
logPath = "/var/opt/simplex-xftp"
|
||||
|
||||
logCfg :: LogConfig
|
||||
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
setLogLevel LogDebug -- change to LogError in production
|
||||
withGlobalLogging logCfg $ xftpServerCLI cfgPath logPath
|
||||
@@ -0,0 +1,6 @@
|
||||
module Main where
|
||||
|
||||
import Simplex.FileTransfer.Client.Main
|
||||
|
||||
main :: IO ()
|
||||
main = xftpClientCLI
|
||||
@@ -0,0 +1,32 @@
|
||||
packages: .
|
||||
-- packages: . ../direct-sqlcipher ../sqlcipher-simple
|
||||
-- packages: . ../hs-socks
|
||||
-- packages: . ../http2
|
||||
-- packages: . ../network-transport
|
||||
|
||||
with-compiler: ghc-8.10.7
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/aeson.git
|
||||
tag: aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/hs-socks.git
|
||||
tag: a30cc7a79a08d8108316094f8f2f82a0c5e1ac51
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/kazu-yamamoto/http2.git
|
||||
tag: f5525b755ff2418e6e6ecc69e877363b0d0bcaeb
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/direct-sqlcipher.git
|
||||
tag: 34309410eb2069b029b8fc1872deb1e0db123294
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/sqlcipher-simple.git
|
||||
tag: 5e154a2aeccc33ead6c243ec07195ab673137221
|
||||
@@ -0,0 +1,3 @@
|
||||
# Streamlined NTRU Prime: sntrup761
|
||||
|
||||
The implementation of sntrup761 is the _exact_ copy from this [Internet draft](https://www.ietf.org/archive/id/draft-josefsson-ntruprime-streamlined-00.html).
|
||||
@@ -0,0 +1,9 @@
|
||||
#include <openssl/sha.h>
|
||||
#include "sha512.h"
|
||||
|
||||
void crypto_hash_sha512 (unsigned char *out,
|
||||
const unsigned char *in,
|
||||
unsigned long long inlen)
|
||||
{
|
||||
SHA512(in, inlen, out);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
void crypto_hash_sha512 (unsigned char *out,
|
||||
const unsigned char *in,
|
||||
unsigned long long inlen);
|
||||
+1035
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Derived from public domain source, written by (in alphabetical order):
|
||||
* - Daniel J. Bernstein
|
||||
* - Chitchanok Chuengsatiansup
|
||||
* - Tanja Lange
|
||||
* - Christine van Vredendaal
|
||||
*/
|
||||
|
||||
#ifndef SNTRUP761_H
|
||||
#define SNTRUP761_H
|
||||
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define SNTRUP761_SECRETKEY_SIZE 1763
|
||||
#define SNTRUP761_PUBLICKEY_SIZE 1158
|
||||
#define SNTRUP761_CIPHERTEXT_SIZE 1039
|
||||
#define SNTRUP761_SIZE 32
|
||||
|
||||
typedef void sntrup761_random_func (void *ctx, size_t length, uint8_t *dst);
|
||||
|
||||
void
|
||||
sntrup761_keypair (uint8_t *pk, uint8_t *sk,
|
||||
void *random_ctx, sntrup761_random_func *random);
|
||||
|
||||
void
|
||||
sntrup761_enc (uint8_t *c, uint8_t *k, const uint8_t *pk,
|
||||
void *random_ctx, sntrup761_random_func *random);
|
||||
|
||||
void
|
||||
sntrup761_dec (uint8_t *k, const uint8_t *c, const uint8_t *sk);
|
||||
|
||||
#endif /* SNTRUP761_H */
|
||||
@@ -0,0 +1,30 @@
|
||||
indentation: 2
|
||||
column-limit: none
|
||||
function-arrows: trailing
|
||||
comma-style: trailing
|
||||
import-export-style: trailing
|
||||
indent-wheres: true
|
||||
record-brace-space: true
|
||||
newlines-between-decls: 1
|
||||
haddock-style: single-line
|
||||
haddock-style-module: null
|
||||
let-style: inline
|
||||
in-style: right-align
|
||||
single-constraint-parens: never
|
||||
unicode: never
|
||||
respectful: true
|
||||
fixities:
|
||||
- infixr 9 .
|
||||
- infixr 8 .:, .:., .=
|
||||
- infixr 6 <>
|
||||
- infixr 5 ++
|
||||
- infixl 4 <$>, <$, $>, <$$>, <$?>
|
||||
- infixl 4 <*>, <*, *>, <**>
|
||||
- infix 4 ==, /=
|
||||
- infixr 3 &&
|
||||
- infixl 3 <|>
|
||||
- infixr 2 ||
|
||||
- infixl 1 >>, >>=
|
||||
- infixr 1 =<<, >=>, <=<
|
||||
- infixr 0 $, $!
|
||||
reexports: []
|
||||
+1
-1
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 506.94 131.5"><path d="m109.65 58.21-18.21-10.07-15.37 9.38-.19 9.59-7.47-4.92-10.16 6.19-.44-10.48-10.48-7 10-5.17c-.08 0 0 1-1.48-34.34l-23.65-11.39-32.2 10 7.19 34.63 10.81 8.37-8.22 3.9 5.37 26.1 7.55 7.09-5.4 3.29 4.19 20.18 16.94 17.94c.08-.1 1.78-1.41 21.79-17.32l-.58-13.71 8.58 7.28c.12-.12 1.84-1.39 17.56-13.89l.61-10.14 6.48 4.5c.13-.12 1.58-1.22 14.26-11.33z" fill="#231f20"/><path d="m94.54 69 15.11-10.79-18.21-10.07-15.37 9.38z"/><path d="m92.87 88.2 1.67-19.2-18.47-11.48-.38 18.75z" fill="#004b16"/><path d="m68.22 107.73v-19.89l-18.66-14.15 1.47 19.54z" fill="#004b16"/><path d="m68.22 87.84 18.74-13.43-18.55-12.22-18.85 11.5z"/><path d="m38.43 131.48-2.98-20.32-18.15-17.8 4.19 20.18z" fill="#004b16"/><path d="m35.45 111.16 23.91-17.06-18.53-15.09-23.53 14.35z"/><path d="m33.9 100.6-3.94-26.88-20.22-16.81 5.41 26.07z" fill="#004b16"/><path d="m29.96 73.72 27.85-15.82-20.8-13.94-27.27 12.95z"/><path d="m28.07 60.88-5.4-36.81-22.67-14.06 7.19 34.62z" fill="#004b16"/><path d="m22.67 24.07 33.14-12.7-23.61-11.37-32.2 10.01z"/><g fill="#1cb35c"><path d="m107.13 76.87c-14.02 11.13-14.33 11.33-14.26 11.33 1.74-20.1 1.59-19.2 1.67-19.2 16-11.45 15-10.79 15.11-10.79z"/><path d="m85.78 93.84c-17.35 13.8-17.63 13.89-17.56 13.89-.17-20.82-.07-19.89 0-19.89 20-14.3 18.67-13.43 18.74-13.43z"/><path d="m60.22 114.16c-21.66 17.22-21.86 17.32-21.79 17.32-3.07-20.94-3-20.32-3-20.32 25.47-18.16 23.86-17.06 23.93-17.06z"/><path d="m55.81 11.37c1.52 35.37 1.4 34.34 1.48 34.34-28.66 14.89-29.29 15.17-29.22 15.17-5.52-37.63-5.47-36.81-5.4-36.81z"/><path d="m57.81 57.9c1.15 26.81 1 25.88 1.11 25.88-24.81 16.67-25.09 16.82-25 16.82-4-27.58-4-26.88-3.94-26.88z"/></g><path xmlns="http://www.w3.org/2000/svg" d="m151.61 14.24 16.58-4v79.88q0 13.13 7.83 15.65-3.84 7.31-13.13 7.3-11.28 0-11.28-15.66z"/><path d="m186.66 111.72v-57.42h-9.08v-13.6h25.86v71zm8.56-98.54a9.62 9.62 0 1 1 -9.62 9.62 9.63 9.63 0 0 1 9.62-9.62z"/><path d="m262.94 111.74v-41.06c0-6.05-1.16-10.48-3.48-13.26s-6.11-4.18-11.37-4.18a17.74 17.74 0 0 0 -7.8 2.06 18 18 0 0 0 -6.46 5.1v51.34h-16.59v-71h11.94l3 6.64q6.76-8 20-8 12.66 0 20 7.59t7.33 21.19v43.58z"/><path d="m288.42 76.06q0-16.26 9.38-26.47t24.77-10.21q16.19 0 25.14 9.82t9 26.86q0 17-9.12 27t-25 10q-16.18 0-25.17-10.12t-9-26.88zm17.24 0q0 23.47 16.91 23.48a14.54 14.54 0 0 0 12.31-6.11q4.55-6.09 4.54-17.37 0-23.14-16.85-23.15a14.61 14.61 0 0 0 -12.33 6.09q-4.58 6.11-4.58 17.06z"/><path d="m412.37 111.74v-4.31c-1.37 1.5-3.71 2.82-7 3.95a31.33 31.33 0 0 1 -10.15 1.69q-14.87 0-23.38-9.42t-8.52-26.27q0-16.84 9.78-27.42a32 32 0 0 1 24.51-10.58 32.37 32.37 0 0 1 14.72 3.32v-28.46l16.58-4v101.5zm0-54.06a17.6 17.6 0 0 0 -11.07-4.24q-10 0-15.33 6.07t-5.37 17.41q0 22.15 21.36 22.15a16.11 16.11 0 0 0 5.87-1.42c2.32-1 3.83-1.92 4.54-2.89z"/><path d="m505.55 81.3h-50.74q.46 8.49 5.83 13.2t14.46 4.7q11.34 0 17.25-5.9l6.43 12.7q-8.75 7.09-26.13 7.1-16.25 0-25.7-9.52t-9.45-26.58q0-16.78 10.38-27.2a33.91 33.91 0 0 1 24.9-10.41q15.45 0 24.81 9.22t9.35 23.48a46.28 46.28 0 0 1 -1.39 9.21zm-50.15-12.47h34.89q-1.73-15.58-17.24-15.59-14.2 0-17.65 15.59z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 506.94 131.5"><path d="m109.65 58.21-18.21-10.07-15.37 9.38-.19 9.59-7.47-4.92-10.16 6.19-.44-10.48-10.48-7 10-5.17c-.08 0 0 1-1.48-34.34l-23.65-11.39-32.2 10 7.19 34.63 10.81 8.37-8.22 3.9 5.37 26.1 7.55 7.09-5.4 3.29 4.19 20.18 16.94 17.94c.08-.1 1.78-1.41 21.79-17.32l-.58-13.71 8.58 7.28c.12-.12 1.84-1.39 17.56-13.89l.61-10.14 6.48 4.5c.13-.12 1.58-1.22 14.26-11.33z" fill="#231f20"/><path d="m94.54 69 15.11-10.79-18.21-10.07-15.37 9.38z"/><path d="m92.87 88.2 1.67-19.2-18.47-11.48-.38 18.75z" fill="#004b16"/><path d="m68.22 107.73v-19.89l-18.66-14.15 1.47 19.54z" fill="#004b16"/><path d="m68.22 87.84 18.74-13.43-18.55-12.22-18.85 11.5z"/><path d="m38.43 131.48-2.98-20.32-18.15-17.8 4.19 20.18z" fill="#004b16"/><path d="m35.45 111.16 23.91-17.06-18.53-15.09-23.53 14.35z"/><path d="m33.9 100.6-3.94-26.88-20.22-16.81 5.41 26.07z" fill="#004b16"/><path d="m29.96 73.72 27.85-15.82-20.8-13.94-27.27 12.95z"/><path d="m28.07 60.88-5.4-36.81-22.67-14.06 7.19 34.62z" fill="#004b16"/><path d="m22.67 24.07 33.14-12.7-23.61-11.37-32.2 10.01z"/><g fill="#1cb35c"><path d="m107.13 76.87c-14.02 11.13-14.33 11.33-14.26 11.33 1.74-20.1 1.59-19.2 1.67-19.2 16-11.45 15-10.79 15.11-10.79z"/><path d="m85.78 93.84c-17.35 13.8-17.63 13.89-17.56 13.89-.17-20.82-.07-19.89 0-19.89 20-14.3 18.67-13.43 18.74-13.43z"/><path d="m60.22 114.16c-21.66 17.22-21.86 17.32-21.79 17.32-3.07-20.94-3-20.32-3-20.32 25.47-18.16 23.86-17.06 23.93-17.06z"/><path d="m55.81 11.37c1.52 35.37 1.4 34.34 1.48 34.34-28.66 14.89-29.29 15.17-29.22 15.17-5.52-37.63-5.47-36.81-5.4-36.81z"/><path d="m57.81 57.9c1.15 26.81 1 25.88 1.11 25.88-24.81 16.67-25.09 16.82-25 16.82-4-27.58-4-26.88-3.94-26.88z"/></g><path xmlns="http://www.w3.org/2000/svg" d="m151.61 14.24 16.58-4v79.88q0 13.13 7.83 15.65-3.84 7.31-13.13 7.3-11.28 0-11.28-15.66z"/><path d="m186.66 111.72v-57.42h-9.08v-13.6h25.86v71zm8.56-98.54a9.62 9.62 0 1 1 -9.62 9.62 9.63 9.63 0 0 1 9.62-9.62z"/><path d="m262.94 111.74v-41.06c0-6.05-1.16-10.48-3.48-13.26s-6.11-4.18-11.37-4.18a17.74 17.74 0 0 0 -7.8 2.06 18 18 0 0 0 -6.46 5.1v51.34h-16.59v-71h11.94l3 6.64q6.76-8 20-8 12.66 0 20 7.59t7.33 21.19v43.58z"/><path d="m288.42 76.06q0-16.26 9.38-26.47t24.77-10.21q16.19 0 25.14 9.82t9 26.86q0 17-9.12 27t-25 10q-16.18 0-25.17-10.12t-9-26.88zm17.24 0q0 23.47 16.91 23.48a14.54 14.54 0 0 0 12.31-6.11q4.55-6.09 4.54-17.37 0-23.14-16.85-23.15a14.61 14.61 0 0 0 -12.33 6.09q-4.58 6.11-4.58 17.06z"/><path d="m412.37 111.74v-4.31c-1.37 1.5-3.71 2.82-7 3.95a31.33 31.33 0 0 1 -10.15 1.69q-14.87 0-23.38-9.42t-8.52-26.27q0-16.84 9.78-27.42a32 32 0 0 1 24.51-10.58 32.37 32.37 0 0 1 14.72 3.32v-28.46l16.58-4v101.5zm0-54.06a17.6 17.6 0 0 0 -11.07-4.24q-10 0-15.33 6.07t-5.37 17.41q0 22.15 21.36 22.15a16.11 16.11 0 0 0 5.87-1.42c2.32-1 3.83-1.92 4.54-2.89z"/><path d="m505.55 81.3h-50.74q.46 8.49 5.83 13.2t14.46 4.7q11.34 0 17.25-5.9l6.43 12.7q-8.75 7.09-26.13 7.1-16.25 0-25.7-9.52t-9.45-26.58q0-16.78 10.38-27.2a33.91 33.91 0 0 1 24.9-10.41q15.45 0 24.81 9.22t9.35 23.48a46.28 46.28 0 0 1 -1.39 9.21zm-50.15-12.47h34.89q-1.73-15.58-17.24-15.59-14.2 0-17.65 15.59z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Executable
+144
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# Links to scripts/configs
|
||||
bin="https://github.com/simplex-chat/simplexmq/releases/latest/download"
|
||||
bin_smp="$bin/smp-server-ubuntu-20_04-x86-64"
|
||||
bin_xftp="$bin/xftp-server-ubuntu-20_04-x86-64"
|
||||
|
||||
scripts="https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/scripts/main"
|
||||
scripts_systemd_smp="$scripts/smp-server.service"
|
||||
scripts_systemd_xftp="$scripts/xftp-server.service"
|
||||
scripts_update="$scripts/simplex-servers-update"
|
||||
scripts_uninstall="$scripts/simplex-servers-uninstall"
|
||||
scripts_stopscript="$scripts/simplex-servers-stopscript"
|
||||
|
||||
# Default installation paths
|
||||
path_bin="/usr/local/bin"
|
||||
path_bin_smp="$path_bin/smp-server"
|
||||
path_bin_xftp="$path_bin/xftp-server"
|
||||
path_bin_update="$path_bin/simplex-servers-update"
|
||||
path_bin_uninstall="$path_bin/simplex-servers-uninstall"
|
||||
path_bin_stopscript="$path_bin/simplex-servers-stopscript"
|
||||
|
||||
path_conf_etc="/etc/opt"
|
||||
path_conf_var="/var/opt"
|
||||
path_conf_smp="$path_conf_etc/simplex $path_conf_var/simplex"
|
||||
path_conf_xftp="$path_conf_etc/simplex-xftp $path_conf_var/simplex-xftp /srv/xftp"
|
||||
|
||||
path_systemd="/etc/systemd/system"
|
||||
path_systemd_smp="$path_systemd/smp-server.service"
|
||||
path_systemd_xftp="$path_systemd/xftp-server.service"
|
||||
|
||||
# Defaut users
|
||||
user_smp="smp"
|
||||
user_xftp="xftp"
|
||||
|
||||
GRN='\033[0;32m'
|
||||
BLU='\033[1;34m'
|
||||
YLW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
logo='
|
||||
____ _ _ __ __
|
||||
/ ___|(_)_ __ ___ _ __ | | ___\ \/ /
|
||||
\___ \| | '"'"'_ ` _ \| '"'"'_ \| |/ _ \\ /
|
||||
___) | | | | | | | |_) | | __// \
|
||||
|____/|_|_| |_| |_| .__/|_|\___/_/\_\
|
||||
|_|
|
||||
'
|
||||
|
||||
welcome="Welcome to SMP/XFTP installation script! Here's what we're going to do:
|
||||
${GRN}1.${NC} Install latest binaries from GitHub releases:
|
||||
- smp: ${YLW}${path_bin_smp}${NC}
|
||||
- xftp: ${YLW}${path_bin_xftp}${NC}
|
||||
${GRN}2.${NC} Create server directories:
|
||||
- smp: ${YLW}${path_conf_smp}${NC}
|
||||
- xftp: ${YLW}${path_conf_xftp}${NC}
|
||||
${GRN}3.${NC} Setup user for each server:
|
||||
- xmp: ${YLW}${user_smp}${NC}
|
||||
- xftp: ${YLW}${user_xftp}${NC}
|
||||
${GRN}4.${NC} Create systemd services:
|
||||
- smp: ${YLW}${path_systemd_smp}${NC}
|
||||
- xftp: ${YLW}${path_systemd_xftp}${NC}
|
||||
${GRN}5.${NC} Install stopscript (systemd), update and uninstallation script:
|
||||
- all: ${YLW}${path_bin_update}${NC}, ${YLW}${path_bin_uninstall}${NC}, ${YLW}${path_bin_stopscript}${NC}
|
||||
|
||||
Press ${GRN}ENTER${NC} to continue or ${RED}Ctrl+C${NC} to cancel installation"
|
||||
|
||||
end="Installtion is complete!
|
||||
|
||||
Please checkout our server guides:
|
||||
- smp: ${GRN}https://simplex.chat/docs/server.html${NC}
|
||||
- xftp: ${GRN}https://simplex.chat/docs/xftp-server.html${NC}
|
||||
|
||||
To uninstall with full clean-up, simply run: ${YLW}sudo /usr/local/bin/simplex-servers-uninstall${NC}
|
||||
"
|
||||
|
||||
setup_bins() {
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_smp" -o "$path_bin_smp" && chmod +x "$path_bin_smp"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_xftp" -o "$path_bin_xftp" && chmod +x "$path_bin_xftp"
|
||||
}
|
||||
|
||||
setup_users() {
|
||||
useradd -M "$user_smp" 2> /dev/null || true
|
||||
useradd -M "$user_xftp" 2> /dev/null || true
|
||||
}
|
||||
|
||||
setup_dirs() {
|
||||
# Unquoted varibles, so field splitting can occur
|
||||
mkdir -p $path_conf_smp
|
||||
chown "$user_smp":"$user_smp" $path_conf_smp
|
||||
mkdir -p $path_conf_xftp
|
||||
chown "$user_xftp":"$user_xftp" $path_conf_xftp
|
||||
}
|
||||
|
||||
setup_systemd() {
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd_smp" -o "$path_systemd_smp"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd_xftp" -o "$path_systemd_xftp"
|
||||
}
|
||||
|
||||
setup_scripts() {
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_update" -o "$path_bin_update" && chmod +x "$path_bin_update"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_uninstall" -o "$path_bin_uninstall" && chmod +x "$path_bin_uninstall"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_stopscript" -o "$path_bin_stopscript" && chmod +x "$path_bin_stopscript"
|
||||
}
|
||||
|
||||
checks() {
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
printf "This script is intended to be run with root privileges. Please re-run script using sudo."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
checks
|
||||
|
||||
printf "%b\n%b\n" "${BLU}$logo${NC}" "$welcome"
|
||||
read ans
|
||||
|
||||
printf "Installing binaries..."
|
||||
setup_bins
|
||||
printf "${GRN} Done!${NC}\n"
|
||||
|
||||
printf "Creating users..."
|
||||
setup_users
|
||||
printf "${GRN} Done!${NC}\n"
|
||||
|
||||
printf "Creating directories..."
|
||||
setup_dirs
|
||||
printf "${GRN} Done!${NC}\n"
|
||||
|
||||
printf "Creating systemd services..."
|
||||
setup_systemd
|
||||
printf "${GRN} Done!${NC}\n"
|
||||
|
||||
printf "Installing stopscript, update and uninstallation script..."
|
||||
setup_scripts
|
||||
printf "${GRN} Done!${NC}\n"
|
||||
|
||||
printf "%b" "$end"
|
||||
}
|
||||
|
||||
main
|
||||
@@ -1,117 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS servers(
|
||||
host TEXT NOT NULL,
|
||||
port TEXT NOT NULL,
|
||||
key_hash BLOB,
|
||||
PRIMARY KEY (host, port)
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rcv_queues(
|
||||
host TEXT NOT NULL,
|
||||
port TEXT NOT NULL,
|
||||
rcv_id BLOB NOT NULL,
|
||||
conn_alias BLOB NOT NULL,
|
||||
rcv_private_key BLOB NOT NULL,
|
||||
snd_id BLOB,
|
||||
snd_key BLOB,
|
||||
decrypt_key BLOB NOT NULL,
|
||||
verify_key BLOB,
|
||||
status TEXT NOT NULL,
|
||||
PRIMARY KEY (host, port, rcv_id),
|
||||
FOREIGN KEY (host, port) REFERENCES servers (host, port),
|
||||
FOREIGN KEY (conn_alias)
|
||||
REFERENCES connections (conn_alias)
|
||||
ON DELETE CASCADE
|
||||
DEFERRABLE INITIALLY DEFERRED,
|
||||
UNIQUE (host, port, snd_id)
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS snd_queues(
|
||||
host TEXT NOT NULL,
|
||||
port TEXT NOT NULL,
|
||||
snd_id BLOB NOT NULL,
|
||||
conn_alias BLOB NOT NULL,
|
||||
snd_private_key BLOB NOT NULL,
|
||||
encrypt_key BLOB NOT NULL,
|
||||
sign_key BLOB NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
PRIMARY KEY (host, port, snd_id),
|
||||
FOREIGN KEY (host, port) REFERENCES servers (host, port),
|
||||
FOREIGN KEY (conn_alias)
|
||||
REFERENCES connections (conn_alias)
|
||||
ON DELETE CASCADE
|
||||
DEFERRABLE INITIALLY DEFERRED
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS connections(
|
||||
conn_alias BLOB NOT NULL,
|
||||
rcv_host TEXT,
|
||||
rcv_port TEXT,
|
||||
rcv_id BLOB,
|
||||
snd_host TEXT,
|
||||
snd_port TEXT,
|
||||
snd_id BLOB,
|
||||
last_internal_msg_id INTEGER NOT NULL, -- TODO add defauls here and below in the new schema
|
||||
last_internal_rcv_msg_id INTEGER NOT NULL,
|
||||
last_internal_snd_msg_id INTEGER NOT NULL,
|
||||
last_external_snd_msg_id INTEGER NOT NULL,
|
||||
last_rcv_msg_hash BLOB NOT NULL,
|
||||
last_snd_msg_hash BLOB NOT NULL,
|
||||
PRIMARY KEY (conn_alias),
|
||||
FOREIGN KEY (rcv_host, rcv_port, rcv_id) REFERENCES rcv_queues (host, port, rcv_id),
|
||||
FOREIGN KEY (snd_host, snd_port, snd_id) REFERENCES snd_queues (host, port, snd_id)
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS messages(
|
||||
conn_alias BLOB NOT NULL,
|
||||
internal_id INTEGER NOT NULL,
|
||||
internal_ts TEXT NOT NULL,
|
||||
internal_rcv_id INTEGER,
|
||||
internal_snd_id INTEGER,
|
||||
body TEXT NOT NULL, -- deprecated
|
||||
PRIMARY KEY (conn_alias, internal_id),
|
||||
FOREIGN KEY (conn_alias)
|
||||
REFERENCES connections (conn_alias)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY (conn_alias, internal_rcv_id)
|
||||
REFERENCES rcv_messages (conn_alias, internal_rcv_id)
|
||||
ON DELETE CASCADE
|
||||
DEFERRABLE INITIALLY DEFERRED,
|
||||
FOREIGN KEY (conn_alias, internal_snd_id)
|
||||
REFERENCES snd_messages (conn_alias, internal_snd_id)
|
||||
ON DELETE CASCADE
|
||||
DEFERRABLE INITIALLY DEFERRED
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rcv_messages(
|
||||
conn_alias BLOB NOT NULL,
|
||||
internal_rcv_id INTEGER NOT NULL,
|
||||
internal_id INTEGER NOT NULL,
|
||||
external_snd_id INTEGER NOT NULL,
|
||||
external_snd_ts TEXT NOT NULL,
|
||||
broker_id BLOB NOT NULL,
|
||||
broker_ts TEXT NOT NULL,
|
||||
rcv_status TEXT NOT NULL,
|
||||
ack_brocker_ts TEXT,
|
||||
ack_sender_ts TEXT,
|
||||
internal_hash BLOB NOT NULL,
|
||||
external_prev_snd_hash BLOB NOT NULL,
|
||||
integrity BLOB NOT NULL,
|
||||
PRIMARY KEY (conn_alias, internal_rcv_id),
|
||||
FOREIGN KEY (conn_alias, internal_id)
|
||||
REFERENCES messages (conn_alias, internal_id)
|
||||
ON DELETE CASCADE
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS snd_messages(
|
||||
conn_alias BLOB NOT NULL,
|
||||
internal_snd_id INTEGER NOT NULL,
|
||||
internal_id INTEGER NOT NULL,
|
||||
snd_status TEXT NOT NULL,
|
||||
sent_ts TEXT,
|
||||
delivered_ts TEXT,
|
||||
internal_hash BLOB NOT NULL,
|
||||
PRIMARY KEY (conn_alias, internal_snd_id),
|
||||
FOREIGN KEY (conn_alias, internal_id)
|
||||
REFERENCES messages (conn_alias, internal_id)
|
||||
ON DELETE CASCADE
|
||||
) WITHOUT ROWID;
|
||||
@@ -1,9 +0,0 @@
|
||||
CREATE TABLE conn_confirmations (
|
||||
confirmation_id BLOB NOT NULL PRIMARY KEY,
|
||||
conn_alias BLOB NOT NULL REFERENCES connections ON DELETE CASCADE,
|
||||
sender_key BLOB NOT NULL,
|
||||
sender_conn_info BLOB NOT NULL,
|
||||
accepted INTEGER NOT NULL,
|
||||
own_conn_info BLOB,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
) WITHOUT ROWID;
|
||||
@@ -1,3 +0,0 @@
|
||||
ALTER TABLE messages ADD msg_body BLOB NOT NULL DEFAULT x''; -- this field replaces body TEXT
|
||||
-- TODO possibly migrate the data from body if it is possible in migration
|
||||
ALTER TABLE snd_messages ADD previous_msg_hash BLOB NOT NULL DEFAULT x'';
|
||||
@@ -1,11 +0,0 @@
|
||||
ALTER TABLE connections ADD conn_mode TEXT NOT NULL DEFAULT 'INV';
|
||||
|
||||
CREATE TABLE conn_invitations (
|
||||
invitation_id BLOB NOT NULL PRIMARY KEY,
|
||||
contact_conn_id BLOB NOT NULL REFERENCES connections ON DELETE CASCADE,
|
||||
cr_invitation BLOB NOT NULL,
|
||||
recipient_conn_info BLOB NOT NULL,
|
||||
accepted INTEGER NOT NULL DEFAULT 0,
|
||||
own_conn_info BLOB,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
) WITHOUT ROWID;
|
||||
@@ -1,9 +0,0 @@
|
||||
# SQLite database migrations
|
||||
|
||||
These migrations are [embedded](../src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs) into the executable and run when SMP agent starts (as a separate executable or as a part of [simplex-chat](https://github.com/simplex-chat/simplex-chat) app).
|
||||
|
||||
Migration file names must have a format `YYYYMMDD-name.sql` - they will be executed in the order or lexicographic sorting of the names, the files with any other extension than `.sql` are ignored.
|
||||
|
||||
The proposed approach is to minimize the number of migrations and merge them together when possible, to align with the agent releases.
|
||||
|
||||
**Please note**: Adding or editing migrations will NOT update the migrations embedded into the executable, unless the [Migrations](../src/Simplex/Messaging/Agent/Store/SQLite/Migrations.hs) module is rebuilt - use `stack build --force-dirty` (in addition to edited files it seems to rebuild the files with TH splices and their dependencies, not all files as with `stack clean`).
|
||||
+77
-18
@@ -1,7 +1,7 @@
|
||||
name: simplexmq
|
||||
version: 0.5.2
|
||||
version: 5.4.0.7
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: |
|
||||
description: |
|
||||
This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
<./docs/Simplex-Messaging-Client.html client> and
|
||||
<./docs/Simplex-Messaging-Agent.html agent> for SMP protocols:
|
||||
@@ -15,62 +15,109 @@ homepage: https://github.com/simplex-chat/simplexmq#readme
|
||||
license: AGPL-3
|
||||
author: simplex.chat
|
||||
maintainer: chat@simplex.chat
|
||||
copyright: 2020 simplex.chat
|
||||
copyright: 2020-2022 simplex.chat
|
||||
category: Chat, Network, Web, System, Cryptography
|
||||
extra-source-files:
|
||||
- README.md
|
||||
- CHANGELOG.md
|
||||
- migrations/*.*
|
||||
- cbits/sha512.h
|
||||
- cbits/sntrup761.h
|
||||
|
||||
dependencies:
|
||||
- aeson == 2.2.*
|
||||
- ansi-terminal >= 0.10 && < 0.12
|
||||
- asn1-encoding == 0.9.*
|
||||
- asn1-types == 0.3.*
|
||||
- async == 2.2.*
|
||||
- attoparsec == 0.13.*
|
||||
- base >= 4.7 && < 5
|
||||
- attoparsec == 0.14.*
|
||||
- base >= 4.14 && < 5
|
||||
- base64-bytestring >= 1.0 && < 1.3
|
||||
- bytestring == 0.10.*
|
||||
- case-insensitive == 1.2.*
|
||||
- composition == 1.0.*
|
||||
- constraints >= 0.12 && < 0.14
|
||||
- containers == 0.6.*
|
||||
- cryptonite >= 0.27 && < 0.30
|
||||
- direct-sqlite == 2.3.*
|
||||
- cryptostore == 0.2.*
|
||||
- data-default == 0.7.*
|
||||
- direct-sqlcipher == 2.3.*
|
||||
- directory == 1.3.*
|
||||
- file-embed >= 0.0.14.0 && <= 0.0.15.0
|
||||
- filepath == 1.4.*
|
||||
- hourglass == 0.2.*
|
||||
- http-types == 0.12.*
|
||||
- generic-random >= 1.3 && < 1.5
|
||||
- http2 >= 4.2.2 && < 4.3
|
||||
- ini == 0.4.1
|
||||
- iproute == 1.7.*
|
||||
- iso8601-time == 0.1.*
|
||||
- memory == 0.15.*
|
||||
- mtl == 2.2.*
|
||||
- network == 3.1.*
|
||||
- network-transport == 0.5.*
|
||||
- QuickCheck == 2.14.*
|
||||
- network >= 3.1.2.7 && < 3.2
|
||||
- network-info >= 0.2 && < 0.3
|
||||
- network-transport == 0.5.4
|
||||
- network-udp >= 0.0 && < 0.1
|
||||
- optparse-applicative >= 0.15 && < 0.17
|
||||
- process == 1.6.*
|
||||
- random >= 1.1 && < 1.3
|
||||
- simple-logger == 0.1.*
|
||||
- sqlite-simple == 0.4.*
|
||||
- socks == 0.6.*
|
||||
- sqlcipher-simple == 0.4.*
|
||||
- stm == 2.5.*
|
||||
- template-haskell == 2.16.*
|
||||
- template-haskell >= 2.16 && < 2.21
|
||||
- temporary == 1.3.*
|
||||
- text == 1.2.*
|
||||
- time == 1.9.*
|
||||
- time-compat == 1.9.*
|
||||
- time-manager == 0.0.*
|
||||
- tls >= 1.6.0 && < 1.7
|
||||
- transformers == 0.5.*
|
||||
- unliftio == 0.2.*
|
||||
- unliftio-core == 0.2.*
|
||||
- websockets == 0.12.*
|
||||
- x509 == 1.7.*
|
||||
- x509-store == 1.6.*
|
||||
- x509-validation == 1.6.*
|
||||
- yaml == 0.11.*
|
||||
|
||||
flags:
|
||||
swift:
|
||||
description: Enable swift JSON format
|
||||
manual: True
|
||||
default: False
|
||||
|
||||
when:
|
||||
- condition: flag(swift)
|
||||
cpp-options:
|
||||
- -DswiftJSON
|
||||
|
||||
library:
|
||||
source-dirs: src
|
||||
c-sources:
|
||||
- cbits/sha512.c
|
||||
- cbits/sntrup761.c
|
||||
include-dirs: cbits
|
||||
extra-libraries: crypto
|
||||
|
||||
executables:
|
||||
smp-server:
|
||||
source-dirs: apps/smp-server
|
||||
main: Main.hs
|
||||
dependencies:
|
||||
- cryptostore == 0.2.*
|
||||
- ini == 0.4.*
|
||||
- optparse-applicative >= 0.15 && < 0.17
|
||||
- simplexmq
|
||||
ghc-options:
|
||||
- -threaded
|
||||
|
||||
ntf-server:
|
||||
source-dirs: apps/ntf-server
|
||||
main: Main.hs
|
||||
dependencies:
|
||||
- simplexmq
|
||||
ghc-options:
|
||||
- -threaded
|
||||
|
||||
xftp-server:
|
||||
source-dirs: apps/xftp-server
|
||||
main: Main.hs
|
||||
dependencies:
|
||||
- simplexmq
|
||||
ghc-options:
|
||||
- -threaded
|
||||
@@ -83,16 +130,28 @@ executables:
|
||||
ghc-options:
|
||||
- -threaded
|
||||
|
||||
xftp:
|
||||
source-dirs: apps/xftp
|
||||
main: Main.hs
|
||||
dependencies:
|
||||
- simplexmq
|
||||
ghc-options:
|
||||
- -threaded
|
||||
|
||||
tests:
|
||||
smp-server-test:
|
||||
simplexmq-test:
|
||||
source-dirs: tests
|
||||
main: Test.hs
|
||||
dependencies:
|
||||
- simplexmq
|
||||
- deepseq == 1.4.*
|
||||
- generic-random >= 1.3 && < 1.5
|
||||
- hspec == 2.7.*
|
||||
- hspec-core == 2.7.*
|
||||
- HUnit == 1.6.*
|
||||
- QuickCheck == 2.14.*
|
||||
- silently == 1.2.*
|
||||
- main-tester == 0.2.*
|
||||
- timeit == 2.0.*
|
||||
|
||||
ghc-options:
|
||||
|
||||
+13
-37
@@ -123,7 +123,7 @@ Each SMP message client body, once decrypted, contains 3 parts (one of them may
|
||||
- to send and to acknowledge user messages (`clientMsg`, `acknowledgeMsg`)
|
||||
- to manage SMP queue rotation (`newQueueMessage`, `deleteQueueMsg`)
|
||||
- to manage encryption key rotation (TODO)
|
||||
- `msgPadding` - an optional message padding to make all SMP messages have constant size, to prevent servers from observing the actual message size.
|
||||
- `msgPadding` - an optional message padding to make all SMP messages have constant size, to prevent servers from observing the actual message size. The only case the message padding can be absent is when the message has exactly the maximum size, in all other cases the message MUST be padded to a fixed size.
|
||||
|
||||
### Messages between SMP agents
|
||||
|
||||
@@ -131,56 +131,34 @@ Message syntax below uses [ABNF][3] with [case-sensitive strings extension][4].
|
||||
|
||||
```abnf
|
||||
decryptedSmpMessageBody = agentMsgHeader CRLF agentMessage CRLF msgPadding
|
||||
agentMsgHeader = agentMsgId SP agentTimestamp SP previousMsgHash ; here `agentMsgId` is sequential ID set by the sending agent
|
||||
agentMsgHeader = agentMsgId SP previousMsgHash ; here `agentMsgId` is sequential ID set by the sending agent
|
||||
agentMsgId = 1*DIGIT
|
||||
agentTimestamp = <date-time> ; RFC3339
|
||||
previousMsgHash = encoded
|
||||
encoded = <base64 encoded>
|
||||
|
||||
agentMessage = helloMsg / replyQueueMsg /
|
||||
clientMsg / invitationMsg/ acknowledgeMsg /
|
||||
clientMsg / invitationMsg /
|
||||
newQueueMessage / deleteQueueMsg
|
||||
|
||||
msgPadding = *OCTET ; optional random bytes to get messages to the same size (as defined in SMP message size)
|
||||
|
||||
helloMsg = %s"HELLO" SP signatureVerificationKey [SP %s"NO_ACK"]
|
||||
; NO_ACK means that acknowledgements to client messages will NOT be sent in this connection by the agent that sent `HELLO` message.
|
||||
signatureVerificationKey = encoded
|
||||
helloMsg = %s"H"
|
||||
|
||||
replyQueueMsg = %s"REPLY" SP connectionRequest ; `connectionRequest` is defined below
|
||||
replyQueueMsg = %s"R" connectionRequest ; `connectionRequest` is defined below
|
||||
; this message can only be sent by the second connection party
|
||||
|
||||
clientMsg = %s"MSG" SP size CRLF clientMsgBody CRLF ; CRLF is in addition to CRLF in decryptedSmpMessageBody
|
||||
size = 1*DIGIT
|
||||
clientMsg = %s"M" clientMsgBody
|
||||
clientMsgBody = *OCTET
|
||||
|
||||
; TODO remove and move to "public" header
|
||||
invitationMsg = %s"INV" SP connReqInvitation SP connInfo
|
||||
; `connReqInvitation` and `connInfo` are defined below
|
||||
|
||||
acknowledgeMsg = %s"ACK" SP agentMsgId SP msgHash SP ackStatus
|
||||
; NOT SUPPORTED in the current implementation
|
||||
|
||||
msgHash = encoded
|
||||
; base64 encoded hash of the received message
|
||||
|
||||
ackStatus = %s"OK" / ackError
|
||||
|
||||
ackError = %s"ERR" SP ackErrorType
|
||||
|
||||
ackErrorType = ackUnknownMsg / ackProhibitedMsg / ackSyntaxErr
|
||||
|
||||
ackUnknownMsg = %s"UNKNOWN"
|
||||
|
||||
ackProhibitedMsg = %s"PROHIBITED" ; unexpected message e.g. "HELLO" or "REPLY"
|
||||
|
||||
ackSyntaxErr = %s"SYNTAX" SP syntaxErrCode
|
||||
syntaxErrCode = 1*DIGIT ; TODO
|
||||
|
||||
newQueueMsg = %s"NEW" SP queueURI
|
||||
newQueueMsg = %s"N" queueURI
|
||||
; this message can be sent by any party to add SMP queue to the connection.
|
||||
; NOT SUPPORTED in the current implementation
|
||||
|
||||
deleteQueueMsg = %s"DEL" SP queueURI
|
||||
deleteQueueMsg = %s"D" queueURI
|
||||
; notification that the queue with passed URI will be deleted
|
||||
; no need to notify the other party about suspending queue separately, as suspended and deleted queues are indistinguishable to the sender
|
||||
; NOT SUPPORTED in the current implementation
|
||||
@@ -214,7 +192,7 @@ This message is sent to add an additional SMP queue to the connection. Unlike RE
|
||||
|
||||
#### DEL message
|
||||
|
||||
This message is sent to notify that the queue with passed URI will be deleted - having received this message, the receiving agent should no longer send messages to this queue. In case it was the only queue in the connection to which the agent could send the messages, it MAY also delete the reply queue(s) in the connection.
|
||||
This message is sent to notify that the queue with passed URI will be deleted - having received this message, the receiving agent should no longer send messages to this queue. In case it was the last remaining send queue in the duplex connection, the agent MAY also delete the reply queue(s) in the connection.
|
||||
|
||||
## SMP agent commands
|
||||
|
||||
@@ -225,13 +203,11 @@ Commands syntax below is provided using [ABNF][3] with [case-sensitive strings e
|
||||
Each transmission between the user and SMP agent must have this format/syntax:
|
||||
|
||||
```abnf
|
||||
agentTransmission = [corrId] CRLF [cAlias] CRLF agentCommand
|
||||
agentTransmission = [corrId] CRLF [connId] CRLF agentCommand
|
||||
|
||||
corrId = 1*(%x21-7F) ; any characters other than control/whitespace
|
||||
|
||||
cAlias = cId / cName
|
||||
cId = encoded
|
||||
cName = 1*(ALPHA / DIGIT / "_" / "-")
|
||||
connId = encoded
|
||||
|
||||
agentCommand = (userCmd / agentMsg) CRLF
|
||||
userCmd = newCmd / joinCmd / letCmd / acceptCmd / subscribeCmd / sendCmd / acknowledgeCmd / suspendCmd / deleteCmd
|
||||
@@ -304,7 +280,7 @@ messageError = %s"MERR" SP agentMsgId SP <errorType>
|
||||
message = %s"MSG" SP msgIntegrity SP recipientMeta SP brokerMeta SP senderMeta SP binaryMsg
|
||||
recipientMeta = %s"R=" agentMsgId "," agentTimestamp ; receiving agent message metadata
|
||||
brokerMeta = %s"B=" brokerMsgId "," brokerTimestamp ; broker (server) message metadata
|
||||
senderMeta = %s"S=" agentMsgId "," agentTimestamp ; sending agent message metadata
|
||||
senderMeta = %s"S=" agentMsgId ; sending agent message ID
|
||||
brokerMsgId = encoded
|
||||
brokerTimestamp = <date-time>
|
||||
msgIntegrity = ok / msgIntegrityError
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
sequenceDiagram
|
||||
participant A as Alice
|
||||
participant AA as Alice's<br>agent
|
||||
participant AS as Alice's<br>server
|
||||
participant BS as Bob's<br>server
|
||||
participant BA as Bob's<br>agent
|
||||
participant B as Bob
|
||||
|
||||
note over AA, BA: status (receive/send): NONE/NONE
|
||||
|
||||
note over A, AA: 1. request connection<br>from agent
|
||||
A ->> AA: NEW: create<br>duplex connection
|
||||
|
||||
note over AA, AS: 2. create Alice's SMP queue
|
||||
AA ->> AS: NEW: create SMP queue
|
||||
AS ->> AA: IDS: SMP queue IDs
|
||||
note over AA: status: NEW/NONE
|
||||
|
||||
AA ->> A: INV: invitation<br>to connect
|
||||
|
||||
note over A, B: 3. out-of-band invitation
|
||||
A ->> B: OOB: invitation to connect
|
||||
|
||||
note over BA, B: 4. accept connection
|
||||
B ->> BA: JOIN:<br>via invitation info
|
||||
note over BA: status: NONE/NEW
|
||||
|
||||
note over BA, BS: 5. create Bob's SMP queue
|
||||
BA ->> BS: NEW: create SMP queue
|
||||
BS ->> BA: IDS: SMP queue IDs
|
||||
note over BA: status: NEW/NEW
|
||||
|
||||
note over BA, AA: 6. establish Alice's SMP queue
|
||||
BA ->> AS: SEND: Bob's info and sender server key (SMP confirmation with reply queues)
|
||||
note over BA: status: NEW/CONFIRMED
|
||||
|
||||
AS ->> AA: MSG: Bob's info and<br>sender server key
|
||||
note over AA: status: CONFIRMED/NONE
|
||||
AA ->> AS: ACK: confirm message
|
||||
AA ->> A: CONF: connection request ID<br>and Bob's info
|
||||
A ->> AA: LET: accept connection request,<br>send Alice's info
|
||||
AA ->> AS: KEY: secure queue
|
||||
note over AA: status: SECURED/NONE
|
||||
|
||||
AA ->> BS: SEND: Alice's info and sender's server key (SMP confirmation without reply queues)
|
||||
note over AA: status: SECURED/CONFIRMED
|
||||
|
||||
BS ->> BA: MSG: Alice's info and<br>sender's server key
|
||||
note over BA: status: CONFIRMED/CONFIRMED
|
||||
BA ->> B: INFO: Alice's info
|
||||
BA ->> BS: ACK: confirm message
|
||||
BA ->> BS: KEY: secure queue
|
||||
note over BA: status: SECURED/CONFIRMED
|
||||
|
||||
BA ->> AS: SEND: HELLO: only needs to be sent once in v2
|
||||
|
||||
note over BA: status: SECURED/ACTIVE
|
||||
note over BA, B: 7a. notify Bob<br>about connection success
|
||||
BA ->> B: CON: connected
|
||||
|
||||
AS ->> AA: MSG: HELLO: Alice's agent<br>knows Bob can send
|
||||
note over AA: status: SECURED/ACTIVE
|
||||
AA ->> AS: ACK: confirm message
|
||||
note over A, AA: 7a. notify Alice<br>about connection success
|
||||
AA ->> A: CON: connected
|
||||
|
||||
AA ->> BS: SEND: HELLO: only needs to be sent once in v2
|
||||
note over AA: status: ACTIVE/ACTIVE
|
||||
BS ->> BA: MSG: HELLO: Bob's agent<br>knows Alice can send
|
||||
note over BA: status: ACTIVE/ACTIVE
|
||||
BA ->> BS: ACK: confirm message
|
||||
@@ -33,8 +33,8 @@ sequenceDiagram
|
||||
AS ->> AA: MSG: Bob's info and<br>sender server key
|
||||
note over AA: status: CONFIRMED/NONE
|
||||
AA ->> AS: ACK: confirm message
|
||||
AA ->> A: REQ: connection request ID<br>and Bob's info
|
||||
A ->> AA: ACPT: accept connection request,<br>send Alice's info
|
||||
AA ->> A: CONF: connection request ID<br>and Bob's info
|
||||
A ->> AA: LET: accept connection request,<br>send Alice's info
|
||||
AA ->> AS: KEY: secure queue
|
||||
note over AA: status: SECURED/NONE
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
sequenceDiagram
|
||||
participant M as mobile app
|
||||
participant C as chat core
|
||||
participant A as agent
|
||||
participant P as push server
|
||||
participant APN as APN
|
||||
|
||||
note over M, APN: get device token
|
||||
M ->> APN: registerForRemoteNotifications()
|
||||
APN ->> M: device token
|
||||
|
||||
note over M, P: register device token with push server
|
||||
M ->> C: /_ntf register <token>
|
||||
C ->> A: registerNtfToken(<token>)
|
||||
A ->> P: TNEW
|
||||
P ->> A: ID (tokenId)
|
||||
A ->> C: registered
|
||||
C ->> M: registered
|
||||
|
||||
note over M, APN: verify device token
|
||||
P ->> APN: E2E encrypted code<br>in background<br>notification
|
||||
APN ->> M: deliver background notification with e2ee verification token
|
||||
M ->> C: /_ntf verify <e2ee code>
|
||||
C ->> A: verifyNtfToken(<e2ee code>)
|
||||
A ->> P: TVFY code
|
||||
P ->> A: OK / ERR
|
||||
A ->> C: verified
|
||||
C ->> M: verified
|
||||
|
||||
note over M, APN: now token ID can be used
|
||||
@@ -0,0 +1,40 @@
|
||||
sequenceDiagram
|
||||
participant M as mobile app
|
||||
participant C as chat core
|
||||
participant A as agent
|
||||
participant S as SMP server
|
||||
participant N as NTF server
|
||||
participant APN as APN
|
||||
|
||||
note over M, APN: register subscription
|
||||
|
||||
alt register existing
|
||||
M -->> A: on /_ntf register, for subscribed queues
|
||||
else create new connection
|
||||
A -->> S: NEW / JOIN
|
||||
note over A, S: ...<br>Connection handshake<br>...
|
||||
S -->> A: CON
|
||||
end
|
||||
A ->> S: NKEY nKey
|
||||
S ->> A: NID nId
|
||||
A ->> N: SNEW tknId dhKey (smpServer, nId, nKey)
|
||||
N ->> A: ID subId dhKey
|
||||
N ->> S: NSUB nId
|
||||
S ->> N: OK [/ NMSG]
|
||||
|
||||
note over M, APN: notify about message
|
||||
|
||||
S ->> N: NMSG
|
||||
N ->> APN: APNSMutableContent<br>ntfQueue, nonce
|
||||
APN ->> M: UNMutableNotificationContent
|
||||
note over M, S: ...<br>Client awaken, message is received<br>...
|
||||
S ->> M: message
|
||||
note over M: mutate notification
|
||||
|
||||
note over M, APN: change APN token
|
||||
|
||||
APN ->> M: new device token
|
||||
M -->> C: /_ntf_sub update tkn
|
||||
C -->> A: updateNtfToken()
|
||||
A -->> N: TUPD tknId newDeviceToken
|
||||
note over M, N: ...<br>Verify token<br>...
|
||||
@@ -0,0 +1,313 @@
|
||||
Revision 1, 2022-01-01
|
||||
|
||||
Evgeny Poberezkin
|
||||
|
||||
# SimpleX: messaging and application platform
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [What is SimpleX](#what-is-simplex)
|
||||
- [SimpleX objectives](#simplex-objectives)
|
||||
- [In Comparison](#in-comparison)
|
||||
- [Technical Details](#technical-details)
|
||||
- [Trust in Servers](#trust-in-servers)
|
||||
- [Client -> Server Communication](#client---server-communication)
|
||||
- [SimpleX Messaging Protocol](#simplex-messaging-protocol)
|
||||
- [SimpleX Agents](#simplex-agents)
|
||||
- [Encryption Primitives Used](#encryption-primitives-used)
|
||||
- [Threat model](#threat-model)
|
||||
- [Acknowledgements](#acknowledgements)
|
||||
|
||||
## Introduction
|
||||
|
||||
#### What is SimpleX
|
||||
|
||||
SimpleX as a whole is a platform upon which applications can be built. [SimpleX Chat](https://github.com/simplex-chat/simplex-chat) is one such application that also serves as an example and reference application.
|
||||
|
||||
- [SimpleX Messaging Protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md) (SMP) is a protocol to send messages in one direction to a recipient, relying on a server in-between. The messages are delivered via uni-directional queues created by recipients.
|
||||
|
||||
- SMP runs over a transport protocol (shown below as TLS) that provides integrity, server authentication, confidentiality, and transport channel binding.
|
||||
|
||||
- A SimpleX Server is one of those servers.
|
||||
|
||||
- The SimpleX Network is the term used for the collective of SimpleX Servers that facilitate SMP.
|
||||
|
||||
- SimpleX Client libraries speak SMP to SimpleX Servers and provide a low-level API not generally intended to be used by applications.
|
||||
|
||||
- SimpleX Agents interface with SimpleX Clients to provide a more high-level API intended to be used by applications. Typically they are embedded as libraries, but are designed so they can also be abstracted into local services.
|
||||
|
||||
|
||||
*Diagram showing the SimpleX Chat app, with logical layers of the chat application interfacing with a SimpleX Agent library, which in turn interfaces with a SimpleX Client library. The Client library in turn speaks the Messaging Protocol to a SimpleX Server.*
|
||||
|
||||
```
|
||||
User's Computer Internet Third-Party Server
|
||||
------------------ | ---------------------- | -------------------------
|
||||
| |
|
||||
SimpleX Chat | |
|
||||
| |
|
||||
+----------------+ | |
|
||||
| Chat App | | |
|
||||
+----------------+ | |
|
||||
| SimpleX Agent | | |
|
||||
+----------------+ -------------- TLS ---------------- +----------------+
|
||||
| SimpleX Client | ------ SimpleX Messaging Protocol ------> | SimpleX Server |
|
||||
+----------------+ ----------------------------------- +----------------+
|
||||
| |
|
||||
```
|
||||
|
||||
#### SimpleX objectives
|
||||
|
||||
1. Provide messaging infrastructure for distributed applications. This infrastructure needs to have the following qualities:
|
||||
|
||||
- Security against passive and active (man-in-the-middle) attacks: the parties should have reliable end-to-end encryption and be able to detect the presence of an active attacker who modified, deleted or added messages.
|
||||
|
||||
- Privacy: protect against traffic correlation attacks to determine the contacts that the users communicate with.
|
||||
|
||||
- Reliability: the messages should be delivered even if some participating network servers or receiving clients fail, with “at least once” delivery guarantee.
|
||||
|
||||
- Integrity: the messages sent in one direction are ordered in a way that sender and recipient agree on; the recipient can detect when a message was removed or changed.
|
||||
|
||||
- Asynchronous delivery: it should not be required that both communicating parties (client devices, services or applications) are online for reliable message delivery.
|
||||
|
||||
- Low latency: the delay introduced by the network should not be higher than 100ms-1s in addition to the underlying TCP network latency.
|
||||
|
||||
2. Provide better communication security and privacy than the alternative instant messaging solutions. In particular SimpleX provides better privacy of metadata (who talks to whom and when) and better security against active network attackers and malicious servers.
|
||||
|
||||
3. Balance user experience with privacy requirements, prioritizing experience of mobile device users.
|
||||
|
||||
#### In Comparison
|
||||
|
||||
SimpleX network has a design similar to P2P networks, but unlike most P2P networks it consists of clients and servers without depending on any centralized component.
|
||||
In comparison to more traditional messaging applications (e.g. WhatsApp, Signal, Telegram) the key differences of SimpleX network are:
|
||||
|
||||
- participants do not need to have globally unique addresses to communicate, instead they use redundant unidirectional (simplex) messaging queues, with a separate set of queues for each contact.
|
||||
|
||||
- connection requests are passed out-of-band, non-optionally protecting key exchange against man-in-the-middle attack.
|
||||
|
||||
- simple message queues provided by network servers are used by the clients to create more complex communication scenarios, such as duplex one-to-one communication, transmitting files, group communication without central servers, and content/communication channels.
|
||||
|
||||
- servers do not store any user information (no user profiles or contacts, or messages once they are delivered), and primarily use in-memory persistence.
|
||||
|
||||
- users can change servers with minimal disruption - even after an in-use server disappears, simply by changing the configuration on which servers the new queues are created.
|
||||
|
||||
## Technical Details
|
||||
|
||||
#### Trust in Servers
|
||||
|
||||
Clients communicate directly with servers (but not with other clients) using SimpleX Messaging Protocol (SMP) running over some transport protocol that provides integrity, server authentication, confidentiality, and transport channel binding. By default, we assume this transport protocol is TLS.
|
||||
|
||||
Users use multiple servers, and choose where to receive their messages. Accordingly, they send messages to their communication partners' chosen servers.
|
||||
|
||||
Although end-to-end encryption is always present, users place a degree of trust in servers. This trust decision is very similar to a user's choice of email provider; however the trust placed in a SimpleX server is significantly less. Notably, there is no re-used identifier or credential between queues on the same (or different) servers. While a user *may* re-use a connection to fetch from multiple queues, or connect to a server from the same IP address, both are choices a user may opt into to break the promise of un-correlatable queues.
|
||||
|
||||
Users may trust a server because:
|
||||
|
||||
- They deploy and control the servers themselves from the available open-source code. This has the trade-offs of strong trust in the server but limited metadata obfuscation to a passive network observer. Techniques such as noise traffic, traffic mixing (incurring latency), and using an onion routing transport protocol can mitigate that latter.
|
||||
|
||||
- They use servers from a trusted commercial provider. The more clients the provider has, the less metadata about the communication times is leaked to the network observers.
|
||||
|
||||
- Users trust their contacts and the servers they chose.
|
||||
|
||||
By default, servers do not retain access logs, and permanently delete messages and queues when requested. Messages persist only in memory until they cross a threshold of time, typically on the order of days.[0] There is still a risk that a server maliciously records all queues and messages (even though encrypted) sent via the same transport connection to gain a partial knowledge of the user’s communications graph and other meta-data.
|
||||
|
||||
SimpleX supports measures (managed transparently to the user at the agent level) to mitigate the trust placed in servers. These include rotating the queues in use between users, noise traffic, and supporting overlay networks such as Tor.
|
||||
|
||||
[0] While configurable by servers, a minimum value is enforced by the default software. SimpleX Agents provide redundant routing over queues to mitigate against message loss.
|
||||
|
||||
|
||||
#### Client -> Server Communication
|
||||
|
||||
Utilizing TLS grants the SimpleX Messaging Protocol (SMP) server authentication and metadata protection to a passive network observer. But SMP does not rely on the transport protocol for message confidentiality or client authentication. The SMP protocol itself provides end-to-end confidentiality, authentication, and integrity of messages between communicating parties.
|
||||
|
||||
Servers have long-lived, self-signed, offline certificates whose hash is pre-shared with clients over secure channels - either provided with the client library or provided in the secure introduction between clients. The offline certificate signs an online certificate used in the transport protocol handshake. [0]
|
||||
|
||||
If the transport protocol's confidentiality is broken, incoming and outgoing messages to the server cannot be correlated by message contents. Additionally, because of encryption at the SMP layer, impersonating the server is not sufficient to pass (and therefore correlate) a message from a sender to recipient - the only attack possible is to drop the messages. Only by additionally *compromising* the server can one pass and correlate messages.
|
||||
|
||||
It's important to note that the SMP protocol does not do server authentication. Instead we rely upon the fact that an attacker who tricks the transport protocol into authenticating the server incorrectly cannot do anything with the SMP messages except drop them.
|
||||
|
||||
After the connection is established, the client sends blocks of a fixed size 16Kb, and the server replies with the blocks of the same size to reduce metadata observable to a network adversary. The protocol has been designed to make traffic correlation attacks difficult, adapting ideas from Tor, remailers, and more general onion and mix networks. It does not try to replace Tor though - SimpleX servers can be deployed as onion services and SimpleX clients can communicate with servers over Tor to further improve participants privacy.
|
||||
|
||||
By using fixed-size blocks, oversized for the expected content, the vast majority of traffic is uniform in nature. When enough traffic is transiting a server simultaneously, the server acts as a (very) low-latency mix node. We can't rely on this behavior to make a security claim, but we have engineered to take advantage of it when we can. As mentioned, this holds true even if the transport connection is compromised.
|
||||
|
||||
The protocol does not protect against attacks targeted at particular users with known identities - e.g., if the attacker wants to prove that two known users are communicating, they can achieve it. At the same time, it substantially complicates large-scale traffic correlation, making determining the real user identities much less effective.
|
||||
|
||||
[0] Future versions of SMP may add support for revocation lists of certificates, presently this risk is mitigated by the SMP protocol itself.
|
||||
|
||||
|
||||
#### SimpleX Messaging Protocol
|
||||
|
||||
SMP is initialized with an in-person or out-of-band introduction message, where Alice provides Bob with details of a server (including IP, port, and hash of the long-lived offline certificate), a queue ID, and Alice's public key for her receiving queue. These introductions are similar to the PANDA key-exchange, in that if observed, the adversary can race to establish the communication channel instead of the intended participant. [0]
|
||||
|
||||
Because queues are uni-directional, Bob provides an identically-formatted introduction message to Alice over Alice's now-established receiving queue.
|
||||
|
||||
When setting up a queue, the server will create separate sender and recipient queue IDs (provided to Alice during set-up and Bob during initial connection). Additionally, during set-up Alice will perform a DH exchange with the server to agree upon a shared secret. This secret will be used to re-encrypt Bob's incoming message before Alice receives it, creating the anti-correlation property earlier-described should the transport encryption be compromised.
|
||||
|
||||
[0] Users can additionally create public 'contact queues' that are only used to receive connection requests.
|
||||
|
||||
#### SimpleX Agents
|
||||
|
||||
SimpleX agents provide higher-level operations compared to SimpleX Clients, who are primarily concerned with creating queues and communicating with servers using SMP. Agent operations include:
|
||||
|
||||
- Managing sets of bi-directional, redundant queues for communication partners
|
||||
|
||||
- Providing end-to-end encryption of messages
|
||||
|
||||
- Rotating queues periodically with communication partners
|
||||
|
||||
- Noise traffic
|
||||
|
||||
#### Encryption Primitives Used
|
||||
|
||||
- Ed448 to sign/verify commands to SMP servers (Ed25519 is also supported via client/server configuration).
|
||||
- Curve25519 for DH exchange to agree:
|
||||
- the shared secret between server and recipient (to encrypt message bodies - it avoids shared cipher-text in sender and recipient traffic)
|
||||
- the shared secret between sender and recipient (to encrypt messages end-to-end in each queue - it avoids shared cipher-text in redundant queues).
|
||||
- [NaCl crypto_box](https://nacl.cr.yp.to/box.html) encryption scheme (curve25519xsalsa20poly1305) for message body encryption between server and recipient and for E2E per-queue encryption.
|
||||
- SHA256 to validate server offline certificates.
|
||||
- [double ratchet](https://signal.org/docs/specifications/doubleratchet/) protocol for end-to-end message encryption between the agents:
|
||||
- Curve448 keys to agree shared secrets required for double ratchet initialization (using [X3DH](https://signal.org/docs/specifications/x3dh/) key agreement with 2 ephemeral keys for each side),
|
||||
- AES-GCM AEAD cipher,
|
||||
- SHA512-based HKDF for key derivation.
|
||||
|
||||
## Threat Model
|
||||
|
||||
#### Global Assumptions
|
||||
|
||||
- A user protects their local database and key material
|
||||
- The user's application is authentic, and no local malware is running
|
||||
- The cryptographic primitives in use are not broken
|
||||
- A user's choice of servers is not directly tied to their identity or otherwise represents distinguishing information about the user.
|
||||
|
||||
#### A passive adversary able to monitor the traffic of one user
|
||||
|
||||
*can:*
|
||||
|
||||
- identify that and when a user is using SimpleX
|
||||
|
||||
- block SimpleX traffic
|
||||
|
||||
- determine which servers the user communicates with
|
||||
|
||||
- observe how much traffic is being sent, and make guesses as to its purpose.
|
||||
|
||||
*cannot:*
|
||||
|
||||
- see who sends messages to the user and who the user sends the messages to
|
||||
|
||||
#### A passive adversary able to monitor a set of senders and recipients
|
||||
|
||||
*can:*
|
||||
|
||||
- identify who and when is using SimpleX
|
||||
|
||||
- learn which SimpleX Messaging Protocol servers are used as receive queues for which users
|
||||
|
||||
- learn when messages are sent and received
|
||||
|
||||
- perform traffic correlation attacks against senders and recipients and correlate senders and recipients within the monitored set, frustrated by the number of users on the servers
|
||||
|
||||
- observe how much traffic is being sent, and make guesses as to its purpose
|
||||
|
||||
*cannot, even in case of a compromised transport protocol:*
|
||||
|
||||
- perform traffic correlation attacks with any increase in efficiency over a non-compromised transport protocol
|
||||
|
||||
#### SimpleX Messaging Protocol server
|
||||
|
||||
*can:*
|
||||
|
||||
- learn when a queue recipient or sender is online
|
||||
|
||||
- know how many messages are sent via the queue (although some may be noise)
|
||||
|
||||
- perform queue correlation (matching multiple queues to a single user) via either a re-used transport connection, user's IP Address, or connection timing regularities
|
||||
|
||||
- learn a user's IP address, track them through other IP addresses they use to access the same queue, and infer information (e.g. employer) based on the IP addresses, as long as Tor is not used.
|
||||
|
||||
- drop all future messages inserted into a queue, detectable only over other, redundant queues
|
||||
|
||||
- lie about the state of a queue to the recipient and/or to the sender (e.g. suspended or deleted when it is not).
|
||||
|
||||
- spam a user with invalid messages
|
||||
|
||||
*cannot:*
|
||||
|
||||
- undetectably add, duplicate, or corrupt individual messages
|
||||
|
||||
- undetectably drop individual messages, so long as a subsequent message is delivered
|
||||
|
||||
- learn the contents of messages
|
||||
|
||||
- distinguish noise messages from regular messages except via timing regularities
|
||||
|
||||
- compromise the user's end-to-end encryption with an active attack
|
||||
|
||||
#### An attacker who obtained Alice's (decrypted) chat database
|
||||
|
||||
*can:*
|
||||
|
||||
- see the history of all messages exchanged by Alice with her communication partners
|
||||
|
||||
- see shared profiles of contacts and groups
|
||||
|
||||
- surreptitiously receive new messages sent to Alice via existing queues; until communication queues are rotated or the Double-Ratchet advances forward
|
||||
|
||||
- prevent Alice from receiving all new messages sent to her - either surreptitiously by emptying the queues regularly or overtly by deleting them
|
||||
|
||||
- send messages from the user to their contacts; recipients will detect it as soon as the user sends the next message, because the previous message hash won’t match (and potentially won’t be able to decrypt them in case they don’t keep the previous ratchet keys).
|
||||
|
||||
*cannot:*
|
||||
|
||||
- impersonate a sender and send messages to the user whose database was stolen. Doing so requires also compromising the server (to place the message in the queue, that is possible until the Double-Ratchet advances forward) or the user's device at a subsequent time (to place the message in the database).
|
||||
|
||||
- undetectably communicate at the same time as Alice with her contacts. Doing so would result in the contact getting different messages with repeated IDs.
|
||||
|
||||
- undetectably monitor message queues in realtime without alerting the user they are doing so, as a second subscription request unsubscribes the first and notifies the second.
|
||||
|
||||
#### A user’s contact
|
||||
|
||||
*can:*
|
||||
|
||||
- spam the user with messages
|
||||
|
||||
- forever retain messages from the user
|
||||
|
||||
*cannot:*
|
||||
|
||||
- cryptographically prove to a third-party that a message came from a user (assuming the user’s device is not seized)
|
||||
|
||||
- prove that two contacts they have is the same user
|
||||
|
||||
- cannot collaborate with another of the user's contacts to confirm they are communicating with the same user
|
||||
|
||||
#### An attacker who observes Alice showing an introduction message to Bob
|
||||
|
||||
*can:*
|
||||
|
||||
- Impersonate Bob to Alice
|
||||
|
||||
*cannot:*
|
||||
|
||||
- Impersonate Alice to Bob
|
||||
|
||||
#### An attacker with Internet access
|
||||
|
||||
*can:*
|
||||
|
||||
- Denial of Service SimpleX messaging servers
|
||||
|
||||
- spam a user's public “contact queue” with connection requests
|
||||
|
||||
*cannot:*
|
||||
|
||||
- send messages to a user who they are not connected with
|
||||
|
||||
- enumerate queues on a SimpleX server
|
||||
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Efim Poberezkin contributed to the design and implementation of [SimpleX Messaging Protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md) and [SimpleX Agent Protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md) since 2019.
|
||||
|
||||
Adam Langley's [Pond](https://github.com/agl/pond) inspired some of the recent improvements and the structure of this document.
|
||||
@@ -1,259 +0,0 @@
|
||||
# SimpleX: messaging and application network
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [SimpleX objectives](#simplex-objectives)
|
||||
- [SimpleX network](#simplex-network)
|
||||
- [Threat model](#threat-model)
|
||||
|
||||
## SimpleX objectives
|
||||
|
||||
1. Provide messaging infrastructure for distributed applications. This infrastructure needs to have the following qualities:
|
||||
|
||||
- Security against passive and active (man-in-the-middle) attacks: the parties should have reliable end-to-end encryption and be able to identify and to some extent compensate for the presence of the active attacker who may modify, delete or add messages.
|
||||
|
||||
- Privacy: network server operators should have no ability to read or modify messages without detection, and it should be impossible or hard to infer the contacts the users communicate with. Non-malicious network operators should retain no record of participants communications.
|
||||
|
||||
- Reliability: the messages should be delivered even if some participating network servers or receiving clients fail, with “at least once” delivery guarantee.
|
||||
|
||||
- Integrity: the messages should be delivered without alteration, in the same order, and any unauthorized message change should be detected by the recipient.
|
||||
|
||||
- Asynchronous delivery: it should not be required that both communicating parties (client devices, services or applications) are online for reliable message delivery.
|
||||
|
||||
- Low latency: the delay introduced by the network should not be higher than 100ms-1s in addition to the underlying TCP network latency.
|
||||
|
||||
2. Provide better communication privacy, in particular meta-data privacy (who talks to whom and when), and security, in particular security against active attacks, than the alternative instant messaging solutions.
|
||||
|
||||
3. Balance user experience with privacy requirements, prioritizing experience of mobile device users.
|
||||
|
||||
## SimpleX network
|
||||
|
||||
### Overview
|
||||
|
||||
SimpleX network provides asynchronous messaging infrastructure that aims to deliver on the above objectives and to avoid the disadvantages of the alternative solutions.
|
||||
|
||||
The key differences of SimpleX network are:
|
||||
|
||||
- participants do not need to have globally unique addresses to communicate, instead they use redundant unidirectional (simplex) messaging queues, with a separate set of queues for each contact.
|
||||
|
||||
- connection requests are passed out-of-band, non-optionally protecting key exchange against man-in-the-middle attack.
|
||||
|
||||
- message queues provided by network servers are used by the clients to create more complex communication scenarios, such as duplex one-to-one communication, transmitting files, group communication without central servers and content/communication channels (requiring a separate type of server to host them, which is out of scope of this document).
|
||||
|
||||
- servers do not store any user information (no user profiles, contacts or messages, once they are delivered), and can use in-memory persistence only.
|
||||
|
||||
- users can change server provider(s) without losing their communication contacts, simply by changing the configuration on which servers the new queues are created.
|
||||
|
||||
SimpleX network has design similar to P2P networks, but unlike most P2P networks it consists of clients and servers, without dependency on any centralized component. Servers provide anonymous unidirectional message queues in order to:
|
||||
|
||||
- continuously accept messages for the recipients, even when they are offline.
|
||||
|
||||
- provide recipient anonymity, as the messaging queues do not identify the users, and different addresses of the same queue are used for senders and recipients to provide additional protection against server traffic correlation, in case transport connections are compromised.
|
||||
|
||||
Coincidentally, SimpleX network reminds [Pond messenger](https://github.com/agl/pond) design, the main differences are:
|
||||
|
||||
- Clients can receive messages via multiple servers (in Pond users use one home server and it serves as a user permanent address),
|
||||
|
||||
- Clients do not need to poll servers to receive messages, the messages from subscribed queues are delivered via an open transport connection as they are received by the servers, resulting in low latency of message delivery.
|
||||
|
||||
### SimpleX servers
|
||||
|
||||
Clients communicate with servers (but not with other clients) using SimpleX Messaging Protocol (SMP) running over some transport protocol that provides server authentication, confidentiality with forward secrecy, integrity and transport channel binding (to prevent SMP commands replay).
|
||||
|
||||
Users can use multiple servers. They choose the servers they use to receive the messages through, and they communicate with the servers chosen by the recipients to send messages.
|
||||
|
||||
It is assumed that users have some degree of trust to the servers, for one of the following reasons:
|
||||
|
||||
- They deploy and control the servers themselves from the available open-source code. The advantage of self-deployed servers is that users have full control of server information and activity, but the downside is that it is easier to correlate incoming and outgoing messages by observing server traffic, assuming small number of messages sent via user-hosted servers and irregular communication time. It can be mitigated by:
|
||||
|
||||
- sending noise traffic.
|
||||
|
||||
- future protocol versions can add configurable per queue message delivery delays (compromising on one of the design objectives of low latency).
|
||||
|
||||
- using SMP servers via an onion routing or mix network.
|
||||
|
||||
- They use servers from the trusted commercial providers. The more clients the provider has, the less meta-data about the communication times is leaked to the network observers, and even if transport confidentiality is compromised it is not possible to correlate sent and received traffic without compromising the server, because the clients use different queue IDs to send and receive messages, and the message delivered to the recipient is additionally encrypted by the server.
|
||||
|
||||
- To some extent, users trust their contacts and the servers they chose that the users send messages to. The client applications could further improve user trust to the servers chosen by their contacts by supporting either the list of servers that are “allowed to send to” or “prohibited to send to” (e.g., known trusted or compromised servers, certain IP ranges, server geographical locations etc.).
|
||||
|
||||
If users use the free servers deployed by the volunteers, there is a risk that the server code can maliciously record all queues and messages (even though encrypted) sent via the same transport connection and to gain a partial knowledge of the user’s communications graph and other meta-data. User clients can mitigate it by using some overlay network that protects the privacy of TCP connections, e.g., Tor, and by sending noise traffic between the end users.
|
||||
|
||||
The servers authorize users to access (send/receive/etc.) their message queues via a digital signature of transmissions using a unique key pair different for each queue sender and recipient, so no information that is required for the server to authorize users allows to aggregate user communications across multiple queues.
|
||||
|
||||
### Transport
|
||||
|
||||
SimpleX network does not rely on transport or overlay network for server or client authentication. SMP protocol itself must provide integrity, forward secure confidentiality of communicating parties and mutual authentication even if transport protocol (but not the server) is compromised by the active attacker.
|
||||
|
||||
To protect against MITM attack on the transport, the clients should verify server self-signed certificate during TLS handshake by validating its pre-shared hash of the certificate in the server address (either as part of client configuration or passed in SMP connection request).
|
||||
|
||||
Clients use TLS1.3 protocol to connect to the server, restricting supported cryptographic protocols as defined in SimpleX Messaging Protocol.
|
||||
|
||||
After TCP transport connection is established, and the server is authenticated, the client sends blocks of a fixed size 16Kb, and the server replies with the blocks of the same size. When the actual message is smaller, it should be padded before encryption, and if it is larger, it can be split to chunks by the client application. The first blocks from the client and the server should include protocol version so that the party with the higher version can confirm compatibility and the transport channel identifier as described in SMP protocol.
|
||||
|
||||
Client can terminate the server transport connection without losing any messages – removal of the message from the server and the delivery of the next message happens when the client acknowledges the message reception (normally, after it is persisted in the client’s database).
|
||||
|
||||
Depending on the privacy requirements of the clients, the client can use a separate transport connection per queue, or receive messages from multiple queues via a single transport connection, for efficiency. In our implementations this configuration will be available to the end users.
|
||||
|
||||
### Server implementation
|
||||
|
||||
- does not store user access logs.
|
||||
|
||||
- does not store messages after the client has received them.
|
||||
|
||||
- permanently removes queues after they are deleted by the users
|
||||
|
||||
- only stores messages in transit in server operating memory; as clients are expected to use multiple servers to deliver each message, the message loss in one of the servers is acceptable.
|
||||
|
||||
### SimpleX clients and agents
|
||||
|
||||
Clients are assumed to be running on the trusted devices and can use the locally encrypted storage for messages, contacts, groups and other resources.
|
||||
|
||||
SimpleX clients implementing application-level protocols communicate with SimpleX servers via agents. SimpleX agents can be accessed in one of the following ways:
|
||||
|
||||
- via TCP network using trusted network connection or an overlay transport network.
|
||||
|
||||
- via local port (when the agent runs on the same device as a separate process).
|
||||
|
||||
- via agent library, when the agent logic is included directly into the client application.
|
||||
|
||||
The last option is the most secure, as it reduces the number of attack vectors in comparison with other options. The current implementation of SimpleX Chat uses this approach.
|
||||
|
||||
SimpleX agents provide the following operations:
|
||||
|
||||
- Creating and managing bi-directional (duplex) client connections and delivering messages via redundant groups of SimpleX queues.
|
||||
|
||||
- Exchanging keys and providing end-to-end encryption. The current prototype uses RSA cryptography with ad-hoc hybrid encryption scheme, we are currently switching it to X3DH for key exchange and double ratchet protocol for E2E encryption.
|
||||
|
||||
Communication between the client and the agent and the communication between the agents (via end-to-end encrypted messages delivered via SMP protocol) is described by SimpleX Agent Protocol.
|
||||
|
||||
Bi-directional client connections consist of multiple “send” and multiple “receive” queues (for redundancy) that agents use to send and receive messages; each queue should be used for limited amount of time, the rotation of queues is negotiated using SimpleX Agent Protocol.
|
||||
|
||||
There are two ways that SimpleX network users can establish the connection:
|
||||
|
||||
- Users communicate the initial connection invitation out-of-band, via link or via QR code. This out-of-band communication is assumed secure against active attacks and insecure against passive attacks. Once the connection is established that users are assumed to have confirmed out-of-band, the connection can no longer be compromised even if out-of-band invitation was obtained by the attacker. This out-of-band invitation does not contain any personal information, only the address of the queue and public key for the key exchange for end-to-end encryption.
|
||||
|
||||
- Users can create a “contact queue(s)” on SMP servers that would be used to receive connection requests. The important distinction with commonly used addressing systems is that this queue is only used to pass the connection request and not for subsequent messages. So, while it is possible to spam users who created such contact queues with connection requests, they can be removed without disrupting the communication with the existing contacts.
|
||||
|
||||
The reply queue addresses and the key exchange for the reply queues is negotiated via the direct queue.
|
||||
|
||||
### Message encryption
|
||||
|
||||
Messages are encrypted end-to-end using double ratchet protocol. Once the sending client deletes messages it won’t be able to decrypt them again. While messages are numbered within the queue, this only reveals to the servers how messages have been exchanged in a given queue that has a limited lifetime, so the total number of messages exchanged between contacts is not revealed.
|
||||
|
||||
The next queue to use is negotiated between agents in encrypted messages, and in most cases, it will be on another server (unless the client is configured to use one server, which is not recommended).
|
||||
|
||||
### Client storage
|
||||
|
||||
It is assumed that the client device and the storage is trusted, but clients can implement storage encryption using a user-provided passphrase.
|
||||
|
||||
## Threat model
|
||||
|
||||
### Assumptions
|
||||
|
||||
User protects their information and keys.
|
||||
|
||||
SimpleX Chat software is authentic.
|
||||
|
||||
User device is not compromised, excluding the scenarios below where it is explicitly stated it is compromised.
|
||||
|
||||
Used cryptographic primitives are not compromised.
|
||||
|
||||
### SimpleX Messaging Protocol server
|
||||
|
||||
**can:**
|
||||
|
||||
- learn when a queue recipient or sender is online via transmission times.
|
||||
|
||||
- correlate multiple queues to a single user in case they are accessed via the same transport connection or from the same IP address.
|
||||
|
||||
- correlate multiple queues to a single user based on the time they are accessed. Clients that use message notification servers can mitigate it by only subscribing to the queues after they receive the notifications and disconnecting after some time of inactivity.
|
||||
|
||||
- know how many messages is sent via the queue, clients can mitigate it with noise traffic.
|
||||
|
||||
- add, drop, duplicate or corrupt messages, but it will be detected by the client on SimpleX Agent Protocol level. Using multiple servers to deliver each message mitigates the loss of messages.
|
||||
|
||||
**cannot:**
|
||||
|
||||
- decrypt messages
|
||||
|
||||
- compromise e2e encryption with MITM attack (as server is only used to pass one of two keys in DH exchange, the first key is passed out-of-band)
|
||||
|
||||
### A global passive adversary monitoring all Internet traffic
|
||||
|
||||
**can:**
|
||||
|
||||
- learn who is using SimpleX Chat.
|
||||
|
||||
- learn when messages are sent and received.
|
||||
|
||||
- learn which SimpleX Messaging Protocol servers are used.
|
||||
|
||||
- in case of low traffic on the servers, correlate senders and recipients by the time messages sent and received.
|
||||
|
||||
- observe the approximate size of the files transmitted – to O(log log M) of file size precision.
|
||||
|
||||
**cannot, even in case of compromised transport protocol:**
|
||||
|
||||
- correlate senders and recipients by the content of messages (as different queue IDs are used for senders and recipients, with additional encryption layer in the delivered messages).
|
||||
|
||||
### A local network attacker
|
||||
|
||||
**can:**
|
||||
|
||||
- observe when a user is using SimpleX Chat.
|
||||
|
||||
- block SimpleX Chat traffic.
|
||||
|
||||
- determine which servers the user communicates with.
|
||||
|
||||
- observe the approximate size of files transmissions and receptions.
|
||||
|
||||
**cannot:**
|
||||
|
||||
- see who sends messages to the user and who the user sends the messages to
|
||||
|
||||
### An attacker who obtained user’s decrypted chat database
|
||||
|
||||
**can:**
|
||||
|
||||
- see the history of all messages
|
||||
|
||||
- see shared profiles of contacts and groups
|
||||
|
||||
- receive new user messages, but the user will detect it as the number and the hash of the previous message won’t match. Also, they will be instantly alerted that an unknown device is connected to their message queues, as long as they are online at the same time, as message queues only allow one active subscription.
|
||||
|
||||
- delete user’s message queues, so the contacts won’t be able to send messages.
|
||||
|
||||
- send messages from the user to their contacts, but the recipients will detect it as soon as the user sends the next message, because the previous message hash won’t match (and potentially won’t be able to decrypt them in case they don’t keep the previous ratchet keys).
|
||||
|
||||
**cannot:**
|
||||
|
||||
- impersonate a sender and send messages to the user whose database was stolen without also compromising the server (to place the message in the queue) or the user's device at a subsequent time (to place the message in the database).
|
||||
|
||||
### A user’s contact
|
||||
|
||||
**can:**
|
||||
|
||||
- spam the user with messages.
|
||||
|
||||
- forever retain messages from the user.
|
||||
|
||||
**cannot:**
|
||||
|
||||
- prove to a third-party that a message came from a user (assuming a user’s device is not seized)
|
||||
|
||||
- prove that two contacts they have is the same user.
|
||||
|
||||
- two contacts cannot confirm if they are communicating with the same user.
|
||||
|
||||
### An attacker with Internet access
|
||||
|
||||
**can:**
|
||||
|
||||
- DoS SimpleX messaging servers.
|
||||
|
||||
- spam the user who created a “contact queue” (queue for accepting connection requests) by sending multiple connection requests.
|
||||
|
||||
**cannot:**
|
||||
|
||||
- send messages to the user who they are not connected with.
|
||||
+289
-117
@@ -14,6 +14,7 @@
|
||||
- [Simplex queue IDs](#simplex-queue-ids)
|
||||
- [Server security requirements](#server-security-requirements)
|
||||
- [Message delivery notifications](#message-delivery-notifications)
|
||||
- [SMP Transmission structure](#smp-transmission-structure)
|
||||
- [SMP commands](#smp-commands)
|
||||
- [Correlating responses with commands](#correlating-responses-with-commands)
|
||||
- [Command authentication](#command-authentication)
|
||||
@@ -23,6 +24,7 @@
|
||||
- [Subscribe to queue](#subscribe-to-queue)
|
||||
- [Secure queue command](#secure-queue-command)
|
||||
- [Enable notifications command](#enable-notifications-command)
|
||||
- [Disable notifications command](#disable-notifications-command)
|
||||
- [Acknowledge message delivery](#acknowledge-message-delivery)
|
||||
- [Suspend queue](#suspend-queue)
|
||||
- [Delete queue](#delete-queue)
|
||||
@@ -49,6 +51,8 @@ It's designed with the focus on communication security and integrity, under the
|
||||
|
||||
It is designed as a low level protocol for other application protocols to solve the problem of secure and private message transmission, making [MITM attack][1] very difficult at any part of the message transmission system.
|
||||
|
||||
This document describes SMP protocol versions 3 and 4, the previous versions are discontinued.
|
||||
|
||||
## Introduction
|
||||
|
||||
The objective of Simplex Messaging Protocol (SMP) is to facilitate the secure and private unidirectional transfer of messages from senders to recipients via persistent simplex queues managed by the message broker (server).
|
||||
@@ -89,7 +93,7 @@ The simplex queue is the main unit of SMP protocol. It is used by:
|
||||
|
||||
- Sender of the queue (who received out-of-band message) to send messages to the server using sender's queue ID, signed by sender's key.
|
||||
|
||||
- Recipient of the queue (who created the queue and sent out-of-band message) will use it to retrieve messages from the server, signing the commands by the recipient key.
|
||||
- Recipient of the queue (who created the queue and sent out-of-band message) will use it to retrieve messages from the server, signing the commands by the recipient key. Recipient decrypts the messages with the key negotiated during the creation of the queue.
|
||||
|
||||
- Participant identities are not shared with the server - new unique keys and queue IDs are used for each queue.
|
||||
|
||||
@@ -99,7 +103,7 @@ This approach is based on the concept of [unidirectional networks][4] that are u
|
||||
|
||||
Access to each queue is controlled with unique (not shared with other queues) asymmetric key pairs, separate for the sender and the recipient. The sender and the receiver have private keys, and the server has associated public keys to authenticate participants' commands by verifying cryptographic signatures.
|
||||
|
||||
The messages sent over the queue are encrypted and decrypted using another key pair that was shared via out-of-band message - the recipient has the private key and the sender has the associated public key.
|
||||
The messages sent over the queue are end-to-end encrypted using the DH secret agreed via out-of-band message and SMP confirmation.
|
||||
|
||||
**Simplex queue diagram:**
|
||||
|
||||
@@ -116,14 +120,19 @@ The SMP queue URIs MUST include server identity, queue hostname, an optional por
|
||||
The [ABNF][8] syntax of the queue URI is:
|
||||
|
||||
```abnf
|
||||
queueURI = %s"smp://" smpServer "/" queueId "#" serverSignaturePublicKey
|
||||
; serverSignaturePublicKey syntax is defined below
|
||||
smpServer = serverIdentity "@" srvHost [":" port]
|
||||
queueURI = %s"smp://" smpServer "/" queueId "#" recipientDhPublicKey
|
||||
smpServer = serverIdentity "@" srvHost [":" port]
|
||||
srvHost = <hostname> ; RFC1123, RFC5891
|
||||
port = 1*DIGIT
|
||||
serverIdentity = base64url
|
||||
queueId = base64url
|
||||
base64url = <base64url encoded binary> ; RFC4648, section 5
|
||||
recipientDhPublicKey = x509UrlEncoded
|
||||
; the recipient's Curve25519 key for DH exchange to derive the secret
|
||||
; that the sender will use to encrypt delivered messages
|
||||
; using [NaCl crypto_box][16] encryption scheme (curve25519xsalsa20poly1305).
|
||||
|
||||
x509UrlEncoded = <base64url X509 key encoding>
|
||||
```
|
||||
|
||||
`hostname` can be IP address or domain name, as defined in RFC 1123, section 2.1.
|
||||
@@ -140,55 +149,61 @@ To create and start using a simplex queue Alice and Bob follow these steps:
|
||||
|
||||
1. Alice creates a simplex queue on the server:
|
||||
|
||||
1. Decides which SMP server to use (can be the same or different server that Alice uses for other queues) and opens secure encrypted transport connection to the chosen SMP server (see [Appendix A](#appendix-a)).
|
||||
1. Decides which SMP server to use (can be the same or different server that Alice uses for other queues) and opens secure encrypted transport connection to the chosen SMP server (see [Appendix A](#appendix-a)).
|
||||
|
||||
2. Generates a new random public/private key pair (encryption key - `EK`) that she did not use before for Bob to encrypt the messages.
|
||||
2. Generates a new random public/private key pair (encryption key - `EK`) that she did not use before for Bob to encrypt the messages.
|
||||
|
||||
3. Generates another new random public/private key pair (recipient key - `RK`) that she did not use before for her to sign commands and to decrypt the transmissions received from the server.
|
||||
3. Generates another new random public/private key pair (recipient key - `RK`) that she did not use before for her to sign commands and to decrypt the transmissions received from the server.
|
||||
|
||||
4. Sends `"NEW"` command to the server to create a simplex queue (see `create` in [Create queue command](#create-queue-command)). This command contains previously generated unique "public" key `RK` that will be used to verify the following commands related to the same queue signed by its private counterpart, for example to subscribe to the messages received to this queue or to update the queue, e.g. by setting the key required to send the messages (initially Alice creates the queue that accepts unsigned messages, so anybody could send the message via this queue if they knew the queue sender's ID and server address).
|
||||
4. Generates one more random key pair (recipient DH key - `RDHK`) to negotiate symmetric key that will be used by the server to encrypt message bodies delivered to Alice (to avoid shared cipher-text inside transport connection).
|
||||
|
||||
5. The server sends `"IDS"` response with queue IDs (`queueIds`):
|
||||
5. Sends `"NEW"` command to the server to create a simplex queue (see `create` in [Create queue command](#create-queue-command)). This command contains previously generated unique "public" keys `RK` and `RDHK`. `RK` will be used to verify the following commands related to the same queue signed by its private counterpart, for example to subscribe to the messages received to this queue or to update the queue, e.g. by setting the key required to send the messages (initially Alice creates the queue that accepts unsigned messages, so anybody could send the message via this queue if they knew the queue sender's ID and server address).
|
||||
|
||||
- Recipient ID `RID` for Alice to manage the queue and to receive the messages.
|
||||
6. The server sends `"IDS"` response with queue IDs (`queueIds`):
|
||||
|
||||
- Sender ID `SID` for Bob to send messages to the queue.
|
||||
- Recipient ID `RID` for Alice to manage the queue and to receive the messages.
|
||||
|
||||
- Sender ID `SID` for Bob to send messages to the queue.
|
||||
|
||||
- Server public DH key (`SDHK`) to negotiate a shared secret for message body encryption, that Alice uses to derive a shared secret with the server `SS`.
|
||||
|
||||
2. Alice sends an out-of-band message to Bob via the alternative channel that both Alice and Bob trust (see [protocol abstract](#simplex-messaging-protocol-abstract)). The message must include:
|
||||
|
||||
- Unique "public" key (`EK`) that Bob must use to encrypt messages.
|
||||
- Unique "public" key (`EK`) that Bob must use for E2E key agreement.
|
||||
|
||||
- SMP server hostname and information to open secure encrypted transport connection (see [Appendix A](#appendix-a)).
|
||||
- SMP server hostname and information to open secure encrypted transport connection (see [Appendix A](#appendix-a)).
|
||||
|
||||
- Sender queue ID `SID` for Bob to use.
|
||||
- Sender queue ID `SID` for Bob to use.
|
||||
|
||||
3. Bob, having received the out-of-band message from Alice, connects to the queue:
|
||||
|
||||
1. Generates a new random public/private key pair (sender key - `SK`) that he did not use before for him to sign messages sent to Alice's server.
|
||||
1. Generates a new random public/private key pair (sender key - `SK`) that he did not use before for him to sign messages sent to Alice's server.
|
||||
|
||||
2. Prepares the confirmation message for Alice to secure the queue. This message includes:
|
||||
2. Prepares the confirmation message for Alice to secure the queue. This message includes:
|
||||
|
||||
- Previously generated "public" key `SK` that will be used by Alice's server to authenticate Bob's messages, once the queue is secured.
|
||||
- Previously generated "public" key `SK` that will be used by Alice's server to authenticate Bob's messages, once the queue is secured.
|
||||
|
||||
- Optionally, any additional information (application specific, e.g. Bob's profile name and details).
|
||||
- Optionally, any additional information (application specific, e.g. Bob's profile name and details).
|
||||
|
||||
3. Encrypts the confirmation body with the "public" key `EK` (that Alice provided via the out-of-band message).
|
||||
3. Encrypts the confirmation body with the "public" key `EK` (that Alice provided via the out-of-band message).
|
||||
|
||||
4. Sends the encrypted message to the server with queue ID `SID` (see `send` in [Send message](#send-message)). This initial message to the queue must not be signed - signed messages will be rejected until Alice secures the queue (below).
|
||||
4. Sends the encrypted message to the server with queue ID `SID` (see `send` in [Send message](#send-message)). This initial message to the queue must not be signed - signed messages will be rejected until Alice secures the queue (below).
|
||||
|
||||
4. Alice receives Bob's message from the server using recipient queue ID `RID` (possibly, via the same transport connection she already has opened - see `message` in [Deliver queue message](#deliver-queue-message)):
|
||||
|
||||
1. She decrypts received message with "private" key `EK`.
|
||||
1. She decrypts received message body using the secret `SS`.
|
||||
|
||||
2. Even though anybody could have sent the message to the queue with ID `SID` before it is secured (e.g. if communication is compromised), Alice would ignore all messages until the decryption succeeds (i.e. the result contains the expected message format). Optionally, in the client application, she also may identify Bob using the information provided, but it is out of scope of SMP protocol.
|
||||
2. She decrypts received message with [key agreed with sender using] "private" key `EK`.
|
||||
|
||||
3. Even though anybody could have sent the message to the queue with ID `SID` before it is secured (e.g. if communication is compromised), Alice would ignore all messages until the decryption succeeds (i.e. the result contains the expected message format). Optionally, in the client application, she also may identify Bob using the information provided, but it is out of scope of SMP protocol.
|
||||
|
||||
5. Alice secures the queue `RID` with `"KEY"` command so only Bob can send messages to it (see [Secure queue command](#secure-queue-command)):
|
||||
|
||||
1. She sends the `KEY` command with `RID` signed with "private" key `RK` to update the queue to only accept requests signed by "private" key `SK` provided by Bob. This command contains unique "public" key `SK` previously generated by Bob.
|
||||
1. She sends the `KEY` command with `RID` signed with "private" key `RK` to update the queue to only accept requests signed by "private" key `SK` provided by Bob. This command contains unique "public" key `SK` previously generated by Bob.
|
||||
|
||||
2. From this moment the server will accept only signed commands to `SID`, so only Bob will be able to send messages to the queue `SID` (corresponding to `RID` that Alice has).
|
||||
2. From this moment the server will accept only signed commands to `SID`, so only Bob will be able to send messages to the queue `SID` (corresponding to `RID` that Alice has).
|
||||
|
||||
3. Once queue is secured, Alice deletes `SID` and `SK` - even if Alice's client is compromised in the future, the attacker would not be able to send messages pretending to be Bob.
|
||||
3. Once queue is secured, Alice deletes `SID` and `SK` - even if Alice's client is compromised in the future, the attacker would not be able to send messages pretending to be Bob.
|
||||
|
||||
6. The simplex queue `RID` is now ready to be used.
|
||||
|
||||
@@ -202,21 +217,21 @@ Bob now can securely send messages to Alice:
|
||||
|
||||
1. Bob sends the message:
|
||||
|
||||
1. He encrypts the message to Alice with "public" key `EK` (provided by Alice, only known to Bob, used only for one simplex queue).
|
||||
1. He encrypts the message to Alice with "public" key `EK` (provided by Alice, only known to Bob, used only for one simplex queue).
|
||||
|
||||
2. He signs `"SEND"` command to the server queue `SID` using the "private" key `SK` (that only he knows, used only for this queue).
|
||||
2. He signs `"SEND"` command to the server queue `SID` using the "private" key `SK` (that only he knows, used only for this queue).
|
||||
|
||||
3. He sends the command to the server (see `send` in [Send message](#send-message)), that the server will authenticate using the "public" key `SK` (that Alice earlier provided to the server).
|
||||
3. He sends the command to the server (see `send` in [Send message](#send-message)), that the server will authenticate using the "public" key `SK` (that Alice earlier received from Bob and provided to the server via `"KEY"` command).
|
||||
|
||||
2. Alice receives the message(s):
|
||||
|
||||
1. She signs `"SUB"` command to the server to subscribe to the queue `RID` with the "private" key `RK` (see `subscribe` in [Subscribe to queue](#subscribe-to-queue)).
|
||||
1. She signs `"SUB"` command to the server to subscribe to the queue `RID` with the "private" key `RK` (see `subscribe` in [Subscribe to queue](#subscribe-to-queue)).
|
||||
|
||||
2. The server, having authenticated Alice's command with the "public" key `RK` that she provided, delivers Bob's message(s) (see `message` in [Deliver queue message](#deliver-queue-message)).
|
||||
2. The server, having authenticated Alice's command with the "public" key `RK` that she provided, delivers Bob's message(s) (see `message` in [Deliver queue message](#deliver-queue-message)).
|
||||
|
||||
3. She decrypts Bob's message(s) with the "private" key `EK` (that only she has).
|
||||
3. She decrypts Bob's message(s) with the "private" key `EK` (that only she has).
|
||||
|
||||
4. She acknowledges the message reception to the server with `"ACK"` so that the server can delete the message and deliver the next messages.
|
||||
4. She acknowledges the message reception to the server with `"ACK"` so that the server can delete the message and deliver the next messages.
|
||||
|
||||
This flow is show on sequence diagram below.
|
||||
|
||||
@@ -332,7 +347,7 @@ Simplex messaging server implementations MUST NOT create, store or send to any o
|
||||
|
||||
- Snapshots of the database they use to store queues and messages (instead simplex messaging clients must manage redundancy by using more than one simplex messaging server). In-memory persistence is recommended.
|
||||
|
||||
- Any other information that may compromise privacy or [forward secrecy][4] of communication between clients using simplex messaging servers.
|
||||
- Any other information that may compromise privacy or [forward secrecy][4] of communication between clients using simplex messaging servers (the servers cannot compromise forward secrecy of any application layer protocol, such as double ratchet).
|
||||
|
||||
## Message delivery notifications
|
||||
|
||||
@@ -343,34 +358,72 @@ To protect the privacy of the recipients, there are several commands in SMP prot
|
||||
The clients can optionally instruct a dedicated push notification server to subscribe to notifications and deliver push notifications to the device, which can then retrieve the messages in the background and send local notifications to the user - this is out of scope of SMP protocol. The commands that SMP protocol provides to allow it:
|
||||
|
||||
- `enableNotifications` (`"NKEY"`) with `notifierId` (`"NID"`) response - see [Enable notifications command](#enable-notifications-command).
|
||||
- `disableNotifications` (`"NDEL"`) - see [Disable notifications command](#disable-notifications-command).
|
||||
- `subscribeNotifications` (`"NSUB"`) - see [Subscribe to queue notifications](#subscribe-to-queue-notifications).
|
||||
- `messageNotification` (`"NMSG"`) - see [Deliver message notification](#deliver-message-notification).
|
||||
|
||||
[`SEND` command](#send-message) includes the notification flag to instruct SMP server whether to send the notification - this flag is forwarded to the recepient inside encrypted envelope, together with the timestamp and the message body, so even if TLS is compromised this flag cannot be used for traffic correlation.
|
||||
|
||||
## SMP Transmission and transport block structure
|
||||
|
||||
Each transport block has a fixed size of 16384 bytes for traffic uniformity.
|
||||
|
||||
From SMP version 4 each block can contain multiple transmissions, version 3 blocks have 1 transmission.
|
||||
Some parts of SMP transmission are padded to a fixed size; this padding is uniformly added as a word16 encoded in network byte order - see `paddedString` syntax.
|
||||
|
||||
In places where some part of the transmission should be padded, the syntax for `paddedNotation` is used:
|
||||
|
||||
```abnf
|
||||
paddedString = originalLength string pad
|
||||
originalLength = 2*2 OCTET
|
||||
pad = N*N"#" ; where N = paddedLength - originalLength - 2
|
||||
|
||||
paddedNotation = <padded(string, paddedLength)>
|
||||
; string - un-padded string
|
||||
; paddedLength - required length after padding, including 2 bytes for originalLength
|
||||
```
|
||||
|
||||
Each transmission/block for SMP v3 between the client and the server must have this format/syntax:
|
||||
|
||||
```abnf
|
||||
paddedTransmission = <padded(transmission), 16384>
|
||||
transmission = signature signed
|
||||
signed = sessionIdentifier corrId queueId smpCommand
|
||||
; corrId is required in client commands and server responses,
|
||||
; it is empty in server notifications.
|
||||
corrId = length *OCTET
|
||||
queueId = length *OCTET
|
||||
; empty queue ID is used with "create" command and in some server responses
|
||||
signature = length *OCTET
|
||||
; empty signature can be used with "send" before the queue is secured with secure command
|
||||
; signature is always empty with "ping" and "serverMsg"
|
||||
length = 1*1 OCTET
|
||||
```
|
||||
|
||||
`base64` encoding should be used with padding, as defined in section 4 of [RFC 4648][9]
|
||||
|
||||
Transport block for SMP v4 has this syntax:
|
||||
|
||||
```abnf
|
||||
paddedTransportBlock = <padded(transportBlock), 16384>
|
||||
transportBlock = transmissionCount transmissions
|
||||
transmissionCount = 1*1 OCTET ; equal or greater than 1
|
||||
transmissions = transmissionLength transmission [transmissions]
|
||||
transmissionLength = 2*2 OCTET ; word16 encoded in network byte order
|
||||
```
|
||||
|
||||
## SMP commands
|
||||
|
||||
Commands syntax below is provided using [ABNF][8] with [case-sensitive strings extension][8a].
|
||||
|
||||
Each transmission between the client and the server must have this format/syntax (after the decryption):
|
||||
|
||||
```abnf
|
||||
transmission = [signature] SP signed SP pad ; pad to the fixed block size
|
||||
signed = sessionIdentifier SP [corrId] SP [queueId] SP cmd ; corrId is required in client commands and server responses,
|
||||
; corrId is empty in server notifications.
|
||||
cmd = ping / recipientCmd / send / subscribeNotifications / serverMsg
|
||||
recipientCmd = create / subscribe / secure / enableNotifications /
|
||||
smpCommand = ping / recipientCmd / send / subscribeNotifications / serverMsg
|
||||
recipientCmd = create / subscribe / secure / enableNotifications / disableNotifications /
|
||||
acknowledge / suspend / delete
|
||||
serverMsg = queueIds / message / notifierId / messageNotification /
|
||||
unsubscribed / ok / error
|
||||
corrId = 1*(%x21-7F) ; any characters other than control/whitespace
|
||||
queueId = encoded ; empty queue ID is used with "create" command
|
||||
signature = encoded
|
||||
; empty signature can be used with "send" before the queue is secured with secure command
|
||||
; signature is always empty with "ping" and "serverMsg"
|
||||
encoded = <base64 encoded binary>
|
||||
```
|
||||
|
||||
`base64` encoding should be used with padding, as defined in section 4 of [RFC 4648][9]
|
||||
|
||||
The syntax of specific commands and responses is defined below.
|
||||
|
||||
### Correlating responses with commands
|
||||
@@ -402,45 +455,36 @@ Sending any of the commands in this section (other than `create`, that is sent w
|
||||
This command is sent by the recipient to the SMP server to create a new queue. The syntax is:
|
||||
|
||||
```abnf
|
||||
create = %s"NEW" SP recipientSignaturePublicKey SP recipientDhPublicKey
|
||||
recipientSignaturePublicKey = signaturePublicKey
|
||||
; the recipient's public key to verify commands for this queue
|
||||
create = %s"NEW " recipientSignaturePublicKey recipientDhPublicKey
|
||||
recipientSignaturePublicKey = length x509encoded
|
||||
; the recipient's Ed25519 or Ed448 public key to verify commands for this queue
|
||||
|
||||
signaturePublicKey = signatureScheme ":" x509encoded
|
||||
signatureScheme = %s"rsa" | %s"ed25519" | %s"ed448"
|
||||
; "rsa" means deprecated RSA-PSS signature scheme,
|
||||
; it must not be used for the new queues.
|
||||
|
||||
recipientDhPublicKey = dhPublicKey
|
||||
dhPublicKey = encryptionScheme ":" x509encoded
|
||||
; the recipient's key for DH exchange to derive the secret
|
||||
recipientDhPublicKey = length x509encoded
|
||||
; the recipient's Curve25519 key for DH exchange to derive the secret
|
||||
; that the server will use to encrypt delivered message bodies
|
||||
; using [NaCl crypto_box][16] encryption scheme (curve25519xsalsa20poly1305).
|
||||
|
||||
encryptionScheme = %s"x25519" | %s"x448"
|
||||
; TODO change to define the encryption scheme, e.g. "crypto_box"
|
||||
x509encoded = <binary X509 key encoding>
|
||||
|
||||
x509encoded = <base64 X509 key encoding>
|
||||
length = 1*1 OCTET
|
||||
```
|
||||
|
||||
If the queue is created successfully, the server must send `queueIds` response with the recipient's and sender's queue IDs and public keys to sign all responses and messages and to encrypt delivered message bodies:
|
||||
|
||||
```abnf
|
||||
queueIds = %s"IDS" SP recipientId SP senderId
|
||||
SP serverSignaturePublicKey SP serverDhPublicKey
|
||||
serverSignaturePublicKey = signatureKey
|
||||
; the server's public key to verify responses and messages for this queue
|
||||
serverDhPublicKey = dhPublicKey
|
||||
; the server's key for DH exchange to derive the secret
|
||||
queueIds = %s"IDS " recipientId senderId srvDhPublicKey
|
||||
serverDhPublicKey = length x509encoded
|
||||
; the server's Curve25519 key for DH exchange to derive the secret
|
||||
; that the server will use to encrypt delivered message bodies to the recipient
|
||||
recipientId = encoded
|
||||
senderId = encoded
|
||||
recipientId = length *OCTET ; 16-24 bytes
|
||||
senderId = length *OCTET ; 16-24 bytes
|
||||
```
|
||||
|
||||
Once the queue is created, the recipient gets automatically subscribed to receive the messages from that queue, until the transport connection is closed. The `subscribe` command is needed only to start receiving the messages from the existing queue when the new transport connection is opened.
|
||||
|
||||
`NEW` transmission MUST be signed using the private part of the `recipientSignaturePublicKey` – this verifies that the client has the private key that will be used to sign subsequent commands for this queue.
|
||||
|
||||
`IDS` response transmission MUST be sent signed with `serverSignaturePublicKey` – this verifies that the server has the private key that will be used to sign subsequent responses and messages for this queue. This response should be sent with empty queue ID (the third part of the transmission).
|
||||
`IDS` response transmission MUST be sent with empty queue ID (the third part of the transmission).
|
||||
|
||||
#### Subscribe to queue
|
||||
|
||||
@@ -461,9 +505,9 @@ This transmission and its response MUST be signed.
|
||||
This command is sent by the recipient to the server to add sender's key to the queue:
|
||||
|
||||
```abnf
|
||||
secure = %s"KEY" SP senderSignaturePublicKey
|
||||
senderSignaturePublicKey = signaturePublicKey
|
||||
; the sender's key to verify SEND commands for this queue
|
||||
secure = %s"KEY " senderSignaturePublicKey
|
||||
senderSignaturePublicKey = length x509encoded
|
||||
; the sender's Ed25519 or Ed448 key to verify SEND commands for this queue
|
||||
```
|
||||
|
||||
`senderKey` is received from the sender as part of the first message - see [Send Message](#send-message) command.
|
||||
@@ -475,21 +519,42 @@ Once the queue is secured only signed messages can be sent to it.
|
||||
This command is sent by the recipient to the server to add notifier's key to the queue, to allow push notifications server to receive notifications when the message arrives, via a separate queue ID, without receiving message content.
|
||||
|
||||
```abnf
|
||||
enableNotifications = %s"NKEY" SP notifierKey
|
||||
notifierKey = signatureScheme ":" x509encoded ; the notifier's public key public key to verify NSUB command for this queue
|
||||
enableNotifications = %s"NKEY " notifierKey recipientNotificationDhPublicKey
|
||||
notifierKey = length x509encoded
|
||||
; the notifier's Ed25519 or Ed448 public key public key to verify NSUB command for this queue
|
||||
|
||||
recipientNotificationDhPublicKey = length x509encoded
|
||||
; the recipient's Curve25519 key for DH exchange to derive the secret
|
||||
; that the server will use to encrypt notification metadata (encryptedNMsgMeta in NMSG)
|
||||
; using [NaCl crypto_box][16] encryption scheme (curve25519xsalsa20poly1305).
|
||||
```
|
||||
|
||||
The server will respond with `notifierId` response if notifications were enabled and the notifier's key was successfully added to the queue:
|
||||
|
||||
```abnf
|
||||
notifierId = %s"NID" SP notifierId
|
||||
recipientId = encoded
|
||||
notifierId = %s"NID " notifierId srvNotificationDhPublicKey
|
||||
notifierId = length *OCTET ; 16-24 bytes
|
||||
srvNotificationDhPublicKey = length x509encoded
|
||||
; the server's Curve25519 key for DH exchange to derive the secret
|
||||
; that the server will use to encrypt notification metadata to the recipient (encryptedNMsgMeta in NMSG)
|
||||
```
|
||||
|
||||
This response is sent with the recipient's queue ID (the third part of the transmission).
|
||||
|
||||
To receive the message notifications, `subscribeNotifications` command ("NSUB") must be sent signed with the notifier's key.
|
||||
|
||||
#### Disable notifications command
|
||||
|
||||
This command is sent by the recipient to the server to remove notifier's credentials from the queue:
|
||||
|
||||
```abnf
|
||||
disableNotifications = %s"NDEL"
|
||||
```
|
||||
|
||||
The server must respond `ok` to this command if it was successful.
|
||||
|
||||
Once notifier's credentials are removed server will no longer send "NMSG" for this queue to notifier.
|
||||
|
||||
#### Acknowledge message delivery
|
||||
|
||||
The recipient should send the acknowledgement of message delivery once the message was stored in the client, to notify the server that the message should be deleted:
|
||||
@@ -537,10 +602,19 @@ Currently SMP defines only one command that can be used by senders - `send` mess
|
||||
This command is sent to the server by the sender both to confirm the queue after the sender received out-of-band message from the recipient and to send messages after the queue is secured:
|
||||
|
||||
```abnf
|
||||
send = %s"SEND" SP size SP msgBody SP
|
||||
; the last SP is in addition to SP in the transmission
|
||||
size = 1*DIGIT ; size in bytes
|
||||
msgBody = *OCTET ; any binary content of specified size
|
||||
send = %s"SEND " msgFlags SP smpEncMessage
|
||||
msgFlags = notificationFlag reserved
|
||||
notificationFlag = %s"T" / %s"F"
|
||||
smpEncMessage = smpPubHeader sentMsgBody ; message up to 16088 bytes
|
||||
smpPubHeader = smpClientVersion ("1" senderPublicDhKey / "0")
|
||||
smpClientVersion = word16
|
||||
senderPublicDhKey = length x509encoded
|
||||
; sender's Curve25519 public key to agree DH secret for E2E encryption in this queue
|
||||
; it is only sent in confirmation message
|
||||
x509encoded = <binary X509 key encoding>
|
||||
sentMsgBody = 16032*16032 OCTET
|
||||
; E2E-encrypted smpClientMessage padded to 16032 bytes before encryption
|
||||
word16 = 2*2 OCTET
|
||||
```
|
||||
|
||||
The first message is sent to confirm the queue - it should contain sender's server key (see decrypted message syntax below) - this first message must be sent without signature.
|
||||
@@ -558,15 +632,93 @@ Until the queue is secured, the server should accept any number of unsigned mess
|
||||
The body should be encrypted with the recipient's "public" key (`EK`); once decrypted it must have this format:
|
||||
|
||||
```abnf
|
||||
decryptedBody = [clientHeader] CRLF clientBody CRLF
|
||||
clientHeader = senderKeyMsg
|
||||
senderKeyMsg = %s"KEY" SP senderKey
|
||||
senderKey = signatureScheme ":" x509encoded ; the sender's public key to sign SEND commands for this queue
|
||||
clientBody = *OCTET
|
||||
sentMsgBody = <encrypted padded(smpClientMessage, 16032)>
|
||||
smpClientMessage = smpPrivHeader clientMsgBody
|
||||
smpPrivHeader = emptyHeader / smpConfirmationHeader
|
||||
emptyHeader = " "
|
||||
smpConfirmationHeader = %s"K" senderKey
|
||||
senderKey = length x509encoded
|
||||
; the sender's Ed25519 or Ed448 public key to sign SEND commands for this queue
|
||||
clientMsgBody = *OCTET ; up to 16016 in case of emptyHeader
|
||||
```
|
||||
|
||||
`clientHeader` in the initial unsigned message is used to transmit sender's server key and can be used in the future revisions of SMP protocol for other purposes.
|
||||
|
||||
SMP transmission structure for sent messages:
|
||||
|
||||
```
|
||||
------- transmission (= 16384 bytes)
|
||||
2 | originalLength
|
||||
276- | signature sessionId corrId queueId %s"SEND" SP (1+114 + 1+32? + 1+32 + 1+24 + 4+1 = 210)
|
||||
....... smpEncMessage (= 16088 bytes = 16384 - 296 bytes)
|
||||
8- | smpPubHeader (for messages it is only version and '0' to mean "no DH key" = 3 bytes)
|
||||
24 | nonce for smpClientMessage
|
||||
16 | auth tag for smpClientMessage
|
||||
------- smpClientMessage (E2E encrypted, = 16032 bytes = 16088 - 48)
|
||||
2 | originalLength
|
||||
12- | smpPrivHeader
|
||||
.......
|
||||
| clientMsgBody (<= 16016 bytes = 16032 - 14)
|
||||
.......
|
||||
0+ | smpClientMessage pad
|
||||
------- smpClientMessage end
|
||||
|
|
||||
....... smpEncMessage end
|
||||
18+ | transmission pad
|
||||
------- transmission end
|
||||
```
|
||||
|
||||
SMP transmission structure for received messages:
|
||||
|
||||
```
|
||||
------- transmission (= 16384 bytes)
|
||||
2 | originalLength
|
||||
276- | signature sessionId corrId queueId %s"MSG" SP msgId timestamp (1+114 + 1+32? + 1+32 + 1+24 + 3+1 + 24+1 + 8 = 243)
|
||||
16 | auth tag (msgId is used as nonce)
|
||||
------- serverEncryptedMsg (= 16090 bytes = 16384 - 294 bytes)
|
||||
2 | originalLength
|
||||
....... smpEncMessage (= 16088 bytes = 16090 - 2 bytes)
|
||||
16- | smpPubHeader (empty header for the message)
|
||||
24 | nonce for smpClientMessage
|
||||
16 | auth tag for smpClientMessage
|
||||
------- smpClientMessage (E2E encrypted, = 16032 bytes = 16088 - 56 bytes)
|
||||
2 | originalLength
|
||||
16- | smpPrivHeader (empty header for the message)
|
||||
....... clientMsgBody (<= 16016 bytes = 16032 - 16)
|
||||
-- TODO move internal structure (below) to agent protocol
|
||||
20- | agentPublicHeader (the size is for user messages post handshake, without E2E X3DH keys - it is version and 'M' for the messages - 3 bytes in total)
|
||||
....... E2E double-ratchet encrypted (<= 15996 bytes = 16016 - 20)
|
||||
1 | encoded double ratchet header length (it is 123 now)
|
||||
123 | encoded double ratchet header, including:
|
||||
2 | version
|
||||
16 | double-ratchet header iv
|
||||
16 | double-ratchet header auth tag
|
||||
1+88 | double-ratchet header (actual size is 69 bytes, the rest is reserved)
|
||||
16 | message auth tag (IV generated from chain ratchet)
|
||||
------- encrypted agent message (= 15856 bytes = 15996 - 140)
|
||||
2 | originalLength
|
||||
64- | agentHeader (the actual size is 41 = 8 + 1+32)
|
||||
2 | %s"MM"
|
||||
.......
|
||||
| application message (<= 15788 bytes = 15856 - 68)
|
||||
.......
|
||||
0+ | encrypted agent message pad
|
||||
------- encrypted agent message end
|
||||
|
|
||||
....... E2E double-ratchet encrypted end
|
||||
|
|
||||
-- TODO move internal structure (above) to agent protocol
|
||||
....... clientMsgBody end
|
||||
0+ | smpClientMessage pad
|
||||
------- smpClientMessage end
|
||||
|
|
||||
....... smpEncMessage end
|
||||
0+ | serverEncryptedMsg pad
|
||||
------- serverEncryptedMsg end
|
||||
0+ | transmission pad
|
||||
------- transmission end
|
||||
```
|
||||
|
||||
### Notifier commands
|
||||
|
||||
#### Subscribe to queue notifications
|
||||
@@ -594,18 +746,20 @@ See its syntax in [Create queue command](#create-queue-command)
|
||||
The server must deliver messages to all subscribed simplex queues on the currently open transport connection. The syntax for the message delivery is:
|
||||
|
||||
```abnf
|
||||
message = %s"MSG" SP encryptedMessage
|
||||
encryptedMessage = <encrypt sentMessage>
|
||||
sentMessage = msgId SP timestamp SP size SP msgBody SP
|
||||
msgId = encoded
|
||||
timestamp = <date-time defined in RFC3339>
|
||||
message = %s"MSG " msgId encryptedRcvMsgBody
|
||||
encryptedMsgBody = <encrypt paddedSentMsgBody> ; server-encrypted padded sent msgBody
|
||||
paddedSentMsgBody = <padded(sentMsgBody, maxMessageLength + 2)> ; maxMessageLength = 16088
|
||||
encryptedRcvMsgBody = <encrypt rcvMsgBody> ; server-encrypted meta-data and padded sent msgBody
|
||||
rcvMsgBody = timestamp msgFlags SP paddedSentMsgBody
|
||||
msgId = length 24*24OCTET
|
||||
timestamp = 8*8OCTET
|
||||
```
|
||||
|
||||
`msgId` - unique message ID generated by the server based on cryptographically strong random bytes. It should be used by the clients to detect messages that were delivered more than once (in case the transport connection was interrupted and the server did not receive the message delivery acknowledgement).
|
||||
`msgId` - unique message ID generated by the server based on cryptographically strong random bytes. It should be used by the clients to detect messages that were delivered more than once (in case the transport connection was interrupted and the server did not receive the message delivery acknowledgement). Message ID is used as a nonce for server/recipient encryption of message bodies.
|
||||
|
||||
`timestamp` - the UTC time when the server received the message from the sender, must be in date-time format defined by [RFC 3339][10]
|
||||
`timestamp` - system time when the server received the message from the sender as **a number of seconds** since Unix epoch (1970-01-01) encoded as 64-bit integer in network byte order. If a client system/language does not support 64-bit integers, until 2106 it is safe to simply skip the first 4 zero bytes and decode 32-bit unsigned integer (or as signed integer until 2038).
|
||||
|
||||
`msgBody` - see syntax in [Send message](#send-message)
|
||||
`paddedSentMsgBody` - see syntax in [Send message](#send-message)
|
||||
|
||||
When server delivers the messages to the recipient, message body should be encrypted with the secret derived from DH exchange using the keys passed during the queue creation and returned with `queueIds` response.
|
||||
|
||||
@@ -622,10 +776,19 @@ See its syntax in [Enable notifications command](#enable-notifications-command)
|
||||
The server must deliver message notifications to all simplex queues that were subscribed with `subscribeNotifications` command ("NSUB") on the currently open transport connection. The syntax for the message notification delivery is:
|
||||
|
||||
```abnf
|
||||
messageNotification = %s"NMSG"
|
||||
messageNotification = %s"NMSG " nmsgNonce encryptedNMsgMeta
|
||||
|
||||
encryptedNMsgMeta = <encrypted message metadata passed in notification>
|
||||
; metadata E2E encrypted between server and recipient containing server's message ID and timestamp (allows extension),
|
||||
; to be passed to the recipient by the notifier for them to decrypt
|
||||
; with key negotiated in NKEY and NID commands using nmsgNonce
|
||||
|
||||
nmsgNonce = <nonce used in NaCl crypto_box encryption scheme>
|
||||
; nonce used by the server for encryption of message metadata, to be passed to the recipient by the notifier
|
||||
; for them to use in decryption of E2E encrypted metadata
|
||||
```
|
||||
|
||||
Message notification does not contain any message data or meta-data, it only notifies that the message is available.
|
||||
Message notification does not contain any message data or non E2E encrypted metadata.
|
||||
|
||||
#### Subscription END notification
|
||||
|
||||
@@ -640,24 +803,24 @@ No further messages should be delivered to unsubscribed transport connection.
|
||||
#### Error responses
|
||||
|
||||
- incorrect block format, encoding or signature size (`BLOCK`).
|
||||
- missing or different session ID - tls-unique binding of TLS transport (`SESSION`)
|
||||
- command errors (`CMD`):
|
||||
- error parsing command (`SYNTAX`)
|
||||
- prohibited command (`PROHIBITED`) - any server response sent from client or `ACK` sent without active subscription or without message delivery.
|
||||
- incorrect RSA key size in `NEW` or `KEY` commands - only 1024, 2048 and 4096-bit keys are allowed (`KEY_SIZE`).
|
||||
- transmission has no required signature or queue ID (`NO_AUTH`)
|
||||
- transmission has unexpected credentials (`HAS_AUTH`)
|
||||
- transmission has no required queue ID (`NO_QUEUE`)
|
||||
- authentication error (`AUTH`) - incorrect signature, unknown (or suspended) queue, sender's ID is used in place of recipient's and vice versa, and some other cases (see [Send message](#send-message) command).
|
||||
- message queue quota exceeded error (`QUOTA`) - too many messages were sent to the message queue. Further messages can only be sent after the recipient retrieves the messages.
|
||||
- incorrect message body size (`SIZE`).
|
||||
- sent message is too large (> 16088) to be delivered (`LARGE_MSG`).
|
||||
- internal server error (`INTERNAL`).
|
||||
|
||||
The syntax for error responses:
|
||||
|
||||
```abnf
|
||||
error = %s"ERR" SP errorType
|
||||
errorType = %s"BLOCK" / %s"CMD" SP cmdError / %s"AUTH" / %s"SIZE" /%s"INTERNAL"
|
||||
cmdError = %s"SYNTAX" / %s"PROHIBITED" / %s"KEY_SIZE" / %s"NO_AUTH" / %s"HAS_AUTH" / %s"NO_QUEUE"
|
||||
error = %s"ERR " errorType
|
||||
errorType = %s"BLOCK" / %s"SESSION" / %s"CMD " cmdError / %s"AUTH" / %s"LARGE_MSG" /%s"INTERNAL"
|
||||
cmdError = %s"SYNTAX" / %s"PROHIBITED" / %s"NO_AUTH" / %s"HAS_AUTH" / %s"NO_ENTITY"
|
||||
```
|
||||
|
||||
Server implementations must aim to respond within the same time for each command in all cases when `"ERR AUTH"` response is required to prevent timing attacks (e.g., the server should perform signature verification even when the queue does not exist on the server or the signature of different size is sent, using any RSA key with the same size as the signature size).
|
||||
@@ -679,6 +842,7 @@ ok = %s"OK"
|
||||
Both the recipient and the sender can use TCP or some other, possibly higher level, transport protocol to communicate with the server. The default TCP port for SMP server is 5223.
|
||||
|
||||
For scenarios when meta-data privacy is critical, it is recommended that clients:
|
||||
|
||||
- communicating over Tor network,
|
||||
- establish a separate connection for each SMP queue,
|
||||
- send noise traffic (using PING command).
|
||||
@@ -686,38 +850,45 @@ For scenarios when meta-data privacy is critical, it is recommended that clients
|
||||
In addition to that, the servers can be deployed as Tor onion services.
|
||||
|
||||
The transport protocol should provide the following:
|
||||
|
||||
- server authentication (by matching server certificate hash with `serverIdentity`),
|
||||
- forward secrecy (by encrypting the traffic using ephemeral keys agreed during transport handshake),
|
||||
- integrity (preventing data modification by the attacker without detection),
|
||||
- unique channel binding (`sessionIdentifier`) to include in the signed part of SMP transmissions.
|
||||
|
||||
By default, the client and server communicate using [TLS 1.3 protocol][13] restricted to:
|
||||
- TLS_AES_256_GCM_SHA384 cypher suite,
|
||||
|
||||
- TLS_CHACHA20_POLY1305_SHA256 cipher suite (for better performance on mobile devices),
|
||||
- ed25519 and ed448 EdDSA algorithms for signatures,
|
||||
- x25519 and x448 ECDHE groups for key exchange.
|
||||
- servers must send only one self-signed certificate in the handshake, clients must abort the connection in case more than one certificate is sent.
|
||||
- servers must send the chain of exactly 2 self-signed certificates in the handshake, with the first (offline) certificate one signing the second (online) certificate. Offline certificate fingerprint is used as a server identity - it is a part of SMP server address.
|
||||
- The clients must abort the connection in case a different number of certificates is sent.
|
||||
- server and client TLS configuration should not allow resuming the sessions.
|
||||
|
||||
During TLS handshake the client must validate that the hash of the server certificate SPKI block is equal to the `serverIdentity` the client received as part of SMP server address; if the server identity does not match the client must abort the connection.
|
||||
During TLS handshake the client must validate that the fingerprint of the online server certificate is equal to the `serverIdentity` the client received as part of SMP server address; if the server identity does not match the client must abort the connection.
|
||||
|
||||
Once TLS handshake is complete, client and server will exchange blocks of fixed size (16384 bytes).
|
||||
|
||||
The first block sent by the client should be `clientHello` and the server should respond with `serverHello`:
|
||||
The first block sent by the server should be `serverHello` and the client should respond with `clientHello` - these blocks are used to agree SMP protocol version:
|
||||
|
||||
```abnf
|
||||
clientHello = SP smpVersion SP reserved pad
|
||||
serverHello = sessionIdentifier SP smpVersion SP reserved pad
|
||||
sessionIdentifier = <base64 encoded> ; unique session identifier derived from transport connection handshake
|
||||
; it should be included in all SMP transmissions sent in this transport connection.
|
||||
serverHello = minSmpVersion maxSmpVersion sessionIdentifier pad
|
||||
minSmpVersion = smpVersion
|
||||
maxSmpVersion = smpVersion
|
||||
sessionIdentifier = length *OCTET
|
||||
; unique session identifier derived from transport connection handshake
|
||||
; it should be included in all SMP transmissions sent in this transport connection.
|
||||
|
||||
smpVersion = %s"SMP v" 1*DIGIT "." 1*DIGIT "." 1*DIGIT ; semver format, the version in this document is v0.5.0
|
||||
reserved = <reserved for additional information>
|
||||
pad = 1*OCTET
|
||||
clientHello = smpVersion pad
|
||||
; chosen SMP protocol version - it must be the maximum supported version
|
||||
; within the range offered by the server
|
||||
|
||||
smpVersion = 2*2OCTET ; Word16 version number
|
||||
|
||||
pad = *OCTET
|
||||
```
|
||||
|
||||
For TLS 1.3 transport client should assert that `sessionIdentifier` is equal to `tls-unique` channel binding defined in [RFC 5929][14] (TLS Finished message struct); we pass it in `serverHello` block to allow communication over some other transport protocol.
|
||||
|
||||
The communication party (client or server) that has the lower protocol version should assume that this version will be supported by another party, the party with the higher protocol version should abort the connection in case they cannot support the lower version.
|
||||
For TLS transport client should assert that `sessionIdentifier` is equal to `tls-unique` channel binding defined in [RFC 5929][14] (TLS Finished message struct); we pass it in `serverHello` block to allow communication over some other transport protocol (possibly, with another channel binding).
|
||||
|
||||
[1]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack
|
||||
[2]: https://en.wikipedia.org/wiki/End-to-end_encryption
|
||||
@@ -734,3 +905,4 @@ The communication party (client or server) that has the lower protocol version s
|
||||
[13]: https://datatracker.ietf.org/doc/html/rfc8446
|
||||
[14]: https://datatracker.ietf.org/doc/html/rfc5929#section-3
|
||||
[15]: https://www.rfc-editor.org/rfc/rfc8709.html
|
||||
[16]: https://nacl.cr.yp.to/box.html
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
Scheme name: simplex
|
||||
|
||||
Status: Provisional
|
||||
|
||||
Applications/protocols that use this scheme name:
|
||||
This scheme is used for connection requests in SimpleX Agent Protocol,
|
||||
a middle layer protocol for managing bi-directional communication via
|
||||
redundant unidirectional SimpleX Messaging Protocol queues.
|
||||
|
||||
Contact: Evgeny Poberezkin <ep@simplex.chat>
|
||||
|
||||
Change controller: Evgeny Poberezkin <ep@simplex.chat>
|
||||
|
||||
References:
|
||||
The syntax for connection requests in the latest version of SimpleX Agent Protocol:
|
||||
https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md#connection-request
|
||||
SimpleX Messaging Protocol:
|
||||
https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md
|
||||
@@ -0,0 +1,16 @@
|
||||
Scheme name: smp
|
||||
|
||||
Status: Provisional
|
||||
|
||||
Applications/protocols that use this scheme name:
|
||||
This scheme is used for URIs of message queues in SimpleX Messaging Protocol,
|
||||
a client-server protocol for asynchronous distributed unidirectional
|
||||
message transmission via persistent message queues.
|
||||
|
||||
Contact: Evgeny Poberezkin <ep@simplex.chat>
|
||||
|
||||
Change controller: Evgeny Poberezkin <ep@simplex.chat>
|
||||
|
||||
References:
|
||||
The syntax for message queue URIs in the latest version of SimpleX Messaging Protocol:
|
||||
https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#smp-queue-uri
|
||||
@@ -26,20 +26,20 @@ Such connections that can be unidirectional or duplex can be managed by agents v
|
||||
|
||||
The protocol to create and manage such stream could be the following:
|
||||
|
||||
1. The sender client notifies the agent about the stream it has to create for a specific duplex connection (the connection must be duplex for it to be possible): `<conn_alias> SNEW <size>` (where `conn_alias` is the existing duplex connection, `size` is the size for the data to be transferred; 0 for unbounded streams), the response is `<conn_alias> STREAM <stream_id> <size>`.
|
||||
1. The sender client notifies the agent about the stream it has to create for a specific duplex connection (the connection must be duplex for it to be possible): `<conn_id> SNEW <size>` (where `conn_id` is the existing duplex connection, `size` is the size for the data to be transferred; 0 for unbounded streams), the response is `<conn_id> STREAM <stream_id> <size>`.
|
||||
2. The sender agent sends to the recipient agent the "envelope" `STREAM <stream_id> <size>`
|
||||
3. The receiving agent would notify the client about availability of the stream by sending `<conn_alias> STREAM <stream_id> <size>` notification.
|
||||
4. Receiving client would send `<conn_alias> SJOIN <stream_id>` command to the receiving agent.
|
||||
3. The receiving agent would notify the client about availability of the stream by sending `<conn_id> STREAM <stream_id> <size>` notification.
|
||||
4. Receiving client would send `<conn_id> SJOIN <stream_id>` command to the receiving agent.
|
||||
5. Receiving agent provisions the SMP queue on the random SMP server (we planned to remove SMP server from NEW command anyway). Possibly, there will be a limited list of servers that support streams (that is un-throttled number of messages), and stream support can be communicated via welcome header and change with AUTH SMP command.
|
||||
6. Receiving agent sends the envelop to sending agent `SJOIN <stream_id> <invitation>` (where invitation has the same format as in out-of-band message, but it has symmetric AES key and IV instead of asymmetric RSA key - the encryption scheme should codified in the invitation to make it generic).
|
||||
7. Sending agent and receiving agent confirm and secure SMP queue as usual. This is still not solving the problem that the sending agent should sign each command. We might extend SMP protocol to create a new queue type that allows authenticating the sender only once per TCP session so that messages can be sent without signing - the server can see that these queues are special anyway, as there will be much faster traffic there.
|
||||
8. Sending agent, once the queue is confirmed notifies the sending client that the stream is ready to accept data by sending notification `<conn_alias> SREADY <stream_id> <max_chunk_size>`.
|
||||
9. Sending agent client can now send packets into the stream up to total `<size>` limit if it was specified by using command `<conn_alias> SPUT <stream_id> <chunk_no> <chunk_size> <binary> ` (the chunk size should not be bigger than `max_chunk_size`, the agent would pad it to this size before sending to the receiving agent)
|
||||
8. Sending agent, once the queue is confirmed notifies the sending client that the stream is ready to accept data by sending notification `<conn_id> SREADY <stream_id> <max_chunk_size>`.
|
||||
9. Sending agent client can now send packets into the stream up to total `<size>` limit if it was specified by using command `<conn_id> SPUT <stream_id> <chunk_no> <chunk_size> <binary> ` (the chunk size should not be bigger than `max_chunk_size`, the agent would pad it to this size before sending to the receiving agent)
|
||||
10. Sending agent would send the chunk to the receiving agent in `CHUNK <chunk_no> <ts> <chunk_size> <binary>` envelope, where the first 4 bytes in decrypted binary is chunk size.
|
||||
11. Receiving agent would send the chunk to the client as `<conn_alias> SDATA <stream_id> <chunk_no> <ts> <status> <chunk_size> <binary>` (possibly, with three IDs and timestamps as with messages).
|
||||
12. Both the receiving and sending clients can terminate the stream with `<conn_alias> SCLOSE <stream_id>` command. The agent would send `SCLOSED <stream_id>` envelope and the opposite client would receive `<conn_alias> SCLOSED <stream_id>` notification - no more data will be accepted into this stream.
|
||||
11. Receiving agent would send the chunk to the client as `<conn_id> SDATA <stream_id> <chunk_no> <ts> <status> <chunk_size> <binary>` (possibly, with three IDs and timestamps as with messages).
|
||||
12. Both the receiving and sending clients can terminate the stream with `<conn_id> SCLOSE <stream_id>` command. The agent would send `SCLOSED <stream_id>` envelope and the opposite client would receive `<conn_id> SCLOSED <stream_id>` notification - no more data will be accepted into this stream.
|
||||
13. If the stream was bounded with some `size` then both the sending and receiving client would receive `SCLOSED` notification after the last chunk of correct size was sent (and the incorrect chunk would be rejected).
|
||||
14. If the stream was interrupted then the recipient client can request resuming this stream using `<conn_alias> SRESUME <stream_id> <from_chunk_no>`, the recipient agent would send "envelope" `SRESUME <stream_id> <from_chunk_no> <invitation>`.
|
||||
14. If the stream was interrupted then the recipient client can request resuming this stream using `<conn_id> SRESUME <stream_id> <from_chunk_no>`, the recipient agent would send "envelope" `SRESUME <stream_id> <from_chunk_no> <invitation>`.
|
||||
|
||||
To be clarified:
|
||||
1. Possibly the internal stream ID used by clients should be different from stream ID communicated between agents, and in any case it should be connection scoped (same as with messages).
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# SMP confirmation timeout recovery
|
||||
|
||||
## Problem
|
||||
|
||||
When sending an SMP confirmation a network timeout can lead to the following race condition:
|
||||
- server receives the confirmation while the joining party fails to receive the server's response;
|
||||
- joining party deletes the connection together with credentials sent in the confirmation for securing the queue;
|
||||
- initiating party will receive the confirmation from the server and secure the queue;
|
||||
- on subsequent attempt to join via the same invitation link initiating party will generate new credentials and fail authorization.
|
||||
|
||||
This renders the joining party permanently unable to join via that invitation link and complete the connection.
|
||||
|
||||
## Solution
|
||||
|
||||
A possible solution is to keep and try to reuse same credentials on subsequent attempts:
|
||||
- joining party has to remember invitation link when saving the connection;
|
||||
- if SMP confirmation fails due to network timeout joining party doesn't delete the connection and keeps the credentials;
|
||||
- when joining, joining party checks whether such invitation link was already used for a connection, if yes:
|
||||
- joining party tries to send SMP confirmation with the same credentials;
|
||||
- if this SMP confirmation fails with authorization error (for example it can happen due to race condition explained above) joining party tries to send HELLO message;
|
||||
- if HELLO message fails with authorization error (it can happen if connection was deleted or secured with different credentials), the recovery is no longer possible and connection can be deleted.
|
||||
@@ -0,0 +1,160 @@
|
||||
# SimpleX File Transfer protocol
|
||||
|
||||
## Problem
|
||||
|
||||
Sending files as currently implemented in SimpleX Chat is inefficient for these reasons:
|
||||
|
||||
1. Slow: message delivery requires acknowledgement for each message before the server sends the next message.
|
||||
2. Not fully asynchronous:
|
||||
a. SMP queue size is limited, so the whole file cannot be sent/stored in server memory.
|
||||
b. Recipient needs to accept the file before it is sent.
|
||||
3. No broadcast: sending the same file to multiple recipients requires sending it multiple times.
|
||||
|
||||
## Possible solutions
|
||||
|
||||
These problems could be solved within SMP protocol:
|
||||
|
||||
- streaming messages without waiting for acknowledgements.
|
||||
- bigger queue sizes stored on hard drive and accepting large files without waiting for the recipient confirmation.
|
||||
- adding broadcast to SMP servers.
|
||||
|
||||
These solution are complex and they do not scale well.
|
||||
|
||||
Another approach would be adopting some open-source file storage, possibly S3-compatible, to host files while "in transit". The downsides are:
|
||||
|
||||
1. Missing features. S3-compatible file servers require a service layer that we would need to develop to manage user accounts and permissions. There are solutions that allow self-registering users and uploading files without service layer, but they are not flexible enough in managing policies for these files.
|
||||
2. Limited meta-data protection, both on the application and on transport layer: same address for sending and receiving a file, file size is known to the server, etc.
|
||||
3. More difficult to self-host for the users when the server has 2 components (a 3rd party solution with our service component).
|
||||
|
||||
We are not considering P2P solutions because of their bad meta-data privacy and requirement of having a single network - the same reason why P2P design is not used for SimpleX messaging network.
|
||||
|
||||
A proposed solution is to develop a file hosting service (SimpleX File Transfer service) that would have better meta-data protection and functionality than the alternatives, using the ideas from SMP protocol design.
|
||||
|
||||
The parameters of the solution would be:
|
||||
|
||||
1. The server does not have knowledge of the users and actual files – only anonymously uploaded fixed-size chunks (considered 8Mb, maybe we should allow 2 sizes, e.g. 1Mb and 8Mb).
|
||||
2. The server should allow broadcast, so that a chunk can be downloaded by multiple recipients.
|
||||
3. There should be no identifiers and ciphertext in common inside TLS between sender and recipient traffic, and between the traffic of different recipients of the same file. This approach can be extended to support public chunks that have persistent identifiers.
|
||||
4. The server should prevent multiple uploads of the same chunk. This can be extended to allow multiple downloads from the same persistent address.
|
||||
5. Clients can send the chunks from the same file via multiple servers, both by splitting chunks between them and for redundancy.
|
||||
|
||||
## Design approach
|
||||
|
||||
File transfer servers will be chosen by file senders. The servers will allow senders to anonymously upload fixed-size file chunks.
|
||||
|
||||
### Transport protocol
|
||||
|
||||
- binary-encoded commands sent as fixed-size padded block in the body of HTTP2 POST request, similar to SMP and notifications server protocol transmission encodings.
|
||||
- HTTP2 POST with a fixed size padded block body for file upload and download.
|
||||
|
||||
Block size - 4096 bytes (it would fit ~120 Ed25519 recipient keys).
|
||||
|
||||
The reasons to use HTTP2:
|
||||
|
||||
- avoid the need to have two hostnames (or two different ports) for commands and file uploads.
|
||||
- compatibility with the existing HTTP2 client libraries.
|
||||
|
||||
The reason not to use JSON bodies:
|
||||
|
||||
- bigger request size, so fewer recipient keys would fit in a single request
|
||||
- signature over command has to be outside of JSON anyway.
|
||||
|
||||
The reason not to use URI segments / HTTP verbs / REST semantics is to have consistent request size.
|
||||
|
||||
### Required server commands:
|
||||
|
||||
- File sender:
|
||||
- create file chunk record.
|
||||
- Parameters:
|
||||
- Ed25519 key for subsequent sender commands and Ed25519 keys for commands of each recipient.
|
||||
- chunk size.
|
||||
- Response:
|
||||
- chunk ID for the sender and different IDs for all recipients.
|
||||
- add recipients to file chunk
|
||||
- Parameters:
|
||||
- sender's chunk ID
|
||||
- Ed25519 keys for commands of each recipient.
|
||||
- Response:
|
||||
- chunk IDs for new recipients.
|
||||
- upload file chunk.
|
||||
- delete file chunk (invalidates all recipient IDs).
|
||||
- File recipient:
|
||||
- download file chunk:
|
||||
- chunk ID
|
||||
- DH key for additional encryption of the chunk.
|
||||
- command should be signed with the key passed by the sender when creating chunk record.
|
||||
- delete file chunk ID (only for one recipient): signed with the same key.
|
||||
|
||||
### Storage model
|
||||
|
||||
Same as for SMP and notifications server - in-memory storage of the records, with adding to append-only log, restored and compacted on server restart.
|
||||
|
||||
### Sending file
|
||||
|
||||
To send the file, the sender will:
|
||||
|
||||
- compute SHA512 digest
|
||||
- pad the file to match the whole number of chunks in size,
|
||||
- encrypt it with a randomly chosen symmetric key and IV (e.g., using NaCL crypto_secretbox),
|
||||
- split into fixed size chunks
|
||||
- upload each chunk to a randomly chosen server.
|
||||
|
||||
The sending client should generate more per-recipient keys than the actual number of recipients, possibly rounding up to a power of 2, to conceal the actual number of intended recipients.
|
||||
|
||||
Then the sending client will combine addresses of all chunks and other information into "file description", different for each file recipient, that will include:
|
||||
|
||||
- an encryption key that was used to encrypt the file (the same for all recipients).
|
||||
- file SHA512 digest
|
||||
- list of chunk descriptions; information for each chunk:
|
||||
- private Ed25519 key to sign commands for file transfer server.
|
||||
- chunk address (server host and chunk ID).
|
||||
- chunk sha512 digest
|
||||
|
||||
To reduce the size, chunk descriptions will be grouped by the server host.
|
||||
|
||||
This "file description" itself will be sent as a small file over an authenticated channel, to prevent file description modification. To estimate its size:
|
||||
|
||||
- each chunk \* redundancy per chunk, assuming chunks are grouped per server:
|
||||
- 1-based chunk number in the file - 8 bytes (including any overhead)
|
||||
- Ed25519 key (different for each recipient / chunk combination) - 32 bytes \* 4/3 (base64, assuming text encoding)
|
||||
- chunk ID (different for each recipient) - 64 bytes \* 4/3
|
||||
- optional (only in the first chunk occurence) chunk sha512 digests - 64 bytes \* 4/3
|
||||
- server addresses - say, 128 bytes per server
|
||||
- sha512 digest - 64 bytes \* 4/3
|
||||
- encryption key - 32 bytes \* 4/3
|
||||
- IV - 32 bytes \* 4/3
|
||||
- encoding overhead - say, 256 bytes
|
||||
|
||||
For 1gb file, sent via 4 different servers, in 8Mb chunks, with redundancy 2, the size of "file description", assuming text encoding, will be ~45kb (`128 * (8 + 32 + 64) * 2 * 4/3 + 128 * 64 * 4/3 + 128 * 4 + (64 + 32 + 32) * 4/3 + 256`).
|
||||
|
||||
File description format (yml):
|
||||
|
||||
```
|
||||
name: file.ext
|
||||
size: 33200000
|
||||
chunk: 8mb
|
||||
hash: abc=
|
||||
key: abc=
|
||||
iv: abc=
|
||||
parts:
|
||||
- server: xftp://abc=@example1.com
|
||||
chunks: [1:abc=:def=:ghi=, 3:abc=:def=:ghi=]
|
||||
- server: xftp://abc=@example2.com
|
||||
chunks: [2:abc=:def=:ghi=, 4:abc=:def=:ghi=:2mb]
|
||||
- server: xftp://abc=@example3.com
|
||||
chunks: [1:abc=:def=, 4:abc=:def=]
|
||||
- server: xftp://abc=@example4.com
|
||||
chunks: [2:abc=:def=, 3:abc=:def=]
|
||||
```
|
||||
|
||||
This file description is sent to all recipients via normal messages, split to 15780 byte chunks if needed.
|
||||
|
||||
### Receiving file
|
||||
|
||||
Having received the description, the recipient will:
|
||||
|
||||
- download all chunks falling back to secondary servers, if needed
|
||||
- combine the chunks into a file
|
||||
- decrypt the file
|
||||
- un-pad it
|
||||
- validate file digest
|
||||
@@ -0,0 +1,20 @@
|
||||
# SMP and SMP agent protocol extensions to manage queue quotas
|
||||
|
||||
## Problem
|
||||
|
||||
SMP servers define a limit on the number of messages that can be stored on the server. When the sender reaches this limit, they continue trying to send the message with exponential back off, until the server allows it. To reduce the traffic we added expiration on unsent messages of 2 days that:
|
||||
|
||||
- adds its own set of problems because of expired control messages - e.g., group fragmentation.
|
||||
- doesn't solve problem of traffic in active groups with many inactive members - there are still too many retries in queues that reached capacity.
|
||||
|
||||
## Solution
|
||||
|
||||
Sending agent permanently stops trying to send messages having received `ERR QUOTA` (or, for the period of clients migration, increases TTL much more than in cases of network errors).
|
||||
|
||||
Add SMP server event that means "quota reached" (e.g. `QTA` or `MSG QUOTA`) - it will be delivered to the recipient once all messages are retrieved at the same point in message stream where the sender received `ERR QUOTA`. Alternatively, it could be a message flag that is set on the last message in the queue at a point the sender received `ERR QUOTA`.
|
||||
|
||||
Add SMP agent message that means "quota available, resume sending" (e.g. `A_QTA` or `A_RESUME`) – the receiving agent will send it via the reply queue to the sending agent that will resume sending messages at this point.
|
||||
|
||||
Possibly, increase TTL for local messages to 30 days. That might require separately addressing the problem of permanently failing servers.
|
||||
|
||||
We discussed a possible solution of merging all messages to one server into one delivery queue, so that having multiple failing queues on this server will not result in multiple connection attempts, and this proposal also answers the question what to do with the messages when quota is exceeded - the whole queue can be marked as suspended, and processed separately from others - TBD.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Re-sync encryption ratchets, queue rotation, message delivery receipts
|
||||
|
||||
This is very unfocussed doc outlining several problems that seem somewhat related, and some possible solution approaches.
|
||||
|
||||
## Problems
|
||||
|
||||
When old database backup is used, the sending client encrypts the messages in a way that the receiving client cannot decrypt them, because it does not store the old keys, and does not try to decrypt ratchet headers using the old keys. When receiving the messages, the client that uses the old database is unable to decrypt them, either the header or the message itself (if more than 500 messages are skipped, or if root ratchet step happened).
|
||||
|
||||
The same symptoms happen during the queue rotation, unfortunately, as we do not keep the history of received messages once they are acknowledged, so the client reports these error to the users.
|
||||
|
||||
While these are two different problems, to the users they manifest in the same way, so it is difficult to differentiate.
|
||||
|
||||
Another ratchet problem that should be solved as well is not deleting unused skipped keys after some time or some number of events, possibly it should be considered at the same time.
|
||||
|
||||
While it is tempting to consider delivery receipts as an isolated problem, it is somewhat coupled, as the requests to re-sync ratchets and to re-deliver skipped messages are effectively delivery receipts.
|
||||
|
||||
A naive solution to delivery receipts when each message is responded to with a confirmation has two downsides:
|
||||
|
||||
- it doubles the traffic.
|
||||
- in the absense of delays, it also leaks some information about the network latency, that can be used to track user's location.
|
||||
|
||||
Both problems can be mitigated by sending delivery receipts with a random delay, to confirm all messages sent, not each one. But this would only reduce the traffic when the sender sends many messages. Alternatively, we could think how to reduce the block size for SMP commands. The challenge is profile pictures and image previews, so to reduce traffic we need to start sending them as XFTP files, potentially with a smaller 64kb chunks (that would also increase preview quality).
|
||||
|
||||
## Solution for queue rotation
|
||||
|
||||
We need to:
|
||||
- store and show the rotation status in the UI to prevent attempts to rotate the queue multiple times.
|
||||
- process multiple message delivery events while there is a redundancy, to avoid errors in terminal.
|
||||
- differentiate between decryption errors during queue rotation - that would require keeping the messages for some time, at least keeping their IDs and hashes together with some additional status. In this case when duplicate or old message arrives, we can check whether this message was already processed, and do not show error if it was.
|
||||
|
||||
## Solution to re-sync ratchets
|
||||
|
||||
When we establish that the receiving client is indeed unable to decrypt messages, we should:
|
||||
- notify the user - it is already happening, but it happens in case of rotation as well.
|
||||
- optionally, notify another client that message was not decrypted - this effectively becomes a delivery receipt. That could be a separate command triggered by the user in the UI.
|
||||
- clients can get into this state at the same time, how do we handle concurrent requests? Possibly, we could design these messages so this is irrelevant whether the message is sent in response or not, and calculate the new ratchet keys once the clients have both the sent and received messages. After that both clients can send "ratchet reset" message:
|
||||
- EKEY - message containing a new set of keys to initialize the ratchets
|
||||
- EREADY - message confirming that the ratchet is ready, already e2e encrypted
|
||||
|
||||
The downside of this approach, is that unlike the initial handshake, where the second ratchet key is sent in e2e encrypted response, this message will be sent only encrypted with queue key. We could change this protocol by adding EREPLY message, and the clients would know to ingnore EKEY with bigger hash of the keys, so that the client who sent keys with smaller hash would be processing response from another client (that is if EKEY is received after EKEY is sent).
|
||||
|
||||
```
|
||||
A B
|
||||
|
||||
EKEY 1 -> <- EKEY 2 (e2e encrypted with per-queue key only)
|
||||
EKEY 2 <- -> EKEY 1
|
||||
1 < 2, wait 2 > 1, reply EREPLY 3 (e2e encrypted with ratchets)
|
||||
ratchet: 1, 3 ratchet: 3, 1
|
||||
```
|
||||
|
||||
Once ratchets are re-synched the clients could additionally request delivery of skipped messages. As ratchets being out of sync is not the only problem when messages can be lost, it may be managed independently from the ratchet re-sync. This problem seems tied with delivery receipts too, as requesting to re-deliver some messages automatically confirms that some other messages were delivered.
|
||||
|
||||
Alternatively, the request for ratchet sync can already contain the ID of the first failed message - that would allow the agent:
|
||||
- stop sending messages until ratchets are in sync.
|
||||
- re-deliver failed messages first.
|
||||
- only then deliver any pending messages.
|
||||
|
||||
This approach would probably result in a better UX (no out of order delivery), but requires tracking pending messages that are not attempted to deliver and to also insert old messages in the delivery queue with the old IDs.
|
||||
@@ -0,0 +1,170 @@
|
||||
# Delivery receipts
|
||||
|
||||
## Problems
|
||||
|
||||
User experience - users need to know that the messages are delivered to the recipient, as this confirms that the system is functioning.
|
||||
|
||||
The downside of communicating message delivery as it confirms that the recipient was online, and, unless there is a delay in confirming, can be used to track the location via the variation in network latency. So delivery receipts should be delayed with a randomized interval and should be opt in or opt out.
|
||||
|
||||
Another problem of message receipts is that they increase network traffic and server load. This could be avoided if delivery receipts are communicated as part of normal message delivery flow.
|
||||
|
||||
Some other existing and planned features implicitely confirm message delivery and, possibly, should depend on message delivery being enabled:
|
||||
- agent message to resume delivery when quota was exceeded (implemented, [rfc](./2022-12-27-queue-quota.md))
|
||||
- agent message to re-deliver skipped messages or to re-negotiate double ratchet.
|
||||
|
||||
## Solution
|
||||
|
||||
There are three layers where delivery receipts can be implemented:
|
||||
- chat protocol. Pro: logic of when to deliver it is decoupled from the message flow, Con: extra traffic, can only work in duplex connections.
|
||||
- agent client protocol. Pro: can be automated and combined with the protocol to re-deliver skipped messages. Con: extra traffic.
|
||||
- SMP protocol. Pro: minimal extra traffic, Con: complicates server design as it would require pushing receipts when there is no next message.
|
||||
|
||||
The last approach seems the most promising for avoiding additional traffic:
|
||||
- modify client ACK command to include whether delivery receipt should be provided to sender, and, possibly, any e2e encrypted data that should be included in the receipt (e.g., that the receiving client already saw this message in case we use "feedback" variant of roumor-mongering protocol for groups).
|
||||
- server would manage delaying of the receipts, by randomizing the time after which the receipt will be available to the sender, and by combining the receipts when possible.
|
||||
- modify response to SEND command to include any available delivery receipts.
|
||||
- add a separate delivery receipt that will be pushed to the sender in the connection where the message was received by the server.
|
||||
|
||||
## SMP protocol changes
|
||||
|
||||
```haskell
|
||||
data Command (p :: Party) where
|
||||
-- ...
|
||||
ACK :: MsgId -> Maybe ByteString -> Command Recipient
|
||||
-- the presense of ByteString in ACK indicates that the delivery needs to be confirmed.
|
||||
-- the protocol does not define the format of this confirmation, it is application specific, and can be -- an empty string.
|
||||
-- And open question is how to e2e encrypt information in this string - this probably can be handled on Agent client protocol level, and could be the same ratchet key that was used to encrypt and decrypt the message. The downside of this approach is that this key currently is not stored, and storing it requires additional logic to clear these keys if unused after some time.
|
||||
-- TODO consider what could be a better approach.
|
||||
SENT :: MsgId -> [(MsgId, UTCTime, ByteString)] -> Command Sender
|
||||
-- or
|
||||
-- SENT :: MsgId -> Command Sender
|
||||
-- in case we just batch
|
||||
-- this response will be sent to SEND command and will include a sender's message ID generated by the server (currently it does not exist), and posibly an empty list of delivery receipts with the same message IDs as in responses to SEND, timestamps when these receipts became available, and e2e encrypted ByteString passed in ACK command.
|
||||
-- The ID in this response should be different from the ID used in MSG, to keep the promise of not having shared identifiers in sent/received traffic even inside TLS tunnel.
|
||||
-- Keeping the quality of shared ciphertext also requires adding additional encryption layer between the server and the sender, this can be achieved in one of two ways:
|
||||
-- 1) passing a separate DH key in each SEND command, and server including additional DH key in each SENT response, with computed DH secret per message later used to encrypt and decrypt the delivery receipt payloads. This is probably a bad idea as it would increase a cryptographic load on both the server and the client.
|
||||
-- 2) agree a key per queue, in the same way it is done for the recipient. Possibly, it requires additional DH key in confirmation message that the recipient then uses to secure the queue, and passing this key in KEY (secure queue) command. The response to this secure command would the include server's DH key returned to the recipient that would be passed to the sender in HELLO message. Even though recipient could observe both public DH keys, they won't know the computed shared secret. Recipient that controls the server could perform MITM attack on this key exchange, but it doesn't give any benefit over what recipient can do when they have access to the server - the threat model remains the same. The downside of this approach is that it also requires additional changes in client protocol level (confirmation message format and HELLO message).
|
||||
-- 3) also agree on a key per queue, but via separate commands between the sender and the server, once the sender was notified that the queue is secured. This approach is probably better, and the server would simply delay the delivery of delivery receipts until the shared secret is agreed.
|
||||
SKEY :: C.PublicKeyX25519 -> Command Sender
|
||||
SBKEY :: C.PublicKeyX25519 -> BrokerMsg
|
||||
-- these are the command and response to agree secret to encrypt delivery receipt payloads for option 3
|
||||
SSUB :: Command Sender
|
||||
-- subscribe to receive delivery receipts for a given queue - will be sent when the conversation is opened (unless there is an active subscription already), not all queues at once, and won't be re-subscribed on losing the server connection (TBC).
|
||||
RCVD :: MsgId -> UTCTime -> ByteString -> Command Recipient
|
||||
-- delivery receipt. UTCTime is the time when it became available, not the time when ACK was sent by the recipient, to avoid leaking location via network latency.
|
||||
```
|
||||
|
||||
Possibly, there is no need to include delivery receipts into SENT response and instead just use batching of responses that is already supported. As server responses are not signed, there is no per-response overhead that is substantial, and a lot of receipts that are available can be packed into one block (depending on the size of payload that has to be fixed not to leak metadata).
|
||||
|
||||
This all seems rather complicated for SMP protocol, and the approach of doing it on a higher level seems more attractive than initially. Possibly we should reconsider, and reduce traffic by reducing block sizes... Reducing block sizes unfortunately requires supporting variable block sizes, and would leak some metadata during the transition period.
|
||||
|
||||
## Another approach
|
||||
|
||||
Above represents substantial complexity, and at least doubles server code complexity for the feature that is definitely not doubling the value of server software. Moving to variable block size is simpler, but also has a lot of complexity, reduces metadata privacy (at least for the duration of migration period), reduces image preview quality, and requires postponing this feature for multiple releases, until all clients migrate.
|
||||
|
||||
Given that the main traffic is generated by the groups, and direct messages do not create a lot of traffic, a much simpler and better solution is to simply send delivery receipts as the message, in direct conversations only, either as chat protocol message or as agent client protocol message (either on the message or on the envelope layer).
|
||||
|
||||
### Comparison of these two approaches:
|
||||
|
||||
**Chat protocol message**
|
||||
|
||||
Pros:
|
||||
- simpler, more contained change - SMP layer is not aware of this feature
|
||||
- easier to extend protocol with additional application specific payload, e.g. references to group DAG
|
||||
Cons:
|
||||
- ?
|
||||
|
||||
```json
|
||||
// ...
|
||||
"x.msg.delivered": {
|
||||
"properties": {
|
||||
"msgId": {"ref": "base64url"},
|
||||
"params": {
|
||||
"properties": {
|
||||
"msgId": {"ref": "base64url"},
|
||||
},
|
||||
"optionalProperties": {
|
||||
"data": {} // possibly the initial protocol does not need it, with JSON can be added later
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// ...
|
||||
```
|
||||
|
||||
**Agent client protocol envelope**
|
||||
|
||||
Pros:
|
||||
- possibility of using it in a wider range of the applications
|
||||
- possibility to include received message hash to increase communication integrity - the sending client would be then notified, and it can be exposed in the UI, that the received message is not the same as sent.
|
||||
|
||||
Cons:
|
||||
- additional implementation complexity - requires additional events to communicate between chat and agent.
|
||||
|
||||
```haskell
|
||||
data AMessage =
|
||||
-- ...
|
||||
| A_RCVD AgentMsgId MsgHash ByteString -- references to the received message
|
||||
-- ...
|
||||
```
|
||||
|
||||
The weirdness of the above design is that it refers to the data present in the header of another message, the alternative would be to have a separate envelope for delivery receipt:
|
||||
|
||||
```haskell
|
||||
data AgentMessage =
|
||||
-- ...
|
||||
AgentMessageRcvd APrivHeader AgentMsgId MsgHash ByteString -- references to the received message
|
||||
-- ...
|
||||
```
|
||||
|
||||
But probably the first one is a bit better, TBC.
|
||||
|
||||
In any case there should be an additional event to notify chat client:
|
||||
|
||||
```haskell
|
||||
data ACommand (p :: AParty) (e :: AEntity) where
|
||||
-- ...
|
||||
RCVD AgentMsgId MsgMeta ByteString -> ACommand Agent AEConn
|
||||
-- ...
|
||||
```
|
||||
|
||||
On the balance of things, implementing on the level of Agent Client protocol seems better, as the additional complexity is marginal, but it allows for wider range of applications, and also allows for additional delivery integrity validation. The format for payload still requires chat protocol message encoding once we want to add it, but initially it could be just an empty string.
|
||||
|
||||
## Implementation plan
|
||||
|
||||
Currently, we delete sent messages once delivered to the server. It would be helpful if we could keep the records in snd_messages table, and then use them to process delivery receipts, although it may be insufficient (we could add fields). Probably it is not possible to keep them as there is a foreign key constraint with `on delete cascade`.
|
||||
|
||||
The new table will be used to track sent message hashes to correlate their IDs with delivery receipts (that will also be stored in messages/rcv_messages). Receipts need to be processed in chat in the same way as normal messages, so they would be sent to chat with MsgMeta, and the chat will need to ack them once processed.
|
||||
|
||||
Agent ackMessage function will be also used to automatically schedule sending delivery receipts if they are enabled for connection, only for normal messages - no sending receipts to receipts.
|
||||
|
||||
There can be receipt re-delivery to the chat in the same cases as when normal message can be re-delivered (in case of AGENT A_DUPLICATE when it was not ack'd by the user).
|
||||
|
||||
## Other considerations
|
||||
|
||||
### How clients decide whether to send delivery receipts
|
||||
|
||||
Two options are possible - local settings per conversation or chat preferences framework, that allows to have mutual on/off. The latter seems preferable as without knowing whether the other party is sending receipts, it is not possible to uderstand what the absense of the receipt means - network malfunction or receipts disabled.
|
||||
|
||||
Groups are a special case, as while some groups may enable sending the delivery receipts, the group members should be able to disable it locally. This should probably be done via a separate conversation setting in the same way as enabling notifications or favourites. In this case the receipt would only be sent to the group if it is enabled in the group and not disabled by the member. The toggle may be located in the same page as chat preferences, but it should be a separate setting. We might want though to communicate somehow whether a given member sends delivery receipts so that other members know whether to expect them or not.
|
||||
|
||||
### How this functionality is released
|
||||
|
||||
5.2:
|
||||
- support for sending and receiving delivery receipts preference, both in direct messages and in groups (for forward compatibility).
|
||||
- support for sending and receiving delivery receipts in direct chats only, but disable sending them
|
||||
|
||||
5.3:
|
||||
- show receipt preferences in the UI
|
||||
- enable sending receipts in direct chats where they are enabled
|
||||
|
||||
A separate question is how to enable this functionality for the existing contacts. Possible options are:
|
||||
|
||||
1. Enable (as per default) for all contacts, show notification to the user when they open the app for update that delivery notifications are now sent by default to all contacts. Pro: no extra logic to implement. Con: may be perceived as a privacy violation, as to some contacts the delivery receipts will be sent before the user had chance to disable them.
|
||||
2. Ask the user when the new version first runs whether they want to use delivery receipts and offer these options:
|
||||
1) keep enabled for all profiles (and for all contacts)
|
||||
2) enable for all profiles in ~12 or in ~24 hours giving users the chance to review all contacts / profiles and disable some of them. The problem here is also in possibility of the correlation in case they all start sending receipts at the same time. Possibly the option could be to set a random time in 12-15 or 24-30 hours range to avoid the possibility of such correlation.
|
||||
3) disable for all profiles – it will require sending profile updates to all contacts, so the delivery receipts should be kept disabled and profile updates should be sent after random intervals, one by one (not scheduled all at once, as the time may pass while the app is off).
|
||||
3. Offer an option to enable globally later - we could keep it as a one-off option, available only to existing users, and visible on the top level of the Settings - once enabled, the option will disappear and it won't be possible to disable again. The downside here is that the new contacts would be receiving the profile with enabled notifications but they still won't be delivered...
|
||||
4. Another option is to have all new contacts decided based on a global user default (that can be set in the settings and in the dialog on first start), but for the existing contacts keep in unset state that is not interpreted as either on or off, but interpreted as unknown until the user makes a choice... That might be an optimal solution for the users but it would probably require changing the preferences framework or some ad-hoc hacks. That still keeps the question open how to avoid correlation between profiles.
|
||||
5. That might be the case for version agreement too - the availability of the option per contact will depend on the version. It doesn't answer the question what to do with global defaults though...
|
||||
@@ -0,0 +1,34 @@
|
||||
# SMP and XFTP delivery relays
|
||||
|
||||
## Problem
|
||||
|
||||
SimpleX network relies on SMP relays to hold sent messages while the recipient is offline. This design provides many advantages over pure p2p designs [1]. The threat model for relays is covered in [2].
|
||||
|
||||
The problem with this design is that the relays are chosen by the recipients, and they as a result can be used to record transport addresses of the senders.
|
||||
|
||||
Similar problem, but reversed, exists with XFTP design - XFTP relays are chosen by the senders and also can be modified to record transport addresses of the recipients.
|
||||
|
||||
The current solution to this problem is to use onion routing to access relays, and many users do it already. The downside is that it requires additional configuration and can be complex to some users.
|
||||
|
||||
## Solution approach
|
||||
|
||||
Add a second type of relays to the network design.
|
||||
|
||||
For SMP protocol, these relays would accept messages from the senders and deliver them to the recipients. If the recipient were to modify the relay used to receive the messages to also records senders' IP addresses, then they would only succeed in recording the IP address of the relay, not the sender.
|
||||
|
||||
Similar relay can be introduced for XFTP protocol.
|
||||
|
||||
The additional advantage of using the relays is the reduced number of network connections that need to be used, and reduced traffic.
|
||||
|
||||
Compared with Tor this approach has:
|
||||
- lower latency
|
||||
- no centralized components or the registry of the servers
|
||||
- still no visibility of the whole network to the relays, as sending relays do not need to randomly choose the next relay, but would forward the messages directly to the destination relay.
|
||||
|
||||
The downside of this approach is that relays will have more visibility of the network than in the current design.
|
||||
|
||||
## Implementation considerations
|
||||
|
||||
1. Should this new relays be included in the existing SMP and XFTP protocols and servers, or should they be designed as separate protocols and servers?
|
||||
|
||||
2. Should the client aim to choose the same host for delivery relay as the destination server (to reduce the traffic), if possible, or should it aim to choose a different host (to reduce metadata available to single host)? The answer to this would affect the answer to the first question.
|
||||
@@ -0,0 +1,366 @@
|
||||
# Re-sync encryption ratchets
|
||||
|
||||
## Problem
|
||||
|
||||
See https://github.com/simplex-chat/simplexmq/pull/743/files for problem and high-level solution.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Diagnosing ratchet de-synchronization
|
||||
|
||||
Message decryption happens in `agentClientMsg`, in `agentRatchetDecrypt`, which can return decryption result or error. Decryption error can be differentiated in `agentClientMsg` result pattern match, in Left cases, where we already differentiate duplicate error (`AGENT A_DUPLICATE`).
|
||||
|
||||
Question: Which decryption errors can be diagnosed as ratchet de-synchronization?
|
||||
|
||||
Possibly any `AGENT A_CRYPTO` error. Definitely on `RATCHET_HEADER`, TBC other. See `cryptoError :: C.CryptoError -> AgentErrorType` for conversion from decryption errors to `A_CRYPTO` or other agent errors. We're only interested in crypto errors, as other are either other client implementation errors, internal errors, or already processed duplicate error.
|
||||
|
||||
Proposed classification of crypto errors, based on `AgentCryptoError`:
|
||||
|
||||
`DECRYPT_AES` -> re-sync allowed (recommended/required?)
|
||||
|
||||
`DECRYPT_CB` -> re-sync allowed (recommended/required?)
|
||||
|
||||
`RATCHET_HEADER` -> **re-sync required**
|
||||
|
||||
`RATCHET_EARLIER` -> re-sync allowed
|
||||
|
||||
`RATCHET_SKIPPED` -> **re-sync required**
|
||||
|
||||
Ratchet re-synchronization could be started automatically on diagnosing de-synchronization, based on these errors. As a potentially dangerous feature (e.g., implementation error could lead to infinite re-sync loop causing large traffic consumption), initially it will be available via agent functional api for client to call. Ratchet de-synchronization will instead produce an event prompting client to re-synchronize.
|
||||
|
||||
Diagnosing possible ratchet de-synchronization also will be recorded as connection state - `ratchet_desync_state` field in `connections` table. Client should be prohibited to start ratchet re-synchronization unless `ratchet_desync_state` is set.
|
||||
|
||||
Event should not be repeated for following received messages that can't be decrypted - based on `ratchet_desync_state`. If a received message can be decrypted, `ratchet_desync_state` should be set to NULL and a new event sent, indicating ratchet has healed.
|
||||
|
||||
New event - `RDESYNC :: RatchetDesyncState -> ConnectionStats -> ACommand Agent AEConn`
|
||||
|
||||
```haskell
|
||||
data RatchetDesyncState
|
||||
= RDResyncAllowed
|
||||
| RDResyncRequired
|
||||
| RDHealed
|
||||
```
|
||||
|
||||
New field should be added to `ConnectionStats` - `ratchetDesyncState :: Maybe RatchetDesyncState`, based on `ratchet_desync_state`.
|
||||
|
||||
> On `RDESYNC` events chat should create chat item, prompting ratchet re-synchronization or notifying it has healed.
|
||||
> If connection has diagnosed ratchet de-sync, chat item should have a button to start ratchet re-sync.
|
||||
> We'd have to get `ConnectionStats` on chat level for this instead of chat info.
|
||||
> This wouldn't work for groups. One option is to add `ConnectionStats` to `GroupMember` type and update on events.
|
||||
> Same could be done for `Contact` then.
|
||||
|
||||
To consider - allow to start ratchet re-synchronization at any time regardless of this field as an experimental feature. In chat it could be behind "Developer tools" + additional "Experimental" toggle. Agent api would have `force :: Bool` as parameter, allowing to bypass `ratchet_desync_state`. Should `ratchet_resync_state` (see below) still be honored in this case?
|
||||
|
||||
### Re-synchronization process
|
||||
|
||||
\*\*\*\*\*
|
||||
|
||||
Basic idea is the following:
|
||||
|
||||
Both agents send new ratchet keys and compute a new shared secret. Agent that starts re-synchronization should record this fact in the connection state. Agent that receives a new key should respond with a key of its own, unless it has recorded that it itself started re-synchronization in the connection state.
|
||||
|
||||
It can happen that both agents start re-synchronizing simultaneously. In this case they both would record it in the connection state and would not respond with a new message - instead they would use each other's already sent keys.
|
||||
|
||||
Agent has both keys if:
|
||||
|
||||
- It initiates with the first key, and then receives the second key;
|
||||
- It receives the first key and then generates its own in response.
|
||||
|
||||
After agent has both keys, it initiates new ratchet depending on keys ordering. The agent that sent the lower key should use `initRcvRatchet` function, the agent that sent the greater key should use `initSndRatchet` (or vice versa - but they should deterministically choose different sides).
|
||||
|
||||
\*\*\*\*\*
|
||||
|
||||
State whether the ratchet re-synchronization is in progress should be tracked in database via `connections` table new `ratchet_resync_state` field.
|
||||
|
||||
New functional api:
|
||||
|
||||
```haskell
|
||||
resyncConnectionRatchet :: AgentErrorMonad m => AgentClient -> ConnId -> m ConnectionStats
|
||||
```
|
||||
|
||||
or if we want to allow re-synchronizing ratchet at any time even if de-synchronization wasn't diagnosed:
|
||||
|
||||
```haskell
|
||||
resyncConnectionRatchet :: AgentErrorMonad m => AgentClient -> ConnId -> Bool -> m ConnectionStats
|
||||
resyncConnectionRatchet c connId force = ...
|
||||
```
|
||||
|
||||
Possibly client command?
|
||||
|
||||
``` haskell
|
||||
data ACommand (p :: AParty) (e :: AEntity) where
|
||||
...
|
||||
RESYNC_RATCHET :: Bool -> ACommand Client AEConn
|
||||
```
|
||||
|
||||
New event - `RRESYNC :: RatchetResyncState -> ConnectionStats -> ACommand Agent AEConn`
|
||||
|
||||
```haskell
|
||||
data RatchetResyncState
|
||||
= RRStarted
|
||||
| RRAgreedSnd
|
||||
| RRAgreedRcv
|
||||
| RRComplete
|
||||
```
|
||||
|
||||
New `ConnectionStats` field - `ratchetResyncState :: Maybe RatchetResyncState`.
|
||||
|
||||
When called, it should:
|
||||
|
||||
- Generate new keys.
|
||||
- Update database connection state.
|
||||
- Set `ratchet_desync_state` to NULL.
|
||||
- Set `ratchet_resync_state` to `RRStarted`.
|
||||
- Delete old ratchet from `ratchets` (is it safe?), create new ratchet.
|
||||
- Send `AgentRatchetKey` message.
|
||||
- Return updated `ConnectionStats` to client.
|
||||
|
||||
> On `RRESYNC` events chat should create chat item, and reset connection verification.
|
||||
> Parameterized `RRESYNC` allows to distinguish: start and end of re-synchronization for initiating party; chat item direction - `RRESYNC RRStarted` is snd, `RRESYNC RRAgreedSnd/Rcv` and `RRESYNC RRComplete` are rcv (`RRESYNC RRAgreedSnd/Rcv` chat item could be omitted).
|
||||
|
||||
AgentRatchetKey is a new message on the level of AgentMsgEnvelope - encrypted with queue level e2e encryption, but not with connection level e2e encryption (since ratchet de-synchronized).
|
||||
|
||||
```haskell
|
||||
data AgentMsgEnvelope
|
||||
= ...
|
||||
| AgentRatchetKey
|
||||
{ agentVersion :: Version,
|
||||
e2eEncryption :: E2ERatchetParams 'C.X448,
|
||||
info :: ByteString -- for extension
|
||||
}
|
||||
```
|
||||
|
||||
On receiving `AgentRatchetKey`, if the receiving client hasn't started the ratchet re-synchronization itself (check `ratchet_resync_state`), it should:
|
||||
|
||||
- Generate new keys and compute new shared secret, initializing ratchet based on keys comparison.
|
||||
- Update database connection state.
|
||||
- Set `ratchet_resync_state` to `RRAgreedSnd/Rcv` (depending on whether ratchet was initialized as sending or receiving).
|
||||
- Delete old ratchet from `ratchets`, create new ratchet.
|
||||
- Reply with its own `AgentRatchetKey`.
|
||||
- Notify client with `RRESYNC RRAgreedSnd/Rcv`.
|
||||
- If ratchet was initialized as sending, send `EREADY` message, notifying other agent ratchet is re-synced.
|
||||
|
||||
New agent message:
|
||||
|
||||
```haskell
|
||||
data AMessage
|
||||
= ...
|
||||
| -- ratchet re-synchronization is complete, with last decrypted sender message id
|
||||
EREADY PrevExternalSndId
|
||||
```
|
||||
|
||||
On receiving `AgentRatchetKey`, if the receiving client started re-sync:
|
||||
|
||||
- Compute new shared secret, initializing ratchet based on keys comparison.
|
||||
- Update database connection state.
|
||||
- Set `ratchet_resync_state` to `RRAgreedSnd/Rcv` (depending on whether ratchet was initialized as sending or receiving).
|
||||
- Update ratchet.
|
||||
- Notify client with `RRESYNC RRAgreedSnd/Rcv`.
|
||||
- If ratchet was initialized as sending, send `EREADY` message.
|
||||
|
||||
After agent receives `EREADY` (or any other message that successfully decrypts):
|
||||
|
||||
- Reset `ratchet_resync_state` to NULL.
|
||||
- Notify client with `RRESYNC RRComplete`.
|
||||
- If ratchet was initialized as receiving, send reply `EREADY` message.
|
||||
|
||||
### State transitions
|
||||
|
||||
For initiating party:
|
||||
|
||||
```
|
||||
+------------+
|
||||
| Ratchet ok |
|
||||
+------------+
|
||||
|
|
||||
| message received, decryption error
|
||||
* ---->|-----------------------------------+
|
||||
| |
|
||||
V new (clarifying) V
|
||||
+-----------------+ error +------------------+
|
||||
| Re-sync allowed |--------------->| Re-sync required |
|
||||
+-----------------+ +------------------+
|
||||
| |
|
||||
|-----------------------------------|
|
||||
| | alternative - message received,
|
||||
| re-sync started by client | successfully decrypted
|
||||
V V
|
||||
+-----------------+ +------------+
|
||||
| Re-sync started | | Ratchet ok |
|
||||
+-----------------+ +------------+
|
||||
|
|
||||
| other party replied with new ratchet key
|
||||
V
|
||||
+----------------+
|
||||
| Re-sync agreed |----> * message received, decryption error
|
||||
| snd / rcv | (should remember agreed state for reply EREADY?)
|
||||
+----------------+
|
||||
|
|
||||
| message received, successfully decrypted
|
||||
| (can be, but not necessarily, EREADY)
|
||||
V
|
||||
+------------+
|
||||
| Ratchet ok |
|
||||
+------------+
|
||||
```
|
||||
|
||||
For replying party:
|
||||
|
||||
```
|
||||
+------------+
|
||||
| Ratchet ok |
|
||||
+------------+
|
||||
|
|
||||
| other party sent new ratchet key
|
||||
V
|
||||
+----------------+
|
||||
| Re-sync agreed |
|
||||
| snd / rcv |
|
||||
+----------------+
|
||||
|
|
||||
| message received, successfully decrypted
|
||||
| (can be, but not necessarily, EREADY)
|
||||
V
|
||||
+------------+
|
||||
| Ratchet ok |
|
||||
+------------+
|
||||
```
|
||||
|
||||
### Ratchet state model
|
||||
|
||||
#### 2 state variables
|
||||
|
||||
Above we considered model with separate de-sync and re-sync state.
|
||||
|
||||
| Desync \ Resync | Nothing | RRStarted | RRAgreedSnd | RRAgreedRcv |
|
||||
| --- | :---: | :---: | :---: | :---: |
|
||||
| **Nothing** | 1 | 3 | 4 | 4 |
|
||||
| **RDResyncAllowed** | 2 | | 5 | 5 |
|
||||
| **RDResyncRequired** | 2 | | 5 | 5 |
|
||||
|
||||
1: Ratchet is ok.
|
||||
|
||||
2: Re-sync diagnosed, not in progress.
|
||||
|
||||
3: Rs-sync started, diagnosing de-sync is prohibited.
|
||||
|
||||
4: Re-sync agreed.
|
||||
|
||||
5: Re-sync agreed, new de-sync is diagnosed.
|
||||
|
||||
Combination 5 is possible in case de-sync was diagnosed before message that could be decrypted is received, for example if `EREADY` failed to deliver and no other decryptable message followed. We shouldn't prohibit diagnosing de-sync in this case, because agent may never exit "Agreed" state (if new decryptable message is never received). We also shouldn't overwrite/forget state of re-sync, even if we diagnose new possible de-sync, because if the decryptable `EREADY` is received and ratchet is in `RRAgreedRcv` state, it should respond with reply `EREADY`.
|
||||
|
||||
Some combinations should be impossible:
|
||||
|
||||
- `RDResyncAllowed` with `RRStarted`.
|
||||
|
||||
- `RDResyncRequired` with `RRStarted`.
|
||||
|
||||
`RDHealed` is equivalent to `Nothing` and only used for `RDESYNC` event, `Maybe RatchetDesyncState` can be replaced with `RatchetDesyncState`, with single new constructor `RDNoDesync` replacing `Nothing` and `RDHealed`.
|
||||
|
||||
`RRComplete` is equivalent to `Nothing` and only used for `RRESYNC` event, `Maybe RatchetResyncState` can be replaced with `RatchetResyncState`, with single new constructor `RDNoResync` replacing `Nothing` and `RRComplete`.
|
||||
|
||||
#### Single state variable
|
||||
|
||||
Another option is two have a single state variable describing ratchet.
|
||||
|
||||
```haskell
|
||||
data RatchetState
|
||||
= RSOk
|
||||
| RSResyncAllowed
|
||||
| RSResyncRequired
|
||||
| RSResyncStarted
|
||||
| RRResyncAgreedSnd
|
||||
| RRResyncAgreedRcv
|
||||
|
||||
-- When `resyncConnectionRatchet` is not prohibited. Can override with `force`.
|
||||
-- Currently we check:`(isJust ratchetDesyncState || force) && ratchetResyncState /= Just RRStarted`.
|
||||
resyncConnectionRatchetAllowed :: RatchetState -> Bool
|
||||
resyncConnectionRatchetAllowed = \case
|
||||
RSOk -> False
|
||||
RSResyncAllowed -> True
|
||||
RSResyncRequired -> True
|
||||
RSResyncStarted -> False -- `force` shouldn't override
|
||||
RRResyncAgreedSnd -> False
|
||||
RRResyncAgreedRcv -> False
|
||||
|
||||
-- When we register and notify about ratchet de-synchronization.
|
||||
-- Currently we check: `(isNothing ratchetDesyncState && ratchetResyncState /= Just RRStarted)`.
|
||||
-- We should also allow to update from Allowed to Required.
|
||||
shouldNotifyRDESYNC :: RatchetState -> Bool
|
||||
shouldNotifyRDESYNC = \case
|
||||
RSOk -> True
|
||||
RSResyncAllowed -> False -- only if new error implies Required
|
||||
RSResyncRequired -> False
|
||||
RSResyncStarted -> False
|
||||
RRResyncAgreedSnd -> True
|
||||
RRResyncAgreedRcv -> True
|
||||
|
||||
-- When we prohibit connection switch, for `checkRatchetDesync`.
|
||||
-- Currently we check: `(ratchetDesyncState == Just RDResyncRequired || ratchetResyncState == Just RRStarted)`
|
||||
-- Also use in `runSmpQueueMsgDelivery` to pause delivery?
|
||||
ratchetDesynced :: RatchetState -> Bool
|
||||
ratchetDesynced = \case
|
||||
RSOk -> False
|
||||
RSResyncAllowed -> False
|
||||
RSResyncRequired -> True
|
||||
RSResyncStarted -> True
|
||||
RRResyncAgreedSnd -> False
|
||||
RRResyncAgreedRcv -> False
|
||||
```
|
||||
|
||||
Having a single state variable limits differentiation described for combination 5 in matrix. It also limits possible differentiations in client between events when ratchet is healed on its own, and when ratchet re-sync is completed after agents negotiation. Overall, since matrix is not very sparse and allows for more fine-grained decision-making, having separate state variables for de-sync and re-sync seems preferred.
|
||||
|
||||
#### Single state variable simplified (final version)
|
||||
|
||||
```haskell
|
||||
data RatchetSyncState
|
||||
= RSOk
|
||||
| RSAllowed
|
||||
| RSRequired
|
||||
| RSStarted
|
||||
| RSAgreed
|
||||
|
||||
-- event
|
||||
RSYNC :: RatchetSyncState -> ConnectionStats -> ACommand Agent AEConn`
|
||||
|
||||
-- ConnectionStats field
|
||||
ratchetSyncState :: RatchetSyncState
|
||||
```
|
||||
|
||||
Updated design decisions:
|
||||
|
||||
1. Single constructor for "Agreed" state. Differentiating `RRResyncAgreedSnd` and `RRResyncAgreedRcv` allowed for easier processing of `EREADY` by helping to determine whether reply `EREADY` has to be sent. However, it duplicated information already present in ratchet's state, and can be instead worked around by remembering and analyzing ratchet state pre decryption.
|
||||
|
||||
2. Prohibit transition from "Agreed" state to "Desync" states. This would make possible edge-cases that leave ratchet in de-synchronized state without ability to progress (e.g. failed delivery of `AgentRatchetKey`), but would simplify state machine by removing dedicated "Desync" variable. Besides, there's still a recovery way with a `force` option.
|
||||
|
||||
3. Treat "Agreed" as unfinished state - prohibit new messages to be enqueued, etc. Reception of any decryptable message transitions ratchet to "Ok" state.
|
||||
|
||||
Possible improvements:
|
||||
|
||||
- Repeatedly triggering re-synchronization while in "Started"/"Agreed" state re-sends same keys and EREADY.
|
||||
- Cooldown period, during which repeat re-synchronization is prohibited.
|
||||
|
||||
### Skipped messages
|
||||
|
||||
Options:
|
||||
|
||||
1. Ignore skipped messages.
|
||||
2. Stop sending new messages while connection re-synchronizes (can use `ratchet_resync_state`).
|
||||
|
||||
- Initiator shouldn't send new messages until receives `AgentRatchetKey` from second party.
|
||||
- Second party knows new shared secret immediately after processing first `AgentRatchetKey`, so it's not necessary to limit?
|
||||
|
||||
3. 2 + Re-send skipped messages first.
|
||||
|
||||
- Add `last_external_snd_msg_id` to `AgentRatchetKey`? + see link above
|
||||
|
||||
4. Re-send only messages after the latest ratchet step. \*
|
||||
|
||||
It may be okay to ignore skipped messages, or at most implement option 2, as ratchet de-synchronization is usually caused by misuse (human error) - the most common cause of ratchet de-sync seems to be sending and receiving messages after running agent with old database backup. In this case user has already seen most skipped messages, and it can be expected to not have them after switching to an old backup. So in this case the only "really skipped" messages are those that were sent during the latest ratchet step and failed to decrypt, triggering ratchet re-sync (\* another option is to only re-send those).
|
||||
|
||||
Besides, depending on time of backup there may be an arbitrary large number of skipped messages, which may consume a lot of traffic and may halt delivery of up-to-date messages for some time.
|
||||
|
||||
It may be better to have request for repeat delivery as a separate feature, that can be requested in necessary contexts - for example for group stability.
|
||||
|
||||
Can servers delivery failure lead to de-sync? If message is lost on server and never delivered, ratchet wouldn't advance, so there's no room for de-sync? If yes, re-evaluate.
|
||||
@@ -0,0 +1,234 @@
|
||||
# Protecting IP addresses of the users from their contacts
|
||||
|
||||
## 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.
|
||||
|
||||
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".
|
||||
|
||||
Similarly, XFTP protocol relays are chosen by senders, and they can be used to detect file recipients' IP addresses.
|
||||
|
||||
## Possible solutions
|
||||
|
||||
1. Embed Tor in the client.
|
||||
|
||||
Pros:
|
||||
- no changes in the protocols/servers
|
||||
- acceptable threat model for most users
|
||||
- removes complexity of installing and configuring Tor
|
||||
- probably, the most attractive option for Tor users
|
||||
|
||||
Cons:
|
||||
- higher latency and error rate
|
||||
- higher resource usage
|
||||
- requires us updating Tor client regularly
|
||||
- restriction on Tor usage in some networks, so it would require supporting bridges in the app UI
|
||||
- legislative restrictions on Tor usage, so it may require supporting multiple app versions, and won't solve the problem where Tor is not embedded
|
||||
|
||||
2. Thin clients with hosted accounts.
|
||||
|
||||
Host some part of the current client on the server and have another part running on the devices.
|
||||
|
||||
Pros:
|
||||
- substantially reduces the traffic for mobile clients
|
||||
- probably the most attractive option for mass market users who expect to have an account on the server and be able to access it from multiple devices and via web.
|
||||
|
||||
Cons:
|
||||
- a lot of design and development
|
||||
- makes accounts visible to the server
|
||||
- the new protocol design to protect connections graph and message content from the hosting server.
|
||||
- quite different threat model
|
||||
|
||||
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.
|
||||
|
||||
Pros:
|
||||
- no dependency on and lower latency than via Tor
|
||||
- reduces client traffic, both due to retries handled by these proxies and due to the smaller number of connections to the peer-chosen relays. The flip side is that additional commands (and traffic) are needed to create the sessions with relays.
|
||||
- higher transport privacy: protects IP addresses from peers and makes transport correlation harder.
|
||||
- additional function: retrying pending messages, while the queue is not secured yet, without extra traffic for the clients.
|
||||
- improves current threat model by preventing session-based correlation of the traffic by the destination relay (assuming no information sharing with the proxies), as there will be one session between proxy and destination relay, mixing traffic from multiple users.
|
||||
|
||||
Cons:
|
||||
- design and development cost
|
||||
- can undermine delivery stability
|
||||
|
||||
This seems an attractive option, both from technical (reasonable complexity) and positioning (moving SimpleX closer to mix networks, while still avoiding server list visibility) points of view, and it is in the middle between embedding Tor, which would make the product more niche and has downsides and development costs too, and thin clients, which is 10x+ more work, and also creates a different threat model, that is not very attractive for the current stage and users.
|
||||
|
||||
Below considers this design.
|
||||
|
||||
## SMP/XFTP proxy design
|
||||
|
||||
### Design requirements
|
||||
|
||||
1. To avoid increasing the complexity of self-hosting, we should not create additional server types, and existing SMP and XFTP servers should be extended to provide proxy functions.
|
||||
|
||||
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).
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
- holding messages and retrying them for the new connections while the receiving queue is not secured yet.
|
||||
- add delays in message delivery to make traffic correlation harder (same can be done in SMP relays).
|
||||
|
||||
### Implementation considerations
|
||||
|
||||
1. Block size. Possibly, there is not enough spare capacity in the current 16kb block to fit additional headers, any necessary metadata and encryption authentication tags, in which case we cannot send SMP and SMP-proxy traffic in the same transport connection (in case the same server plays both roles). In which case we will need to negotiate role during connection handshake and define a different (sub-)protocol for SMP-proxy.
|
||||
|
||||
2. To be decided if we see it as extension of SMP protocol or as another protocol, irrespective of whether it's provided by the same or another server. Given different roles and block size it may be simpler to see it as a separate protocol, and which protocol is provided in the connection is determined during handshake.
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
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).
|
||||
|
||||
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).
|
||||
|
||||
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).
|
||||
|
||||
With 32 bits per key there will be ~1/1,000,000 false positives (see https://en.wikipedia.org/wiki/Bloom_filter), and the filter would use ~32mb per each proxy connection if we reset relay key every 1000000 messages or more frequently. Given that the only commands accepted via relay would be SEND, replaying it would be interpreted by the receiving client as duplicate message delivery, so a small number of replayed messages won't cause any problems. But without mitigation it could be used to flood the receiving SMP relay queues with repeated messages, effectively causing DoS on these queues, even in case when SMP relays require basic auth to create queues.
|
||||
|
||||
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.
|
||||
|
||||
5. SMP relay sends `response` to proxy applying additional encryption layer, which it then forwards to the client removing the additional 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:
|
||||
|
||||
```
|
||||
------------- ------------- ------------- -------------
|
||||
| sending | | SMP | | SMP | | receiving |
|
||||
| client | | proxy | | relay | | client |
|
||||
------------- ------------- ------------- -------------
|
||||
| `server` | | |
|
||||
| -------------------------> | create TLS session, get keys | |
|
||||
| | ------------------------------> | |
|
||||
| `server_id` | (if doesn't exist) | |
|
||||
| <------------------------- | | |
|
||||
| | | |
|
||||
| TLS(F:s2r(SEND(e2e(msg)))) | | |
|
||||
| -------------------------> | TLS(F:p2r(s2r(SEND(e2e(msg))))) | |
|
||||
| | ------------------------------> | |
|
||||
| | | |
|
||||
| | TLS(R:p2r(s2r(OK/ERR))) | |
|
||||
| TLS(R:s2r(OK/ERR)) | <------------------------------ | |
|
||||
| <------------------------- | | TLS(MSG(r2c(e2e(msg)))) |
|
||||
| | | -----------------------> |
|
||||
| | | |
|
||||
| | | TLS(ACK) |
|
||||
| | | <----------------------- |
|
||||
| | | |
|
||||
| | | |
|
||||
|
||||
```
|
||||
|
||||
Below diagram shows the encrypttion layers for `forward` and `response` commands:
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- r2c (exists now) additional encryption between SMP relay and client to prevent traffic correlation inside TLS.
|
||||
|
||||
```
|
||||
----------------- ----------------- -- TLS -- ----------------- -----------------
|
||||
| | -- TLS -- | | -- p2r -- | | -- TLS -- | |
|
||||
| | -- s2r -- | | -- s2r -- | | -- r2c -- | |
|
||||
| sending | -- e2e -- | | -- e2e -- | | -- e2e -- | receiving |
|
||||
| client | MSG | SMP proxy | MSG | SMP relay | MSG | client |
|
||||
| | -- e2e -- | | -- e2e -- | | -- e2e -- | |
|
||||
| | -- s2r -- | | -- s2r -- | | -- r2c -- | |
|
||||
| | -- TLS -- | | -- p2r -- | | -- TLS -- | |
|
||||
----------------- ----------------- -- 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.
|
||||
|
||||
Below syntax aims to fit in 16kb block using spare capacity in SMP protocol.
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
### Threat model for SMP proxy and changes to threat model for SMP
|
||||
|
||||
#### SMP proxy
|
||||
|
||||
*can:*
|
||||
|
||||
- learn a user's IP address, as long as Tor is not used.
|
||||
|
||||
- learn when a user with a given IP address is online.
|
||||
|
||||
- know how many messages are sent from a given IP address and to a given destination SMP relay.
|
||||
|
||||
- drop all messages from a given IP address or to a given destination relay.
|
||||
|
||||
- unless SMP relay detects repeated public DH keys of senders, replay messages to a destination relay within a single session, causing either duplicate message delivery (which will be detected and ignored by the receiving clients), or, when receiving client is not connected to SMP relay, exhausting capacity of destination queues used within the session.
|
||||
|
||||
*cannot:*
|
||||
|
||||
- perform queue correlation (matching multiple queues to a single user), unless it has additional information from SMP relay.
|
||||
|
||||
- undetectably add, duplicate, or corrupt individual messages.
|
||||
|
||||
- undetectably drop individual messages, so long as a subsequent message is delivered.
|
||||
|
||||
- learn the contents of messages.
|
||||
|
||||
- learn the destination queues of messages.
|
||||
|
||||
- distinguish noise messages from regular messages except via timing regularities.
|
||||
|
||||
- compromise the user's end-to-end encryption with another user via an active attack.
|
||||
|
||||
- compromise the user's end-to-end encryption with destination relay via an active attack.
|
||||
|
||||
#### SMP relay accessed via SMP proxy
|
||||
|
||||
*still can:*
|
||||
|
||||
- perform recipients' queue correlation (matching multiple queues to a single recipient) via either a re-used transport connection, user's IP Address, or connection timing regularities
|
||||
|
||||
*no longer can:*
|
||||
|
||||
- perform senders' queue correlation (matching multiple queues to a single sender) via either a re-used transport connection, user's IP Address, or connection timing regularities, unless it has additional information from SMP proxy.
|
||||
|
||||
- learn a sender's IP address, track them through other IP addresses they use to access the same queue, and infer information (e.g. employer) based on the IP addresses, even if Tor is not used.
|
||||
|
||||
The rest of the threat model for SMP relays remains the same as in [overview](../protocol/overview-tjr.md#simplex-messaging-protocol-server).
|
||||
@@ -0,0 +1,361 @@
|
||||
# SimpleX Remote Control protocol
|
||||
|
||||
Using profiles in SimpleX Chat mobile app from desktop app with minimal risk to the security/threat model of SimpleX protocols.
|
||||
|
||||
## Problem
|
||||
|
||||
Synchronizing profiles that use double ratchet for e2e encryption is effectively impossible in a way that tolerates partitioning between devices without reducing security of double ratchet.
|
||||
|
||||
We are not considering replacing (or weakening) double ratchet to allow profile synchronization, as some other messengers did (e.g., Session). We are also not considering Signal model, when profile is known to the server and adding devices results in changing security code and no visibility of conversation history, as it would be substantially different from the current model, and also effectively weakens the security, as in practives most users don't revalidate security codes when they change.
|
||||
|
||||
## Solution
|
||||
|
||||
The proposed solution is a new remote access/control protocol, when the application on host device (usually mobile) acts as a server, and application on another device (usually desktop) acts as a controller usually running in the same local network.
|
||||
|
||||
Existing service discovery and remote control protocols known to us are vulnerable to spoofing, spamming and MITM attacks. This design aims to solve these problems.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Strong, cryptographically verified identity of the controller device, with the initial connection requiring out-of-band communication of public keys (QR code or link).
|
||||
- Identity verification of the host device during session handshake.
|
||||
- Protection against malicious "controllers" trying to make host connect to them instead of valid controller on the same network.
|
||||
- Protection against replay attacks, both during discovery and during control session.
|
||||
- Additional encryption layer inside TLS, with post-quantum algorithm in key agreement.
|
||||
- Protect host device from unauthorized access in case of controller compromise.
|
||||
- Post-compromised security - that is, even if long term secrets were copied from the controller, and host device was made to connect to malicious controller device, prevent malicious controller from accessing the host device data.
|
||||
- Allow general high-level interactions common for many applications:
|
||||
- RPC pattern for commands executed by the host application.
|
||||
- Events sent by host to update controller UI.
|
||||
- Uploading and downloading files between host and controller, either to be processed by the host or to be presented in the controller UI.
|
||||
|
||||
This design will allow application-level protocol that is quite close to how SimpleX Chat UI interacts with SimpleX Chat core - there is a similar RPC + events protocol and support for files. The application-level protocol is out of scope of this specification.
|
||||
|
||||
## Protocol phases
|
||||
|
||||
Protocol consists of four phases:
|
||||
- controller session invitation
|
||||
- establishing session TLS connection
|
||||
- session verification and protocol negotiation
|
||||
- session operation
|
||||
|
||||
### Session invitation
|
||||
|
||||
The invitation to the first session between host and controller pair MUST be shared out-of-band, to establish a long term identity keys/certificates of the controller to host device.
|
||||
|
||||
The subsequent sessions can be announced via an application-defined site-local multicast group, e.g. `224.0.0.251` (also used in mDNS/bonjour) and an application-defined port (SimpleX Chat uses 5227).
|
||||
|
||||
The session invitation contains this data:
|
||||
- supported version range for remote control protocol
|
||||
- application-specific information, e.g. device name, application name and supported version range, settings, etc.
|
||||
- session start time in seconds since epoch
|
||||
- if multicast is used, counter of announce packets sent by controller
|
||||
- network address (ipv4 address and port) of the controller
|
||||
- CA TLS certificate fingerprint of the controller - this is part of long term identity of the controller established during the first session, and repeated in the subsequent session announcements.
|
||||
- Session Ed25519 public key used to verify the announcement and commands - this mitigates the compromise of the long term signature key, as the controller will have to sign each command with this key first.
|
||||
- Long-term Ed25519 public key used to verify the announcement and commands - this is part of the long term controller identity.
|
||||
- Session X25519 DH key and sntrup761 KEM encapsulation key to agree session encryption (both for multicast announcement and for commands and responses in TLS), as described in https://datatracker.ietf.org/doc/draft-josefsson-ntruprime-hybrid/. The new keys are used for each session, and if client key is already available (from the previous session), the computed shared secret will be used to encrypt the announcement multicast packet. The out-of-band invitation is unencrypted. These DH public key and KEM encapsulation key are always sent unencrypted. NaCL Cryptobox is used for encryption.
|
||||
|
||||
Host device decrypts (except the first session) and validates the invitation:
|
||||
- Session signature is valid.
|
||||
- Timestamp is within some window from the current time.
|
||||
- Long-term key signature is valid.
|
||||
- Long-term CA and signature key are the same as in the first session.
|
||||
- Some version in the offered range is supported.
|
||||
|
||||
OOB session invitation is a URI with this syntax:
|
||||
|
||||
```abnf
|
||||
sessionAddressUri = "xrcp://" encodedCAFingerprint "@" host ":" port "#/?" qsParams
|
||||
encodedCAFingerprint = base64url
|
||||
qsParams = param *("&" param)
|
||||
param = versionRangeParam / appInfoParam / sessionTsParam /
|
||||
sessPubKeyParam / idPubKeyParam / kemEncKeyParam / dhPubKeyParam /
|
||||
sessSignatureParam / idSignatureParam
|
||||
versionRangeParam = "v=" (versionParam / (versionParam "-" versionParam))
|
||||
versionParam = 1*DIGIT
|
||||
appInfoParam = "app=" escapedJSON ; optional
|
||||
sessionTsParam = "ts=" 1*DIGIT
|
||||
sessPubKeyParam = "skey=" base64url ; required
|
||||
idPubKeyParam = "idkey=" base64url ; required
|
||||
dhPubKeyParam = "dh=" base64url ; required
|
||||
sessSignatureParam = "ssig=" base64url ; required, signs the URI with this and idSignatureParam param removed
|
||||
idSignatureParam = "idsig=" base64url ; required, signs the URI with this param removed
|
||||
base64url = <base64url encoded binary> ; RFC4648, section 5
|
||||
```
|
||||
|
||||
Multicast session announcement is a binary encoded packet with this syntax:
|
||||
|
||||
```abnf
|
||||
sessionAddressPacket = dhPubKey nonce encrypted(unpaddedSize sessionAddress packetPad)
|
||||
dhPubKey = length x509encoded ; same as announced
|
||||
nonce = length *OCTET
|
||||
sessionAddress = largeLength sessionAddressUri ; as above
|
||||
length = 1*1 OCTET ; for binary data up to 255 bytes
|
||||
largeLength = 2*2 OCTET ; for binary data up to 65535 bytes
|
||||
packetPad = <pad packet size to 1450 bytes> ; possibly, we may need to move KEM agreement one step later,
|
||||
; with encapsulation key in HELLO block and KEM ciphertext in reply to HELLO.
|
||||
```
|
||||
|
||||
### Establishing session TLS connection
|
||||
|
||||
Host connects to controller via TCP session and validates CA credentials during TLS handshake. Controller acts as a TCP server in this connection, to avoid host device listening on a port, which might create an attack vector. During TLS handshake the controller's TCP server MUST present a self-signed two-certificate chain where the fingerprint of the first certificate MUST be the same as in the announcement.
|
||||
|
||||
Host device presents its own client certificate chain with CA representing a long-term identity of the host device.
|
||||
|
||||
### Session verification and protocol negotiation
|
||||
|
||||
Once TLS session is established, both the host and controller device present a "session security code" to the user who must match them (e.g., visually or via QR code scan) and confirm on the host device. The session security code must be a digest of tlsunique channel binding. As it is computed as a digest of the TLS handshake for both the controller and the host, it will validate that the same TLS certificates are used on both sides, and that the same TLS session is established, mitigating the possibility of MITM attack in the connection.
|
||||
|
||||
Once the session is confirmed by the user, the host device sends "hello" block to the controller. ALPN TLS extension is not used to obtain tlsunique prior to sending any packets.
|
||||
|
||||
Block size should be 16384 bytes.
|
||||
|
||||
Host HELLO must contain:
|
||||
- new session DH key - used to compute new shared secret with the controller keys from the announcement.
|
||||
- encrypted part of hello block (JSON object), containing:
|
||||
- chosen protocol version.
|
||||
- host CA TLS certificate fingerprint - part of host long term identity - must match the one presented in TLS handshake and the previous sessions, otherwise the connection is terminated.
|
||||
- KEM encapsulation key - used to compute new shared secret for the session.
|
||||
- additional application specific parameters, e.g host device name, application version, host settings or JSON encoding format.
|
||||
|
||||
Hello block syntax:
|
||||
|
||||
```abnf
|
||||
hostHello = unpaddedSize %s"HELLO " dhPubKey nonce encrypted(unpaddedSize hostHelloJSON helloPad) pad
|
||||
unpaddedSize = largeLength
|
||||
dhPubKey = length x509encoded
|
||||
pad = <pad block size to 16384 bytes>
|
||||
helloPad = <pad hello size to 12888 bytes>
|
||||
largeLength = 2*2 OCTET
|
||||
```
|
||||
|
||||
Controller decrypts (including the first session) and validates the received hello block:
|
||||
- Chosen versions are supported (must be within offered ranges).
|
||||
- CA fingerprint matches the one presented in TLS handshake and the previous sessions - in subsequent sessions TLS connection should be rejected if the fingerprint is different.
|
||||
|
||||
JTD schema for the encrypted part of host HELLO block `hostHelloJSON`:
|
||||
|
||||
```json
|
||||
{
|
||||
"definitions": {
|
||||
"version": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"format": "[0-9]+"
|
||||
}
|
||||
},
|
||||
"base64url": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"format": "base64url"
|
||||
}
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"v": {"ref": "version"},
|
||||
"ca": {"ref": "base64url"},
|
||||
"kem": {"ref": "base64url"}
|
||||
},
|
||||
"optionalProperties": {
|
||||
"app": {"properties": {}, "additionalProperties": true}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
```
|
||||
|
||||
Controller should reply with with `hello` or `error` response:
|
||||
|
||||
```abnf
|
||||
ctrlHello = unpaddedSize %s"HELLO " kemCyphertext nonce encrypted(unpaddedSize ctrlHelloJSON helloPad) pad
|
||||
; ctrlHelloJSON is encrypted with the hybrid secret,
|
||||
; including both previously agreed DH secret and KEM secret from kemCyphertext
|
||||
unpaddedSize = largeLength
|
||||
kemCyphertext = largeLength *OCTET
|
||||
pad = <pad block size to 16384 bytes>
|
||||
helloPad = <pad hello size to 12888 bytes>
|
||||
largeLength = 2*2 OCTET
|
||||
|
||||
ctrlError = unpaddedSize %s"ERROR " nonce encrypted(unpaddedSize ctrlErrorJSON helloPad) pad
|
||||
; ctrlErrorJSON is encrypted using previously agreed DH secret.
|
||||
```
|
||||
|
||||
JTD schema for the encrypted part of controller HELLO block `ctrlHelloJSON`:
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {},
|
||||
"additionalProperties": true
|
||||
}
|
||||
```
|
||||
|
||||
JTD schema for the encrypted part of controller ERROR block `ctrlErrorJSON`:
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"message": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
```
|
||||
|
||||
Once controller replies HELLO to the valid host HELLO block, it should stop accepting new TCP connections.
|
||||
|
||||
|
||||
### Сontroller/host session operation
|
||||
|
||||
The protocol for communication during the session is out of scope of this protocol.
|
||||
|
||||
SimpleX Chat will use HTTP2 encoding, where host device acts as a server and controller acts as a client (these roles are reversed compared with TLS connection).
|
||||
|
||||
Payloads in the protocol must be encrypted using NaCL cryptobox using the hybrid shared secret agreed during session establishment.
|
||||
|
||||
Commands of the controller must be signed after the encryption using the controller's session and long term Ed25519 keys.
|
||||
|
||||
tlsunique channel binding from TLS session MUST be included in commands (included in the signed body).
|
||||
|
||||
The syntax for encrypted command and response body encoding:
|
||||
|
||||
```
|
||||
commandBody = encBody sessSignature idSignature (attachment / noAttachment)
|
||||
responseBody = encBody attachment; should match counter in the command
|
||||
encBody = nonce encLength32 encrypted(tlsunique counter body)
|
||||
attachment = %x01 nonce encLength32 encrypted(attachment)
|
||||
noAttachment = %x00
|
||||
tlsunique = length 1*OCTET
|
||||
counter = 8*8 OCTET ; int64
|
||||
encLength32 = 4*4 OCTET ; uint32, includes authTag
|
||||
```
|
||||
|
||||
If the command or response includes attachment, it's hash must be included in command/response and validated.
|
||||
|
||||
## Key agreement for announcement packet and for session
|
||||
|
||||
Initial announcement is shared out-of-band (URI with xrcp scheme), and it is not encrypted.
|
||||
|
||||
This announcement contains only DH keys, as KEM key is too large to include in QR code, which are used to agree encryption key for host HELLO block. The host HELLO block will containt DH key in plaintext part and KEM encapsulation (public) key in encrypted part, that will be used to determine the shared secret (using SHA256 over concatenated DH shared secret and KEM encapsulated secret) both for controller HELLO response (that contains KEM cyphertext in plaintext part) and subsequent session commands and responses.
|
||||
|
||||
During the next session the announcement is sent via encrypted multicast block. The shared key for this announcement and for host HELLO block is determined using the KEM shared secred from the previous session and DH shared secret computed using the host DH key from the previous session and the new controller DH key from the announcement.
|
||||
|
||||
For the session, the shared secred is computed again using the KEM shared secret encapsulated by the controller using the new KEM key from the HOST hello block and DH shared secret computed using the host DH key from HELLO block and the new controller DH key from the announcement.
|
||||
|
||||
To describe it in pseudocode:
|
||||
|
||||
```
|
||||
// session 1
|
||||
hostHelloSecret(1) = dhSecret(1)
|
||||
sessionSecret(1) = sha256(dhSecret(1) || kemSecret(1)) // to encrypt session 1 data, incl. controller hello
|
||||
dhSecret(1) = dh(hostHelloDhKey(1), controllerInvitationDhKey(1))
|
||||
kemCiphertext(1) = enc(kemSecret(1), kemEncKey(1))
|
||||
// kemEncKey is included in host HELLO, kemCiphertext - in controller HELLO
|
||||
kemSecret(1) = dec(kemCiphertext(1), kemDecKey(1))
|
||||
|
||||
// multicast announcement for session n
|
||||
announcementSecret(n) = sha256(dhSecret(n'))
|
||||
dhSecret(n') = dh(hostHelloDhKey(n - 1), controllerDhKey(n))
|
||||
|
||||
// session n
|
||||
hostHelloSecret(n) = dhSecret(n)
|
||||
sessionSecret(n) = sha256(dhSecret(n) || kemSecret(n)) // to encrypt session n data, incl. controller hello
|
||||
dhSecret(n) = dh(hostHelloDhKey(n), controllerDhKey(n))
|
||||
// controllerDhKey(n) is either from invitation or from multicast announcement
|
||||
kemCiphertext(n) = enc(kemSecret(n), kemEncKey(n))
|
||||
kemSecret(n) = dec(kemCiphertext(n), kemDecKey(n))
|
||||
```
|
||||
|
||||
If controller fails to store the new host DH key after receiving HELLO block, the encryption will become out of sync and the host won't be able to decrypt the next announcement. To mitigate it, the host should keep the last session DH key and also previous session DH key to try to decrypt the next announcement computing shared secret using both keys (first the new one, and in case it fails - the previous).
|
||||
|
||||
To decrypt multicast announcement, the host should try to decrypt it using the keys of all known (paired) remote controllers.
|
||||
|
||||
## Other options
|
||||
|
||||
The proposed design has these pros/cons:
|
||||
|
||||
Pros:
|
||||
- mobile host that has sensitive data doesn't act as TLS server.
|
||||
- multicast is optional - all sessions can happen via QR code only.
|
||||
|
||||
Cons:
|
||||
- reversing of client/server roles between TLS and HTTP2.
|
||||
- in the first session mobile host TLS client credentials are verified after TLS connection is accepted.
|
||||
- cannot be used with host that runs in VM.
|
||||
|
||||
The alternative design will use mobile host device as TLS server. The session negotiation process:
|
||||
|
||||
- desktop shares its initial credentials via QR code, only for the first session
|
||||
- mobile sends encrypted multicast with session address, TLS CA fingerprint, DH key in clear text
|
||||
- desktop connects to mobile
|
||||
- session tlsunique presented to users on both devices - either user would have to confirm session on both devices or the mobile would have to send an additional "ready" block.
|
||||
|
||||
Pros:
|
||||
- no reversing server role between TLS and HTTP2
|
||||
- TLS credentials are exchanged before TLS, so invalid credentials can be rejected during the handshake of the first session.
|
||||
- if some other way to pass data from host to controller is added, then it can be used with host running in VM.
|
||||
|
||||
Cons:
|
||||
- multicast is mandatory, as there is no efficient way to communicate from mobile to desktop.
|
||||
- still needs hello or confirmation on both devices
|
||||
- mobile is now acting as TLS server creating additional attack vector
|
||||
|
||||
In both proposed and alternative design mobile host has chat data, acts as HTTP2 server, commands are signed with desktop key presented out-of–band, and both commands and responses are encrypted inside TLS session.
|
||||
|
||||
Other considered options:
|
||||
- SSH - more work integrating it.
|
||||
- use SMP connection to negotiate TLS session - it seems wrong to require Internet connection to negotiate a local network connection between desktop and mobile.
|
||||
- other multicast service discovery protocols - quite insecure.
|
||||
|
||||
## Threat model
|
||||
|
||||
#### A passive network adversary able to monitor the site-local traffic:
|
||||
|
||||
*can:*
|
||||
- observe session times, duration and volume of the transmitted data between host and controller.
|
||||
|
||||
*cannot:*
|
||||
- observe the content of the transmitted data.
|
||||
- substitute the transmitted commands or responses.
|
||||
- replay transmitted commands or events from the hosts.
|
||||
|
||||
#### An active network adversary able to intercept and substitute the site-local traffic:
|
||||
|
||||
*can:*
|
||||
- prevent host and controller devices from establishing the session
|
||||
|
||||
*cannot:*
|
||||
- same as passive adversary, provided that user visually verified session code out-of-band.
|
||||
|
||||
#### An active adversary with the access to the network:
|
||||
|
||||
*can:*
|
||||
- spam controller device.
|
||||
|
||||
*cannot:*
|
||||
- compromise host or controller devices.
|
||||
|
||||
#### An active adversary with the access to the network who also observed OOB announcement:
|
||||
|
||||
*can:*
|
||||
- connect to controller instead of the host.
|
||||
- present incorrect data to the controller.
|
||||
|
||||
*cannot:*
|
||||
- connect to the host or make host connect to itself.
|
||||
|
||||
#### Compromised controller device:
|
||||
|
||||
*can:*
|
||||
- observe the content of the transmitted data.
|
||||
- access any data of the controlled host application, within the capabilities of the provided API.
|
||||
|
||||
*cannot:*
|
||||
- access other data on the host device.
|
||||
- compromise host device.
|
||||
|
||||
#### Compromised host device:
|
||||
|
||||
*can:*
|
||||
- present incorrect data to the controller.
|
||||
- incorrectly interpret controller commands.
|
||||
|
||||
*cannot:*
|
||||
- access controller data, even related to this host device.
|
||||
@@ -31,7 +31,7 @@ Agent disconnected from host:port
|
||||
### Received command from the client
|
||||
|
||||
```
|
||||
n --> A : corrId connAlias parsed_command // raw_command
|
||||
n --> A : corrId connId parsed_command // raw_command
|
||||
```
|
||||
|
||||
`raw_command` is added only in case of parsing failure.
|
||||
@@ -53,13 +53,13 @@ In case the response is a message or notification, corrId should be replaced wit
|
||||
### Interpreted ("unwrapped") SMP message as agent message
|
||||
|
||||
```
|
||||
Agent msg : connAlias parsed_message // raw_message
|
||||
Agent msg : connId parsed_message // raw_message
|
||||
```
|
||||
|
||||
### Sent response / message to the client
|
||||
|
||||
```
|
||||
n <-- A : corrId connAlias parsed_command // raw_command
|
||||
n <-- A : corrId connId parsed_command // raw_command
|
||||
```
|
||||
|
||||
### Database changes
|
||||
|
||||
@@ -17,7 +17,7 @@ Additional parameters in commands in SMP agent protocol:
|
||||
|
||||
- `NEW` command will have a parameter `INV` or `CON` to create an invitation or a permanent contact connection.
|
||||
|
||||
`conn_alias? OPEN` (or `PUB`, `NEWPUB`, tbc) - to create an "open"/"public" queue, the response is an invitation in a different format (TBC):
|
||||
`conn_id? OPEN` (or `PUB`, `NEWPUB`, tbc) - to create an "open"/"public" queue, the response is an invitation in a different format (TBC):
|
||||
- should allow multiple servers (probably the original invitation should be extended to support it)
|
||||
- should have a marker to indicate it's an open/public queue (probably the original invitation should be extended to include an invitation type).
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# Notification server
|
||||
|
||||
## Background and motivation
|
||||
|
||||
SimpleX Chat clients should receive message notifications when not being online and/or subscribed to SMP servers.
|
||||
|
||||
To avoid revealing identities of clients directly to SMP servers via any kind of push notification tokens, a new party called SimpleX Notification Server is introduced to act as a service for subscribing to SMP server queue notifications on behalf of clients and sending push notifications to them.
|
||||
|
||||
## Proposal
|
||||
|
||||
TCP service using the same TLS transport as SMP server, with the fixed size blocks (256 bytes?) and the following set of commands:
|
||||
|
||||
### Protocol
|
||||
|
||||
#### Create subscription
|
||||
|
||||
Command:
|
||||
|
||||
`%s"CREATE " ntfSmpQueueURI ntfPrivateKey token subPublicKey`
|
||||
|
||||
Response:
|
||||
|
||||
`s%"OK"`
|
||||
|
||||
#### Check subscription status
|
||||
|
||||
Command:
|
||||
|
||||
`%s"CHECK " ntfSmpQueueURI`
|
||||
|
||||
Response:
|
||||
|
||||
```abnf
|
||||
statusResp = %s"STAT " status
|
||||
status = %s"ERR AUTH" / "ERR SMP AUTH" / %s"ERR SMP TIMEOUT" / %s"ACTIVE" / %s"PENDING"
|
||||
```
|
||||
|
||||
#### Update subscription device token
|
||||
|
||||
Command:
|
||||
|
||||
`%s"TOKEN " ntfSmpQueueURI token`
|
||||
|
||||
Response:
|
||||
|
||||
`s%"OK" / %s"ERR"`
|
||||
|
||||
#### Delete subscription (e.g. when deleting the queue or moving to another notification server)
|
||||
|
||||
Command:
|
||||
|
||||
`%s"DELETE " SP ntfSmpQueueURI`
|
||||
|
||||
Response:
|
||||
|
||||
`s%"OK" / %s"ERR"`
|
||||
|
||||
### Agent schema changes
|
||||
|
||||
See [migration](../src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20220322_notifications.hs)
|
||||
|
||||
### Agent code
|
||||
|
||||
```haskell
|
||||
data NtfOptions = NtfOptions
|
||||
{ ntfServer :: Server, -- same type as for SMP servers, probably will be renamed
|
||||
ntfToken :: ByteString,
|
||||
ntfInitialCheckDelay :: Int, -- initial check delay after subscription is created, seconds
|
||||
ntfPeriodicCheckInterval :: Int -- subscription check interval, seconds
|
||||
}
|
||||
|
||||
data AgentConfig = AgentConfig {
|
||||
-- ...
|
||||
initialNtfOpts :: Maybe NtfOptions
|
||||
-- ...
|
||||
}
|
||||
|
||||
data AgentClient = AgentClient {
|
||||
-- ...
|
||||
ntfOpts :: TVar (Maybe NtfOptions)
|
||||
-- ...
|
||||
}
|
||||
```
|
||||
|
||||
A configuration parameter `initialNtfOpts :: Maybe NtfOptions` - if it is set or changes the agent would automatically manage subscriptions as SMP queues are subscribed/created/deleted and as the token or server changes.
|
||||
|
||||
There will be a method to update notifications configuration in case token or server changes.
|
||||
|
||||
All subscriptions will be managed in a separate subscription management loop, that would always take the earliest un-updated subscription that requires some action (ntf_sub_action column) and perform this action - the table of subscription would serve both as the table of existing subscriptions and required actions.
|
||||
|
||||
E.g. if the queue is subscribed and there is no notification subscription, it will be created in the table with "create" action, and the loop would create it and schedule "check" action on it.
|
||||
@@ -0,0 +1,34 @@
|
||||
# SMP protocol changes to support push notifications on iOS
|
||||
|
||||
## Problem
|
||||
|
||||
There are already commands/responses to allow subscriptions to message notifications - NKEY/NID, NSUB/NMSG. These commands will be used by SMP agent (NKEY/NID) and by notification server (NSUB/NMSG) to have message notifications delivered to notification server, so it can forward them to APNS server using device token.
|
||||
|
||||
There are two remaining problems that these commands do not solve.
|
||||
|
||||
1. Receiving the message when notification arrives.
|
||||
|
||||
iOS requires creating a bundled notification service extension (NSE) that runs in isolated container and, if we were to use the existing commands, would have SMP subscription to the same SMP servers as the main app, triggering resubscriptions every time the message reception switches between the app and NSE. That would cause a substantial increase in traffic and battery consumption to the users.
|
||||
|
||||
2. Showing notifications for service messages.
|
||||
|
||||
Users do not expect to see notifications for every single SMP messages - e.g., we currently do not show notifications when messages are edited and deleted, and users do not expect them. NSE requires that for every received push notification there should be some notification shown to the users. So only we would have to show a notification for message deletes and updates, we would have to show it for all service messages - e.g. user accepted file invitation, or file transmission has started, contact profile updates and so on.
|
||||
|
||||
We considered differentiating whether notifications are sent per queue, from the recipient side, so we do not send notifications for file queues. But it seems insufficient, particularly if we add such features as message receipts, status updates, etc.
|
||||
|
||||
## Proposal
|
||||
|
||||
1. To retrieve messages when push notifications arrive, we will add 2 SMP commands:
|
||||
|
||||
- GET: retrieve one message from the connection. Resonse could be either MSG (the same as when MSG is delivered, but with the correlation id) or GMSG (to simplify processing) – TBC during implementation. If message is not available, the response could be ERR NO_MSG
|
||||
- ACK or GACK: acknowledge that the message was processed by the client and can be removed - TBC which one is better. The response is OK or ERR NO_MSG if there was nothing to acknowledge (same as with ACK now)
|
||||
|
||||
This would allow receiving a single message from the queue without subscription, this way avoiding that the main app is unsubscribed from the queue.
|
||||
|
||||
2. The only way to avoid showing unnecessary notifications (status updates, service messages, etc.) is to avoid sending them. That requires instructing SMP server whether notification should be sent both per queue, from the recipient side, and per message - from the sender side. So the notification would only be sent if the queue has them enabled (via NKEY command) and the sender includes an additional flag in SEND command. The same flag should be included into MSG, so when the message is retrieved with GET command, the client knows, on the agent or chat level (or both), whether this message should have notification shown to the user, and if not - retrieve the next one(s) as well.
|
||||
|
||||
This is a substantial change to SMP protocol, that would require client and server upgrade for notifications to be supported.
|
||||
|
||||
We should consider whether to increase the SMP protocol version number to 2, so that the new clients can connect to the old clients but without notifications, or we could keep the old commands in the protocol and instead of adding flags to the existing commands, create new commands.
|
||||
|
||||
We can also consider making commands extensible so that the new flags can be added (and ignored by parsers if not supported) to at least some existing commands.
|
||||
@@ -0,0 +1,75 @@
|
||||
# DB access and processing messages for iOS notification service extension
|
||||
|
||||
## Problem
|
||||
|
||||
The only way to receive/process notificaitons is via a separate NSE process that requires a concurrent DB and network access.
|
||||
|
||||
SQLite concurenncy does not work, so we need to sync database access.
|
||||
|
||||
The problem is complex, as we do not directly control db access from the app, it can be triggered by the message arriving and it may fail to complete in case the app is suspended in the background.
|
||||
|
||||
So we need to prevent db access from starting when we know the app is about to be suspended.
|
||||
|
||||
The last problem is how to receive and process messages in NSE - should it use recently added GET command or should it subscribe to the connections that receive messages and process messages normally.
|
||||
|
||||
To summarize, 2 problems need to be solved:
|
||||
|
||||
1. sync db access between 2 (or more, if we add share extension) processes
|
||||
|
||||
2. prevent access from starting when the process is due to suspend, only complete operations.
|
||||
|
||||
3. Receiveing and processing agent messages in NSE
|
||||
|
||||
## Proposed solution
|
||||
|
||||
For problem 1, we can use Posix semaphores from our core code, in the same bracket that manages database connection - it would wait for semaphore to be free and unlock it once the db operation is complete.
|
||||
|
||||
For problem 2, we need to communicate from the app when it goes to the background to prevent database access starting and completing before the suspension. This would set some `aboutToSuspend` STM flag (or the opposite) that would prevent operations from progressing (using STM retry, that would block until the flag has the value allowing operation to progress).
|
||||
|
||||
Several possibilities can be considered:
|
||||
|
||||
- use this flag in the bracket that provides DB connections. While simple, it may lock some operations in the middle and may also lead to the situation when network operation succeeds but database access was blocked, and the database is not updated.
|
||||
- use this flag to stop network operations that would require database updates - like sending messages, subscriptions and ACK - all these operations would require database access once they succeed.
|
||||
- use two flags, for both cases above, but set them at different times since going to background - block new network operations as soon as the app goes to the background and block database access once the app is about to be suspended.
|
||||
|
||||
The last option seems more robust. To do it, there will be an api allowing the app to communicat its phase:
|
||||
|
||||
- app going to the background would trigger blocking new network operations and start a new background task - `background` phase.
|
||||
- background task receiving completion warning, or, maybe, some time after it is started - probably 20 seconds - or whatever happens earlier - would trigger call blocking db access - `suspended` phase.
|
||||
- app becoming active would trigger unblocking both flags - `active` phase.
|
||||
|
||||
`/_app phase <phase>` where `phase` can be one of the above values.
|
||||
|
||||
NSE would also use the same phases:
|
||||
|
||||
- sending `active` when it is started (the process starts as active, but it is possible that the new notification arrives to the same process, after the previous one sent background/suspension)
|
||||
- sending `background + suspended` (or `suspended` should set both flags) once it is finished processing the notification, provided no new notification arrived and started processing - this should be tracked in NSE.
|
||||
|
||||
For problem 3, NSE can do one of the following:
|
||||
|
||||
- use SUB and process messages normally - the downside is that the app will have to resubscribe and it has to be tracked.
|
||||
- use GET and process messages by pushing them through the processing function - the downside it that a rewiring of message processing is needed.
|
||||
- use GET but deliver messages through the same queue as when they arrive normally (in which case getMessage agent function should not return the message, but will return a flag showing whether the message was received, or, possibly, or, possibly will return a message but the message would also be sent to the queue?).
|
||||
- process messages in agent manually and in chat via the queue.
|
||||
|
||||
One of the downside of GET is that it requires calling GET again after ACK. We could have two variants of ACK (or additional ACK parameter) - one that never delivers a new message, and another one that does. In this case, if get needs to process the next message (when the current one has no notification flag), it can call ACK that delivers the next message. But, it is probably a premature optimization, and having general support of batched commands would add more value.
|
||||
|
||||
Additional problem is concurrency in NSE - if the new notification from the same queue arrives before the current one finishes processing in the same process one of the following can happen:
|
||||
|
||||
- the 2nd notification naively call GET and receives the same message.
|
||||
- the 2nd notification waits until the first finished processing, in which case it can run out of time.
|
||||
|
||||
The problem is that the app won't know it's the same queue, as nId is encrypted, so the agent should handle this scenario when the new call to getMessage is made before the previous one finished processing, and differentiate between calls made for additional messages (possibly, getMessage should include previous message ID, if it is available) and the first call.
|
||||
|
||||
EDIT: GETs have to be sent from UI to chat and from chat to agent as function calls, but the agent will have to queue get calls to make sure they return different messages. GET call would return message flags (incl. notification flag), so that the UI can send the next GET if needed without waiting.
|
||||
|
||||
Considered alternative: include notification content in the message and have NSE only perform decryption, without any network IO. In this case notification content would be in SEND and in NMSG, e2e encrypted.
|
||||
|
||||
While promising, as it solves network coordination issues and makes GET unnecessary, it creates mutliple other problems, so it was rejected:
|
||||
|
||||
- message content is exposed to centralized ntf and apns servers, creating additional attack vector.
|
||||
- it adds complexity in security critical parts of the stack - double ratchet encryption, as it requires either storing message keys and using different IVs for notifications, or initializing completely separate ratchet for notifications content.
|
||||
- it reduces the size of the message.
|
||||
- it makes user experience worse, as:
|
||||
- it would not accelerate handshake for new contacts and for file delivery - this approach only works for content messages.
|
||||
- it would open the app without the new messages - the users would have to wait until the messages are received. It is also bad for "security optics" - the users might think that the message content was exposed to notifications.
|
||||
@@ -0,0 +1,61 @@
|
||||
sequenceDiagram
|
||||
participant M as iOS message<br>notification
|
||||
participant S as iOS system
|
||||
participant N as iOS NSE
|
||||
participant U as iOS UI
|
||||
participant C as Core chat
|
||||
participant A as Core agent
|
||||
|
||||
M ->> N: notification
|
||||
S ->> N: get app pref
|
||||
note over N: ignore,<br>app is active
|
||||
|
||||
note over M, A: app going to background
|
||||
S ->> U: phase: background<br>(possibly, "will" method)
|
||||
U ->> S: set app pref "pausing"
|
||||
U ->> C: /_app phase paused, result CRCmdOk
|
||||
C ->> A: pauseAgent<br>(no new network IO)
|
||||
M ->> N: notification
|
||||
S ->> N: get app pref
|
||||
note over N: wait/poll for<br>"paused"/"suspending"/"suspended"<br>event/pref
|
||||
A ->> C: event "IO paused"<br>(after in-flight op completed)<br>PHASE PAUSED
|
||||
C ->> U: event "IO paused" (CRAppPaused)
|
||||
U ->> S: set shared pref "paused"
|
||||
|
||||
note over M, A: process notification
|
||||
M ->> N: notification
|
||||
S ->> N: get app pref<br>continue if<br>"paused"/"suspending"/"suspended"
|
||||
N ->> S: set NSE pref "active"
|
||||
N ->> C: /_get message
|
||||
C ->> A: getMessage
|
||||
A ->> C: msg flags
|
||||
C ->> N: msg flags
|
||||
note over N: get messages<br>until notification flag set
|
||||
A ->> C: MSG/CONF/INFO
|
||||
C ->> N: some event
|
||||
N ->> S: set NSE pref "completed"
|
||||
N ->> S: show notification
|
||||
|
||||
note over M, A: app about to be suspended<br>(or 15-20 sec after background)
|
||||
S ->> U: background task notice
|
||||
U ->> S: set app pref "suspending"
|
||||
U ->> C: /_app phase suspended, response ok
|
||||
C ->> A: suspendAgent<br>(no new DB)
|
||||
A ->> C: event "DB paused"<br>(after in-flight op completed)<br>PHASE SUSPENDED
|
||||
C ->> U: event "DB paused" (CRAppSuspended)
|
||||
U ->> S: set app pref "suspended"
|
||||
|
||||
note over M, A: app about to be activated
|
||||
S ->> U: phase: active<br>(or inactive?)<br><br>(possibly, "will" method)
|
||||
S ->> U: get NSE pref
|
||||
U ->> S: set app pref "activating"
|
||||
alt nse active?
|
||||
U ->> C: /_app phase inactive
|
||||
note over U: poll/wait till NSE pref is "completed"
|
||||
end
|
||||
|
||||
U ->> C: /_app phase active (response result)
|
||||
C ->> A: activateAgent<br>(allow IO/DB)
|
||||
A ->> C: result ()
|
||||
C ->> U: CRCmdOk
|
||||
U ->> S: set app pref "active"
|
||||
@@ -0,0 +1,26 @@
|
||||
# Accessing SMP servers via Tor
|
||||
|
||||
## Problem
|
||||
|
||||
While SMP protocol is focussed on minimizing application-level meta-data by using pair-wise identifiers instead of user profile identifiers, it is important for many users to protect their IP addresses.
|
||||
|
||||
Further, even if IP addresses are hidden by onion routing, clients should be able to choose to use a separate TCP connection to subscribe to each queue, even though it increases traffic and battery consumption, as otherwise the servers can observe multiple queues accessed by the same client.
|
||||
|
||||
## Solution and requirements
|
||||
|
||||
While some users may want to access SMP servers via tor, some other users (even their contacts) may want the opposite - e.g., if they use the network when accessing Tor would be suspicious (or blocked).
|
||||
|
||||
Therefore we need to support the connections when one of the user accesses the same server via Tor (and, possibly, via onion address), while another user accesses this server without Tor.
|
||||
|
||||
At the same time the user accessing the server via Tor may not want that their contacts access this server without Tor, and it also may be possible that the server is not available under a normal (not .onion) address.
|
||||
|
||||
The proposed options for connecting via Tor are:
|
||||
|
||||
1. Access servers via Socks proxy: no/yes (specify port?)
|
||||
2. Use .onion addresses: no/when available/warn/always
|
||||
3. Require senders to use .onion addresses: yes/no
|
||||
4. Use separate TCP connection for each queue
|
||||
|
||||
While it should be possible for SMP servers to have two addresses (with and without Tor), the queues should only use one server address - if the queue started being accessed via .onion address it should not be possible to access it via a normal address. Queue addresses in connection invitations should support dual server addresses (when senders are not required ot use .onion address).
|
||||
|
||||
At the same time, the queue with the normal addresses can be accessed with and without Tor, depending on the current device settings.
|
||||
@@ -0,0 +1,53 @@
|
||||
# SMP queue rotation and redundancy
|
||||
|
||||
## Problem
|
||||
|
||||
1. Long term usage of the queue allows the servers to analyze long term traffic patterns.
|
||||
|
||||
2. If the user changes the configured SMP server(s), the previously created contacts do not migrate to the new server(s).
|
||||
|
||||
3. Server can lose messages.
|
||||
|
||||
## Solution
|
||||
|
||||
Additional messages exchanged by SMP agents to negotiate addition and removal of queues to the connection.
|
||||
|
||||
This approach allows for both rotation and redundancy, as changing queue will be done be adding a queue and then removing the existing queue.
|
||||
|
||||
The reason for this approach is that otherwise it's non-trivial to switch from one queue to another without losing messages or delivering them out of order, it's easier to have messages delivered via both queues during the switch, however short or long time it is.
|
||||
|
||||
### Messages
|
||||
|
||||
Additional agent messages required for the protocol:
|
||||
|
||||
QADD_ -> "QA"
|
||||
QKEY_ -> "QK"
|
||||
QUSE_ -> "QU"
|
||||
QTEST_ -> "QT"
|
||||
|
||||
`QADD`: add the new queue address(es), the same format as `REPLY` message, encoded as `QA`.
|
||||
|
||||
`QKEY`: pass sender's key via existing connection (SMP confirmation message will not be used, to avoid the same "race" of the initial key exchange that would create the risk of intercepting the queue for the attacker), encoded as `QK`.
|
||||
|
||||
`QUSE`: instruct the sender to use the new queue with sender's queue ID as parameter, encoded as `QU`.
|
||||
|
||||
`QTEST`: send test message to the new connection, encoded as `QT`. Any other message can be sent if available to continue rotation, the absence of this message is not an error. Once this message is successfully sent the sender will stop using the old queue. Once this message (or any other message in the new queue) is received, the recipient will stop using the old queue and delete it.
|
||||
|
||||
### Protocol
|
||||
|
||||
```
|
||||
participant A as Alice
|
||||
participant B as Bob
|
||||
participant R as Server that has A's receive queue
|
||||
participant S as Server that has A's send queue (B's receive queue)
|
||||
participant R' as Server that hosts the new A's receive queue
|
||||
|
||||
A ->> R': create new queue
|
||||
A ->> S ->> B: QADD (R'): snd address of the new queue(s)
|
||||
B ->> S(R) ->> A: QKEY (R'): sender's key for the new queue(s) (to avoid the race of SMP confirmation for the initial exchange)
|
||||
A ->> S(R'): secure new queue
|
||||
A ->> S ->> B: QUSE (R'): instruction to use new queue(s)
|
||||
B ->> S(R') ->> A: QTEST
|
||||
A ->> S(R): DEL: delete the old queue
|
||||
B ->> S(R') ->> A: send all new messages to the new queue
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
# SMP Basic Auth
|
||||
|
||||
## Problem
|
||||
|
||||
Users who host their own servers do not want unknown people to be able to create messaging queues on their servers after discovering server address in groups or after making a connection. As the number of self-hosted servers is growing it became more important than it was when we excluded it from the original design.
|
||||
|
||||
## Solution
|
||||
|
||||
Single access password that can be optionally included in server address that is passed to app configuration. It will not be allowed in the existing contexts (and parsing will fail), to avoid accidentally leaking it. Server address with password will look this way: `smp://fingerprint:password@hosts`
|
||||
|
||||
## Implementation plan
|
||||
|
||||
1. A separate type to include server and password, so it can only be used where allowed.
|
||||
|
||||
2. Server password to create queues will be configured in TRANSPORT section of INI file, as `create_password` parameter.
|
||||
|
||||
3. The password will only be required in server configuration/address to create queues only, it won't be required for other receiving queue operations on already existing queues.
|
||||
|
||||
4. If new command is attempted in the session that does not allow creating queues, the server will send `ERR AUTH` response
|
||||
|
||||
5. Passing password to the server can be done in one of the several ways, we need to decide:
|
||||
|
||||
- as a parameter of NEW command. Pros: a local change, that only needs checking when queue is created. Cons: protocol version change.
|
||||
- as a separate command AUTH. Pros: allows to include additional parameters and potentially be extended beyond basic auth. Cons: more complex to manage server state, can be more difficult syntax in the future, if extended.
|
||||
- as part of handshake (we currently ignore the unparsed part of handshake block, so it can be extended). Pros: probably, the simplest, and independent of the commands protocol – establishes create permission for the current session. Cons: the client won't know about whether it is able to create the queue until it tries (same as in case 1).
|
||||
|
||||
My preference is the last option. As a variant of the last option, we can add a server response/message that includes permission to create queues - it will only be sent to the clients who pass credential in handshake - that might simplify testing server connection (we currently do not do it). It might be unnecessary, as we could simply create and delete queue in case credential is passed as part of testing connection (and even sending a message to it).
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env sh
|
||||
confd='/etc/opt/simplex'
|
||||
logd='/var/opt/simplex/'
|
||||
|
||||
# Check if server has been initialized
|
||||
if [ ! -f "${confd}/smp-server.ini" ]; then
|
||||
# If not, determine ip or domain
|
||||
case "${ADDR}" in
|
||||
'') printf 'Please specify $ADDR environment variable.\n'; exit 1 ;;
|
||||
*[a-zA-Z]*)
|
||||
case "${ADDR}" in
|
||||
*:*) set -- --ip "${ADDR}" ;;
|
||||
*) set -- -n "${ADDR}" ;;
|
||||
esac
|
||||
;;
|
||||
*) set -- --ip "${ADDR}" ;;
|
||||
esac
|
||||
|
||||
# Optionally, set password
|
||||
case "${PASS}" in
|
||||
'') set -- "$@" --no-password ;;
|
||||
*) set -- "$@" --password "${PASS}" ;;
|
||||
esac
|
||||
|
||||
# And init certificates and configs
|
||||
smp-server init -y -l "$@"
|
||||
fi
|
||||
|
||||
# Backup store log just in case
|
||||
#
|
||||
# Uses the UTC (universal) time zone and this
|
||||
# format: YYYY-mm-dd'T'HH:MM:SS
|
||||
# year, month, day, letter T, hour, minute, second
|
||||
#
|
||||
# This is the ISO 8601 format without the time zone at the end.
|
||||
#
|
||||
_file="${logd}/smp-server-store.log"
|
||||
if [ -f "${_file}" ]; then
|
||||
_backup_extension="$(date -u '+%Y-%m-%dT%H:%M:%S')"
|
||||
cp -v -p "${_file}" "${_file}.${_backup_extension:-date-failed}"
|
||||
unset -v _backup_extension
|
||||
fi
|
||||
unset -v _file
|
||||
|
||||
# Finally, run smp-sever. Notice that "exec" here is important:
|
||||
# smp-server replaces our helper script, so that it can catch INT signal
|
||||
exec smp-server start +RTS -N -RTS
|
||||
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env sh
|
||||
confd='/etc/opt/simplex-xftp'
|
||||
logd='/var/opt/simplex-xftp'
|
||||
|
||||
# Check if server has been initialized
|
||||
if [ ! -f "${confd}/file-server.ini" ]; then
|
||||
# If not, determine ip or domain
|
||||
case "${ADDR}" in
|
||||
'') printf 'Please specify $ADDR environment variable.\n'; exit 1 ;;
|
||||
*[a-zA-Z]*)
|
||||
case "${ADDR}" in
|
||||
*:*) set -- --ip "${ADDR}" ;;
|
||||
*) set -- -n "${ADDR}" ;;
|
||||
esac
|
||||
;;
|
||||
*) set -- --ip "${ADDR}" ;;
|
||||
esac
|
||||
|
||||
# Set quota
|
||||
case "${QUOTA}" in
|
||||
'') printf 'Please specify $QUOTA environment variable.\n'; exit 1 ;;
|
||||
*GB) QUOTA="$(printf ${QUOTA} | tr '[:upper:]' '[:lower:]')"; set -- "$@" --quota "${QUOTA}" ;;
|
||||
*gb) set -- "$@" --quota "${QUOTA}" ;;
|
||||
*) printf 'Wrong format. Format should be: 1gb, 10gb, 100gb.\n'; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Init the certificates and configs
|
||||
xftp-server init -l -p /srv/xftp "$@"
|
||||
fi
|
||||
|
||||
# Backup store log just in case
|
||||
#
|
||||
# Uses the UTC (universal) time zone and this
|
||||
# format: YYYY-mm-dd'T'HH:MM:SS
|
||||
# year, month, day, letter T, hour, minute, second
|
||||
#
|
||||
# This is the ISO 8601 format without the time zone at the end.
|
||||
#
|
||||
_file="${logd}/file-server-store.log"
|
||||
if [ -f "${_file}" ]; then
|
||||
_backup_extension="$(date -u '+%Y-%m-%dT%H:%M:%S')"
|
||||
cp -v -p "${_file}" "${_file}.${_backup_extension:-date-failed}"
|
||||
unset -v _backup_extension
|
||||
fi
|
||||
unset -v _file
|
||||
|
||||
# Finally, run xftp-sever. Notice that "exec" here is important:
|
||||
# smp-server replaces our helper script, so that it can catch INT signal
|
||||
exec xftp-server start +RTS -N -RTS
|
||||
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
path_conf_var="/var/opt"
|
||||
path_conf_smp="$path_conf_var/simplex"
|
||||
path_conf_xftp="$path_conf_var/simplex-xftp"
|
||||
path_conf_storelog_smp="$path_conf_smp/smp-server-store.log"
|
||||
path_conf_storelog_xftp="$path_conf_xftp/file-server-store.log"
|
||||
date="$(date -u '+%Y-%m-%dT%H:%M:%S')"
|
||||
|
||||
backup_smp() {
|
||||
if [ -e "$path_conf_storelog_smp" ]; then
|
||||
cp "$path_conf_storelog_smp" "${path_conf_storelog_smp}.${date:-date-failed}"
|
||||
fi
|
||||
}
|
||||
|
||||
backup_xftp() {
|
||||
if [ -e "$path_conf_storelog_xftp" ]; then
|
||||
cp "$path_conf_storelog_xftp" "${path_conf_storelog_xftp}.${date:-date-failed}"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$1" = 'smp-server' ]; then
|
||||
backup_smp
|
||||
elif [ "$1" = 'xftp-server' ]; then
|
||||
backup_xftp
|
||||
else
|
||||
backup_smp
|
||||
backup_xftp
|
||||
fi
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
GRN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
printf "This script is intended to be run with root privileges. Please re-run script using sudo."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "${RED}This action will permanently remove all configs, directories, binaries from Installation Script. Please backup any relevant configs if they are needed.${NC}\n\nPress ${GRN}ENTER${NC} to continue or ${RED}Ctrl+C${NC} to cancel installation"
|
||||
read ans
|
||||
|
||||
systemctl stop smp-server
|
||||
systemctl stop xftp-server
|
||||
|
||||
rm -rf /var/opt/simplex /etc/opt/simplex /var/opt/simplex-xftp /etc/opt/simplex-xftp /srv/xftp /etc/systemd/system/smp-server.service /etc/systemd/system/xftp-server.service /usr/local/bin/smp-server /usr/local/bin/xftp-server /usr/local/bin/simplex-servers-update /usr/local/bin/simplex-servers-uninstall
|
||||
|
||||
userdel smp && userdel xftp
|
||||
|
||||
printf "Uninstallation is complete! Thanks for trying out SimpleX!\n"
|
||||
Executable
+174
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# Links to scripts/configs
|
||||
bin="https://github.com/simplex-chat/simplexmq/releases/latest/download"
|
||||
bin_smp="$bin/smp-server-ubuntu-20_04-x86-64"
|
||||
bin_xftp="$bin/xftp-server-ubuntu-20_04-x86-64"
|
||||
|
||||
scripts="https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/scripts/main"
|
||||
scripts_systemd_smp="$scripts/smp-server.service"
|
||||
scripts_systemd_xftp="$scripts/xftp-server.service"
|
||||
scripts_update="$scripts/simplex-servers-update"
|
||||
scripts_uninstall="$scripts/simplex-servers-uninstall"
|
||||
scripts_stopscript="$scripts/simplex-servers-stopscript"
|
||||
|
||||
# Default installation paths
|
||||
path_bin="/usr/local/bin"
|
||||
path_bin_smp="$path_bin/smp-server"
|
||||
path_bin_xftp="$path_bin/xftp-server"
|
||||
path_bin_update="$path_bin/simplex-servers-update"
|
||||
path_bin_uninstall="$path_bin/simplex-servers-uninstall"
|
||||
path_bin_stopscript="$path_bin/simplex-servers-stopscript"
|
||||
|
||||
path_systemd="/etc/systemd/system"
|
||||
path_systemd_smp="$path_systemd/smp-server.service"
|
||||
path_systemd_xftp="$path_systemd/xftp-server.service"
|
||||
|
||||
# Temporary paths
|
||||
path_tmp_bin="$(mktemp -d)"
|
||||
path_tmp_bin_update="$path_tmp_bin/simplex-servers-update"
|
||||
path_tmp_bin_uninstall="$path_tmp_bin/simplex-servers-uninstall"
|
||||
path_tmp_bin_stopscript="$path_tmp_bin/simplex-servers-stopscript"
|
||||
path_tmp_systemd_smp="$path_tmp_bin/smp-server.service"
|
||||
path_tmp_systemd_xftp="$path_tmp_bin/xftp-server.service"
|
||||
|
||||
GRN='\033[0;32m'
|
||||
BLU='\033[1;36m'
|
||||
YLW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Currently, XFTP default to v0.1.0, so it doesn't make sense to check its version
|
||||
local_version="$($path_bin_smp -v | awk '{print $3}')"
|
||||
remote_version="$(curl --proto '=https' --tlsv1.2 -sSf -L https://api.github.com/repos/simplex-chat/simplexmq/releases/latest | grep -i "tag_name" | awk -F \" '{print $4}')"
|
||||
|
||||
update_scripts() {
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_update" -o "$path_tmp_bin_update" && chmod +x "$path_tmp_bin_update"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_uninstall" -o "$path_tmp_bin_uninstall" && chmod +x "$path_tmp_bin_uninstall"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_stopscript" -o "$path_tmp_bin_stopscript" && chmod +x "$path_tmp_bin_stopscript"
|
||||
|
||||
if diff -q "$path_bin_uninstall" "$path_tmp_bin_uninstall" > /dev/null 2>&1; then
|
||||
printf -- "- ${YLW}Uninstall script is up-to-date${NC}.\n"
|
||||
rm "$path_tmp_bin_uninstall"
|
||||
else
|
||||
printf -- "- Updating uninstall script..."
|
||||
mv "$path_tmp_bin_uninstall" "$path_bin_uninstall"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
fi
|
||||
if diff -q "$path_bin_stopscript" "$path_tmp_bin_stopscript" > /dev/null 2>&1; then
|
||||
printf -- "- ${YLW}Stopscript script is up-to-date${NC}.\n"
|
||||
rm "$path_tmp_bin_stopscript"
|
||||
else
|
||||
printf -- "- Updating stopscript script..."
|
||||
mv "$path_tmp_bin_stopscript" "$path_bin_stopscript"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
fi
|
||||
if diff -q "$path_bin_update" "$path_tmp_bin_update" > /dev/null 2>&1; then
|
||||
printf -- "- ${YLW}Update script is up-to-date${NC}.\n"
|
||||
rm "$path_tmp_bin_update"
|
||||
else
|
||||
printf -- "- Updating update script..."
|
||||
mv "$path_tmp_bin_update" "$path_bin_update"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
printf "Re-executing Update script with latest updates..."
|
||||
exec sh "$path_bin_update" "continue"
|
||||
fi
|
||||
}
|
||||
|
||||
update_systemd() {
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd_smp" -o "$path_tmp_systemd_smp"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd_xftp" -o "$path_tmp_systemd_xftp"
|
||||
|
||||
if diff -q "$path_systemd_smp" "$path_tmp_systemd_smp" > /dev/null 2>&1; then
|
||||
printf -- "- ${YLW}smp-server service is up-to-date${NC}.\n"
|
||||
rm "$path_tmp_systemd_smp"
|
||||
else
|
||||
printf -- "- Updating smp-server service..."
|
||||
mv "$path_tmp_systemd_smp" "$path_systemd_smp"
|
||||
systemctl daemon-reload
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
fi
|
||||
if diff -q "$path_systemd_xftp" "$path_tmp_systemd_xftp" > /dev/null 2>&1; then
|
||||
printf -- "- ${YLW}xftp-server service is up-to-date${NC}.\n"
|
||||
rm "$path_tmp_systemd_xftp"
|
||||
else
|
||||
printf -- "- Updating xftp-server service..."
|
||||
mv "$path_tmp_systemd_xftp" "$path_systemd_xftp"
|
||||
systemctl daemon-reload
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
update_bins() {
|
||||
if [ "$local_version" != "$remote_version" ]; then
|
||||
if systemctl is-active --quiet smp-server; then
|
||||
printf -- "- Stopping smp-server service..."
|
||||
systemctl stop smp-server
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
|
||||
printf -- "- Updating smp-server bin to %s..." "$remote_version"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_smp" -o "$path_bin_smp" && chmod +x "$path_bin_smp"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
|
||||
printf -- "- Starting smp-server service..."
|
||||
systemctl start smp-server
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
else
|
||||
printf -- "- Updating smp-server bin..."
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_smp" -o "$path_bin_smp" && chmod +x "$path_bin_smp"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
fi
|
||||
|
||||
if systemctl is-active --quiet xftp-server; then
|
||||
printf -- "- Stopping xftp-server service..."
|
||||
systemctl stop xftp-server
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
|
||||
printf -- "- Updating xftp-server bin to %s..." "$remote_version"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_xftp" -o "$path_bin_xftp" && chmod +x "$path_bin_xftp"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
|
||||
printf -- "- Starting xftp-server service..."
|
||||
systemctl start xftp-server
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
else
|
||||
printf -- "- Updating xftp-server bin..."
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_smp" -o "$path_bin_xftp" && chmod +x "$path_bin_xftp"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
fi
|
||||
else
|
||||
printf -- "- ${YLW}smp-server and xftp-server binaries is up-to-date${NC}.\n"
|
||||
fi
|
||||
}
|
||||
|
||||
checks() {
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
printf "This script is intended to be run with root privileges. Please re-run script using sudo.\n"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
checks
|
||||
|
||||
set +u
|
||||
if [ "$1" != "continue" ]; then
|
||||
set -u
|
||||
printf "Updating scripts...\n"
|
||||
update_scripts
|
||||
else
|
||||
set -u
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
fi
|
||||
|
||||
printf "Updating systemd services...\n"
|
||||
update_systemd
|
||||
|
||||
printf "Updating simplex server binaries...\n"
|
||||
update_bins
|
||||
|
||||
rm -rf "$path_tmp_bin"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=SMP server
|
||||
|
||||
[Service]
|
||||
User=smp
|
||||
Group=smp
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/smp-server start +RTS -N -RTS
|
||||
ExecStopPost=/usr/local/bin/simplex-servers-stopscript smp-server
|
||||
LimitNOFILE=65535
|
||||
KillSignal=SIGINT
|
||||
TimeoutStopSec=infinity
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=XFTP server
|
||||
|
||||
[Service]
|
||||
User=xftp
|
||||
Group=xftp
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/xftp-server start +RTS -N -RTS
|
||||
ExecStopPost=/usr/local/bin/simplex-servers-stopscript xftp-server
|
||||
LimitNOFILE=65535
|
||||
KillSignal=SIGINT
|
||||
TimeoutStopSec=infinity
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,178 @@
|
||||
#!/bin/bash
|
||||
|
||||
# <UDF name="enable_store_log" label="Store log - persist notification subscriptions to append only log and restore them upon server restart." default="on" oneof="on, off" />
|
||||
# <UDF name="api_token" label="Linode API token - enable Linode to create tags with server address, fingerprint and version. Note: minimal permissions token should have are read/write access to `linodes` (to create tags) and `domains` (to add A record for the third level domain if FQDN is provided)." default="" />
|
||||
# <UDF name="fqdn" label="FQDN (Fully Qualified Domain Name) - provide third level domain name (e.g. ntf.example.com). If provided use `ntf://fingerprint@FQDN` as server address in the client. If FQDN is not provided use `ntf://fingerprint@IP` instead." default="" />
|
||||
# <UDF name="apns_key_id" label="APNS key ID." default="" />
|
||||
|
||||
# Log all stdout output to stackscript.log
|
||||
exec &> >(tee -i /var/log/stackscript.log)
|
||||
|
||||
# Uncomment next line to enable debugging features
|
||||
# set -xeo pipefail
|
||||
|
||||
cd $HOME
|
||||
|
||||
# https://superuser.com/questions/1638779/automatic-yess-to-linux-update-upgrade
|
||||
# https://superuser.com/questions/1412054/non-interactive-apt-upgrade
|
||||
sudo DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get \
|
||||
-o Dpkg::Options::=--force-confold \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
update
|
||||
|
||||
sudo DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get \
|
||||
-o Dpkg::Options::=--force-confold \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
dist-upgrade
|
||||
|
||||
# TODO install unattended-upgrades
|
||||
sudo DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get \
|
||||
-o Dpkg::Options::=--force-confold \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
install jq
|
||||
|
||||
# Add firewall
|
||||
echo "y" | ufw enable
|
||||
|
||||
# Open ports
|
||||
ufw allow ssh
|
||||
ufw allow https
|
||||
ufw allow 5223
|
||||
|
||||
# Increase file descriptors limit
|
||||
echo 'fs.file-max = 1000000' >> /etc/sysctl.conf
|
||||
echo 'fs.inode-max = 1000000' >> /etc/sysctl.conf
|
||||
echo 'root soft nofile unlimited' >> /etc/security/limits.conf
|
||||
echo 'root hard nofile unlimited' >> /etc/security/limits.conf
|
||||
|
||||
# Download latest release
|
||||
bin_dir="/opt/simplex-notifications/bin"
|
||||
binary="$bin_dir/ntf-server"
|
||||
mkdir -p $bin_dir
|
||||
curl -L -o $binary https://github.com/simplex-chat/simplexmq/releases/latest/download/ntf-server-ubuntu-20_04-x86-64
|
||||
chmod +x $binary
|
||||
|
||||
# / Add to PATH
|
||||
cat > /etc/profile.d/simplex.sh << EOF
|
||||
#!/bin/bash
|
||||
|
||||
export PATH="$PATH:$bin_dir"
|
||||
|
||||
EOF
|
||||
# Add to PATH /
|
||||
|
||||
# Source and test PATH
|
||||
source /etc/profile.d/simplex.sh
|
||||
ntf-server --version
|
||||
|
||||
# Initialize server
|
||||
init_opts=()
|
||||
|
||||
[[ $ENABLE_STORE_LOG == "on" ]] && init_opts+=(-l)
|
||||
|
||||
ip_address=$(curl ifconfig.me)
|
||||
init_opts+=(--ip $ip_address)
|
||||
|
||||
[[ -n "$FQDN" ]] && init_opts+=(-n $FQDN)
|
||||
|
||||
ntf-server init "${init_opts[@]}"
|
||||
|
||||
# Server fingerprint
|
||||
fingerprint=$(cat /etc/opt/simplex-notifications/fingerprint)
|
||||
|
||||
# Determine server address to specify in welcome script and Linode tag
|
||||
if [[ -n "$FQDN" ]]; then
|
||||
server_address=$FQDN
|
||||
else
|
||||
server_address=$ip_address
|
||||
fi
|
||||
|
||||
# Set up welcome script
|
||||
on_login_script="/opt/simplex-notifications/on_login.sh"
|
||||
|
||||
# / Welcome script
|
||||
cat > $on_login_script << EOF
|
||||
#!/bin/bash
|
||||
|
||||
fingerprint=\$1
|
||||
server_address=\$2
|
||||
|
||||
cat << EOF2
|
||||
********************************************************************************
|
||||
|
||||
SimpleX notifications server address: ntf://\$fingerprint@\$server_address
|
||||
Check server status with: systemctl status ntf-server
|
||||
|
||||
To keep this server secure, the UFW firewall is enabled.
|
||||
All ports are BLOCKED except 22 (SSH), 443 (HTTPS), 5223 (notifications server).
|
||||
|
||||
********************************************************************************
|
||||
To stop seeing this message delete line - bash /opt/simplex-notifications/on_login.sh - from /root/.bashrc
|
||||
EOF2
|
||||
|
||||
EOF
|
||||
# Welcome script /
|
||||
|
||||
chmod +x $on_login_script
|
||||
echo "bash $on_login_script $fingerprint $server_address" >> /root/.bashrc
|
||||
|
||||
# Create A record and update Linode's tags
|
||||
if [[ -n "$API_TOKEN" ]]; then
|
||||
if [[ -n "$FQDN" ]]; then
|
||||
domain_address=$(echo $FQDN | rev | cut -d "." -f 1,2 | rev)
|
||||
domain_id=$(curl -H "Authorization: Bearer $API_TOKEN" https://api.linode.com/v4/domains \
|
||||
| jq --arg da "$domain_address" '.data[] | select( .domain == $da ) | .id')
|
||||
if [[ -n $domain_id ]]; then
|
||||
curl \
|
||||
-s -H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-X POST -d "{\"type\":\"A\",\"name\":\"$FQDN\",\"target\":\"$ip_address\"}" \
|
||||
https://api.linode.com/v4/domains/${domain_id}/records
|
||||
fi
|
||||
fi
|
||||
|
||||
version=$(ntf-server --version | cut -d ' ' -f 3-)
|
||||
|
||||
curl \
|
||||
-s -H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-X PUT -d "{\"tags\":[\"$server_address\",\"$fingerprint\",\"$version\"]}" \
|
||||
https://api.linode.com/v4/linode/instances/$LINODE_ID
|
||||
fi
|
||||
|
||||
# / Create systemd service
|
||||
cat > /etc/systemd/system/ntf-server.service << EOF
|
||||
[Unit]
|
||||
Description=SimpleX notifications server
|
||||
|
||||
[Service]
|
||||
Environment="APNS_KEY_FILE=/etc/opt/simplex-notifications/AuthKey.p8"
|
||||
Environment="APNS_KEY_ID=$APNS_KEY_ID"
|
||||
Type=simple
|
||||
ExecStart=/bin/sh -c "exec $binary start >> /var/opt/simplex-notifications/ntf-server.log 2>&1"
|
||||
KillSignal=SIGINT
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
LimitNOFILE=1000000
|
||||
LimitNOFILESoft=1000000
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
EOF
|
||||
# Create systemd service /
|
||||
|
||||
# Start systemd service
|
||||
chmod 644 /etc/systemd/system/ntf-server.service
|
||||
sudo systemctl enable ntf-server
|
||||
# ! APNS key file and certificate have to be copied manually
|
||||
# sudo systemctl start ntf-server
|
||||
|
||||
# Reboot Linode to apply upgrades
|
||||
# sudo reboot
|
||||
@@ -1,10 +1,4 @@
|
||||
# Server image for DigitalOcean
|
||||
|
||||
The current image used for 1-click deployment on DigitalOcean does not contain the source or binary of SMP Server - it downloads the compiled binary of the latest release (rather than a particular release) from GitHub.
|
||||
|
||||
The upside is that the new image does not have to be created and approved by DigitalOcean every time when the new release is created.
|
||||
|
||||
The downside is that while the release is being prepared in CI, when the release object is already created in GitHub but the server binary is not attached yet, the attempt to install the server would fail - it can last anything from several to 20 minutes, depending on whether the cached dependencies were used or everything was recompiled. Currently, when there is a small number of users, it is not a big problem, but we should consider some possible solutions in the future. Linode StackScript has the same issue.
|
||||
# SMP server image for DigitalOcean
|
||||
|
||||
## How to build an image
|
||||
|
||||
@@ -22,3 +16,8 @@ brew install hashicorp/tap/packer
|
||||
cd ./scripts/smp-server-digitalocean-droplet
|
||||
DIGITALOCEAN_TOKEN=$YOUR_TOKEN packer build -on-error=ask -color=false ./marketplace-image.json
|
||||
```
|
||||
|
||||
**TODO** (see Linode script)
|
||||
|
||||
- Increase file descriptors limit
|
||||
- Configure Restart for systemd service
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Download latest release
|
||||
bin_dir="/opt/simplex/bin"
|
||||
binary="$bin_dir/smp-server"
|
||||
mkdir -p $bin_dir
|
||||
curl -L -o $binary https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64
|
||||
chmod +x $binary
|
||||
|
||||
# / Add to PATH
|
||||
cat > /etc/profile.d/simplex.sh << EOF
|
||||
#!/bin/bash
|
||||
|
||||
export PATH="$PATH:$bin_dir"
|
||||
|
||||
EOF
|
||||
# Add to PATH /
|
||||
|
||||
# Source and test PATH
|
||||
source /etc/profile.d/simplex.sh
|
||||
smp-server --version
|
||||
|
||||
# Initialize server
|
||||
ip_address=$(curl ifconfig.me)
|
||||
smp-server init -l --ip $ip_address
|
||||
|
||||
# Server fingerprint
|
||||
fingerprint=$(cat /etc/opt/simplex/fingerprint)
|
||||
|
||||
# Set up welcome script
|
||||
echo "bash /opt/simplex/on_login.sh $fingerprint $ip_address" >> /root/.bashrc
|
||||
|
||||
# / Create systemd service for SMP server
|
||||
cat > /etc/systemd/system/smp-server.service << EOF
|
||||
[Unit]
|
||||
Description=SMP server
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/bin/sh -c "exec $binary start >> /var/opt/simplex/smp-server.log 2>&1"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
EOF
|
||||
# Create systemd service for SMP server /
|
||||
|
||||
# Start systemd service for SMP server
|
||||
chmod 644 /etc/systemd/system/smp-server.service
|
||||
sudo systemctl enable smp-server
|
||||
sudo systemctl start smp-server
|
||||
@@ -1,17 +1,16 @@
|
||||
#!/bin/bash
|
||||
# receives pubkey_hash file location as the first parameter
|
||||
|
||||
ip_address=$(hostname -I | awk '{print$1}')
|
||||
hash=$(cat $1)
|
||||
fingerprint=$1
|
||||
server_address=$2
|
||||
|
||||
cat <<EOF
|
||||
********************************************************************************
|
||||
|
||||
SMP server address: $ip_address#$hash
|
||||
SMP server address: smp://$fingerprint@$server_address
|
||||
Check SMP server status with: systemctl status smp-server
|
||||
|
||||
To keep this server secure, the UFW firewall is enabled.
|
||||
All ports are BLOCKED except 22 (SSH), 80 (HTTP), 5223 (SMP server).
|
||||
All ports are BLOCKED except 22 (SSH), 443 (HTTPS), 5223 (SMP server).
|
||||
|
||||
********************************************************************************
|
||||
To stop seeing this message delete line - bash /opt/simplex/on_login.sh - from /root/.bashrc
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
if [[ ! -f /opt/simplex/do_initialize_server ]]; then
|
||||
touch /opt/simplex/do_initialize_server
|
||||
elif [[ ! -f /etc/opt/simplex/smp-server.ini ]]; then
|
||||
chmod +x /opt/simplex/initialize_server.sh
|
||||
/opt/simplex/initialize_server.sh
|
||||
else
|
||||
echo "SMP server already initialized"
|
||||
fi
|
||||
@@ -1,8 +1,25 @@
|
||||
#!/bin/sh
|
||||
|
||||
# update apt
|
||||
apt-get -y update
|
||||
apt-get -y upgrade
|
||||
# https://superuser.com/questions/1638779/automatic-yess-to-linux-update-upgrade
|
||||
# https://superuser.com/questions/1412054/non-interactive-apt-upgrade
|
||||
sudo DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get \
|
||||
-o Dpkg::Options::=--force-confold \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
update
|
||||
|
||||
# packages needed for smp-server init-start.sh
|
||||
apt-get install -y jq
|
||||
sudo DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get \
|
||||
-o Dpkg::Options::=--force-confold \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
dist-upgrade
|
||||
|
||||
# TODO install unattended-upgrades; jq is not needed on DigitalOcean
|
||||
# sudo DEBIAN_FRONTEND=noninteractive \
|
||||
# apt-get \
|
||||
# -o Dpkg::Options::=--force-confold \
|
||||
# -o Dpkg::Options::=--force-confdef \
|
||||
# -y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
# install jq
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
# add firewall
|
||||
# Add firewall
|
||||
echo "y" | ufw enable
|
||||
|
||||
# open ports
|
||||
# Open ports
|
||||
ufw allow ssh
|
||||
ufw allow http
|
||||
ufw allow https
|
||||
ufw allow 5223
|
||||
|
||||
@@ -1,57 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
bin_dir="/opt/simplex/bin"
|
||||
conf_dir="/etc/opt/simplex"
|
||||
var_dir="/var/opt/simplex"
|
||||
mkdir -p $bin_dir
|
||||
mkdir -p $conf_dir
|
||||
mkdir -p $var_dir
|
||||
chmod +x /opt/simplex/server_bootstrap.sh
|
||||
|
||||
echo "downloading the latest SMP server release"
|
||||
curl -s https://api.github.com/repos/simplex-chat/simplexmq/releases/latest > release.json
|
||||
jq '.assets[].browser_download_url | select(test("smp-server-ubuntu-20_04-x86-64"))' release.json \
|
||||
| tr -d \" \
|
||||
| wget -qi -
|
||||
|
||||
release_version=$(jq '.tag_name' release.json | tr -d \")
|
||||
echo "downloaded SMP server $release_version"
|
||||
rm release.json
|
||||
|
||||
echo "preparing for SMP server initiaization"
|
||||
mv smp-server-ubuntu-20_04-x86-64 $bin_dir/smp-server
|
||||
chmod +x $bin_dir/smp-server
|
||||
|
||||
cat <<EOT >> /etc/profile.d/simplex.sh
|
||||
#!/bin/bash
|
||||
|
||||
export PATH="$PATH:$bin_dir"
|
||||
|
||||
EOT
|
||||
source /etc/profile.d/simplex.sh
|
||||
|
||||
# prepare SMP server systemd service
|
||||
cat <<EOT >> /etc/systemd/system/smp-server.service
|
||||
# / Create systemd service for server bootstrap script
|
||||
cat > /etc/systemd/system/server-bootstrap.service << EOF
|
||||
[Unit]
|
||||
Description=SMP server systemd service
|
||||
Description=Server bootstrap script that downloads and initializes SMP server from the latest release
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/bin/sh -c "$bin_dir/smp-server start"
|
||||
Type=oneshot
|
||||
ExecStart=/opt/simplex/server_bootstrap.sh
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
EOT
|
||||
chmod 644 /etc/systemd/system/smp-server.service
|
||||
EOF
|
||||
# Create systemd service for server bootstrap script /
|
||||
|
||||
echo "initializing SMP server"
|
||||
hash_file="$conf_dir/pubkey_hash"
|
||||
smp-server init -l | grep "transport key hash:" | cut -f2 -d":" | xargs > $hash_file
|
||||
# turn off websockets support
|
||||
sed -e '/websockets/s/^/# /g' -i $conf_dir/smp-server.ini
|
||||
# add welcome script to .bashrc
|
||||
echo "bash /opt/simplex/on_login.sh $hash_file" >> /root/.bashrc
|
||||
|
||||
echo "starting SMP server"
|
||||
sudo systemctl enable smp-server
|
||||
sudo systemctl start smp-server
|
||||
# Start systemd service for server bootstrap script
|
||||
chmod 644 /etc/systemd/system/server-bootstrap.service
|
||||
sudo systemctl enable server-bootstrap
|
||||
sudo systemctl start server-bootstrap
|
||||
|
||||
+120
-70
@@ -1,125 +1,175 @@
|
||||
#!/bin/bash
|
||||
# <UDF name="enable_store_log" label="Store log - persists SMP queues to append only log and restores them upon server restart." default="on" oneof="on, off" />
|
||||
# <UDF name="api_token" label="Linode API token - enables StackScript to create tags containing SMP server domain/ip address, transport key hash and server version. Use `domain#hash` or `ip#hash` as SMP server address in the client. Note: minimal permissions token should have are - read/write access to `linodes` (to update linode tags - you need them) and `domains` (to add A record for the chosen 3rd level domain)" default="" />
|
||||
# <UDF name="fqdn" label="FQDN (Fully qualified domain name) - provide third level domain name (ex: smp.example.com). If provided can be used instead of ip address." default="" />
|
||||
|
||||
# log all stdout output to stackscript.log
|
||||
# <UDF name="enable_store_log" label="Store log - persist SMP queues to append only log and restore them upon server restart." default="on" oneof="on, off" />
|
||||
# <UDF name="api_token" label="Linode API token - enable Linode to create tags with server address, fingerprint and version. Note: minimal permissions token should have are read/write access to `linodes` (to create tags) and `domains` (to add A record for the third level domain if FQDN is provided)." default="" />
|
||||
# <UDF name="fqdn" label="FQDN (Fully Qualified Domain Name) - provide third level domain name (e.g. smp.example.com). If provided use `smp://fingerprint@FQDN` as server address in the client. If FQDN is not provided use `smp://fingerprint@IP` instead." default="" />
|
||||
|
||||
# Log all stdout output to stackscript.log
|
||||
exec &> >(tee -i /var/log/stackscript.log)
|
||||
# uncomment next line to enable debugging features
|
||||
|
||||
# Uncomment next line to enable debugging features
|
||||
# set -xeo pipefail
|
||||
|
||||
cd $HOME
|
||||
|
||||
sudo apt-get -y update
|
||||
sudo apt-get -y upgrade
|
||||
sudo apt-get install -y jq
|
||||
# https://superuser.com/questions/1638779/automatic-yess-to-linux-update-upgrade
|
||||
# https://superuser.com/questions/1412054/non-interactive-apt-upgrade
|
||||
sudo DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get \
|
||||
-o Dpkg::Options::=--force-confold \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
update
|
||||
|
||||
# add firewall
|
||||
sudo DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get \
|
||||
-o Dpkg::Options::=--force-confold \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
dist-upgrade
|
||||
|
||||
# TODO install unattended-upgrades
|
||||
sudo DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get \
|
||||
-o Dpkg::Options::=--force-confold \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
install jq
|
||||
|
||||
# Add firewall
|
||||
echo "y" | ufw enable
|
||||
# open ports
|
||||
|
||||
# Open ports
|
||||
ufw allow ssh
|
||||
ufw allow http
|
||||
ufw allow https
|
||||
ufw allow 5223
|
||||
|
||||
# Increase file descriptors limit
|
||||
echo 'fs.file-max = 1000000' >> /etc/sysctl.conf
|
||||
echo 'fs.inode-max = 1000000' >> /etc/sysctl.conf
|
||||
echo 'root soft nofile unlimited' >> /etc/security/limits.conf
|
||||
echo 'root hard nofile unlimited' >> /etc/security/limits.conf
|
||||
|
||||
# Download latest release
|
||||
bin_dir="/opt/simplex/bin"
|
||||
conf_dir="/etc/opt/simplex"
|
||||
var_dir="/var/opt/simplex"
|
||||
binary="$bin_dir/smp-server"
|
||||
mkdir -p $bin_dir
|
||||
mkdir -p $conf_dir
|
||||
mkdir -p $var_dir
|
||||
curl -L -o $binary https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64
|
||||
chmod +x $binary
|
||||
|
||||
# retrieve latest release info and download smp-server executable
|
||||
curl -s https://api.github.com/repos/simplex-chat/simplexmq/releases/latest > release.json
|
||||
jq '.assets[].browser_download_url | select(test("smp-server-ubuntu-20_04-x86-64"))' release.json \
|
||||
| tr -d \" \
|
||||
| wget -qi -
|
||||
|
||||
mv smp-server-ubuntu-20_04-x86-64 $bin_dir/smp-server
|
||||
chmod +x $bin_dir/smp-server
|
||||
|
||||
cat <<EOT >> /etc/profile.d/simplex.sh
|
||||
# / Add to PATH
|
||||
cat > /etc/profile.d/simplex.sh << EOF
|
||||
#!/bin/bash
|
||||
|
||||
export PATH="$PATH:$bin_dir"
|
||||
|
||||
EOT
|
||||
EOF
|
||||
# Add to PATH /
|
||||
|
||||
# Source and test PATH
|
||||
source /etc/profile.d/simplex.sh
|
||||
smp-server --version
|
||||
|
||||
# initialize SMP server
|
||||
# Initialize server
|
||||
init_opts=()
|
||||
|
||||
[[ $ENABLE_STORE_LOG == "on" ]] && init_opts+=(-l)
|
||||
hash_file="$conf_dir/pubkey_hash"
|
||||
smp-server init "${init_opts[@]}" | grep "transport key hash:" | cut -f2 -d":" | xargs > $hash_file
|
||||
# turn off websockets support
|
||||
sed -e '/websockets/s/^/# /g' -i $conf_dir/smp-server.ini
|
||||
# create script that will run on login
|
||||
|
||||
ip_address=$(curl ifconfig.me)
|
||||
init_opts+=(--ip $ip_address)
|
||||
|
||||
[[ -n "$FQDN" ]] && init_opts+=(-n $FQDN)
|
||||
|
||||
smp-server init "${init_opts[@]}"
|
||||
|
||||
# Server fingerprint
|
||||
fingerprint=$(cat /etc/opt/simplex/fingerprint)
|
||||
|
||||
# Determine server address to specify in welcome script and Linode tag
|
||||
if [[ -n "$FQDN" ]]; then
|
||||
server_address=$FQDN
|
||||
else
|
||||
server_address=$ip_address
|
||||
fi
|
||||
|
||||
# Set up welcome script
|
||||
on_login_script="/opt/simplex/on_login.sh"
|
||||
cat <<EOT >> $on_login_script
|
||||
|
||||
# / Welcome script
|
||||
cat > $on_login_script << EOF
|
||||
#!/bin/bash
|
||||
# receives pubkey_hash file location as the first parameter
|
||||
|
||||
ip_address=\$(hostname -I | awk '{print\$1}')
|
||||
hash=\$(cat \$1)
|
||||
fingerprint=\$1
|
||||
server_address=\$2
|
||||
|
||||
cat <<EOF
|
||||
cat << EOF2
|
||||
********************************************************************************
|
||||
|
||||
SMP server address: \$ip_address#\$hash
|
||||
SMP server address: smp://\$fingerprint@\$server_address
|
||||
Check SMP server status with: systemctl status smp-server
|
||||
|
||||
To keep this server secure, the UFW firewall is enabled.
|
||||
All ports are BLOCKED except 22 (SSH), 80 (HTTP), 5223 (SMP server).
|
||||
All ports are BLOCKED except 22 (SSH), 443 (HTTPS), 5223 (SMP server).
|
||||
|
||||
********************************************************************************
|
||||
To stop seeing this message delete line - bash /opt/simplex/on_login.sh - from /root/.bashrc
|
||||
EOF2
|
||||
|
||||
EOF
|
||||
# Welcome script /
|
||||
|
||||
EOT
|
||||
chmod +x $on_login_script
|
||||
echo "bash $on_login_script $hash_file" >> /root/.bashrc
|
||||
echo "bash $on_login_script $fingerprint $server_address" >> /root/.bashrc
|
||||
|
||||
# create A record and update linode's tags
|
||||
if [ ! -z "$API_TOKEN" ]; then
|
||||
ip_address=$(curl ifconfig.me)
|
||||
address=$ip_address
|
||||
if [ ! -z "$FQDN" ]; then
|
||||
domain_address=$(echo $FQDN | rev | cut -d "." -f 1,2 | rev)
|
||||
domain_id=$(curl -H "Authorization: Bearer $API_TOKEN" https://api.linode.com/v4/domains \
|
||||
| jq --arg da "$domain_address" '.data[] | select( .domain == $da ) | .id')
|
||||
if [[ ! -z $domain_id ]]; then
|
||||
curl -s -H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-X POST -d "{\"type\":\"A\",\"name\":\"$FQDN\",\"target\":\"$ip_address\"}" \
|
||||
https://api.linode.com/v4/domains/${domain_id}/records
|
||||
address=$FQDN
|
||||
fi
|
||||
fi
|
||||
# Create A record and update Linode's tags
|
||||
if [[ -n "$API_TOKEN" ]]; then
|
||||
if [[ -n "$FQDN" ]]; then
|
||||
domain_address=$(echo $FQDN | rev | cut -d "." -f 1,2 | rev)
|
||||
domain_id=$(curl -H "Authorization: Bearer $API_TOKEN" https://api.linode.com/v4/domains \
|
||||
| jq --arg da "$domain_address" '.data[] | select( .domain == $da ) | .id')
|
||||
if [[ -n $domain_id ]]; then
|
||||
curl \
|
||||
-s -H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-X POST -d "{\"type\":\"A\",\"name\":\"$FQDN\",\"target\":\"$ip_address\"}" \
|
||||
https://api.linode.com/v4/domains/${domain_id}/records
|
||||
fi
|
||||
fi
|
||||
|
||||
hash=$(cat $hash_file)
|
||||
release_version=$(jq '.tag_name' release.json | tr -d \")
|
||||
version=$(smp-server --version | cut -d ' ' -f 3-)
|
||||
|
||||
curl -s -H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-X PUT -d "{\"tags\":[\"$address\",\"#$hash\",\"$release_version\"]}" \
|
||||
https://api.linode.com/v4/linode/instances/$LINODE_ID
|
||||
curl \
|
||||
-s -H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-X PUT -d "{\"tags\":[\"$server_address\",\"$fingerprint\",\"$version\"]}" \
|
||||
https://api.linode.com/v4/linode/instances/$LINODE_ID
|
||||
fi
|
||||
|
||||
# create, enable and start SMP server systemd service
|
||||
cat <<EOT >> /etc/systemd/system/smp-server.service
|
||||
# / Create systemd service
|
||||
cat > /etc/systemd/system/smp-server.service << EOF
|
||||
[Unit]
|
||||
Description=SMP server systemd service
|
||||
Description=SMP server
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/bin/sh -c "$bin_dir/smp-server start"
|
||||
ExecStart=/bin/sh -c "exec $binary start >> /var/opt/simplex/smp-server.log 2>&1"
|
||||
KillSignal=SIGINT
|
||||
TimeoutStopSec=infinity
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
LimitNOFILE=1000000
|
||||
LimitNOFILESoft=1000000
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
EOT
|
||||
EOF
|
||||
# Create systemd service /
|
||||
|
||||
# Start systemd service
|
||||
chmod 644 /etc/systemd/system/smp-server.service
|
||||
sudo systemctl enable smp-server
|
||||
sudo systemctl start smp-server
|
||||
|
||||
# cleanup
|
||||
rm release.json
|
||||
# Reboot Linode to apply upgrades
|
||||
sudo reboot
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
1. Install `tor` following [official guide](https://community.torproject.org/onion-services/setup/install/).
|
||||
|
||||
2. Modify `/etc/tor/torrc` configuration file:
|
||||
|
||||
```sh
|
||||
...
|
||||
# Disable anonymous mode for better connectivity
|
||||
## Needed for HiddenServiceNonAnonymousMode
|
||||
SOCKSPort 0
|
||||
## Needed for HiddenServiceSingleHopMode
|
||||
HiddenServiceNonAnonymousMode 1
|
||||
## Flag to disable anonymous mode
|
||||
## This option reduces the latency of server connection, but it makes server itself not anonymous,
|
||||
## it only protects the anonymity of the users connecting to the server.
|
||||
## In case your server address has both public and onion hostnames it is not anonymous anyway,
|
||||
## so this is what you want.
|
||||
HiddenServiceSingleHopMode 1
|
||||
|
||||
# Specify folder for smp-tor
|
||||
## Folder for keys, address, etc.
|
||||
HiddenServiceDir /var/lib/tor/simplex-smp/
|
||||
## Map smp port (5223) to tor
|
||||
HiddenServicePort 5223 localhost:5223
|
||||
...
|
||||
```
|
||||
|
||||
3. Restart `tor` system service:
|
||||
|
||||
```
|
||||
sudo systemctl restart tor
|
||||
```
|
||||
|
||||
4. Onion address can be obtained from following file:
|
||||
|
||||
```sh
|
||||
sudo cat /var/lib/tor/simplex-smp/hostname
|
||||
```
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
# systemd has to be configured to use SIGINT to save and restore undelivered messages after restart.
|
||||
# Add this to [Service] section:
|
||||
# KillSignal=SIGINT
|
||||
curl -L -o /opt/simplex/bin/smp-server-new https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64
|
||||
systemctl stop smp-server
|
||||
cp /var/opt/simplex/smp-server-store.log /var/opt/simplex/smp-server-store.log.bak
|
||||
mv /opt/simplex/bin/smp-server /opt/simplex/bin/smp-server-old
|
||||
mv /opt/simplex/bin/smp-server-new /opt/simplex/bin/smp-server
|
||||
chmod +x /opt/simplex/bin/smp-server
|
||||
systemctl start smp-server
|
||||
+449
-51
@@ -1,13 +1,11 @@
|
||||
cabal-version: 1.12
|
||||
|
||||
-- This file has been generated from package.yaml by hpack version 0.34.4.
|
||||
-- This file has been generated from package.yaml by hpack version 0.36.0.
|
||||
--
|
||||
-- see: https://github.com/sol/hpack
|
||||
--
|
||||
-- hash: f5e649e0c359792d9528dec6e6cf07af772a521d746e66b8cc2b86d8c604afb2
|
||||
|
||||
name: simplexmq
|
||||
version: 0.5.2
|
||||
version: 5.4.0.7
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
<./docs/Simplex-Messaging-Client.html client> and
|
||||
@@ -21,86 +19,275 @@ category: Chat, Network, Web, System, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplexmq#readme
|
||||
author: simplex.chat
|
||||
maintainer: chat@simplex.chat
|
||||
copyright: 2020 simplex.chat
|
||||
copyright: 2020-2022 simplex.chat
|
||||
license: AGPL-3
|
||||
license-file: LICENSE
|
||||
build-type: Simple
|
||||
extra-source-files:
|
||||
README.md
|
||||
CHANGELOG.md
|
||||
migrations/20210101_initial.sql
|
||||
migrations/20210624_confirmations.sql
|
||||
migrations/20210809_snd_messages.sql
|
||||
migrations/20211202_connection_mode.sql
|
||||
migrations/README.md
|
||||
cbits/sha512.h
|
||||
cbits/sntrup761.h
|
||||
|
||||
flag swift
|
||||
description: Enable swift JSON format
|
||||
manual: True
|
||||
default: False
|
||||
|
||||
library
|
||||
exposed-modules:
|
||||
Simplex.FileTransfer.Agent
|
||||
Simplex.FileTransfer.Chunks
|
||||
Simplex.FileTransfer.Client
|
||||
Simplex.FileTransfer.Client.Agent
|
||||
Simplex.FileTransfer.Client.Main
|
||||
Simplex.FileTransfer.Client.Presets
|
||||
Simplex.FileTransfer.Crypto
|
||||
Simplex.FileTransfer.Description
|
||||
Simplex.FileTransfer.Protocol
|
||||
Simplex.FileTransfer.Server
|
||||
Simplex.FileTransfer.Server.Env
|
||||
Simplex.FileTransfer.Server.Main
|
||||
Simplex.FileTransfer.Server.Stats
|
||||
Simplex.FileTransfer.Server.Store
|
||||
Simplex.FileTransfer.Server.StoreLog
|
||||
Simplex.FileTransfer.Transport
|
||||
Simplex.FileTransfer.Types
|
||||
Simplex.FileTransfer.Util
|
||||
Simplex.Messaging.Agent
|
||||
Simplex.Messaging.Agent.Client
|
||||
Simplex.Messaging.Agent.Env.SQLite
|
||||
Simplex.Messaging.Agent.Lock
|
||||
Simplex.Messaging.Agent.NtfSubSupervisor
|
||||
Simplex.Messaging.Agent.Protocol
|
||||
Simplex.Messaging.Agent.QueryString
|
||||
Simplex.Messaging.Agent.RetryInterval
|
||||
Simplex.Messaging.Agent.Server
|
||||
Simplex.Messaging.Agent.Store
|
||||
Simplex.Messaging.Agent.Store.SQLite
|
||||
Simplex.Messaging.Agent.Store.SQLite.Common
|
||||
Simplex.Messaging.Agent.Store.SQLite.DB
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220101_initial
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220301_snd_queue_keys
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220322_notifications
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220608_v2
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220625_v2_ntf_mode
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220811_onion_hosts
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220817_connection_ntfs
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220905_commands
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220915_connection_queues
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230110_users
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230117_fkey_indexes
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230120_delete_errors
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230217_server_key_hash
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230223_files
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230320_retry_state
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230401_snd_files
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230510_files_pending_replicas_indexes
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230516_encrypted_rcv_message_hashes
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230531_switch_status
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230615_ratchet_sync
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230701_delivery_receipts
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230720_delete_expired_messages
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230722_indexes
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230814_indexes
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230829_crypto_files
|
||||
Simplex.Messaging.Agent.TAsyncs
|
||||
Simplex.Messaging.Agent.TRcvQueues
|
||||
Simplex.Messaging.Client
|
||||
Simplex.Messaging.Client.Agent
|
||||
Simplex.Messaging.Crypto
|
||||
Simplex.Messaging.Crypto.File
|
||||
Simplex.Messaging.Crypto.Lazy
|
||||
Simplex.Messaging.Crypto.Ratchet
|
||||
Simplex.Messaging.Crypto.SNTRUP761
|
||||
Simplex.Messaging.Crypto.SNTRUP761.Bindings
|
||||
Simplex.Messaging.Crypto.SNTRUP761.Bindings.Defines
|
||||
Simplex.Messaging.Crypto.SNTRUP761.Bindings.FFI
|
||||
Simplex.Messaging.Crypto.SNTRUP761.Bindings.RNG
|
||||
Simplex.Messaging.Encoding
|
||||
Simplex.Messaging.Encoding.String
|
||||
Simplex.Messaging.Notifications.Client
|
||||
Simplex.Messaging.Notifications.Protocol
|
||||
Simplex.Messaging.Notifications.Server
|
||||
Simplex.Messaging.Notifications.Server.Env
|
||||
Simplex.Messaging.Notifications.Server.Main
|
||||
Simplex.Messaging.Notifications.Server.Push.APNS
|
||||
Simplex.Messaging.Notifications.Server.Push.APNS.Internal
|
||||
Simplex.Messaging.Notifications.Server.Stats
|
||||
Simplex.Messaging.Notifications.Server.Store
|
||||
Simplex.Messaging.Notifications.Server.StoreLog
|
||||
Simplex.Messaging.Notifications.Transport
|
||||
Simplex.Messaging.Notifications.Types
|
||||
Simplex.Messaging.Parsers
|
||||
Simplex.Messaging.Protocol
|
||||
Simplex.Messaging.Server
|
||||
Simplex.Messaging.Server.CLI
|
||||
Simplex.Messaging.Server.Control
|
||||
Simplex.Messaging.Server.Env.STM
|
||||
Simplex.Messaging.Server.Expiration
|
||||
Simplex.Messaging.Server.Main
|
||||
Simplex.Messaging.Server.MsgStore
|
||||
Simplex.Messaging.Server.MsgStore.STM
|
||||
Simplex.Messaging.Server.QueueStore
|
||||
Simplex.Messaging.Server.QueueStore.STM
|
||||
Simplex.Messaging.Server.Stats
|
||||
Simplex.Messaging.Server.StoreLog
|
||||
Simplex.Messaging.TMap
|
||||
Simplex.Messaging.Transport
|
||||
Simplex.Messaging.Transport.Buffer
|
||||
Simplex.Messaging.Transport.Client
|
||||
Simplex.Messaging.Transport.Credentials
|
||||
Simplex.Messaging.Transport.HTTP2
|
||||
Simplex.Messaging.Transport.HTTP2.Client
|
||||
Simplex.Messaging.Transport.HTTP2.File
|
||||
Simplex.Messaging.Transport.HTTP2.Server
|
||||
Simplex.Messaging.Transport.KeepAlive
|
||||
Simplex.Messaging.Transport.Server
|
||||
Simplex.Messaging.Transport.WebSockets
|
||||
Simplex.Messaging.Util
|
||||
Simplex.Messaging.Version
|
||||
Simplex.RemoteControl.Client
|
||||
Simplex.RemoteControl.Discovery
|
||||
Simplex.RemoteControl.Discovery.Multicast
|
||||
Simplex.RemoteControl.Invitation
|
||||
Simplex.RemoteControl.Types
|
||||
other-modules:
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
src
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns
|
||||
include-dirs:
|
||||
cbits
|
||||
c-sources:
|
||||
cbits/sha512.c
|
||||
cbits/sntrup761.c
|
||||
extra-libraries:
|
||||
crypto
|
||||
build-depends:
|
||||
QuickCheck ==2.14.*
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, asn1-encoding ==0.9.*
|
||||
, asn1-types ==0.3.*
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.13.*
|
||||
, base >=4.7 && <5
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.10.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, direct-sqlite ==2.3.*
|
||||
, cryptostore ==0.2.*
|
||||
, data-default ==0.7.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, file-embed >=0.0.14.0 && <=0.0.15.0
|
||||
, filepath ==1.4.*
|
||||
, generic-random >=1.3 && <1.5
|
||||
, hourglass ==0.2.*
|
||||
, http-types ==0.12.*
|
||||
, http2 >=4.2.2 && <4.3
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network ==3.1.*
|
||||
, network-transport ==0.5.*
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-info ==0.2.*
|
||||
, network-transport ==0.5.4
|
||||
, network-udp ==0.0.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
, simple-logger ==0.1.*
|
||||
, sqlite-simple ==0.4.*
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.16.*
|
||||
, template-haskell >=2.16 && <2.21
|
||||
, temporary ==1.3.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, transformers ==0.5.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
|
||||
executable ntf-server
|
||||
main-is: Main.hs
|
||||
other-modules:
|
||||
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
|
||||
build-depends:
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, asn1-encoding ==0.9.*
|
||||
, asn1-types ==0.3.*
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.10.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, cryptostore ==0.2.*
|
||||
, data-default ==0.7.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, filepath ==1.4.*
|
||||
, hourglass ==0.2.*
|
||||
, http-types ==0.12.*
|
||||
, http2 >=4.2.2 && <4.3
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-info ==0.2.*
|
||||
, network-transport ==0.5.4
|
||||
, network-udp ==0.0.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
, simple-logger ==0.1.*
|
||||
, simplexmq
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell >=2.16 && <2.21
|
||||
, temporary ==1.3.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, transformers ==0.5.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
|
||||
executable smp-agent
|
||||
main-is: Main.hs
|
||||
@@ -110,44 +297,63 @@ executable smp-agent
|
||||
apps/smp-agent
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
|
||||
build-depends:
|
||||
QuickCheck ==2.14.*
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, asn1-encoding ==0.9.*
|
||||
, asn1-types ==0.3.*
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.13.*
|
||||
, base >=4.7 && <5
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.10.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, direct-sqlite ==2.3.*
|
||||
, cryptostore ==0.2.*
|
||||
, data-default ==0.7.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, file-embed >=0.0.14.0 && <=0.0.15.0
|
||||
, filepath ==1.4.*
|
||||
, generic-random >=1.3 && <1.5
|
||||
, hourglass ==0.2.*
|
||||
, http-types ==0.12.*
|
||||
, http2 >=4.2.2 && <4.3
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network ==3.1.*
|
||||
, network-transport ==0.5.*
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-info ==0.2.*
|
||||
, network-transport ==0.5.4
|
||||
, network-udp ==0.0.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
, simple-logger ==0.1.*
|
||||
, simplexmq
|
||||
, sqlite-simple ==0.4.*
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.16.*
|
||||
, template-haskell >=2.16 && <2.21
|
||||
, temporary ==1.3.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, transformers ==0.5.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
|
||||
executable smp-server
|
||||
main-is: Main.hs
|
||||
@@ -157,60 +363,228 @@ executable smp-server
|
||||
apps/smp-server
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
|
||||
build-depends:
|
||||
QuickCheck ==2.14.*
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, asn1-encoding ==0.9.*
|
||||
, asn1-types ==0.3.*
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.13.*
|
||||
, base >=4.7 && <5
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.10.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, cryptostore ==0.2.*
|
||||
, direct-sqlite ==2.3.*
|
||||
, data-default ==0.7.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, file-embed >=0.0.14.0 && <=0.0.15.0
|
||||
, filepath ==1.4.*
|
||||
, generic-random >=1.3 && <1.5
|
||||
, hourglass ==0.2.*
|
||||
, http-types ==0.12.*
|
||||
, ini ==0.4.*
|
||||
, http2 >=4.2.2 && <4.3
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network ==3.1.*
|
||||
, network-transport ==0.5.*
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-info ==0.2.*
|
||||
, network-transport ==0.5.4
|
||||
, network-udp ==0.0.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
, simple-logger ==0.1.*
|
||||
, simplexmq
|
||||
, sqlite-simple ==0.4.*
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.16.*
|
||||
, template-haskell >=2.16 && <2.21
|
||||
, temporary ==1.3.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, transformers ==0.5.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
|
||||
test-suite smp-server-test
|
||||
executable xftp
|
||||
main-is: Main.hs
|
||||
other-modules:
|
||||
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
|
||||
build-depends:
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, asn1-encoding ==0.9.*
|
||||
, asn1-types ==0.3.*
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.10.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, cryptostore ==0.2.*
|
||||
, data-default ==0.7.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, filepath ==1.4.*
|
||||
, hourglass ==0.2.*
|
||||
, http-types ==0.12.*
|
||||
, http2 >=4.2.2 && <4.3
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-info ==0.2.*
|
||||
, network-transport ==0.5.4
|
||||
, network-udp ==0.0.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
, simple-logger ==0.1.*
|
||||
, simplexmq
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell >=2.16 && <2.21
|
||||
, temporary ==1.3.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, transformers ==0.5.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
|
||||
executable xftp-server
|
||||
main-is: Main.hs
|
||||
other-modules:
|
||||
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
|
||||
build-depends:
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, asn1-encoding ==0.9.*
|
||||
, asn1-types ==0.3.*
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.10.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, cryptostore ==0.2.*
|
||||
, data-default ==0.7.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, filepath ==1.4.*
|
||||
, hourglass ==0.2.*
|
||||
, http-types ==0.12.*
|
||||
, http2 >=4.2.2 && <4.3
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-info ==0.2.*
|
||||
, network-transport ==0.5.4
|
||||
, network-udp ==0.0.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
, simple-logger ==0.1.*
|
||||
, simplexmq
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell >=2.16 && <2.21
|
||||
, temporary ==1.3.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, transformers ==0.5.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
|
||||
test-suite simplexmq-test
|
||||
type: exitcode-stdio-1.0
|
||||
main-is: Test.hs
|
||||
other-modules:
|
||||
AgentTests
|
||||
AgentTests.ConnectionRequestTests
|
||||
AgentTests.DoubleRatchetTests
|
||||
AgentTests.FunctionalAPITests
|
||||
AgentTests.MigrationTests
|
||||
AgentTests.NotificationTests
|
||||
AgentTests.SchemaDump
|
||||
AgentTests.SQLiteTests
|
||||
ProtocolErrorTests
|
||||
CLITests
|
||||
CoreTests.BatchingTests
|
||||
CoreTests.CryptoFileTests
|
||||
CoreTests.CryptoTests
|
||||
CoreTests.EncodingTests
|
||||
CoreTests.ProtocolErrorTests
|
||||
CoreTests.RetryIntervalTests
|
||||
CoreTests.UtilTests
|
||||
CoreTests.VersionRangeTests
|
||||
FileDescriptionTests
|
||||
NtfClient
|
||||
NtfServerTests
|
||||
RemoteControl
|
||||
ServerTests
|
||||
SMPAgentClient
|
||||
SMPClient
|
||||
XFTPAgent
|
||||
XFTPCLI
|
||||
XFTPClient
|
||||
XFTPServerTests
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
tests
|
||||
@@ -218,43 +592,67 @@ test-suite smp-server-test
|
||||
build-depends:
|
||||
HUnit ==1.6.*
|
||||
, QuickCheck ==2.14.*
|
||||
, aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, asn1-encoding ==0.9.*
|
||||
, asn1-types ==0.3.*
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.13.*
|
||||
, base >=4.7 && <5
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.10.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, direct-sqlite ==2.3.*
|
||||
, cryptostore ==0.2.*
|
||||
, data-default ==0.7.*
|
||||
, deepseq ==1.4.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, file-embed >=0.0.14.0 && <=0.0.15.0
|
||||
, filepath ==1.4.*
|
||||
, generic-random >=1.3 && <1.5
|
||||
, hourglass ==0.2.*
|
||||
, hspec ==2.7.*
|
||||
, hspec-core ==2.7.*
|
||||
, http-types ==0.12.*
|
||||
, http2 >=4.2.2 && <4.3
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, main-tester ==0.2.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network ==3.1.*
|
||||
, network-transport ==0.5.*
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-info ==0.2.*
|
||||
, network-transport ==0.5.4
|
||||
, network-udp ==0.0.*
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
, silently ==1.2.*
|
||||
, simple-logger ==0.1.*
|
||||
, simplexmq
|
||||
, sqlite-simple ==0.4.*
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.16.*
|
||||
, template-haskell >=2.16 && <2.21
|
||||
, temporary ==1.3.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time-manager ==0.0.*
|
||||
, timeit ==2.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, transformers ==0.5.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
|
||||
@@ -0,0 +1,587 @@
|
||||
{-# LANGUAGE ConstraintKinds #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Simplex.FileTransfer.Agent
|
||||
( startXFTPWorkers,
|
||||
closeXFTPAgent,
|
||||
toFSFilePath,
|
||||
-- Receiving files
|
||||
xftpReceiveFile',
|
||||
xftpDeleteRcvFile',
|
||||
-- Sending files
|
||||
xftpSendFile',
|
||||
deleteSndFileInternal,
|
||||
deleteSndFileRemote,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Logger.Simple (logError)
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Reader
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Int (Int64)
|
||||
import Data.List (foldl', sortOn)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Text (Text)
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import Data.Time.Format (defaultTimeLocale, formatTime)
|
||||
import Simplex.FileTransfer.Client (XFTPChunkSpec (..))
|
||||
import Simplex.FileTransfer.Client.Main
|
||||
import Simplex.FileTransfer.Crypto
|
||||
import Simplex.FileTransfer.Description
|
||||
import Simplex.FileTransfer.Protocol (FileParty (..), SFileParty (..))
|
||||
import Simplex.FileTransfer.Transport (XFTPRcvChunkSpec (..))
|
||||
import Simplex.FileTransfer.Types
|
||||
import Simplex.FileTransfer.Util (removePath, uniqueCombine)
|
||||
import Simplex.Messaging.Agent.Client
|
||||
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.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.Protocol (EntityId, XFTPServer)
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Util (liftError, tshow, unlessM, whenM)
|
||||
import System.FilePath (takeFileName, (</>))
|
||||
import UnliftIO
|
||||
import UnliftIO.Directory
|
||||
|
||||
startXFTPWorkers :: AgentMonad m => AgentClient -> Maybe FilePath -> m ()
|
||||
startXFTPWorkers c workDir = do
|
||||
wd <- asks $ xftpWorkDir . xftpAgent
|
||||
atomically $ writeTVar wd workDir
|
||||
startRcvFiles
|
||||
startSndFiles
|
||||
startDelFiles
|
||||
where
|
||||
startRcvFiles = do
|
||||
rcvFilesTTL <- asks $ rcvFilesTTL . config
|
||||
pendingRcvServers <- withStore' c (`getPendingRcvFilesServers` rcvFilesTTL)
|
||||
forM_ pendingRcvServers $ \s -> addXFTPRcvWorker 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
|
||||
addXFTPRcvWorker c Nothing
|
||||
startSndFiles = do
|
||||
sndFilesTTL <- asks $ sndFilesTTL . config
|
||||
-- start worker for files pending encryption/creation
|
||||
addXFTPSndWorker c Nothing
|
||||
pendingSndServers <- withStore' c (`getPendingSndFilesServers` sndFilesTTL)
|
||||
forM_ pendingSndServers $ \s -> addXFTPSndWorker c (Just s)
|
||||
startDelFiles = do
|
||||
rcvFilesTTL <- asks $ rcvFilesTTL . config
|
||||
pendingDelServers <- withStore' c (`getPendingDelFilesServers` rcvFilesTTL)
|
||||
forM_ pendingDelServers $ addXFTPDelWorker c
|
||||
|
||||
closeXFTPAgent :: MonadUnliftIO m => XFTPAgent -> m ()
|
||||
closeXFTPAgent XFTPAgent {xftpRcvWorkers, xftpSndWorkers} = do
|
||||
stopWorkers xftpRcvWorkers
|
||||
stopWorkers xftpSndWorkers
|
||||
where
|
||||
stopWorkers wsSel = do
|
||||
ws <- atomically $ stateTVar wsSel (,M.empty)
|
||||
mapM_ (uninterruptibleCancel . snd) ws
|
||||
|
||||
xftpReceiveFile' :: AgentMonad m => AgentClient -> UserId -> ValidFileDescription 'FRecipient -> Maybe CryptoFileArgs -> m RcvFileId
|
||||
xftpReceiveFile' c userId (ValidFileDescription fd@FileDescription {chunks}) cfArgs = do
|
||||
g <- asks random
|
||||
prefixPath <- getPrefixPath "rcv.xftp"
|
||||
createDirectory prefixPath
|
||||
let relPrefixPath = takeFileName prefixPath
|
||||
relTmpPath = relPrefixPath </> "xftp.encrypted"
|
||||
relSavePath = relPrefixPath </> "xftp.decrypted"
|
||||
createDirectory =<< toFSFilePath relTmpPath
|
||||
createEmptyFile =<< toFSFilePath relSavePath
|
||||
let saveFile = CryptoFile relSavePath cfArgs
|
||||
fId <- withStore c $ \db -> createRcvFile db g userId fd relPrefixPath relTmpPath saveFile
|
||||
forM_ chunks downloadChunk
|
||||
pure fId
|
||||
where
|
||||
downloadChunk :: AgentMonad m => FileChunk -> m ()
|
||||
downloadChunk FileChunk {replicas = (FileChunkReplica {server} : _)} = do
|
||||
addXFTPRcvWorker c (Just server)
|
||||
downloadChunk _ = throwError $ INTERNAL "no replicas"
|
||||
|
||||
getPrefixPath :: AgentMonad m => String -> m 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 f = (</> f) <$> getXFTPWorkPath
|
||||
|
||||
createEmptyFile :: AgentMonad m => FilePath -> m ()
|
||||
createEmptyFile fPath = liftIO $ B.writeFile fPath ""
|
||||
|
||||
addXFTPRcvWorker :: AgentMonad m => AgentClient -> Maybe XFTPServer -> m ()
|
||||
addXFTPRcvWorker c = addWorker c xftpRcvWorkers runXFTPRcvWorker runXFTPRcvLocalWorker
|
||||
|
||||
addWorker ::
|
||||
AgentMonad m =>
|
||||
AgentClient ->
|
||||
(XFTPAgent -> TMap (Maybe XFTPServer) (TMVar (), Async ())) ->
|
||||
(AgentClient -> XFTPServer -> TMVar () -> m ()) ->
|
||||
(AgentClient -> TMVar () -> m ()) ->
|
||||
Maybe XFTPServer ->
|
||||
m ()
|
||||
addWorker c wsSel runWorker runWorkerNoSrv srv_ = do
|
||||
ws <- asks $ wsSel . xftpAgent
|
||||
atomically (TM.lookup srv_ ws) >>= \case
|
||||
Nothing -> do
|
||||
doWork <- newTMVarIO ()
|
||||
let runWorker' = case srv_ of
|
||||
Just srv -> runWorker c srv doWork
|
||||
Nothing -> runWorkerNoSrv c doWork
|
||||
worker <- async $ runWorker' `agentFinally` atomically (TM.delete srv_ ws)
|
||||
atomically $ TM.insert srv_ (doWork, worker) ws
|
||||
Just (doWork, _) ->
|
||||
void . atomically $ tryPutTMVar doWork ()
|
||||
|
||||
runXFTPRcvWorker :: forall m. AgentMonad m => AgentClient -> XFTPServer -> TMVar () -> m ()
|
||||
runXFTPRcvWorker c srv doWork = do
|
||||
forever $ do
|
||||
void . atomically $ readTMVar doWork
|
||||
atomically $ assertAgentForeground c
|
||||
runXFTPOperation
|
||||
where
|
||||
noWorkToDo = void . atomically $ tryTakeTMVar doWork
|
||||
runXFTPOperation :: m ()
|
||||
runXFTPOperation = do
|
||||
rcvFilesTTL <- asks (rcvFilesTTL . config)
|
||||
nextChunk <- withStore' c $ \db -> getNextRcvChunkToDownload db srv rcvFilesTTL
|
||||
case nextChunk of
|
||||
Nothing -> noWorkToDo
|
||||
Just RcvFileChunk {rcvFileId, rcvFileEntityId, fileTmpPath, replicas = []} -> rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) "chunk has no replicas"
|
||||
Just fc@RcvFileChunk {userId, rcvFileId, rcvFileEntityId, digest, fileTmpPath, replicas = replica@RcvFileChunkReplica {rcvChunkReplicaId, server, delay} : _} -> do
|
||||
ri <- asks $ reconnectInterval . config
|
||||
let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay
|
||||
withRetryInterval ri' $ \delay' loop ->
|
||||
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
|
||||
notifyOnRetry <- asks (xftpNotifyErrsOnRetry . config)
|
||||
when notifyOnRetry $ notify c rcvFileEntityId $ RFERR e
|
||||
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 {userId, rcvFileId, rcvFileEntityId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath} replica = do
|
||||
fsFileTmpPath <- 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
|
||||
liftIO $ updateRcvFileChunkReceived db (rcvChunkReplicaId replica) rcvChunkId relChunkPath
|
||||
RcvFile {size = FileSize total, chunks} <- ExceptT $ getRcvFile db rcvFileId
|
||||
let rcvd = receivedSize chunks
|
||||
complete = all chunkReceived chunks
|
||||
liftIO . when complete $ updateRcvFileStatus db rcvFileId RFSReceived
|
||||
pure (complete, RFPROG rcvd total)
|
||||
notify c rcvFileEntityId progress
|
||||
when complete $ addXFTPRcvWorker c Nothing
|
||||
where
|
||||
receivedSize :: [RcvFileChunk] -> Int64
|
||||
receivedSize = foldl' (\sz ch -> sz + receivedChunkSize ch) 0
|
||||
receivedChunkSize ch@RcvFileChunk {chunkSize = s}
|
||||
| chunkReceived ch = fromIntegral (unFileSize s)
|
||||
| otherwise = 0
|
||||
chunkReceived RcvFileChunk {replicas} = any received replicas
|
||||
|
||||
retryOnError :: AgentMonad m => Text -> m a -> m a -> AgentErrorType -> m 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 c rcvFileId rcvFileEntityId tmpPath internalErrStr = do
|
||||
forM_ tmpPath (removePath <=< toFSFilePath)
|
||||
withStore' c $ \db -> updateRcvFileError db rcvFileId internalErrStr
|
||||
notify c rcvFileEntityId $ RFERR $ INTERNAL internalErrStr
|
||||
|
||||
runXFTPRcvLocalWorker :: forall m. AgentMonad m => AgentClient -> TMVar () -> m ()
|
||||
runXFTPRcvLocalWorker c doWork = do
|
||||
forever $ do
|
||||
void . atomically $ readTMVar doWork
|
||||
atomically $ assertAgentForeground c
|
||||
runXFTPOperation
|
||||
where
|
||||
runXFTPOperation :: m ()
|
||||
runXFTPOperation = do
|
||||
rcvFilesTTL <- asks (rcvFilesTTL . config)
|
||||
nextFile <- withStore' c (`getNextRcvFileToDecrypt` rcvFilesTTL)
|
||||
case nextFile of
|
||||
Nothing -> noWorkToDo
|
||||
Just f@RcvFile {rcvFileId, rcvFileEntityId, tmpPath} ->
|
||||
decryptFile f `catchAgentError` (rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath . show)
|
||||
noWorkToDo = void . atomically $ tryTakeTMVar doWork
|
||||
decryptFile :: RcvFile -> m ()
|
||||
decryptFile RcvFile {rcvFileId, rcvFileEntityId, key, nonce, tmpPath, saveFile, status, chunks} = do
|
||||
let CryptoFile savePath cfArgs = saveFile
|
||||
fsSavePath <- toFSFilePath savePath
|
||||
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
|
||||
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)
|
||||
where
|
||||
getChunkPaths :: [RcvFileChunk] -> m [FilePath]
|
||||
getChunkPaths [] = pure []
|
||||
getChunkPaths (RcvFileChunk {chunkTmpPath = Just path} : cs) = do
|
||||
ps <- getChunkPaths cs
|
||||
fsPath <- 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)
|
||||
|
||||
notify :: forall m e. (MonadUnliftIO 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' c userId file numRecipients = do
|
||||
g <- asks random
|
||||
prefixPath <- getPrefixPath "snd.xftp"
|
||||
createDirectory prefixPath
|
||||
let relPrefixPath = takeFileName prefixPath
|
||||
key <- liftIO C.randomSbKey
|
||||
nonce <- liftIO C.randomCbNonce
|
||||
-- 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
|
||||
addXFTPSndWorker c Nothing
|
||||
pure fId
|
||||
|
||||
addXFTPSndWorker :: AgentMonad m => AgentClient -> Maybe XFTPServer -> m ()
|
||||
addXFTPSndWorker c = addWorker c xftpSndWorkers runXFTPSndWorker runXFTPSndPrepareWorker
|
||||
|
||||
runXFTPSndPrepareWorker :: forall m. AgentMonad m => AgentClient -> TMVar () -> m ()
|
||||
runXFTPSndPrepareWorker c doWork = do
|
||||
forever $ do
|
||||
void . atomically $ readTMVar doWork
|
||||
atomically $ assertAgentForeground c
|
||||
runXFTPOperation
|
||||
where
|
||||
runXFTPOperation :: m ()
|
||||
runXFTPOperation = do
|
||||
sndFilesTTL <- asks (sndFilesTTL . config)
|
||||
nextFile <- withStore' c (`getNextSndFileToPrepare` sndFilesTTL)
|
||||
case nextFile of
|
||||
Nothing -> noWorkToDo
|
||||
Just f@SndFile {sndFileId, sndFileEntityId, prefixPath} ->
|
||||
prepareFile f `catchAgentError` (sndWorkerInternalError c sndFileId sndFileEntityId prefixPath . show)
|
||||
noWorkToDo = void . atomically $ tryTakeTMVar doWork
|
||||
prepareFile :: SndFile -> m ()
|
||||
prepareFile SndFile {prefixPath = Nothing} =
|
||||
throwError $ INTERNAL "no prefix path"
|
||||
prepareFile 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
|
||||
when (status == SFSEncrypting) . whenM (doesFileExist fsEncPath) $
|
||||
removeFile fsEncPath
|
||||
withStore' c $ \db -> updateSndFileStatus db sndFileId SFSEncrypting
|
||||
(digest, chunkSpecsDigests) <- encryptFileForUpload sndFile fsEncPath
|
||||
withStore c $ \db -> do
|
||||
updateSndFileEncrypted db sndFileId digest chunkSpecsDigests
|
||||
getSndFile db sndFileId
|
||||
else pure sndFile
|
||||
maxRecipients <- asks (xftpMaxRecipientsPerRequest . config)
|
||||
let numRecipients' = min numRecipients maxRecipients
|
||||
-- concurrently?
|
||||
forM_ (filter (not . chunkCreated) chunks) $ createChunk numRecipients'
|
||||
withStore' c $ \db -> updateSndFileStatus db sndFileId SFSUploading
|
||||
where
|
||||
encryptFileForUpload :: SndFile -> FilePath -> m (FileDigest, [(XFTPChunkSpec, FileDigest)])
|
||||
encryptFileForUpload SndFile {key, nonce, srcFile} fsEncPath = do
|
||||
let CryptoFile {filePath} = srcFile
|
||||
fileName = takeFileName filePath
|
||||
fileSize <- liftIO $ fromInteger <$> CF.getFileContentsSize srcFile
|
||||
when (fileSize > maxFileSize) $ 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'
|
||||
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 ()
|
||||
createChunk numRecipients' ch = do
|
||||
atomically $ assertAgentForeground c
|
||||
(replica, ProtoServerWithAuth srv _) <- tryCreate
|
||||
withStore' c $ \db -> createSndFileReplica db ch replica
|
||||
addXFTPSndWorker c $ Just srv
|
||||
where
|
||||
tryCreate = do
|
||||
ri <- asks $ messageRetryInterval . config
|
||||
usedSrvs <- newTVarIO ([] :: [XFTPServer])
|
||||
withRetryInterval (riFast ri) $ \_ loop ->
|
||||
createWithNextSrv usedSrvs
|
||||
`catchAgentError` \e -> retryOnError "XFTP prepare worker" (retryLoop loop) (throwError e) e
|
||||
where
|
||||
retryLoop loop = atomically (assertAgentForeground c) >> loop
|
||||
createWithNextSrv usedSrvs = do
|
||||
deleted <- withStore' c $ \db -> getSndFileDeleted db sndFileId
|
||||
when deleted $ throwError $ INTERNAL "file deleted, aborting chunk creation"
|
||||
withNextSrv c userId usedSrvs [] $ \srvAuth -> do
|
||||
replica <- agentXFTPNewChunk c ch numRecipients' srvAuth
|
||||
pure (replica, srvAuth)
|
||||
|
||||
sndWorkerInternalError :: AgentMonad m => AgentClient -> DBSndFileId -> SndFileId -> Maybe FilePath -> String -> m ()
|
||||
sndWorkerInternalError c sndFileId sndFileEntityId prefixPath internalErrStr = do
|
||||
forM_ prefixPath $ removePath <=< toFSFilePath
|
||||
withStore' c $ \db -> updateSndFileError db sndFileId internalErrStr
|
||||
notify c sndFileEntityId $ SFERR $ INTERNAL internalErrStr
|
||||
|
||||
runXFTPSndWorker :: forall m. AgentMonad m => AgentClient -> XFTPServer -> TMVar () -> m ()
|
||||
runXFTPSndWorker c srv doWork = do
|
||||
forever $ do
|
||||
void . atomically $ readTMVar doWork
|
||||
atomically $ assertAgentForeground c
|
||||
runXFTPOperation
|
||||
where
|
||||
noWorkToDo = void . atomically $ tryTakeTMVar doWork
|
||||
runXFTPOperation :: m ()
|
||||
runXFTPOperation = do
|
||||
sndFilesTTL <- asks (sndFilesTTL . config)
|
||||
nextChunk <- withStore' c $ \db -> getNextSndChunkToUpload db srv sndFilesTTL
|
||||
case nextChunk of
|
||||
Nothing -> noWorkToDo
|
||||
Just SndFileChunk {sndFileId, sndFileEntityId, filePrefixPath, replicas = []} -> sndWorkerInternalError c sndFileId sndFileEntityId (Just filePrefixPath) "chunk has no replicas"
|
||||
Just fc@SndFileChunk {userId, sndFileId, sndFileEntityId, filePrefixPath, digest, replicas = replica@SndFileChunkReplica {sndChunkReplicaId, server, delay} : _} -> do
|
||||
ri <- asks $ reconnectInterval . config
|
||||
let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay
|
||||
withRetryInterval ri' $ \delay' loop ->
|
||||
uploadFileChunk 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
|
||||
notifyOnRetry <- asks (xftpNotifyErrsOnRetry . config)
|
||||
when notifyOnRetry $ notify c sndFileEntityId $ SFERR e
|
||||
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 :: SndFileChunk -> SndFileChunkReplica -> m ()
|
||||
uploadFileChunk sndFileChunk@SndFileChunk {sndFileId, userId, chunkSpec = chunkSpec@XFTPChunkSpec {filePath}, digest = chunkDigest} replica = do
|
||||
replica'@SndFileChunkReplica {sndChunkReplicaId} <- addRecipients sndFileChunk replica
|
||||
fsFilePath <- toFSFilePath filePath
|
||||
unlessM (doesFileExist fsFilePath) $ throwError $ INTERNAL "encrypted file doesn't exist on upload"
|
||||
let chunkSpec' = chunkSpec {filePath = fsFilePath} :: XFTPChunkSpec
|
||||
atomically $ assertAgentForeground c
|
||||
agentXFTPUploadChunk c userId chunkDigest replica' chunkSpec'
|
||||
atomically $ waitUntilForeground c
|
||||
sf@SndFile {sndFileEntityId, prefixPath, chunks} <- withStore c $ \db -> do
|
||||
updateSndChunkReplicaStatus db sndChunkReplicaId SFRSUploaded
|
||||
getSndFile db sndFileId
|
||||
let uploaded = uploadedSize chunks
|
||||
total = totalSize chunks
|
||||
complete = all chunkUploaded chunks
|
||||
notify c sndFileEntityId $ SFPROG uploaded total
|
||||
when complete $ do
|
||||
(sndDescr, rcvDescrs) <- sndFileToDescrs sf
|
||||
notify c sndFileEntityId $ SFDONE sndDescr rcvDescrs
|
||||
forM_ prefixPath $ removePath <=< toFSFilePath
|
||||
withStore' c $ \db -> updateSndFileComplete db sndFileId
|
||||
where
|
||||
addRecipients :: SndFileChunk -> SndFileChunkReplica -> m SndFileChunkReplica
|
||||
addRecipients ch@SndFileChunk {numRecipients} cr@SndFileChunkReplica {rcvIdsKeys}
|
||||
| length rcvIdsKeys > numRecipients = throwError $ INTERNAL "too many recipients"
|
||||
| length rcvIdsKeys == numRecipients = pure cr
|
||||
| otherwise = do
|
||||
maxRecipients <- asks $ xftpMaxRecipientsPerRequest . config
|
||||
let numRecipients' = min (numRecipients - length rcvIdsKeys) maxRecipients
|
||||
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 {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
|
||||
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}
|
||||
validFdSnd <- either (throwError . INTERNAL) pure $ validateFileDescription fdSnd
|
||||
-- rcv descriptions
|
||||
let fdRcv = FileDescription {party = SFRecipient, size, digest, key, nonce, chunkSize, chunks = []}
|
||||
fdRcvs = createRcvFileDescriptions fdRcv chunks
|
||||
validFdRcvs <- either (throwError . INTERNAL) pure $ mapM validateFileDescription fdRcvs
|
||||
pure (validFdSnd, validFdRcvs)
|
||||
toSndDescrChunk :: SndFileChunk -> m 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
|
||||
replicas = [FileChunkReplica {server, replicaId, replicaKey}]
|
||||
pure FileChunk {chunkNo, digest = chDigest, chunkSize, replicas}
|
||||
createRcvFileDescriptions :: FileDescription 'FRecipient -> [SndFileChunk] -> [FileDescription 'FRecipient]
|
||||
createRcvFileDescriptions fd sndChunks = map (\chunks -> (fd :: (FileDescription 'FRecipient)) {chunks}) rcvChunks
|
||||
where
|
||||
rcvReplicas :: [SentRecipientReplica]
|
||||
rcvReplicas = concatMap toSentRecipientReplicas sndChunks
|
||||
toSentRecipientReplicas :: SndFileChunk -> [SentRecipientReplica]
|
||||
toSentRecipientReplicas ch@SndFileChunk {chunkNo, digest, replicas} =
|
||||
let chunkSize = FileSize $ sndChunkSize ch
|
||||
in concatMap
|
||||
( \SndFileChunkReplica {server, rcvIdsKeys} ->
|
||||
zipWith
|
||||
(\rcvNo (replicaId, replicaKey) -> SentRecipientReplica {chunkNo, server, rcvNo, replicaId, replicaKey, digest, chunkSize})
|
||||
[1 ..]
|
||||
rcvIdsKeys
|
||||
)
|
||||
replicas
|
||||
rcvChunks :: [[FileChunk]]
|
||||
rcvChunks = map (sortChunks . M.elems) $ M.elems $ foldl' addRcvChunk M.empty rcvReplicas
|
||||
sortChunks :: [FileChunk] -> [FileChunk]
|
||||
sortChunks = map reverseReplicas . sortOn (\FileChunk {chunkNo} -> chunkNo)
|
||||
reverseReplicas ch@FileChunk {replicas} = (ch :: FileChunk) {replicas = reverse replicas}
|
||||
addRcvChunk :: Map Int (Map Int FileChunk) -> SentRecipientReplica -> Map Int (Map Int FileChunk)
|
||||
addRcvChunk m SentRecipientReplica {chunkNo, server, rcvNo, replicaId, replicaKey, digest, chunkSize} =
|
||||
M.alter (Just . addOrChangeRecipient) rcvNo m
|
||||
where
|
||||
addOrChangeRecipient :: Maybe (Map Int FileChunk) -> Map Int FileChunk
|
||||
addOrChangeRecipient = \case
|
||||
Just m' -> M.alter (Just . addOrChangeChunk) chunkNo m'
|
||||
_ -> M.singleton chunkNo $ FileChunk {chunkNo, digest, chunkSize, replicas = [replica']}
|
||||
addOrChangeChunk :: Maybe FileChunk -> FileChunk
|
||||
addOrChangeChunk = \case
|
||||
Just ch@FileChunk {replicas} -> ch {replicas = replica' : replicas}
|
||||
_ -> FileChunk {chunkNo, digest, chunkSize, replicas = [replica']}
|
||||
replica' = FileChunkReplica {server, replicaId, replicaKey}
|
||||
uploadedSize :: [SndFileChunk] -> Int64
|
||||
uploadedSize = foldl' (\sz ch -> sz + uploadedChunkSize ch) 0
|
||||
uploadedChunkSize ch
|
||||
| chunkUploaded ch = fromIntegral (sndChunkSize ch)
|
||||
| otherwise = 0
|
||||
totalSize :: [SndFileChunk] -> Int64
|
||||
totalSize = foldl' (\sz ch -> sz + fromIntegral (sndChunkSize ch)) 0
|
||||
chunkUploaded :: SndFileChunk -> Bool
|
||||
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)
|
||||
|
||||
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)
|
||||
where
|
||||
deleteFileChunk :: FileChunk -> m ()
|
||||
deleteFileChunk FileChunk {digest, replicas = replica@FileChunkReplica {server} : _} = do
|
||||
withStore' c $ \db -> createDeletedSndChunkReplica db userId replica digest
|
||||
addXFTPDelWorker c server
|
||||
deleteFileChunk _ = pure ()
|
||||
|
||||
addXFTPDelWorker :: AgentMonad m => AgentClient -> XFTPServer -> m ()
|
||||
addXFTPDelWorker c srv = do
|
||||
ws <- asks $ xftpDelWorkers . xftpAgent
|
||||
atomically (TM.lookup srv ws) >>= \case
|
||||
Nothing -> do
|
||||
doWork <- newTMVarIO ()
|
||||
worker <- async $ runXFTPDelWorker c srv doWork `agentFinally` atomically (TM.delete srv ws)
|
||||
atomically $ TM.insert srv (doWork, worker) ws
|
||||
Just (doWork, _) ->
|
||||
void . atomically $ tryPutTMVar doWork ()
|
||||
|
||||
runXFTPDelWorker :: forall m. AgentMonad m => AgentClient -> XFTPServer -> TMVar () -> m ()
|
||||
runXFTPDelWorker c srv doWork = do
|
||||
forever $ do
|
||||
void . atomically $ readTMVar doWork
|
||||
atomically $ assertAgentForeground c
|
||||
runXFTPOperation
|
||||
where
|
||||
noWorkToDo = void . atomically $ tryTakeTMVar doWork
|
||||
runXFTPOperation :: m ()
|
||||
runXFTPOperation = do
|
||||
-- no point in deleting files older than rcv ttl, as they will be expired on server
|
||||
rcvFilesTTL <- asks (rcvFilesTTL . config)
|
||||
nextReplica <- withStore' c $ \db -> getNextDeletedSndChunkReplica db srv rcvFilesTTL
|
||||
case nextReplica of
|
||||
Nothing -> noWorkToDo
|
||||
Just replica@DeletedSndChunkReplica {deletedSndChunkReplicaId, userId, server, chunkDigest, delay} -> do
|
||||
ri <- asks $ reconnectInterval . config
|
||||
let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay
|
||||
withRetryInterval ri' $ \delay' loop ->
|
||||
deleteChunkReplica replica
|
||||
`catchAgentError` \e -> retryOnError "XFTP del worker" (retryLoop loop e delay') (retryDone e) e
|
||||
where
|
||||
retryLoop loop e replicaDelay = do
|
||||
flip catchAgentError (\_ -> pure ()) $ do
|
||||
notifyOnRetry <- asks (xftpNotifyErrsOnRetry . config)
|
||||
when notifyOnRetry $ notify c "" $ SFERR e
|
||||
closeXFTPServerClient c userId server chunkDigest
|
||||
withStore' c $ \db -> updateDeletedSndChunkReplicaDelay db deletedSndChunkReplicaId replicaDelay
|
||||
atomically $ assertAgentForeground c
|
||||
loop
|
||||
retryDone = delWorkerInternalError c deletedSndChunkReplicaId
|
||||
deleteChunkReplica :: DeletedSndChunkReplica -> m ()
|
||||
deleteChunkReplica replica@DeletedSndChunkReplica {userId, deletedSndChunkReplicaId} = do
|
||||
agentXFTPDeleteChunk c userId replica
|
||||
withStore' c $ \db -> deleteDeletedSndChunkReplica db deletedSndChunkReplicaId
|
||||
|
||||
delWorkerInternalError :: AgentMonad m => AgentClient -> Int64 -> AgentErrorType -> m ()
|
||||
delWorkerInternalError c deletedSndChunkReplicaId e = do
|
||||
withStore' c $ \db -> deleteDeletedSndChunkReplica db deletedSndChunkReplicaId
|
||||
notify c "" $ SFERR e
|
||||
|
||||
assertAgentForeground :: AgentClient -> STM ()
|
||||
assertAgentForeground c = do
|
||||
throwWhenInactive c
|
||||
waitUntilForeground c
|
||||
@@ -0,0 +1,35 @@
|
||||
module Simplex.FileTransfer.Chunks where
|
||||
|
||||
import Data.Word (Word32)
|
||||
|
||||
serverChunkSizes :: [Word32]
|
||||
serverChunkSizes = [chunkSize0, chunkSize1, chunkSize2, chunkSize3]
|
||||
{-# INLINE serverChunkSizes #-}
|
||||
|
||||
chunkSize0 :: Word32
|
||||
chunkSize0 = kb 64
|
||||
{-# INLINE chunkSize0 #-}
|
||||
|
||||
chunkSize1 :: Word32
|
||||
chunkSize1 = kb 256
|
||||
{-# INLINE chunkSize1 #-}
|
||||
|
||||
chunkSize2 :: Word32
|
||||
chunkSize2 = mb 1
|
||||
{-# INLINE chunkSize2 #-}
|
||||
|
||||
chunkSize3 :: Word32
|
||||
chunkSize3 = mb 4
|
||||
{-# INLINE chunkSize3 #-}
|
||||
|
||||
kb :: Integral a => a -> a
|
||||
kb n = 1024 * n
|
||||
{-# INLINE kb #-}
|
||||
|
||||
mb :: Integral a => a -> a
|
||||
mb n = 1024 * kb n
|
||||
{-# INLINE mb #-}
|
||||
|
||||
gb :: Integral a => a -> a
|
||||
gb n = 1024 * mb n
|
||||
{-# INLINE gb #-}
|
||||
@@ -0,0 +1,220 @@
|
||||
{-# LANGUAGE BlockArguments #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Simplex.FileTransfer.Client where
|
||||
|
||||
import Control.Monad.Except
|
||||
import Data.Bifunctor (first)
|
||||
import Data.ByteString.Builder (Builder, byteString)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Int (Int64)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import Data.Time (UTCTime)
|
||||
import Data.Word (Word32)
|
||||
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.Transport
|
||||
import Simplex.Messaging.Client
|
||||
( NetworkConfig (..),
|
||||
ProtocolClientError (..),
|
||||
TransportSession,
|
||||
chooseTransportHost,
|
||||
defaultNetworkConfig,
|
||||
proxyUsername,
|
||||
transportClientConfig,
|
||||
)
|
||||
import Simplex.Messaging.Client.Agent ()
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol
|
||||
( BasicAuth,
|
||||
Protocol (..),
|
||||
ProtocolServer (..),
|
||||
RecipientId,
|
||||
SenderId,
|
||||
)
|
||||
import Simplex.Messaging.Transport (supportedParameters)
|
||||
import Simplex.Messaging.Transport.Client (TransportClientConfig, TransportHost)
|
||||
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 UnliftIO
|
||||
import UnliftIO.Directory
|
||||
|
||||
data XFTPClient = XFTPClient
|
||||
{ http2Client :: HTTP2Client,
|
||||
transportSession :: TransportSession FileResponse,
|
||||
config :: XFTPClientConfig
|
||||
}
|
||||
|
||||
data XFTPClientConfig = XFTPClientConfig
|
||||
{ xftpNetworkConfig :: NetworkConfig,
|
||||
uploadTimeoutPerMb :: Int64
|
||||
}
|
||||
|
||||
data XFTPChunkBody = XFTPChunkBody
|
||||
{ chunkSize :: Int,
|
||||
chunkPart :: Int -> IO ByteString,
|
||||
http2Body :: HTTP2Body
|
||||
}
|
||||
|
||||
data XFTPChunkSpec = XFTPChunkSpec
|
||||
{ filePath :: FilePath,
|
||||
chunkOffset :: Int64,
|
||||
chunkSize :: Word32
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
type XFTPClientError = ProtocolClientError XFTPErrorType
|
||||
|
||||
defaultXFTPClientConfig :: XFTPClientConfig
|
||||
defaultXFTPClientConfig =
|
||||
XFTPClientConfig
|
||||
{ xftpNetworkConfig = defaultNetworkConfig,
|
||||
uploadTimeoutPerMb = 10000000 -- 10 seconds
|
||||
}
|
||||
|
||||
getXFTPClient :: TransportSession FileResponse -> XFTPClientConfig -> (XFTPClient -> IO ()) -> IO (Either XFTPClientError XFTPClient)
|
||||
getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {xftpNetworkConfig} disconnected = runExceptT $ do
|
||||
let tcConfig = transportClientConfig xftpNetworkConfig
|
||||
http2Config = xftpHTTP2Config tcConfig config
|
||||
username = proxyUsername transportSession
|
||||
ProtocolServer _ host port keyHash = srv
|
||||
useHost <- liftEither $ chooseTransportHost xftpNetworkConfig host
|
||||
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}
|
||||
atomically $ writeTVar clientVar $ Just c
|
||||
pure c
|
||||
|
||||
closeXFTPClient :: XFTPClient -> IO ()
|
||||
closeXFTPClient XFTPClient {http2Client} = closeHTTP2Client http2Client
|
||||
|
||||
xftpClientServer :: XFTPClient -> String
|
||||
xftpClientServer = B.unpack . strEncode . snd3 . transportSession
|
||||
where
|
||||
snd3 (_, s, _) = s
|
||||
|
||||
xftpTransportHost :: XFTPClient -> TransportHost
|
||||
xftpTransportHost XFTPClient {http2Client = HTTP2Client {client_ = HClient {host}}} = host
|
||||
|
||||
xftpSessionTs :: XFTPClient -> UTCTime
|
||||
xftpSessionTs = sessionTs . http2Client
|
||||
|
||||
xftpHTTP2Config :: TransportClientConfig -> XFTPClientConfig -> HTTP2ClientConfig
|
||||
xftpHTTP2Config transportConfig XFTPClientConfig {xftpNetworkConfig = NetworkConfig {tcpConnectTimeout}} =
|
||||
defaultHTTP2ClientConfig
|
||||
{ bodyHeadSize = xftpBlockSize,
|
||||
suportedTLSParams = supportedParameters,
|
||||
connTimeout = tcpConnectTimeout,
|
||||
transportConfig
|
||||
}
|
||||
|
||||
xftpClientError :: HTTP2ClientError -> XFTPClientError
|
||||
xftpClientError = \case
|
||||
HCResponseTimeout -> PCEResponseTimeout
|
||||
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 XFTPClient {config, http2Client = http2@HTTP2Client {sessionId}} pKey fId cmd chunkSpec_ = do
|
||||
t <-
|
||||
liftEither . first PCETransportError $
|
||||
xftpEncodeTransmission sessionId (Just pKey) ("", fId, FileCmd (sFileParty @p) cmd)
|
||||
let req = H.requestStreaming N.methodPost "/" [] $ streamBody t
|
||||
reqTimeout = (\XFTPChunkSpec {chunkSize} -> chunkTimeout config chunkSize) <$> chunkSpec_
|
||||
HTTP2Response {respBody = body@HTTP2Body {bodyHead}} <- liftEitherError xftpClientError $ sendRequest http2 req 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
|
||||
case respOrErr of
|
||||
Right r -> case protocolError r of
|
||||
Just e -> throwError $ PCEProtocolError e
|
||||
_ -> pure (r, body)
|
||||
Left e -> throwError $ PCEResponseError e
|
||||
where
|
||||
streamBody :: ByteString -> (Builder -> IO ()) -> IO () -> IO ()
|
||||
streamBody t send done = do
|
||||
send $ byteString t
|
||||
forM_ chunkSpec_ $ \XFTPChunkSpec {filePath, chunkOffset, chunkSize} ->
|
||||
withFile filePath ReadMode $ \h -> do
|
||||
hSeek h AbsoluteSeek $ fromIntegral chunkOffset
|
||||
hSendFile h send $ fromIntegral chunkSize
|
||||
done
|
||||
|
||||
createXFTPChunk ::
|
||||
XFTPClient ->
|
||||
C.APrivateSignKey ->
|
||||
FileInfo ->
|
||||
NonEmpty C.APublicVerifyKey ->
|
||||
Maybe BasicAuth ->
|
||||
ExceptT XFTPClientError IO (SenderId, NonEmpty RecipientId)
|
||||
createXFTPChunk c spKey file rcps auth_ =
|
||||
sendXFTPCommand c spKey "" (FNEW file rcps auth_) Nothing >>= \case
|
||||
(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 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 c spKey fId chunkSpec =
|
||||
sendXFTPCommand c spKey fId FPUT (Just chunkSpec) >>= okResponse
|
||||
|
||||
downloadXFTPChunk :: XFTPClient -> C.APrivateSignKey -> XFTPFileId -> XFTPRcvChunkSpec -> ExceptT XFTPClientError IO ()
|
||||
downloadXFTPChunk c@XFTPClient {config} rpKey fId chunkSpec@XFTPRcvChunkSpec {filePath, chunkSize} = do
|
||||
(rDhKey, rpDhKey) <- liftIO C.generateKeyPair'
|
||||
sendXFTPCommand c rpKey fId (FGET rDhKey) Nothing >>= \case
|
||||
(FRFile sDhKey cbNonce, HTTP2Body {bodyHead = _bg, bodySize = _bs, bodyPart}) -> case bodyPart of
|
||||
-- TODO atm bodySize is set to 0, so chunkSize will be incorrect - validate once set
|
||||
Just chunkPart -> do
|
||||
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
|
||||
where
|
||||
download cbState =
|
||||
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
|
||||
|
||||
deleteXFTPChunk :: XFTPClient -> C.APrivateSignKey -> SenderId -> ExceptT XFTPClientError IO ()
|
||||
deleteXFTPChunk c spKey sId = sendXFTPCommand c spKey sId FDEL Nothing >>= okResponse
|
||||
|
||||
ackXFTPChunk :: XFTPClient -> C.APrivateSignKey -> RecipientId -> ExceptT XFTPClientError IO ()
|
||||
ackXFTPChunk c rpKey rId = sendXFTPCommand c rpKey rId FACK Nothing >>= okResponse
|
||||
|
||||
okResponse :: (FileResponse, HTTP2Body) -> ExceptT XFTPClientError IO ()
|
||||
okResponse = \case
|
||||
(FROk, body) -> noFile body ()
|
||||
(r, _) -> throwError . PCEUnexpectedResponse $ bshow r
|
||||
|
||||
-- TODO this currently does not check anything because response size is not set and bodyPart is always Just
|
||||
noFile :: HTTP2Body -> a -> ExceptT XFTPClientError IO a
|
||||
noFile HTTP2Body {bodyPart} a = case bodyPart of
|
||||
Just _ -> pure a -- throwError $ PCEResponseError HAS_FILE
|
||||
_ -> pure a
|
||||
|
||||
-- FACK :: FileCommand Recipient
|
||||
-- PING :: FileCommand Recipient
|
||||
@@ -0,0 +1,127 @@
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Simplex.FileTransfer.Client.Agent where
|
||||
|
||||
import Control.Logger.Simple (logInfo)
|
||||
import Control.Monad.Except
|
||||
import Data.Bifunctor (first)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (decodeUtf8)
|
||||
import Simplex.FileTransfer.Client
|
||||
import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientError (..), temporaryClientError)
|
||||
import Simplex.Messaging.Client.Agent ()
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (ProtocolServer (..), XFTPServer)
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Util (catchAll_, tryError)
|
||||
import UnliftIO
|
||||
|
||||
type XFTPClientVar = TMVar (Either XFTPClientAgentError XFTPClient)
|
||||
|
||||
data XFTPClientAgent = XFTPClientAgent
|
||||
{ xftpClients :: TMap XFTPServer XFTPClientVar,
|
||||
config :: XFTPClientAgentConfig
|
||||
}
|
||||
|
||||
data XFTPClientAgentConfig = XFTPClientAgentConfig
|
||||
{ xftpConfig :: XFTPClientConfig,
|
||||
reconnectInterval :: RetryInterval
|
||||
}
|
||||
|
||||
defaultXFTPClientAgentConfig :: XFTPClientAgentConfig
|
||||
defaultXFTPClientAgentConfig =
|
||||
XFTPClientAgentConfig
|
||||
{ xftpConfig = defaultXFTPClientConfig,
|
||||
reconnectInterval =
|
||||
RetryInterval
|
||||
{ initialInterval = 5_000000,
|
||||
increaseAfter = 10_000000,
|
||||
maxInterval = 60_000000
|
||||
}
|
||||
}
|
||||
|
||||
data XFTPClientAgentError = XFTPClientAgentError XFTPServer XFTPClientError
|
||||
deriving (Show, Exception)
|
||||
|
||||
newXFTPAgent :: XFTPClientAgentConfig -> STM XFTPClientAgent
|
||||
newXFTPAgent config = do
|
||||
xftpClients <- TM.empty
|
||||
pure XFTPClientAgent {xftpClients, config}
|
||||
|
||||
type ME a = ExceptT XFTPClientAgentError IO a
|
||||
|
||||
getXFTPServerClient :: XFTPClientAgent -> XFTPServer -> ME XFTPClient
|
||||
getXFTPServerClient XFTPClientAgent {xftpClients, config} srv = do
|
||||
atomically getClientVar >>= either newXFTPClient waitForXFTPClient
|
||||
where
|
||||
connectClient :: ME XFTPClient
|
||||
connectClient =
|
||||
ExceptT $
|
||||
first (XFTPClientAgentError srv)
|
||||
<$> getXFTPClient (1, srv, Nothing) (xftpConfig config) clientDisconnected
|
||||
|
||||
clientDisconnected :: XFTPClient -> IO ()
|
||||
clientDisconnected _ = do
|
||||
atomically $ TM.delete srv xftpClients
|
||||
logInfo $ "disconnected from " <> showServer srv
|
||||
|
||||
getClientVar :: STM (Either XFTPClientVar XFTPClientVar)
|
||||
getClientVar = maybe (Left <$> newClientVar) (pure . Right) =<< TM.lookup srv xftpClients
|
||||
where
|
||||
newClientVar :: STM XFTPClientVar
|
||||
newClientVar = do
|
||||
var <- newEmptyTMVar
|
||||
TM.insert srv var xftpClients
|
||||
pure var
|
||||
|
||||
waitForXFTPClient :: XFTPClientVar -> ME XFTPClient
|
||||
waitForXFTPClient clientVar = do
|
||||
let XFTPClientConfig {xftpNetworkConfig = NetworkConfig {tcpConnectTimeout}} = xftpConfig config
|
||||
client_ <- tcpConnectTimeout `timeout` atomically (readTMVar clientVar)
|
||||
liftEither $ case client_ of
|
||||
Just (Right c) -> Right c
|
||||
Just (Left e) -> Left e
|
||||
Nothing -> Left $ XFTPClientAgentError srv PCEResponseTimeout
|
||||
|
||||
newXFTPClient :: XFTPClientVar -> ME XFTPClient
|
||||
newXFTPClient clientVar = tryConnectClient tryConnectAsync
|
||||
where
|
||||
tryConnectClient :: ME () -> ME XFTPClient
|
||||
tryConnectClient retryAction =
|
||||
tryError connectClient >>= \r -> case r of
|
||||
Right client -> do
|
||||
logInfo $ "connected to " <> showServer srv
|
||||
atomically $ putTMVar clientVar r
|
||||
pure client
|
||||
Left e@(XFTPClientAgentError _ e') -> do
|
||||
if temporaryClientError e'
|
||||
then retryAction
|
||||
else atomically $ do
|
||||
putTMVar clientVar r
|
||||
TM.delete srv xftpClients
|
||||
throwError e
|
||||
tryConnectAsync :: ME ()
|
||||
tryConnectAsync = void . async $ do
|
||||
withRetryInterval (reconnectInterval config) $ \_ loop -> void $ tryConnectClient loop
|
||||
|
||||
showServer :: XFTPServer -> Text
|
||||
showServer ProtocolServer {host, port} =
|
||||
decodeUtf8 $ strEncode host <> B.pack (if null port then "" else ':' : port)
|
||||
|
||||
closeXFTPServerClient :: XFTPClientAgent -> XFTPServer -> IO ()
|
||||
closeXFTPServerClient XFTPClientAgent {xftpClients, config} srv =
|
||||
atomically (TM.lookupDelete srv xftpClients) >>= mapM_ closeClient
|
||||
where
|
||||
closeClient cVar = do
|
||||
let NetworkConfig {tcpConnectTimeout} = xftpNetworkConfig $ xftpConfig config
|
||||
tcpConnectTimeout `timeout` atomically (readTMVar cVar) >>= \case
|
||||
Just (Right client) -> closeXFTPClient client `catchAll_` pure ()
|
||||
_ -> pure ()
|
||||
@@ -0,0 +1,598 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Simplex.FileTransfer.Client.Main
|
||||
( SendOptions (..),
|
||||
CLIError (..),
|
||||
xftpClientCLI,
|
||||
cliSendFile,
|
||||
cliSendFileOpts,
|
||||
prepareChunkSizes,
|
||||
prepareChunkSpecs,
|
||||
maxFileSize,
|
||||
fileSizeLen,
|
||||
getChunkDigest,
|
||||
SentRecipientReplica (..),
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Crypto.Random (getRandomBytes)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bifunctor (first)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Char (toLower)
|
||||
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 qualified Data.Text as T
|
||||
import Data.Word (Word32)
|
||||
import GHC.Records (HasField (getField))
|
||||
import Options.Applicative
|
||||
import Simplex.FileTransfer.Chunks
|
||||
import Simplex.FileTransfer.Client
|
||||
import Simplex.FileTransfer.Client.Agent
|
||||
import Simplex.FileTransfer.Client.Presets
|
||||
import Simplex.FileTransfer.Crypto
|
||||
import Simplex.FileTransfer.Description
|
||||
import Simplex.FileTransfer.Protocol
|
||||
import Simplex.FileTransfer.Transport (XFTPRcvChunkSpec (..))
|
||||
import Simplex.FileTransfer.Types
|
||||
import Simplex.FileTransfer.Util (uniqueCombine)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..), FTCryptoError (..))
|
||||
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 (StrEncoding (..))
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), SenderId, SndPrivateSignKey, XFTPServer, XFTPServerWithAuth)
|
||||
import Simplex.Messaging.Server.CLI (getCliCommand')
|
||||
import Simplex.Messaging.Util (groupAllOn, ifM, tshow, whenM)
|
||||
import System.Exit (exitFailure)
|
||||
import System.FilePath (splitFileName, (</>))
|
||||
import System.IO.Temp (getCanonicalTemporaryDirectory)
|
||||
import System.Random (StdGen, newStdGen, randomR)
|
||||
import UnliftIO
|
||||
import UnliftIO.Directory
|
||||
|
||||
xftpClientVersion :: String
|
||||
xftpClientVersion = "1.0.1"
|
||||
|
||||
maxFileSize :: Int64
|
||||
maxFileSize = gb 1
|
||||
|
||||
maxFileSizeStr :: String
|
||||
maxFileSizeStr = B.unpack . strEncode $ FileSize maxFileSize
|
||||
|
||||
fileSizeLen :: Int64
|
||||
fileSizeLen = 8
|
||||
|
||||
newtype CLIError = CLIError String
|
||||
deriving (Eq, Show, Exception)
|
||||
|
||||
cliCryptoError :: FTCryptoError -> CLIError
|
||||
cliCryptoError = \case
|
||||
FTCECryptoError e -> CLIError $ "Error decrypting file: " <> show e
|
||||
FTCEInvalidHeader e -> CLIError $ "Invalid file header: " <> e
|
||||
FTCEInvalidAuthTag -> CLIError "Error decrypting file: incorrect auth tag"
|
||||
FTCEInvalidFileSize -> CLIError "Error decrypting file: incorrect file size"
|
||||
FTCEFileIOError e -> CLIError $ "File IO error: " <> show e
|
||||
|
||||
data CliCommand
|
||||
= SendFile SendOptions
|
||||
| ReceiveFile ReceiveOptions
|
||||
| DeleteFile DeleteOptions
|
||||
| RandomFile RandomFileOptions
|
||||
| FileDescrInfo InfoOptions
|
||||
|
||||
data SendOptions = SendOptions
|
||||
{ filePath :: FilePath,
|
||||
outputDir :: Maybe FilePath,
|
||||
numRecipients :: Int,
|
||||
xftpServers :: [XFTPServerWithAuth],
|
||||
retryCount :: Int,
|
||||
tempPath :: Maybe FilePath,
|
||||
verbose :: Bool
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data ReceiveOptions = ReceiveOptions
|
||||
{ fileDescription :: FilePath,
|
||||
filePath :: Maybe FilePath,
|
||||
retryCount :: Int,
|
||||
tempPath :: Maybe FilePath,
|
||||
verbose :: Bool,
|
||||
yes :: Bool
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data DeleteOptions = DeleteOptions
|
||||
{ fileDescription :: FilePath,
|
||||
retryCount :: Int,
|
||||
verbose :: Bool,
|
||||
yes :: Bool
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
newtype InfoOptions = InfoOptions
|
||||
{ fileDescription :: FilePath
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data RandomFileOptions = RandomFileOptions
|
||||
{ filePath :: FilePath,
|
||||
fileSize :: FileSize Int64
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
defaultRetryCount :: Int
|
||||
defaultRetryCount = 3
|
||||
|
||||
cliCommandP :: Parser CliCommand
|
||||
cliCommandP =
|
||||
hsubparser
|
||||
( command "send" (info (SendFile <$> sendP) (progDesc "Send file"))
|
||||
<> command "recv" (info (ReceiveFile <$> receiveP) (progDesc "Receive file"))
|
||||
<> command "del" (info (DeleteFile <$> deleteP) (progDesc "Delete file from server(s)"))
|
||||
<> command "info" (info (FileDescrInfo <$> infoP) (progDesc "Show file description"))
|
||||
)
|
||||
<|> hsubparser (command "rand" (info (RandomFile <$> randomP) (progDesc "Generate a random file of a given size")) <> internal)
|
||||
where
|
||||
sendP :: Parser SendOptions
|
||||
sendP =
|
||||
SendOptions
|
||||
<$> argument str (metavar "FILE" <> help "File to send")
|
||||
<*> optional (argument str $ metavar "DIR" <> help "Directory to save file descriptions (default: current directory)")
|
||||
<*> option auto (short 'n' <> metavar "COUNT" <> help "Number of recipients" <> value 1 <> showDefault)
|
||||
<*> xftpServers
|
||||
<*> retryCountP
|
||||
<*> temp
|
||||
<*> verboseP
|
||||
receiveP :: Parser ReceiveOptions
|
||||
receiveP =
|
||||
ReceiveOptions
|
||||
<$> fileDescrArg
|
||||
<*> optional (argument str $ metavar "DIR" <> help "Directory to save file (default: system Downloads directory)")
|
||||
<*> retryCountP
|
||||
<*> temp
|
||||
<*> verboseP
|
||||
<*> yesP
|
||||
deleteP :: Parser DeleteOptions
|
||||
deleteP =
|
||||
DeleteOptions
|
||||
<$> fileDescrArg
|
||||
<*> retryCountP
|
||||
<*> verboseP
|
||||
<*> yesP
|
||||
infoP :: Parser InfoOptions
|
||||
infoP = InfoOptions <$> fileDescrArg
|
||||
randomP :: Parser RandomFileOptions
|
||||
randomP =
|
||||
RandomFileOptions
|
||||
<$> argument str (metavar "FILE" <> help "Path to save file")
|
||||
<*> argument str (metavar "SIZE" <> help "File size (bytes/kb/mb/gb)")
|
||||
fileDescrArg = argument str (metavar "FILE" <> help "File description file")
|
||||
retryCountP = option auto (long "retry" <> short 'r' <> metavar "RETRY" <> help "Number of network retries" <> value defaultRetryCount <> showDefault)
|
||||
temp = optional (strOption $ long "tmp" <> metavar "TMP" <> help "Directory for temporary encrypted file (default: system temp directory)")
|
||||
verboseP = switch (long "verbose" <> short 'v' <> help "Verbose output")
|
||||
yesP = switch (long "yes" <> short 'y' <> help "Yes to questions")
|
||||
xftpServers =
|
||||
option
|
||||
parseXFTPServers
|
||||
( long "servers"
|
||||
<> short 's'
|
||||
<> metavar "SERVER"
|
||||
<> help "Semicolon-separated list of XFTP server(s) to use (each server can have more than one hostname)"
|
||||
<> value []
|
||||
)
|
||||
parseXFTPServers = eitherReader $ parseAll xftpServersP . B.pack
|
||||
xftpServersP = strP `A.sepBy1` A.char ';'
|
||||
|
||||
data SentFileChunk = SentFileChunk
|
||||
{ chunkNo :: Int,
|
||||
sndId :: SenderId,
|
||||
sndPrivateKey :: SndPrivateSignKey,
|
||||
chunkSize :: FileSize Word32,
|
||||
digest :: FileDigest,
|
||||
replicas :: [SentFileChunkReplica]
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data SentFileChunkReplica = SentFileChunkReplica
|
||||
{ server :: XFTPServer,
|
||||
recipients :: [(ChunkReplicaId, C.APrivateSignKey)]
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data SentRecipientReplica = SentRecipientReplica
|
||||
{ chunkNo :: Int,
|
||||
server :: XFTPServer,
|
||||
rcvNo :: Int,
|
||||
replicaId :: ChunkReplicaId,
|
||||
replicaKey :: C.APrivateSignKey,
|
||||
digest :: FileDigest,
|
||||
chunkSize :: FileSize Word32
|
||||
}
|
||||
|
||||
logCfg :: LogConfig
|
||||
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
xftpClientCLI :: IO ()
|
||||
xftpClientCLI =
|
||||
getCliCommand' cliCommandP clientVersion >>= \case
|
||||
SendFile opts -> runLogE opts $ cliSendFile opts
|
||||
ReceiveFile opts -> runLogE opts $ cliReceiveFile opts
|
||||
DeleteFile opts -> runLogE opts $ cliDeleteFile opts
|
||||
FileDescrInfo opts -> runE $ cliFileDescrInfo opts
|
||||
RandomFile opts -> cliRandomFile opts
|
||||
where
|
||||
clientVersion = "SimpleX XFTP client v" <> xftpClientVersion
|
||||
|
||||
runLogE :: HasField "verbose" a Bool => a -> ExceptT CLIError IO () -> IO ()
|
||||
runLogE opts a
|
||||
| getField @"verbose" opts = setLogLevel LogDebug >> withGlobalLogging logCfg (runE a)
|
||||
| otherwise = runE a
|
||||
|
||||
runE :: ExceptT CLIError IO () -> IO ()
|
||||
runE a =
|
||||
runExceptT a >>= \case
|
||||
Left (CLIError e) -> putStrLn e >> exitFailure
|
||||
_ -> pure ()
|
||||
|
||||
cliSendFile :: SendOptions -> ExceptT CLIError IO ()
|
||||
cliSendFile opts = cliSendFileOpts opts True $ printProgress "Uploaded"
|
||||
|
||||
cliSendFileOpts :: SendOptions -> Bool -> (Int64 -> Int64 -> IO ()) -> ExceptT CLIError IO ()
|
||||
cliSendFileOpts SendOptions {filePath, outputDir, numRecipients, xftpServers, retryCount, tempPath, verbose} printInfo notifyProgress = do
|
||||
let (_, fileName) = splitFileName filePath
|
||||
liftIO $ when printInfo $ printNoNewLine "Encrypting file..."
|
||||
(encPath, fdRcv, fdSnd, chunkSpecs, encSize) <- encryptFileForUpload fileName
|
||||
liftIO $ when printInfo $ printNoNewLine "Uploading file..."
|
||||
uploadedChunks <- newTVarIO []
|
||||
sentChunks <- uploadFile chunkSpecs uploadedChunks encSize
|
||||
whenM (doesFileExist encPath) $ removeFile encPath
|
||||
-- TODO if only small chunks, use different default size
|
||||
liftIO $ do
|
||||
let fdRcvs = createRcvFileDescriptions fdRcv sentChunks
|
||||
fdSnd' = createSndFileDescription fdSnd sentChunks
|
||||
(fdRcvPaths, fdSndPath) <- writeFileDescriptions fileName fdRcvs fdSnd'
|
||||
when printInfo $ do
|
||||
printNoNewLine "File uploaded!"
|
||||
putStrLn $ "\nSender file description: " <> fdSndPath
|
||||
putStrLn "Pass file descriptions to the recipient(s):"
|
||||
forM_ fdRcvPaths putStrLn
|
||||
where
|
||||
encryptFileForUpload :: String -> ExceptT CLIError IO (FilePath, FileDescription 'FRecipient, FileDescription 'FSender, [XFTPChunkSpec], Int64)
|
||||
encryptFileForUpload fileName = do
|
||||
fileSize <- fromInteger <$> getFileSize filePath
|
||||
when (fileSize > maxFileSize) $ throwError $ CLIError $ "Files bigger than " <> maxFileSizeStr <> " are not supported"
|
||||
encPath <- getEncPath tempPath "xftp"
|
||||
key <- liftIO C.randomSbKey
|
||||
nonce <- liftIO C.randomCbNonce
|
||||
let fileHdr = smpEncode FileHeader {fileName, fileExtra = Nothing}
|
||||
fileSize' = fromIntegral (B.length fileHdr) + fileSize
|
||||
chunkSizes = prepareChunkSizes $ fileSize' + fileSizeLen + authTagSize
|
||||
defChunkSize = head chunkSizes
|
||||
chunkSizes' = map fromIntegral chunkSizes
|
||||
encSize = sum chunkSizes'
|
||||
srcFile = CF.plain filePath
|
||||
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 = []}
|
||||
logInfo $ "encrypted file to " <> tshow encPath
|
||||
pure (encPath, fdRcv, fdSnd, chunkSpecs, encSize)
|
||||
uploadFile :: [XFTPChunkSpec] -> TVar [Int64] -> Int64 -> ExceptT CLIError IO [SentFileChunk]
|
||||
uploadFile chunks uploadedChunks encSize = do
|
||||
a <- atomically $ newXFTPAgent defaultXFTPClientAgentConfig
|
||||
gen <- newTVarIO =<< liftIO newStdGen
|
||||
let xftpSrvs = fromMaybe defaultXFTPServers (nonEmpty xftpServers)
|
||||
srvs <- liftIO $ replicateM (length chunks) $ getXFTPServer gen xftpSrvs
|
||||
let thd3 (_, _, x) = x
|
||||
chunks' = groupAllOn thd3 $ zip3 [1 ..] chunks srvs
|
||||
-- TODO shuffle/unshuffle chunks
|
||||
-- the reason we don't do pooled downloads here within one server is that http2 library doesn't handle cleint concurrency, even though
|
||||
-- upload doesn't allow other requests within the same client until complete (but download does allow).
|
||||
logInfo $ "uploading " <> tshow (length chunks) <> " chunks..."
|
||||
map snd . sortOn fst . concat <$> pooledForConcurrentlyN 16 chunks' (mapM $ uploadFileChunk a)
|
||||
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) <- liftIO $ C.generateSignatureKeyPair C.SEd25519
|
||||
rKeys <- liftIO $ L.fromList <$> replicateM numRecipients (C.generateSignatureKeyPair C.SEd25519)
|
||||
digest <- liftIO $ getChunkDigest chunkSpec
|
||||
let ch = FileInfo {sndKey, size = fromIntegral chunkSize, digest}
|
||||
c <- withRetry retryCount $ getXFTPServerClient a xftpServer
|
||||
(sndId, rIds) <- withRetry retryCount $ createXFTPChunk c spKey ch (L.map fst rKeys) auth
|
||||
withReconnect a xftpServer retryCount $ \c' -> uploadXFTPChunk c' spKey sndId chunkSpec
|
||||
logInfo $ "uploaded chunk " <> tshow chunkNo
|
||||
uploaded <- atomically . stateTVar uploadedChunks $ \cs ->
|
||||
let cs' = fromIntegral chunkSize : cs in (sum cs', cs')
|
||||
liftIO $ do
|
||||
notifyProgress uploaded encSize
|
||||
when verbose $ putStrLn ""
|
||||
let recipients = L.toList $ L.map ChunkReplicaId rIds `L.zip` L.map snd rKeys
|
||||
replicas = [SentFileChunkReplica {server = xftpServer, recipients}]
|
||||
pure (chunkNo, SentFileChunk {chunkNo, sndId, sndPrivateKey = spKey, chunkSize = FileSize $ fromIntegral chunkSize, digest = FileDigest digest, replicas})
|
||||
getXFTPServer :: TVar StdGen -> NonEmpty XFTPServerWithAuth -> IO XFTPServerWithAuth
|
||||
getXFTPServer gen = \case
|
||||
srv :| [] -> pure srv
|
||||
servers -> do
|
||||
atomically $ (servers L.!!) <$> stateTVar gen (randomR (0, L.length servers - 1))
|
||||
|
||||
-- M chunks, R replicas, N recipients
|
||||
-- rcvReplicas: M[SentFileChunk] -> M * R * N [SentRecipientReplica]
|
||||
-- rcvChunks: M * R * N [SentRecipientReplica] -> N[ M[FileChunk] ]
|
||||
createRcvFileDescriptions :: FileDescription 'FRecipient -> [SentFileChunk] -> [FileDescription 'FRecipient]
|
||||
createRcvFileDescriptions fd sentChunks = map (\chunks -> (fd :: (FileDescription 'FRecipient)) {chunks}) rcvChunks
|
||||
where
|
||||
rcvReplicas :: [SentRecipientReplica]
|
||||
rcvReplicas =
|
||||
concatMap
|
||||
( \SentFileChunk {chunkNo, digest, chunkSize, replicas} ->
|
||||
concatMap
|
||||
( \SentFileChunkReplica {server, recipients} ->
|
||||
zipWith (\rcvNo (replicaId, replicaKey) -> SentRecipientReplica {chunkNo, server, rcvNo, replicaId, replicaKey, digest, chunkSize}) [1 ..] recipients
|
||||
)
|
||||
replicas
|
||||
)
|
||||
sentChunks
|
||||
rcvChunks :: [[FileChunk]]
|
||||
rcvChunks = map (sortChunks . M.elems) $ M.elems $ foldl' addRcvChunk M.empty rcvReplicas
|
||||
sortChunks :: [FileChunk] -> [FileChunk]
|
||||
sortChunks = map reverseReplicas . sortOn (\FileChunk {chunkNo} -> chunkNo)
|
||||
reverseReplicas ch@FileChunk {replicas} = (ch :: FileChunk) {replicas = reverse replicas}
|
||||
addRcvChunk :: Map Int (Map Int FileChunk) -> SentRecipientReplica -> Map Int (Map Int FileChunk)
|
||||
addRcvChunk m SentRecipientReplica {chunkNo, server, rcvNo, replicaId, replicaKey, digest, chunkSize} =
|
||||
M.alter (Just . addOrChangeRecipient) rcvNo m
|
||||
where
|
||||
addOrChangeRecipient :: Maybe (Map Int FileChunk) -> Map Int FileChunk
|
||||
addOrChangeRecipient = \case
|
||||
Just m' -> M.alter (Just . addOrChangeChunk) chunkNo m'
|
||||
_ -> M.singleton chunkNo $ FileChunk {chunkNo, digest, chunkSize, replicas = [replica]}
|
||||
addOrChangeChunk :: Maybe FileChunk -> FileChunk
|
||||
addOrChangeChunk = \case
|
||||
Just ch@FileChunk {replicas} -> ch {replicas = replica : replicas}
|
||||
_ -> FileChunk {chunkNo, digest, chunkSize, replicas = [replica]}
|
||||
replica = FileChunkReplica {server, replicaId, replicaKey}
|
||||
createSndFileDescription :: FileDescription 'FSender -> [SentFileChunk] -> FileDescription 'FSender
|
||||
createSndFileDescription fd sentChunks = fd {chunks = sndChunks}
|
||||
where
|
||||
sndChunks :: [FileChunk]
|
||||
sndChunks =
|
||||
map
|
||||
( \SentFileChunk {chunkNo, sndId, sndPrivateKey, chunkSize, digest, replicas} ->
|
||||
FileChunk {chunkNo, digest, chunkSize, replicas = sndReplicas replicas (ChunkReplicaId sndId) sndPrivateKey}
|
||||
)
|
||||
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 [] _ _ = []
|
||||
sndReplicas (SentFileChunkReplica {server} : _) replicaId replicaKey = [FileChunkReplica {server, replicaId, replicaKey}]
|
||||
writeFileDescriptions :: String -> [FileDescription 'FRecipient] -> FileDescription 'FSender -> IO ([FilePath], FilePath)
|
||||
writeFileDescriptions fileName fdRcvs fdSnd = do
|
||||
outDir <- uniqueCombine (fromMaybe "." outputDir) (fileName <> ".xftp")
|
||||
createDirectoryIfMissing True outDir
|
||||
fdRcvPaths <- forM (zip [1 ..] fdRcvs) $ \(i :: Int, fd) -> do
|
||||
let fdPath = outDir </> ("rcv" <> show i <> ".xftp")
|
||||
B.writeFile fdPath $ strEncode fd
|
||||
pure fdPath
|
||||
let fdSndPath = outDir </> "snd.xftp.private"
|
||||
B.writeFile fdSndPath $ strEncode fdSnd
|
||||
pure (fdRcvPaths, fdSndPath)
|
||||
|
||||
getChunkDigest :: XFTPChunkSpec -> IO ByteString
|
||||
getChunkDigest XFTPChunkSpec {filePath = chunkPath, chunkOffset, chunkSize} =
|
||||
withFile chunkPath ReadMode $ \h -> do
|
||||
hSeek h AbsoluteSeek $ fromIntegral chunkOffset
|
||||
LC.sha256Hash <$> LB.hGet h (fromIntegral chunkSize)
|
||||
|
||||
cliReceiveFile :: ReceiveOptions -> ExceptT CLIError IO ()
|
||||
cliReceiveFile ReceiveOptions {fileDescription, filePath, retryCount, tempPath, verbose, yes} =
|
||||
getFileDescription' fileDescription >>= receive
|
||||
where
|
||||
receive :: ValidFileDescription 'FRecipient -> ExceptT CLIError IO ()
|
||||
receive (ValidFileDescription FileDescription {size, digest, key, nonce, chunks}) = do
|
||||
encPath <- getEncPath tempPath "xftp"
|
||||
createDirectory encPath
|
||||
a <- atomically $ newXFTPAgent defaultXFTPClientAgentConfig
|
||||
liftIO $ printNoNewLine "Downloading file..."
|
||||
downloadedChunks <- newTVarIO []
|
||||
let srv FileChunk {replicas} = case replicas of
|
||||
[] -> error "empty FileChunk.replicas"
|
||||
FileChunkReplica {server} : _ -> server
|
||||
srvChunks = groupAllOn srv chunks
|
||||
chunkPaths <- map snd . sortOn fst . concat <$> pooledForConcurrentlyN 16 srvChunks (mapM $ downloadFileChunk a encPath size downloadedChunks)
|
||||
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
|
||||
when (FileSize encSize /= size) $ throwError $ CLIError "File size mismatch"
|
||||
liftIO $ printNoNewLine "Decrypting file..."
|
||||
CryptoFile path _ <- withExceptT cliCryptoError $ decryptChunks encSize chunkPaths key nonce $ fmap CF.plain . getFilePath
|
||||
forM_ chunks $ acknowledgeFileChunk a
|
||||
whenM (doesPathExist encPath) $ removeDirectoryRecursive encPath
|
||||
liftIO $ do
|
||||
printNoNewLine $ "File downloaded: " <> path
|
||||
removeFD yes fileDescription
|
||||
downloadFileChunk :: XFTPClientAgent -> FilePath -> FileSize Int64 -> TVar [Int64] -> FileChunk -> ExceptT CLIError IO (Int, FilePath)
|
||||
downloadFileChunk a encPath (FileSize encSize) downloadedChunks FileChunk {chunkNo, chunkSize, digest, replicas = replica : _} = do
|
||||
let FileChunkReplica {server, replicaId, replicaKey} = replica
|
||||
logInfo $ "downloading chunk " <> tshow chunkNo <> " from " <> showServer server <> "..."
|
||||
chunkPath <- uniqueCombine encPath $ show chunkNo
|
||||
let chunkSpec = XFTPRcvChunkSpec chunkPath (unFileSize chunkSize) (unFileDigest digest)
|
||||
withReconnect a server retryCount $ \c -> downloadXFTPChunk c replicaKey (unChunkReplicaId replicaId) chunkSpec
|
||||
logInfo $ "downloaded chunk " <> tshow chunkNo <> " to " <> T.pack chunkPath
|
||||
downloaded <- atomically . stateTVar downloadedChunks $ \cs ->
|
||||
let cs' = fromIntegral (unFileSize chunkSize) : cs in (sum cs', cs')
|
||||
liftIO $ do
|
||||
printProgress "Downloaded" downloaded encSize
|
||||
when verbose $ putStrLn ""
|
||||
pure (chunkNo, chunkPath)
|
||||
downloadFileChunk _ _ _ _ _ = throwError $ CLIError "chunk has no replicas"
|
||||
getFilePath :: String -> ExceptT String IO FilePath
|
||||
getFilePath name =
|
||||
case filePath of
|
||||
Just path ->
|
||||
ifM (doesDirectoryExist path) (uniqueCombine path name) $
|
||||
ifM (doesFileExist path) (throwError "File already exists") (pure path)
|
||||
_ -> (`uniqueCombine` name) . (</> "Downloads") =<< getHomeDirectory
|
||||
acknowledgeFileChunk :: XFTPClientAgent -> FileChunk -> ExceptT CLIError IO ()
|
||||
acknowledgeFileChunk a FileChunk {replicas = replica : _} = do
|
||||
let FileChunkReplica {server, replicaId, replicaKey} = replica
|
||||
c <- withRetry retryCount $ getXFTPServerClient a server
|
||||
withRetry retryCount $ ackXFTPChunk c replicaKey (unChunkReplicaId replicaId)
|
||||
acknowledgeFileChunk _ _ = throwError $ CLIError "chunk has no replicas"
|
||||
|
||||
printProgress :: String -> Int64 -> Int64 -> IO ()
|
||||
printProgress s part total = printNoNewLine $ s <> " " <> show ((part * 100) `div` total) <> "%"
|
||||
|
||||
printNoNewLine :: String -> IO ()
|
||||
printNoNewLine s = do
|
||||
putStr $ s <> replicate (max 0 $ 25 - length s) ' ' <> "\r"
|
||||
hFlush stdout
|
||||
|
||||
cliDeleteFile :: DeleteOptions -> ExceptT CLIError IO ()
|
||||
cliDeleteFile DeleteOptions {fileDescription, retryCount, yes} = do
|
||||
getFileDescription' fileDescription >>= deleteFile
|
||||
where
|
||||
deleteFile :: ValidFileDescription 'FSender -> ExceptT CLIError IO ()
|
||||
deleteFile (ValidFileDescription FileDescription {chunks}) = do
|
||||
a <- atomically $ newXFTPAgent defaultXFTPClientAgentConfig
|
||||
forM_ chunks $ deleteFileChunk a
|
||||
liftIO $ do
|
||||
printNoNewLine "File deleted!"
|
||||
removeFD yes fileDescription
|
||||
deleteFileChunk :: XFTPClientAgent -> FileChunk -> ExceptT CLIError IO ()
|
||||
deleteFileChunk a FileChunk {chunkNo, replicas = replica : _} = do
|
||||
let FileChunkReplica {server, replicaId, replicaKey} = replica
|
||||
withReconnect a server retryCount $ \c -> deleteXFTPChunk c replicaKey (unChunkReplicaId replicaId)
|
||||
logInfo $ "deleted chunk " <> tshow chunkNo <> " from " <> showServer server
|
||||
deleteFileChunk _ _ = throwError $ CLIError "chunk has no replicas"
|
||||
|
||||
cliFileDescrInfo :: InfoOptions -> ExceptT CLIError IO ()
|
||||
cliFileDescrInfo InfoOptions {fileDescription} = do
|
||||
getFileDescription fileDescription >>= \case
|
||||
AVFD (ValidFileDescription FileDescription {party, size, chunkSize, chunks}) -> do
|
||||
let replicas = groupReplicasByServer chunkSize chunks
|
||||
liftIO $ do
|
||||
printParty
|
||||
putStrLn $ "File download size: " <> strEnc size
|
||||
putStrLn "File server(s):"
|
||||
forM_ replicas $ \srvReplicas@(FileServerReplica {server} :| _) -> do
|
||||
let chSizes = fmap (\FileServerReplica {chunkSize = chSize_} -> unFileSize $ fromMaybe chunkSize chSize_) srvReplicas
|
||||
putStrLn $ strEnc server <> ": " <> strEnc (FileSize $ sum chSizes)
|
||||
where
|
||||
printParty :: IO ()
|
||||
printParty = case party of
|
||||
SFRecipient -> putStrLn "Recipient file description"
|
||||
SFSender -> putStrLn "Sender file description"
|
||||
|
||||
strEnc :: StrEncoding a => a -> String
|
||||
strEnc = B.unpack . strEncode
|
||||
|
||||
getFileDescription :: FilePath -> ExceptT CLIError IO AValidFileDescription
|
||||
getFileDescription path =
|
||||
ExceptT $ first (CLIError . ("Failed to parse file description: " <>)) . strDecode <$> B.readFile path
|
||||
|
||||
getFileDescription' :: FilePartyI p => FilePath -> ExceptT CLIError IO (ValidFileDescription p)
|
||||
getFileDescription' path =
|
||||
getFileDescription path >>= \case
|
||||
AVFD fd -> either (throwError . CLIError) pure $ checkParty fd
|
||||
|
||||
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)
|
||||
size34 sz = (fromIntegral sz * 3) `div` 4
|
||||
prepareSizes 0 = []
|
||||
prepareSizes size
|
||||
| size >= fromIntegral bigSize = replicate (fromIntegral n1) bigSize <> prepareSizes remSz
|
||||
| size > size34 bigSize = [bigSize]
|
||||
| otherwise = replicate (fromIntegral n2') smallSize
|
||||
where
|
||||
(n1, remSz) = size `divMod` fromIntegral bigSize
|
||||
n2' = let (n2, remSz2) = (size `divMod` fromIntegral smallSize) in if remSz2 == 0 then n2 else n2 + 1
|
||||
|
||||
prepareChunkSpecs :: FilePath -> [Word32] -> [XFTPChunkSpec]
|
||||
prepareChunkSpecs filePath chunkSizes = reverse . snd $ foldl' addSpec (0, []) chunkSizes
|
||||
where
|
||||
addSpec :: (Int64, [XFTPChunkSpec]) -> Word32 -> (Int64, [XFTPChunkSpec])
|
||||
addSpec (chunkOffset, specs) sz =
|
||||
let spec = XFTPChunkSpec {filePath, chunkOffset, chunkSize = fromIntegral sz}
|
||||
in (chunkOffset + fromIntegral sz, spec : specs)
|
||||
|
||||
getEncPath :: MonadIO m => Maybe FilePath -> String -> m FilePath
|
||||
getEncPath path name = (`uniqueCombine` (name <> ".encrypted")) =<< maybe (liftIO getCanonicalTemporaryDirectory) pure path
|
||||
|
||||
withReconnect :: Show e => XFTPClientAgent -> XFTPServer -> Int -> (XFTPClient -> ExceptT e IO a) -> ExceptT CLIError IO a
|
||||
withReconnect a srv n run = withRetry n $ do
|
||||
c <- withRetry n $ getXFTPServerClient a srv
|
||||
withExceptT (CLIError . show) (run c) `catchError` \e -> do
|
||||
liftIO $ closeXFTPServerClient a srv
|
||||
throwError e
|
||||
|
||||
withRetry :: Show e => Int -> ExceptT e IO a -> ExceptT CLIError IO a
|
||||
withRetry retryCount = withRetry' retryCount . withExceptT (CLIError . show)
|
||||
where
|
||||
withRetry' :: Int -> ExceptT CLIError IO a -> ExceptT CLIError IO a
|
||||
withRetry' 0 _ = throwError $ CLIError "internal: no retry attempts"
|
||||
withRetry' 1 a = a
|
||||
withRetry' n a =
|
||||
a `catchError` \e -> do
|
||||
logWarn ("retrying: " <> tshow e)
|
||||
withRetry' (n - 1) a
|
||||
|
||||
removeFD :: Bool -> FilePath -> IO ()
|
||||
removeFD yes fd
|
||||
| yes = do
|
||||
removeFile fd
|
||||
putStrLn $ "\nFile description " <> fd <> " is deleted."
|
||||
| otherwise = do
|
||||
y <- liftIO . getConfirmation $ "\nFile description " <> fd <> " can't be used again. Delete it"
|
||||
when y $ removeFile fd
|
||||
|
||||
getConfirmation :: String -> IO Bool
|
||||
getConfirmation prompt = do
|
||||
putStr $ prompt <> " (Y/n): "
|
||||
hFlush stdout
|
||||
s <- getLine
|
||||
case map toLower s of
|
||||
"y" -> pure True
|
||||
"" -> pure True
|
||||
"n" -> pure False
|
||||
_ -> getConfirmation prompt
|
||||
|
||||
cliRandomFile :: RandomFileOptions -> IO ()
|
||||
cliRandomFile RandomFileOptions {filePath, fileSize = FileSize size} = do
|
||||
withFile filePath WriteMode (`saveRandomFile` size)
|
||||
putStrLn $ "File created: " <> filePath
|
||||
where
|
||||
saveRandomFile h sz = do
|
||||
bytes <- getRandomBytes $ fromIntegral $ min mb' sz
|
||||
B.hPut h bytes
|
||||
when (sz > mb') $ saveRandomFile h (sz - mb')
|
||||
mb' = mb 1
|
||||
@@ -0,0 +1,17 @@
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Simplex.FileTransfer.Client.Presets where
|
||||
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import Simplex.Messaging.Protocol (XFTPServerWithAuth)
|
||||
|
||||
defaultXFTPServers :: NonEmpty XFTPServerWithAuth
|
||||
defaultXFTPServers =
|
||||
[ "xftp://da1aH3nOT-9G8lV7bWamhxpDYdJ1xmW7j3JpGaDR5Ug=@xftp1.simplex.im,blisztokwh6alt5oem45f2eksm2xdowxszmhjesntcyolmhutoon7sid.onion",
|
||||
"xftp://5vog2Imy1ExJB_7zDZrkV1KDWi96jYFyy9CL6fndBVw=@xftp2.simplex.im,jk6jybnel5hin2am5omd7g3hyvb4tfkyl7ea4vx5ldi7gpvz5ogqtcqd.onion",
|
||||
"xftp://PYa32DdYNFWi0uZZOprWQoQpIk5qyjRJ3EF7bVpbsn8=@xftp3.simplex.im,pqhchr4bpkzef5wra4hnmzvppn5j5mjrpzkh5ixx6mhylnxxd6lmwnad.onion",
|
||||
"xftp://k_GgQl40UZVV0Y4BX9ZTyMVqX5ZewcLW0waQIl7AYDE=@xftp4.simplex.im,q2lrjgiyo2efyweksavwxh3dlsrxetaxjztde2x5nateugrtrwzvmdid.onion",
|
||||
"xftp://-bIo6o8wuVc4wpZkZD3tH-rCeYaeER_0lz1ffQcSJDs=@xftp5.simplex.im,2kikdbdhlrluburwt6q6rnoxvezcm2ifx2hj463jlunkeftlzm7hf3qd.onion",
|
||||
"xftp://6nSvtY9pJn6PXWTAIMNl95E1Kk1vD7FM2TeOA64CFLg=@xftp6.simplex.im,tg3fmu6houq7vfqlsipm2qqzhtpgxesnuydqozx3khojxydhs53sbbyd.onion"
|
||||
]
|
||||
@@ -0,0 +1,112 @@
|
||||
{-# LANGUAGE BangPatterns #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.FileTransfer.Crypto where
|
||||
|
||||
import Control.Monad.Except
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bifunctor (first)
|
||||
import qualified Data.ByteArray as BA
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Int (Int64)
|
||||
import Simplex.FileTransfer.Types (FileHeader (..), authTagSize)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..), FTCryptoError (..))
|
||||
import qualified Simplex.Messaging.Crypto.File as CF
|
||||
import Simplex.Messaging.Crypto.Lazy (LazyByteString)
|
||||
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Util (liftEitherWith)
|
||||
import UnliftIO
|
||||
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
|
||||
let lenStr = smpEncode fileSize'
|
||||
(hdr, !sb') = LC.sbEncryptChunk sb $ lenStr <> fileHdr
|
||||
padLen = encSize - authTagSize - fileSize' - 8
|
||||
liftIO $ B.hPut w hdr
|
||||
sb2 <- encryptChunks r w (sb', fileSize' - fromIntegral (B.length fileHdr))
|
||||
CF.hGetTag r
|
||||
sb3 <- encryptPad w (sb2, padLen)
|
||||
let tag = BA.convert $ LC.sbAuth sb3
|
||||
liftIO $ B.hPut w tag
|
||||
where
|
||||
encryptChunks r = encryptChunks_ $ liftIO . CF.hGet r . fromIntegral
|
||||
encryptPad = encryptChunks_ $ \sz -> pure $ B.replicate (fromIntegral sz) '#'
|
||||
encryptChunks_ :: (Int64 -> IO ByteString) -> Handle -> (LC.SbState, Int64) -> ExceptT FTCryptoError IO LC.SbState
|
||||
encryptChunks_ get w (!sb, !len)
|
||||
| len == 0 = pure sb
|
||||
| otherwise = do
|
||||
let chSize = min len 65536
|
||||
ch <- liftIO $ get chSize
|
||||
when (B.length ch /= fromIntegral chSize) $ throwError $ FTCEFileIOError "encrypting file: unexpected EOF"
|
||||
let (ch', sb') = LC.sbEncryptChunk sb ch
|
||||
liftIO $ B.hPut w ch'
|
||||
encryptChunks_ get w (sb', len - chSize)
|
||||
|
||||
decryptChunks :: Int64 -> [FilePath] -> C.SbKey -> C.CbNonce -> (String -> ExceptT String IO CryptoFile) -> ExceptT FTCryptoError IO CryptoFile
|
||||
decryptChunks _ [] _ _ _ = throwError $ FTCEInvalidHeader "empty"
|
||||
decryptChunks encSize (chPath : chPaths) key nonce getDestFile = case reverse chPaths of
|
||||
[] -> do
|
||||
(!authOk, !f) <- liftEither . first FTCECryptoError . LC.sbDecryptTailTag key nonce (encSize - authTagSize) =<< liftIO (LB.readFile chPath)
|
||||
unless authOk $ throwError FTCEInvalidAuthTag
|
||||
(FileHeader {fileName}, !f') <- parseFileHeader f
|
||||
destFile <- withExceptT FTCEFileIOError $ getDestFile fileName
|
||||
CF.writeFile destFile f'
|
||||
pure destFile
|
||||
lastPath : chPaths' -> do
|
||||
(state, expectedLen, ch) <- decryptFirstChunk
|
||||
(FileHeader {fileName}, ch') <- parseFileHeader ch
|
||||
destFile@(CryptoFile path _) <- withExceptT FTCEFileIOError $ getDestFile fileName
|
||||
authOk <- CF.withFile destFile WriteMode $ \h -> liftIO $ do
|
||||
CF.hPut h ch'
|
||||
state' <- foldM (decryptChunk h) state $ reverse chPaths'
|
||||
decryptLastChunk h state' expectedLen
|
||||
unless authOk $ do
|
||||
removeFile path
|
||||
throwError FTCEInvalidAuthTag
|
||||
pure destFile
|
||||
where
|
||||
decryptFirstChunk = do
|
||||
sb <- liftEitherWith FTCECryptoError $ LC.sbInit key nonce
|
||||
ch <- liftIO $ LB.readFile chPath
|
||||
let (ch1, !sb') = LC.sbDecryptChunkLazy sb ch
|
||||
(!expectedLen, ch2) <- liftEitherWith FTCECryptoError $ LC.splitLen ch1
|
||||
let len1 = LB.length ch2
|
||||
pure ((sb', len1), expectedLen, ch2)
|
||||
decryptChunk h (!sb, !len) chPth = do
|
||||
ch <- LB.readFile chPth
|
||||
let len' = len + LB.length ch
|
||||
(ch', sb') = LC.sbDecryptChunkLazy sb ch
|
||||
CF.hPut h ch'
|
||||
pure (sb', len')
|
||||
decryptLastChunk h (!sb, !len) expectedLen = do
|
||||
ch <- LB.readFile lastPath
|
||||
let (ch1, tag') = LB.splitAt (LB.length ch - authTagSize) ch
|
||||
tag'' = LB.toStrict tag'
|
||||
(ch2, sb') = LC.sbDecryptChunkLazy sb ch1
|
||||
len' = len + LB.length ch2
|
||||
ch3 = LB.take (LB.length ch2 - len' + expectedLen) ch2
|
||||
tag :: ByteString = BA.convert (LC.sbAuth sb')
|
||||
CF.hPut h ch3
|
||||
CF.hPutTag h
|
||||
pure $ B.length tag'' == 16 && BA.constEq tag'' tag
|
||||
where
|
||||
parseFileHeader :: LazyByteString -> ExceptT FTCryptoError IO (FileHeader, LazyByteString)
|
||||
parseFileHeader s = do
|
||||
let (hdrStr, s') = LB.splitAt 1024 s
|
||||
case A.parse smpP $ LB.toStrict hdrStr of
|
||||
A.Fail _ _ e -> throwError $ FTCEInvalidHeader e
|
||||
A.Partial _ -> throwError $ FTCEInvalidHeader "incomplete"
|
||||
A.Done rest hdr -> pure (hdr, LB.fromStrict rest <> s')
|
||||
|
||||
readChunks :: [FilePath] -> IO LB.ByteString
|
||||
readChunks = foldM (\s path -> (s <>) <$> LB.readFile path) ""
|
||||
@@ -0,0 +1,343 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||
|
||||
module Simplex.FileTransfer.Description
|
||||
( FileDescription (..),
|
||||
AFileDescription (..),
|
||||
ValidFileDescription, -- constructor is not exported, use pattern
|
||||
pattern ValidFileDescription,
|
||||
AValidFileDescription (..),
|
||||
FileDigest (..),
|
||||
FileChunk (..),
|
||||
FileChunkReplica (..),
|
||||
FileServerReplica (..),
|
||||
FileSize (..),
|
||||
ChunkReplicaId (..),
|
||||
YAMLFileDescription (..), -- for tests
|
||||
YAMLServerReplicas (..), -- for tests
|
||||
validateFileDescription,
|
||||
groupReplicasByServer,
|
||||
replicaServer,
|
||||
fdSeparator,
|
||||
kb,
|
||||
mb,
|
||||
gb,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Applicative (optional)
|
||||
import Control.Monad ((<=<))
|
||||
import Data.Aeson (FromJSON, ToJSON)
|
||||
import qualified Data.Aeson.TH as J
|
||||
import Data.Attoparsec.ByteString.Char8 (Parser)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bifunctor (first)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Int (Int64)
|
||||
import Data.List (foldl', sortOn)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map (Map)
|
||||
import qualified Data.Map as M
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.String
|
||||
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 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, (<$?>))
|
||||
|
||||
data FileDescription (p :: FileParty) = FileDescription
|
||||
{ party :: SFileParty p,
|
||||
size :: FileSize Int64,
|
||||
digest :: FileDigest,
|
||||
key :: C.SbKey,
|
||||
nonce :: C.CbNonce,
|
||||
chunkSize :: FileSize Word32,
|
||||
chunks :: [FileChunk]
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data AFileDescription = forall p. FilePartyI p => AFD (FileDescription p)
|
||||
|
||||
newtype ValidFileDescription p = ValidFD (FileDescription p)
|
||||
deriving (Eq, Show)
|
||||
|
||||
pattern ValidFileDescription :: FileDescription p -> ValidFileDescription p
|
||||
pattern ValidFileDescription fd = ValidFD fd
|
||||
|
||||
{-# COMPLETE ValidFileDescription #-}
|
||||
|
||||
data AValidFileDescription = forall p. FilePartyI p => AVFD (ValidFileDescription p)
|
||||
|
||||
fdSeparator :: IsString s => s
|
||||
fdSeparator = "################################\n"
|
||||
|
||||
newtype FileDigest = FileDigest {unFileDigest :: ByteString}
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding FileDigest where
|
||||
strEncode (FileDigest fd) = strEncode fd
|
||||
strDecode s = FileDigest <$> strDecode s
|
||||
strP = FileDigest <$> strP
|
||||
|
||||
instance FromJSON FileDigest where
|
||||
parseJSON = strParseJSON "FileDigest"
|
||||
|
||||
instance ToJSON FileDigest where
|
||||
toJSON = strToJSON
|
||||
toEncoding = strToJEncoding
|
||||
|
||||
instance FromField FileDigest where fromField f = FileDigest <$> fromField f
|
||||
|
||||
instance ToField FileDigest where toField (FileDigest s) = toField s
|
||||
|
||||
data FileChunk = FileChunk
|
||||
{ chunkNo :: Int,
|
||||
chunkSize :: FileSize Word32,
|
||||
digest :: FileDigest,
|
||||
replicas :: [FileChunkReplica]
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data FileChunkReplica = FileChunkReplica
|
||||
{ server :: XFTPServer,
|
||||
replicaId :: ChunkReplicaId,
|
||||
replicaKey :: C.APrivateSignKey
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
newtype ChunkReplicaId = ChunkReplicaId {unChunkReplicaId :: ByteString}
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding ChunkReplicaId where
|
||||
strEncode (ChunkReplicaId fid) = strEncode fid
|
||||
strP = ChunkReplicaId <$> strP
|
||||
|
||||
instance FromJSON ChunkReplicaId where
|
||||
parseJSON = strParseJSON "ChunkReplicaId"
|
||||
|
||||
instance ToJSON ChunkReplicaId where
|
||||
toJSON = strToJSON
|
||||
toEncoding = strToJEncoding
|
||||
|
||||
instance FromField ChunkReplicaId where fromField f = ChunkReplicaId <$> fromField f
|
||||
|
||||
instance ToField ChunkReplicaId where toField (ChunkReplicaId s) = toField s
|
||||
|
||||
data YAMLFileDescription = YAMLFileDescription
|
||||
{ party :: FileParty,
|
||||
size :: String,
|
||||
digest :: FileDigest,
|
||||
key :: C.SbKey,
|
||||
nonce :: C.CbNonce,
|
||||
chunkSize :: String,
|
||||
replicas :: [YAMLServerReplicas]
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data YAMLServerReplicas = YAMLServerReplicas
|
||||
{ server :: XFTPServer,
|
||||
chunks :: [String]
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data FileServerReplica = FileServerReplica
|
||||
{ chunkNo :: Int,
|
||||
server :: XFTPServer,
|
||||
replicaId :: ChunkReplicaId,
|
||||
replicaKey :: C.APrivateSignKey,
|
||||
digest :: Maybe FileDigest,
|
||||
chunkSize :: Maybe (FileSize Word32)
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
newtype FileSize a = FileSize {unFileSize :: a}
|
||||
deriving (Eq, Show)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''YAMLServerReplicas)
|
||||
|
||||
$(J.deriveJSON defaultJSON ''YAMLFileDescription)
|
||||
|
||||
instance FilePartyI p => StrEncoding (ValidFileDescription p) where
|
||||
strEncode (ValidFD fd) = strEncode fd
|
||||
strDecode s = strDecode s >>= (\(AVFD fd) -> checkParty fd)
|
||||
strP = strDecode <$?> A.takeByteString
|
||||
|
||||
instance StrEncoding AValidFileDescription where
|
||||
strEncode (AVFD fd) = strEncode fd
|
||||
strDecode = (\(AFD fd) -> AVFD <$> validateFileDescription fd) <=< strDecode
|
||||
strP = strDecode <$?> A.takeByteString
|
||||
|
||||
instance FilePartyI p => StrEncoding (FileDescription p) where
|
||||
strEncode = Y.encode . encodeFileDescription
|
||||
strDecode s = strDecode s >>= (\(AFD fd) -> checkParty fd)
|
||||
strP = strDecode <$?> A.takeByteString
|
||||
|
||||
instance StrEncoding AFileDescription where
|
||||
strEncode (AFD fd) = strEncode fd
|
||||
strDecode = decodeFileDescription <=< first show . Y.decodeEither'
|
||||
strP = strDecode <$?> A.takeByteString
|
||||
|
||||
validateFileDescription :: FileDescription p -> Either String (ValidFileDescription p)
|
||||
validateFileDescription fd@FileDescription {size, chunks}
|
||||
| chunkNos /= [1 .. length chunks] = Left "chunk numbers are not sequential"
|
||||
| chunksSize chunks /= unFileSize size = Left "chunks total size is different than file size"
|
||||
| otherwise = Right $ ValidFD fd
|
||||
where
|
||||
chunkNos = map (\FileChunk {chunkNo} -> chunkNo) chunks
|
||||
chunksSize = fromIntegral . foldl' (\s FileChunk {chunkSize} -> s + unFileSize chunkSize) 0
|
||||
|
||||
encodeFileDescription :: FileDescription p -> YAMLFileDescription
|
||||
encodeFileDescription FileDescription {party, size, digest, key, nonce, chunkSize, chunks} =
|
||||
YAMLFileDescription
|
||||
{ party = toFileParty party,
|
||||
size = B.unpack $ strEncode size,
|
||||
digest,
|
||||
key,
|
||||
nonce,
|
||||
chunkSize = B.unpack $ strEncode chunkSize,
|
||||
replicas = encodeFileReplicas chunkSize chunks
|
||||
}
|
||||
|
||||
instance (Integral a, Show a) => StrEncoding (FileSize a) where
|
||||
strEncode (FileSize b)
|
||||
| b' /= 0 = bshow b
|
||||
| ks' /= 0 = bshow ks <> "kb"
|
||||
| ms' /= 0 = bshow ms <> "mb"
|
||||
| otherwise = bshow gs <> "gb"
|
||||
where
|
||||
(ks, b') = b `divMod` 1024
|
||||
(ms, ks') = ks `divMod` 1024
|
||||
(gs, ms') = ms `divMod` 1024
|
||||
strP =
|
||||
FileSize
|
||||
<$> A.choice
|
||||
[ gb <$> A.decimal <* "gb",
|
||||
mb <$> A.decimal <* "mb",
|
||||
kb <$> A.decimal <* "kb",
|
||||
A.decimal
|
||||
]
|
||||
|
||||
instance (Integral a, Show a) => IsString (FileSize a) where
|
||||
fromString = either error id . strDecode . B.pack
|
||||
|
||||
instance FromField a => FromField (FileSize a) where fromField f = FileSize <$> fromField f
|
||||
|
||||
instance ToField a => ToField (FileSize a) where toField (FileSize s) = toField s
|
||||
|
||||
groupReplicasByServer :: FileSize Word32 -> [FileChunk] -> [NonEmpty FileServerReplica]
|
||||
groupReplicasByServer defChunkSize =
|
||||
L.groupAllWith (\FileServerReplica {server} -> server) . unfoldChunksToReplicas defChunkSize
|
||||
|
||||
encodeFileReplicas :: FileSize Word32 -> [FileChunk] -> [YAMLServerReplicas]
|
||||
encodeFileReplicas defChunkSize =
|
||||
map encodeServerReplicas . groupReplicasByServer defChunkSize
|
||||
where
|
||||
encodeServerReplicas fs@(FileServerReplica {server} :| _) =
|
||||
YAMLServerReplicas
|
||||
{ server,
|
||||
chunks = map (B.unpack . encodeServerReplica) $ L.toList fs
|
||||
}
|
||||
|
||||
replicaServer :: FileServerReplica -> XFTPServer
|
||||
replicaServer = server
|
||||
|
||||
encodeServerReplica :: FileServerReplica -> ByteString
|
||||
encodeServerReplica FileServerReplica {chunkNo, replicaId, replicaKey, digest, chunkSize} =
|
||||
bshow chunkNo
|
||||
<> ":"
|
||||
<> strEncode replicaId
|
||||
<> ":"
|
||||
<> strEncode replicaKey
|
||||
<> maybe "" ((":" <>) . strEncode) digest
|
||||
<> maybe "" ((":" <>) . strEncode) chunkSize
|
||||
|
||||
serverReplicaP :: XFTPServer -> Parser FileServerReplica
|
||||
serverReplicaP server = do
|
||||
chunkNo <- A.decimal
|
||||
replicaId <- A.char ':' *> strP
|
||||
replicaKey <- A.char ':' *> strP
|
||||
digest <- optional (A.char ':' *> strP)
|
||||
chunkSize <- optional (A.char ':' *> strP)
|
||||
pure FileServerReplica {chunkNo, server, replicaId, replicaKey, digest, chunkSize}
|
||||
|
||||
unfoldChunksToReplicas :: FileSize Word32 -> [FileChunk] -> [FileServerReplica]
|
||||
unfoldChunksToReplicas defChunkSize = concatMap chunkReplicas
|
||||
where
|
||||
chunkReplicas c@FileChunk {replicas} = zipWith (replicaToServerReplica c) [1 ..] replicas
|
||||
replicaToServerReplica :: FileChunk -> Int -> FileChunkReplica -> FileServerReplica
|
||||
replicaToServerReplica FileChunk {chunkNo, digest, chunkSize} replicaNo FileChunkReplica {server, replicaId, replicaKey} =
|
||||
let chunkSize' = if chunkSize /= defChunkSize && replicaNo == 1 then Just chunkSize else Nothing
|
||||
digest' = if replicaNo == 1 then Just digest else Nothing
|
||||
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
|
||||
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}
|
||||
where
|
||||
decodeFileParts = fmap concat . mapM decodeYAMLServerReplicas
|
||||
|
||||
decodeYAMLServerReplicas :: YAMLServerReplicas -> Either String [FileServerReplica]
|
||||
decodeYAMLServerReplicas YAMLServerReplicas {server, chunks} =
|
||||
mapM (parseAll (serverReplicaP server) . B.pack) chunks
|
||||
|
||||
-- this function should fail if:
|
||||
-- 1. no replica has digest or two replicas have different digests
|
||||
-- 2. two replicas have different chunk sizes
|
||||
foldReplicasToChunks :: FileSize Word32 -> [FileServerReplica] -> Either String [FileChunk]
|
||||
foldReplicasToChunks defChunkSize fs = do
|
||||
sd <- foldSizesDigests fs
|
||||
-- TODO validate (check that chunks match) or in separate function
|
||||
sortOn (\FileChunk {chunkNo} -> chunkNo) . map reverseReplicas . M.elems <$> foldChunks sd fs
|
||||
where
|
||||
foldSizesDigests :: [FileServerReplica] -> Either String (Map Int (FileSize Word32), Map Int FileDigest)
|
||||
foldSizesDigests = foldl' addSizeDigest $ Right (M.empty, M.empty)
|
||||
addSizeDigest :: Either String (Map Int (FileSize Word32), Map Int FileDigest) -> FileServerReplica -> Either String (Map Int (FileSize Word32), Map Int FileDigest)
|
||||
addSizeDigest (Left e) _ = Left e
|
||||
addSizeDigest (Right (ms, md)) FileServerReplica {chunkNo, chunkSize, digest} =
|
||||
(,) <$> combineChunk ms chunkNo chunkSize <*> combineChunk md chunkNo digest
|
||||
combineChunk :: Eq a => Map Int a -> Int -> Maybe a -> Either String (Map Int a)
|
||||
combineChunk m _ Nothing = Right m
|
||||
combineChunk m chunkNo (Just value) = case M.lookup chunkNo m of
|
||||
Nothing -> Right $ M.insert chunkNo value m
|
||||
Just v -> if v == value then Right m else Left "different size or digest in chunk replicas"
|
||||
foldChunks :: (Map Int (FileSize Word32), Map Int FileDigest) -> [FileServerReplica] -> Either String (Map Int FileChunk)
|
||||
foldChunks sd = foldl' (addReplica sd) (Right M.empty)
|
||||
addReplica :: (Map Int (FileSize Word32), Map Int FileDigest) -> Either String (Map Int FileChunk) -> FileServerReplica -> Either String (Map Int FileChunk)
|
||||
addReplica _ (Left e) _ = Left e
|
||||
addReplica (ms, md) (Right cs) FileServerReplica {chunkNo, server, replicaId, replicaKey} = do
|
||||
case M.lookup chunkNo cs of
|
||||
Just chunk@FileChunk {replicas} ->
|
||||
let replica = FileChunkReplica {server, replicaId, replicaKey}
|
||||
in Right $ M.insert chunkNo ((chunk :: FileChunk) {replicas = replica : replicas}) cs
|
||||
_ -> do
|
||||
case M.lookup chunkNo md of
|
||||
Just digest' ->
|
||||
let replica = FileChunkReplica {server, replicaId, replicaKey}
|
||||
chunkSize' = fromMaybe defChunkSize $ M.lookup chunkNo ms
|
||||
chunk = FileChunk {chunkNo, digest = digest', chunkSize = chunkSize', replicas = [replica]}
|
||||
in Right $ M.insert chunkNo chunk cs
|
||||
_ -> Left "no digest for chunk"
|
||||
reverseReplicas c@FileChunk {replicas} = (c :: FileChunk) {replicas = reverse replicas}
|
||||
@@ -0,0 +1,420 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# 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
|
||||
import Data.Kind (Type)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import Data.Maybe (isNothing)
|
||||
import Data.Type.Equality
|
||||
import Data.Word (Word32)
|
||||
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,
|
||||
CommandError (..),
|
||||
Protocol (..),
|
||||
ProtocolEncoding (..),
|
||||
ProtocolErrorType (..),
|
||||
ProtocolMsgTag (..),
|
||||
ProtocolType (..),
|
||||
RcvPublicDhKey,
|
||||
RcvPublicVerifyKey,
|
||||
RecipientId,
|
||||
SenderId,
|
||||
SentRawTransmission,
|
||||
SignedTransmission,
|
||||
SndPublicVerifyKey,
|
||||
Transmission,
|
||||
encodeTransmission,
|
||||
messageTagP,
|
||||
tDecodeParseValidate,
|
||||
tEncode,
|
||||
tEncodeBatch,
|
||||
tParse,
|
||||
)
|
||||
import Simplex.Messaging.Transport (SessionId, TransportError (..))
|
||||
import Simplex.Messaging.Util (bshow, (<$?>))
|
||||
import Simplex.Messaging.Version
|
||||
|
||||
currentXFTPVersion :: Version
|
||||
currentXFTPVersion = 1
|
||||
|
||||
xftpBlockSize :: Int
|
||||
xftpBlockSize = 16384
|
||||
|
||||
-- | File protocol clients
|
||||
data FileParty = FRecipient | FSender
|
||||
deriving (Eq, Show)
|
||||
|
||||
data SFileParty :: FileParty -> Type where
|
||||
SFRecipient :: SFileParty FRecipient
|
||||
SFSender :: SFileParty FSender
|
||||
|
||||
instance TestEquality SFileParty where
|
||||
testEquality SFRecipient SFRecipient = Just Refl
|
||||
testEquality SFSender SFSender = Just Refl
|
||||
testEquality _ _ = Nothing
|
||||
|
||||
deriving instance Eq (SFileParty p)
|
||||
|
||||
deriving instance Show (SFileParty p)
|
||||
|
||||
data AFileParty = forall p. FilePartyI p => AFP (SFileParty p)
|
||||
|
||||
toFileParty :: SFileParty p -> FileParty
|
||||
toFileParty = \case
|
||||
SFRecipient -> FRecipient
|
||||
SFSender -> FSender
|
||||
|
||||
aFileParty :: FileParty -> AFileParty
|
||||
aFileParty = \case
|
||||
FRecipient -> AFP SFRecipient
|
||||
FSender -> AFP SFSender
|
||||
|
||||
class FilePartyI (p :: FileParty) where sFileParty :: SFileParty p
|
||||
|
||||
instance FilePartyI FRecipient where sFileParty = SFRecipient
|
||||
|
||||
instance FilePartyI FSender where sFileParty = SFSender
|
||||
|
||||
data FileCommandTag (p :: FileParty) where
|
||||
FNEW_ :: FileCommandTag FSender
|
||||
FADD_ :: FileCommandTag FSender
|
||||
FPUT_ :: FileCommandTag FSender
|
||||
FDEL_ :: FileCommandTag FSender
|
||||
FGET_ :: FileCommandTag FRecipient
|
||||
FACK_ :: FileCommandTag FRecipient
|
||||
PING_ :: FileCommandTag FRecipient
|
||||
|
||||
deriving instance Show (FileCommandTag p)
|
||||
|
||||
data FileCmdTag = forall p. FilePartyI p => FCT (SFileParty p) (FileCommandTag p)
|
||||
|
||||
instance FilePartyI p => Encoding (FileCommandTag p) where
|
||||
smpEncode = \case
|
||||
FNEW_ -> "FNEW"
|
||||
FADD_ -> "FADD"
|
||||
FPUT_ -> "FPUT"
|
||||
FDEL_ -> "FDEL"
|
||||
FGET_ -> "FGET"
|
||||
FACK_ -> "FACK"
|
||||
PING_ -> "PING"
|
||||
smpP = messageTagP
|
||||
|
||||
instance Encoding FileCmdTag where
|
||||
smpEncode (FCT _ t) = smpEncode t
|
||||
smpP = messageTagP
|
||||
|
||||
instance ProtocolMsgTag FileCmdTag where
|
||||
decodeTag = \case
|
||||
"FNEW" -> Just $ FCT SFSender FNEW_
|
||||
"FADD" -> Just $ FCT SFSender FADD_
|
||||
"FPUT" -> Just $ FCT SFSender FPUT_
|
||||
"FDEL" -> Just $ FCT SFSender FDEL_
|
||||
"FGET" -> Just $ FCT SFRecipient FGET_
|
||||
"FACK" -> Just $ FCT SFRecipient FACK_
|
||||
"PING" -> Just $ FCT SFRecipient PING_
|
||||
_ -> Nothing
|
||||
|
||||
instance FilePartyI p => ProtocolMsgTag (FileCommandTag p) where
|
||||
decodeTag s = decodeTag s >>= (\(FCT _ t) -> checkParty' t)
|
||||
|
||||
instance Protocol XFTPErrorType FileResponse where
|
||||
type ProtoCommand FileResponse = FileCmd
|
||||
type ProtoType FileResponse = 'PXFTP
|
||||
protocolClientHandshake = ntfClientHandshake
|
||||
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
|
||||
FPUT :: FileCommand FSender
|
||||
FDEL :: FileCommand FSender
|
||||
FGET :: RcvPublicDhKey -> FileCommand FRecipient
|
||||
FACK :: FileCommand FRecipient
|
||||
PING :: FileCommand FRecipient
|
||||
|
||||
deriving instance Show (FileCommand p)
|
||||
|
||||
data FileCmd = forall p. FilePartyI p => FileCmd (SFileParty p) (FileCommand p)
|
||||
|
||||
deriving instance Show FileCmd
|
||||
|
||||
data FileInfo = FileInfo
|
||||
{ sndKey :: SndPublicVerifyKey,
|
||||
size :: Word32,
|
||||
digest :: ByteString
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
type XFTPFileId = ByteString
|
||||
|
||||
instance FilePartyI p => ProtocolEncoding XFTPErrorType (FileCommand p) where
|
||||
type Tag (FileCommand p) = FileCommandTag p
|
||||
encodeProtocol _v = \case
|
||||
FNEW file rKeys auth_ -> e (FNEW_, ' ', file, rKeys, auth_)
|
||||
FADD rKeys -> e (FADD_, ' ', rKeys)
|
||||
FPUT -> e FPUT_
|
||||
FDEL -> e FDEL_
|
||||
FGET rKey -> e (FGET_, ' ', rKey)
|
||||
FACK -> e FACK_
|
||||
PING -> e PING_
|
||||
where
|
||||
e :: Encoding a => a -> ByteString
|
||||
e = smpEncode
|
||||
|
||||
protocolP v tag = (\(FileCmd _ c) -> checkParty c) <$?> protocolP v (FCT (sFileParty @p) tag)
|
||||
|
||||
fromProtocolError = fromProtocolError @XFTPErrorType @FileResponse
|
||||
{-# INLINE fromProtocolError #-}
|
||||
|
||||
checkCredentials (sig, _, fileId, _) cmd = case cmd of
|
||||
-- FNEW must not have signature and chunk ID
|
||||
FNEW {}
|
||||
| isNothing sig -> Left $ CMD NO_AUTH
|
||||
| not (B.null fileId) -> Left $ CMD HAS_AUTH
|
||||
| otherwise -> Right cmd
|
||||
PING
|
||||
| isNothing sig && 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
|
||||
| otherwise -> Right cmd
|
||||
|
||||
instance ProtocolEncoding XFTPErrorType FileCmd where
|
||||
type Tag FileCmd = FileCmdTag
|
||||
encodeProtocol _v (FileCmd _ c) = encodeProtocol _v c
|
||||
|
||||
protocolP _v = \case
|
||||
FCT SFSender tag ->
|
||||
FileCmd SFSender <$> case tag of
|
||||
FNEW_ -> FNEW <$> _smpP <*> smpP <*> smpP
|
||||
FADD_ -> FADD <$> _smpP
|
||||
FPUT_ -> pure FPUT
|
||||
FDEL_ -> pure FDEL
|
||||
FCT SFRecipient tag ->
|
||||
FileCmd SFRecipient <$> case tag of
|
||||
FGET_ -> FGET <$> _smpP
|
||||
FACK_ -> pure FACK
|
||||
PING_ -> pure PING
|
||||
|
||||
fromProtocolError = fromProtocolError @XFTPErrorType @FileResponse
|
||||
{-# INLINE fromProtocolError #-}
|
||||
|
||||
checkCredentials t (FileCmd p c) = FileCmd p <$> checkCredentials t c
|
||||
|
||||
instance Encoding FileInfo where
|
||||
smpEncode FileInfo {sndKey, size, digest} = smpEncode (sndKey, size, digest)
|
||||
smpP = FileInfo <$> smpP <*> smpP <*> smpP
|
||||
|
||||
instance StrEncoding FileInfo where
|
||||
strEncode FileInfo {sndKey, size, digest} = strEncode (sndKey, size, digest)
|
||||
strP = FileInfo <$> strP_ <*> strP_ <*> strP
|
||||
|
||||
data FileResponseTag
|
||||
= FRSndIds_
|
||||
| FRRcvIds_
|
||||
| FRFile_
|
||||
| FROk_
|
||||
| FRErr_
|
||||
| FRPong_
|
||||
deriving (Show)
|
||||
|
||||
instance Encoding FileResponseTag where
|
||||
smpEncode = \case
|
||||
FRSndIds_ -> "SIDS"
|
||||
FRRcvIds_ -> "RIDS"
|
||||
FRFile_ -> "FILE"
|
||||
FROk_ -> "OK"
|
||||
FRErr_ -> "ERR"
|
||||
FRPong_ -> "PONG"
|
||||
smpP = messageTagP
|
||||
|
||||
instance ProtocolMsgTag FileResponseTag where
|
||||
decodeTag = \case
|
||||
"SIDS" -> Just FRSndIds_
|
||||
"RIDS" -> Just FRRcvIds_
|
||||
"FILE" -> Just FRFile_
|
||||
"OK" -> Just FROk_
|
||||
"ERR" -> Just FRErr_
|
||||
"PONG" -> Just FRPong_
|
||||
_ -> Nothing
|
||||
|
||||
data FileResponse
|
||||
= FRSndIds SenderId (NonEmpty RecipientId)
|
||||
| FRRcvIds (NonEmpty RecipientId)
|
||||
| FRFile RcvPublicDhKey C.CbNonce
|
||||
| FROk
|
||||
| FRErr XFTPErrorType
|
||||
| FRPong
|
||||
deriving (Show)
|
||||
|
||||
instance ProtocolEncoding XFTPErrorType FileResponse where
|
||||
type Tag FileResponse = FileResponseTag
|
||||
encodeProtocol _v = \case
|
||||
FRSndIds fId rIds -> e (FRSndIds_, ' ', fId, rIds)
|
||||
FRRcvIds rIds -> e (FRRcvIds_, ' ', rIds)
|
||||
FRFile rDhKey nonce -> e (FRFile_, ' ', rDhKey, nonce)
|
||||
FROk -> e FROk_
|
||||
FRErr err -> e (FRErr_, ' ', err)
|
||||
FRPong -> e FRPong_
|
||||
where
|
||||
e :: Encoding a => a -> ByteString
|
||||
e = smpEncode
|
||||
|
||||
protocolP _v = \case
|
||||
FRSndIds_ -> FRSndIds <$> _smpP <*> smpP
|
||||
FRRcvIds_ -> FRRcvIds <$> _smpP
|
||||
FRFile_ -> FRFile <$> _smpP <*> smpP
|
||||
FROk_ -> pure FROk
|
||||
FRErr_ -> FRErr <$> _smpP
|
||||
FRPong_ -> pure FRPong
|
||||
|
||||
fromProtocolError = \case
|
||||
PECmdSyntax -> CMD SYNTAX
|
||||
PECmdUnknown -> CMD UNKNOWN
|
||||
PESession -> SESSION
|
||||
PEBlock -> BLOCK
|
||||
{-# INLINE fromProtocolError #-}
|
||||
|
||||
checkCredentials (_, _, entId, _) cmd = case cmd of
|
||||
FRSndIds {} -> noEntity
|
||||
-- ERR response does not always have entity ID
|
||||
FRErr _ -> Right cmd
|
||||
-- PONG response must not have queue ID
|
||||
FRPong -> noEntity
|
||||
-- other server responses must have entity ID
|
||||
_
|
||||
| B.null entId -> Left $ CMD NO_ENTITY
|
||||
| otherwise -> Right cmd
|
||||
where
|
||||
noEntity
|
||||
| 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
|
||||
Nothing -> Left "incorrect XFTP party"
|
||||
|
||||
checkParty' :: forall t p p'. (FilePartyI p, FilePartyI p') => t p' -> Maybe (t p)
|
||||
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)
|
||||
|
||||
-- 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
|
||||
|
||||
xftpDecodeTransmission :: ProtocolEncoding e c => SessionId -> ByteString -> Either XFTPErrorType (SignedTransmission e c)
|
||||
xftpDecodeTransmission sessionId t = do
|
||||
t' <- first (const BLOCK) $ C.unPad t
|
||||
case tParse True t' of
|
||||
t'' :| [] -> Right $ tDecodeParseValidate sessionId currentXFTPVersion t''
|
||||
_ -> Left BLOCK
|
||||
|
||||
$(J.deriveJSON (enumJSON $ dropPrefix "F") ''FileParty)
|
||||
|
||||
$(J.deriveJSON (sumTypeJSON id) ''XFTPErrorType)
|
||||
@@ -0,0 +1,403 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.FileTransfer.Server where
|
||||
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Unlift (MonadUnliftIO)
|
||||
import Control.Monad.Reader
|
||||
import Crypto.Random (getRandomBytes)
|
||||
import Data.Bifunctor (first)
|
||||
import qualified Data.ByteString.Base64.URL as B64
|
||||
import Data.ByteString.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 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 Network.HTTP.Types as N
|
||||
import qualified Network.HTTP2.Server as H
|
||||
import Simplex.FileTransfer.Protocol
|
||||
import Simplex.FileTransfer.Server.Env
|
||||
import Simplex.FileTransfer.Server.Stats
|
||||
import Simplex.FileTransfer.Server.Store
|
||||
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.String
|
||||
import Simplex.Messaging.Protocol (CorrId, RcvPublicDhKey, RcvPublicVerifyKey, RecipientId)
|
||||
import Simplex.Messaging.Server (dummyVerifyCmd, verifyCmdSignature)
|
||||
import Simplex.Messaging.Server.Expiration
|
||||
import Simplex.Messaging.Server.Stats
|
||||
import Simplex.Messaging.Transport.HTTP2
|
||||
import Simplex.Messaging.Transport.HTTP2.Server
|
||||
import Simplex.Messaging.Util
|
||||
import System.Exit (exitFailure)
|
||||
import System.FilePath ((</>))
|
||||
import System.IO (BufferMode (..), hPutStrLn, hSetBuffering)
|
||||
import UnliftIO (IOMode (..), withFile)
|
||||
import UnliftIO.Concurrent (threadDelay)
|
||||
import UnliftIO.Directory (doesFileExist, removeFile, renameFile)
|
||||
import UnliftIO.Exception
|
||||
import UnliftIO.STM
|
||||
|
||||
type M a = ReaderT XFTPEnv IO a
|
||||
|
||||
runXFTPServer :: XFTPServerConfig -> IO ()
|
||||
runXFTPServer cfg = do
|
||||
started <- newEmptyTMVarIO
|
||||
runXFTPServerBlocking started cfg
|
||||
|
||||
runXFTPServerBlocking :: TMVar Bool -> XFTPServerConfig -> IO ()
|
||||
runXFTPServerBlocking started cfg = newXFTPServerEnv cfg >>= runReaderT (xftpServer cfg started)
|
||||
|
||||
xftpServer :: XFTPServerConfig -> TMVar Bool -> M ()
|
||||
xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig} started = do
|
||||
restoreServerStats
|
||||
raceAny_ (runServer : expireFilesThread_ cfg <> serverStatsThread_ cfg) `finally` stopServer
|
||||
where
|
||||
runServer :: M ()
|
||||
runServer = do
|
||||
serverParams <- asks tlsServerParams
|
||||
env <- ask
|
||||
liftIO $
|
||||
runHTTP2Server started xftpPort defaultHTTP2BufferSize serverParams transportConfig $ \sessionId r sendResponse -> do
|
||||
reqBody <- getHTTP2Body r xftpBlockSize
|
||||
processRequest HTTP2Request {sessionId, request = r, reqBody, sendResponse} `runReaderT` env
|
||||
|
||||
stopServer :: M ()
|
||||
stopServer = do
|
||||
withFileLog closeStoreLog
|
||||
saveServerStats
|
||||
|
||||
expireFilesThread_ :: XFTPServerConfig -> [M ()]
|
||||
expireFilesThread_ XFTPServerConfig {fileExpiration = Just fileExp} = [expireFiles fileExp]
|
||||
expireFilesThread_ _ = []
|
||||
|
||||
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
|
||||
|
||||
serverStatsThread_ :: XFTPServerConfig -> [M ()]
|
||||
serverStatsThread_ XFTPServerConfig {logStatsInterval = Just interval, logStatsStartTime, serverStatsLogFile} =
|
||||
[logServerStats logStatsStartTime interval serverStatsLogFile]
|
||||
serverStatsThread_ _ = []
|
||||
|
||||
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
|
||||
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
|
||||
let interval = 1_000_000 * logInterval
|
||||
forever $ do
|
||||
withFile statsFilePath AppendMode $ \h -> liftIO $ do
|
||||
hSetBuffering h LineBuffering
|
||||
ts <- getCurrentTime
|
||||
fromTime' <- atomically $ swapTVar fromTime ts
|
||||
filesCreated' <- atomically $ swapTVar filesCreated 0
|
||||
fileRecipients' <- atomically $ swapTVar fileRecipients 0
|
||||
filesUploaded' <- atomically $ swapTVar filesUploaded 0
|
||||
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
|
||||
hPutStrLn h $
|
||||
intercalate
|
||||
","
|
||||
[ iso8601Show $ utctDay fromTime',
|
||||
show filesCreated',
|
||||
show fileRecipients',
|
||||
show filesUploaded',
|
||||
show filesDeleted',
|
||||
dayCount files,
|
||||
weekCount files,
|
||||
monthCount files,
|
||||
show fileDownloads',
|
||||
show fileDownloadAcks',
|
||||
show filesCount',
|
||||
show filesSize'
|
||||
]
|
||||
liftIO $ threadDelay' interval
|
||||
|
||||
data ServerFile = ServerFile
|
||||
{ filePath :: FilePath,
|
||||
fileSize :: Word32,
|
||||
sbState :: LC.SbState
|
||||
}
|
||||
|
||||
processRequest :: HTTP2Request -> M ()
|
||||
processRequest HTTP2Request {sessionId, 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 cmdOrErr of
|
||||
Right cmd -> do
|
||||
verifyXFTPTransmission sig_ signed fId cmd >>= \case
|
||||
VRVerified req -> uncurry send =<< processXFTPRequest body req
|
||||
VRFailed -> send (FRErr AUTH) Nothing
|
||||
Left e -> send (FRErr e) Nothing
|
||||
where
|
||||
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)
|
||||
liftIO $ sendResponse $ H.responseStreaming N.ok200 [] $ streamBody t_
|
||||
where
|
||||
streamBody t_ send done = do
|
||||
case t_ of
|
||||
Left _ -> do
|
||||
send "padding error" -- TODO respond with BLOCK error?
|
||||
done
|
||||
Right t -> do
|
||||
send $ byteString t
|
||||
-- timeout sending file in the same way as receiving
|
||||
forM_ serverFile_ $ \ServerFile {filePath, fileSize, sbState} -> do
|
||||
withFile filePath ReadMode $ \h -> sendEncFile h send sbState (fromIntegral fileSize)
|
||||
done
|
||||
|
||||
data VerificationResult = VRVerified XFTPRequest | VRFailed
|
||||
|
||||
verifyXFTPTransmission :: Maybe C.ASignature -> ByteString -> XFTPFileId -> FileCmd -> M VerificationResult
|
||||
verifyXFTPTransmission sig_ signed fId cmd =
|
||||
case cmd of
|
||||
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
|
||||
verifyCmd :: SFileParty p -> M VerificationResult
|
||||
verifyCmd party = do
|
||||
st <- asks store
|
||||
atomically $ verify <$> getFile st party fId
|
||||
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
|
||||
|
||||
processXFTPRequest :: HTTP2Body -> XFTPRequest -> M (FileResponse, Maybe ServerFile)
|
||||
processXFTPRequest HTTP2Body {bodyPart} = \case
|
||||
XFTPReqNew file rks auth -> noFile =<< ifM allowNew (createFile file rks) (pure $ FRErr AUTH)
|
||||
where
|
||||
allowNew = do
|
||||
XFTPServerConfig {allowNewFiles, newFileBasicAuth} <- asks config
|
||||
pure $ allowNewFiles && maybe True ((== auth) . Just) newFileBasicAuth
|
||||
XFTPReqCmd fId fr (FileCmd _ cmd) -> case cmd of
|
||||
FADD rks -> noFile =<< addRecipients fId rks
|
||||
FPUT -> noFile =<< receiveServerFile fr
|
||||
FDEL -> noFile =<< deleteServerFile fr
|
||||
FGET rDhKey -> sendServerFile fr rDhKey
|
||||
FACK -> noFile =<< ackFileReception fId fr
|
||||
-- it should never get to the commands below, they are passed in other constructors of XFTPRequest
|
||||
FNEW {} -> noFile $ FRErr INTERNAL
|
||||
PING -> noFile FRPong
|
||||
XFTPReqPing -> noFile FRPong
|
||||
where
|
||||
noFile resp = pure (resp, Nothing)
|
||||
createFile :: FileInfo -> NonEmpty RcvPublicVerifyKey -> M FileResponse
|
||||
createFile file rks = do
|
||||
st <- asks store
|
||||
r <- runExceptT $ do
|
||||
sizes <- asks $ allowedChunkSizes . config
|
||||
unless (size file `elem` sizes) $ throwError SIZE
|
||||
ts <- liftIO getSystemTime
|
||||
-- TODO validate body empty
|
||||
sId <- ExceptT $ addFileRetry st file 3 ts
|
||||
rcps <- mapM (ExceptT . addRecipientRetry st 3 sId) rks
|
||||
withFileLog $ \sl -> do
|
||||
logAddFile sl sId file ts
|
||||
logAddRecipients sl sId rcps
|
||||
stats <- asks serverStats
|
||||
atomically $ modifyTVar' (filesCreated stats) (+ 1)
|
||||
atomically $ modifyTVar' (fileRecipients stats) (+ length rks)
|
||||
let rIds = L.map (\(FileRecipient rId _) -> rId) rcps
|
||||
pure $ FRSndIds sId rIds
|
||||
pure $ either FRErr id r
|
||||
addFileRetry :: FileStore -> FileInfo -> Int -> SystemTime -> M (Either XFTPErrorType XFTPFileId)
|
||||
addFileRetry st file n ts =
|
||||
retryAdd n $ \sId -> runExceptT $ do
|
||||
ExceptT $ addFile st sId file ts
|
||||
pure sId
|
||||
addRecipientRetry :: FileStore -> Int -> XFTPFileId -> RcvPublicVerifyKey -> M (Either XFTPErrorType FileRecipient)
|
||||
addRecipientRetry st n sId rpk =
|
||||
retryAdd n $ \rId -> runExceptT $ do
|
||||
let rcp = FileRecipient rId rpk
|
||||
ExceptT $ addRecipient st sId rcp
|
||||
pure rcp
|
||||
retryAdd :: Int -> (XFTPFileId -> STM (Either XFTPErrorType a)) -> M (Either XFTPErrorType a)
|
||||
retryAdd 0 _ = pure $ Left INTERNAL
|
||||
retryAdd n add = do
|
||||
fId <- getFileId
|
||||
atomically (add fId) >>= \case
|
||||
Left DUPLICATE_ -> retryAdd (n - 1) add
|
||||
r -> pure r
|
||||
addRecipients :: XFTPFileId -> NonEmpty RcvPublicVerifyKey -> 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
|
||||
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
|
||||
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
|
||||
where
|
||||
remove fPath = whenM (doesFileExist fPath) (removeFile fPath) `catch` logFileError
|
||||
|
||||
sendServerFile :: FileRec -> RcvPublicDhKey -> M (FileResponse, Maybe ServerFile)
|
||||
sendServerFile FileRec {senderId, filePath, fileInfo = FileInfo {size}} rDhKey = do
|
||||
readTVarIO filePath >>= \case
|
||||
Just path -> do
|
||||
(sDhKey, spDhKey) <- liftIO C.generateKeyPair'
|
||||
let dhSecret = C.dh' rDhKey spDhKey
|
||||
cbNonce <- liftIO C.randomCbNonce
|
||||
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)
|
||||
|
||||
logFileError :: SomeException -> IO ()
|
||||
logFileError e = logError $ "Error deleting file: " <> tshow e
|
||||
|
||||
ackFileReception :: RecipientId -> FileRec -> M FileResponse
|
||||
ackFileReception rId fr = do
|
||||
withFileLog (`logAckFile` rId)
|
||||
st <- asks store
|
||||
atomically $ deleteRecipient st rId fr
|
||||
stats <- asks serverStats
|
||||
atomically $ modifyTVar' (fileDownloadAcks stats) (+ 1)
|
||||
pure FROk
|
||||
|
||||
randomId :: (MonadUnliftIO m, MonadReader XFTPEnv m) => Int -> m ByteString
|
||||
randomId n = do
|
||||
gVar <- asks idsDrg
|
||||
atomically (C.pseudoRandomBytes n gVar)
|
||||
|
||||
getFileId :: M XFTPFileId
|
||||
getFileId = liftIO . getRandomBytes =<< asks (fileIdSize . config)
|
||||
|
||||
withFileLog :: (MonadIO m, MonadReader XFTPEnv m) => (StoreLog 'WriteMode -> IO a) -> m ()
|
||||
withFileLog action = liftIO . mapM_ action =<< asks storeLog
|
||||
|
||||
incFileStat :: (FileServerStats -> TVar Int) -> M ()
|
||||
incFileStat statSel = do
|
||||
stats <- asks serverStats
|
||||
atomically $ modifyTVar (statSel stats) (+ 1)
|
||||
|
||||
saveServerStats :: M ()
|
||||
saveServerStats =
|
||||
asks (serverStatsBackupFile . config)
|
||||
>>= mapM_ (\f -> asks serverStats >>= atomically . getFileServerStatsData >>= liftIO . saveStats f)
|
||||
where
|
||||
saveStats f stats = do
|
||||
logInfo $ "saving server stats to file " <> T.pack f
|
||||
B.writeFile f $ strEncode stats
|
||||
logInfo "server stats saved"
|
||||
|
||||
restoreServerStats :: M ()
|
||||
restoreServerStats = asks (serverStatsBackupFile . config) >>= mapM_ restoreStats
|
||||
where
|
||||
restoreStats f = whenM (doesFileExist f) $ do
|
||||
logInfo $ "restoring server stats from file " <> T.pack f
|
||||
liftIO (strDecode <$> B.readFile f) >>= \case
|
||||
Right d -> 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
|
||||
atomically $ setFileServerStats s d {_filesCount, _filesSize}
|
||||
renameFile f $ f <> ".bak"
|
||||
logInfo "server stats restored"
|
||||
Left e -> do
|
||||
logInfo $ "error restoring server stats: " <> T.pack e
|
||||
liftIO exitFailure
|
||||
@@ -0,0 +1,98 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE StrictData #-}
|
||||
|
||||
module Simplex.FileTransfer.Server.Env where
|
||||
|
||||
import Control.Logger.Simple (logInfo)
|
||||
import Control.Monad
|
||||
import Control.Monad.IO.Unlift
|
||||
import Crypto.Random
|
||||
import Data.Int (Int64)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
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.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.Server.Expiration
|
||||
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,
|
||||
fileIdSize :: Int,
|
||||
storeLogFile :: Maybe FilePath,
|
||||
filesPath :: FilePath,
|
||||
-- | server storage quota
|
||||
fileSizeQuota :: Maybe Int64,
|
||||
-- | allowed file chunk sizes
|
||||
allowedChunkSizes :: [Word32],
|
||||
-- | set to False to prohibit creating new files
|
||||
allowNewFiles :: Bool,
|
||||
-- | simple password that the clients need to pass in handshake to be able to create new files
|
||||
newFileBasicAuth :: Maybe BasicAuth,
|
||||
-- | time after which the files can be removed and check interval, seconds
|
||||
fileExpiration :: Maybe ExpirationConfig,
|
||||
-- CA certificate private key is not needed for initialization
|
||||
caCertificateFile :: FilePath,
|
||||
privateKeyFile :: FilePath,
|
||||
certificateFile :: FilePath,
|
||||
-- stats config - see SMP server config
|
||||
logStatsInterval :: Maybe Int64,
|
||||
logStatsStartTime :: Int64,
|
||||
serverStatsLogFile :: FilePath,
|
||||
serverStatsBackupFile :: Maybe FilePath,
|
||||
transportConfig :: TransportServerConfig
|
||||
}
|
||||
|
||||
data XFTPEnv = XFTPEnv
|
||||
{ config :: XFTPServerConfig,
|
||||
store :: FileStore,
|
||||
storeLog :: Maybe (StoreLog 'WriteMode),
|
||||
idsDrg :: TVar ChaChaDRG,
|
||||
serverIdentity :: C.KeyHash,
|
||||
tlsServerParams :: T.ServerParams,
|
||||
serverStats :: FileServerStats
|
||||
}
|
||||
|
||||
defFileExpirationHours :: Int64
|
||||
defFileExpirationHours = 48
|
||||
|
||||
defaultFileExpiration :: ExpirationConfig
|
||||
defaultFileExpiration =
|
||||
ExpirationConfig
|
||||
{ ttl = defFileExpirationHours * 3600, -- seconds
|
||||
checkInterval = 2 * 3600 -- seconds, 2 hours
|
||||
}
|
||||
|
||||
newXFTPServerEnv :: (MonadUnliftIO m, MonadRandom m) => XFTPServerConfig -> m XFTPEnv
|
||||
newXFTPServerEnv config@XFTPServerConfig {storeLogFile, fileSizeQuota, caCertificateFile, certificateFile, privateKeyFile} = do
|
||||
idsDrg <- drgNew >>= newTVarIO
|
||||
store <- atomically newFileStore
|
||||
storeLog <- liftIO $ mapM (`readWriteFileStore` store) storeLogFile
|
||||
used <- readTVarIO (usedStorage store)
|
||||
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
|
||||
Fingerprint fp <- liftIO $ loadFingerprint caCertificateFile
|
||||
serverStats <- atomically . newFileServerStats =<< liftIO getCurrentTime
|
||||
pure XFTPEnv {config, store, storeLog, idsDrg, tlsServerParams, serverIdentity = C.KeyHash fp, serverStats}
|
||||
|
||||
data XFTPRequest
|
||||
= XFTPReqNew FileInfo (NonEmpty RcvPublicVerifyKey) (Maybe BasicAuth)
|
||||
| XFTPReqCmd XFTPFileId FileRec FileCmd
|
||||
| XFTPReqPing
|
||||
@@ -0,0 +1,229 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
|
||||
module Simplex.FileTransfer.Server.Main where
|
||||
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Either (fromRight)
|
||||
import Data.Functor (($>))
|
||||
import Data.Ini (lookupValue, readIniFile)
|
||||
import Data.Int (Int64)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import qualified Data.Text as T
|
||||
import Network.Socket (HostName)
|
||||
import Options.Applicative
|
||||
import Simplex.FileTransfer.Chunks
|
||||
import Simplex.FileTransfer.Description (FileSize (..))
|
||||
import Simplex.FileTransfer.Server (runXFTPServer)
|
||||
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defFileExpirationHours, defaultFileExpiration)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
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.Client (TransportHost (..))
|
||||
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig)
|
||||
import System.Directory (createDirectoryIfMissing, doesFileExist)
|
||||
import System.FilePath (combine)
|
||||
import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
|
||||
import Text.Read (readMaybe)
|
||||
|
||||
xftpServerVersion :: String
|
||||
xftpServerVersion = "1.1.3"
|
||||
|
||||
xftpServerCLI :: FilePath -> FilePath -> IO ()
|
||||
xftpServerCLI cfgPath logPath = do
|
||||
getCliCommand' (cliCommandP cfgPath logPath iniFile) serverVersion >>= \case
|
||||
Init opts ->
|
||||
doesFileExist iniFile >>= \case
|
||||
True -> exitError $ "Error: server is already initialized (" <> iniFile <> " exists).\nRun `" <> executableName <> " start`."
|
||||
_ -> initializeServer opts
|
||||
Start ->
|
||||
doesFileExist iniFile >>= \case
|
||||
True -> readIniFile iniFile >>= either exitError runServer
|
||||
_ -> exitError $ "Error: server is not initialized (" <> iniFile <> " does not exist).\nRun `" <> executableName <> " init`."
|
||||
Delete -> do
|
||||
confirmOrExit "WARNING: deleting the server will make all queues inaccessible, because the server identity (certificate fingerprint) will change.\nTHIS CANNOT BE UNDONE!"
|
||||
deleteDirIfExists cfgPath
|
||||
deleteDirIfExists logPath
|
||||
putStrLn "Deleted configuration and log files"
|
||||
where
|
||||
iniFile = combine cfgPath "file-server.ini"
|
||||
serverVersion = "SimpleX XFTP server v" <> xftpServerVersion
|
||||
defaultServerPort = "443"
|
||||
executableName = "file-server"
|
||||
storeLogFilePath = combine logPath "file-server-store.log"
|
||||
initializeServer InitOptions {enableStoreLog, signAlgorithm, ip, fqdn, filesPath, fileSizeQuota} = do
|
||||
clearDirIfExists cfgPath
|
||||
clearDirIfExists logPath
|
||||
createDirectoryIfMissing True cfgPath
|
||||
createDirectoryIfMissing True logPath
|
||||
let x509cfg = defaultX509Config {commonName = fromMaybe ip fqdn, signAlgorithm}
|
||||
fp <- createServerX509 cfgPath x509cfg
|
||||
let host = fromMaybe (if ip == "127.0.0.1" then "<hostnames>" else ip) fqdn
|
||||
srv = ProtoServerWithAuth (XFTPServer [THDomainName host] "" (C.KeyHash fp)) Nothing
|
||||
writeFile iniFile $ iniFileContent host
|
||||
putStrLn $ "Server initialized, you can modify configuration in " <> iniFile <> ".\nRun `" <> executableName <> " start` to start server."
|
||||
warnCAPrivateKeyFile cfgPath x509cfg
|
||||
printServiceInfo serverVersion srv
|
||||
where
|
||||
iniFileContent host =
|
||||
"[STORE_LOG]\n\
|
||||
\# The server uses STM memory for persistence,\n\
|
||||
\# that will be lost on restart (e.g., as with redis).\n\
|
||||
\# This option enables saving memory to append only log,\n\
|
||||
\# and restoring it when the server is started.\n\
|
||||
\# Log is compacted on start (deleted objects are removed).\n"
|
||||
<> ("enable: " <> onOff enableStoreLog <> "\n\n")
|
||||
<> "# Expire files after the specified number of hours.\n"
|
||||
<> ("expire_files_hours: " <> show defFileExpirationHours <> "\n\n")
|
||||
<> "log_stats: off\n\
|
||||
\\n\
|
||||
\[AUTH]\n\
|
||||
\# Set new_files option to off to completely prohibit uploading new files.\n\
|
||||
\# This can be useful when you want to decommission the server, but still allow downloading the existing files.\n\
|
||||
\new_files: on\n\
|
||||
\\n\
|
||||
\# Use create_password option to enable basic auth to upload new files.\n\
|
||||
\# The password should be used as part of server address in client configuration:\n\
|
||||
\# xftp://fingerprint:password@host1,host2\n\
|
||||
\# The password will not be shared with file recipients, you must share it only\n\
|
||||
\# 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\
|
||||
\[TRANSPORT]\n\
|
||||
\# host is only used to print server address on start\n"
|
||||
<> ("host: " <> host <> "\n")
|
||||
<> ("port: " <> defaultServerPort <> "\n")
|
||||
<> "log_tls_errors: off\n\
|
||||
\\n\
|
||||
\[FILES]\n"
|
||||
<> ("path: " <> filesPath <> "\n")
|
||||
<> ("storage_quota: " <> B.unpack (strEncode fileSizeQuota) <> "\n")
|
||||
runServer ini = do
|
||||
hSetBuffering stdout LineBuffering
|
||||
hSetBuffering stderr LineBuffering
|
||||
fp <- checkSavedFingerprint cfgPath defaultX509Config
|
||||
let host = fromRight "<hostnames>" $ T.unpack <$> lookupValue "TRANSPORT" "host" ini
|
||||
port = T.unpack $ strictIni "TRANSPORT" "port" ini
|
||||
srv = ProtoServerWithAuth (XFTPServer [THDomainName host] (if port == "443" then "" else port) (C.KeyHash fp)) Nothing
|
||||
printServiceInfo serverVersion srv
|
||||
printXFTPConfig serverConfig
|
||||
runXFTPServer serverConfig
|
||||
where
|
||||
enableStoreLog = settingIsOn "STORE_LOG" "enable" ini
|
||||
logStats = settingIsOn "STORE_LOG" "log_stats" ini
|
||||
c = combine cfgPath . ($ defaultX509Config)
|
||||
printXFTPConfig XFTPServerConfig {allowNewFiles, newFileBasicAuth, xftpPort, storeLogFile, fileExpiration} = do
|
||||
putStrLn $ case storeLogFile of
|
||||
Just f -> "Store log: " <> f
|
||||
_ -> "Store log disabled."
|
||||
putStrLn $ case fileExpiration of
|
||||
Just ExpirationConfig {ttl} -> "expiring files after " <> showTTL ttl
|
||||
_ -> "not expiring files"
|
||||
putStrLn $
|
||||
"Uploading new files "
|
||||
<> if allowNewFiles
|
||||
then maybe "allowed." (const "requires password.") newFileBasicAuth
|
||||
else "NOT allowed."
|
||||
putStrLn $ "Listening on port " <> xftpPort <> "..."
|
||||
|
||||
serverConfig =
|
||||
XFTPServerConfig
|
||||
{ xftpPort = T.unpack $ strictIni "TRANSPORT" "port" ini,
|
||||
fileIdSize = 16,
|
||||
storeLogFile = enableStoreLog $> storeLogFilePath,
|
||||
filesPath = T.unpack $ strictIni "FILES" "path" ini,
|
||||
fileSizeQuota = either error unFileSize <$> strDecodeIni "FILES" "storage_quota" ini,
|
||||
allowedChunkSizes = serverChunkSizes,
|
||||
allowNewFiles = fromMaybe True $ iniOnOff "AUTH" "new_files" ini,
|
||||
newFileBasicAuth = either error id <$> strDecodeIni "AUTH" "create_password" ini,
|
||||
fileExpiration =
|
||||
Just
|
||||
defaultFileExpiration
|
||||
{ ttl = 3600 * readIniDefault defFileExpirationHours "STORE_LOG" "expire_files_hours" ini
|
||||
},
|
||||
caCertificateFile = c caCrtFile,
|
||||
privateKeyFile = c serverKeyFile,
|
||||
certificateFile = c serverCrtFile,
|
||||
logStatsInterval = logStats $> 86400, -- seconds
|
||||
logStatsStartTime = 0, -- seconds from 00:00 UTC
|
||||
serverStatsLogFile = combine logPath "file-server-stats.daily.log",
|
||||
serverStatsBackupFile = logStats $> combine logPath "file-server-stats.log",
|
||||
transportConfig =
|
||||
defaultTransportServerConfig
|
||||
{ logTLSErrors = fromMaybe False $ iniOnOff "TRANSPORT" "log_tls_errors" ini
|
||||
}
|
||||
}
|
||||
|
||||
data CliCommand
|
||||
= Init InitOptions
|
||||
| Start
|
||||
| Delete
|
||||
|
||||
data InitOptions = InitOptions
|
||||
{ enableStoreLog :: Bool,
|
||||
signAlgorithm :: SignAlgorithm,
|
||||
ip :: HostName,
|
||||
fqdn :: Maybe HostName,
|
||||
filesPath :: FilePath,
|
||||
fileSizeQuota :: FileSize Int64
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
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 "start" (info (pure Start) (progDesc $ "Start server (configuration: " <> iniFile <> ")"))
|
||||
<> command "delete" (info (pure Delete) (progDesc "Delete configuration and log files"))
|
||||
)
|
||||
where
|
||||
initP :: Parser InitOptions
|
||||
initP =
|
||||
InitOptions
|
||||
<$> switch
|
||||
( long "store-log"
|
||||
<> short 'l'
|
||||
<> help "Enable store log for persistence"
|
||||
)
|
||||
<*> option
|
||||
(maybeReader readMaybe)
|
||||
( long "sign-algorithm"
|
||||
<> short 'a'
|
||||
<> help "Signature algorithm used for TLS certificates: ED25519, ED448"
|
||||
<> value ED448
|
||||
<> showDefault
|
||||
<> metavar "ALG"
|
||||
)
|
||||
<*> strOption
|
||||
( long "ip"
|
||||
<> help
|
||||
"Server IP address, used as Common Name for TLS online certificate if FQDN is not supplied"
|
||||
<> value "127.0.0.1"
|
||||
<> showDefault
|
||||
<> metavar "IP"
|
||||
)
|
||||
<*> (optional . strOption)
|
||||
( long "fqdn"
|
||||
<> short 'n'
|
||||
<> help "Server FQDN used as Common Name for TLS online certificate"
|
||||
<> showDefault
|
||||
<> metavar "FQDN"
|
||||
)
|
||||
<*> strOption
|
||||
( long "path"
|
||||
<> short 'p'
|
||||
<> help "Path to the directory to store files"
|
||||
<> metavar "PATH"
|
||||
)
|
||||
<*> strOption
|
||||
( long "quota"
|
||||
<> short 'q'
|
||||
<> help "File storage quota (e.g. 100gb)"
|
||||
<> metavar "QUOTA"
|
||||
)
|
||||
@@ -0,0 +1,106 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Simplex.FileTransfer.Server.Stats where
|
||||
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Int (Int64)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (SenderId)
|
||||
import Simplex.Messaging.Server.Stats (PeriodStats, PeriodStatsData, getPeriodStatsData, newPeriodStats, setPeriodStats)
|
||||
import UnliftIO.STM
|
||||
|
||||
data FileServerStats = FileServerStats
|
||||
{ fromTime :: TVar UTCTime,
|
||||
filesCreated :: TVar Int,
|
||||
fileRecipients :: TVar Int,
|
||||
filesUploaded :: TVar Int,
|
||||
filesDeleted :: TVar Int,
|
||||
filesDownloaded :: PeriodStats SenderId,
|
||||
fileDownloads :: TVar Int,
|
||||
fileDownloadAcks :: TVar Int,
|
||||
filesCount :: TVar Int,
|
||||
filesSize :: TVar Int64
|
||||
}
|
||||
|
||||
data FileServerStatsData = FileServerStatsData
|
||||
{ _fromTime :: UTCTime,
|
||||
_filesCreated :: Int,
|
||||
_fileRecipients :: Int,
|
||||
_filesUploaded :: Int,
|
||||
_filesDeleted :: Int,
|
||||
_filesDownloaded :: PeriodStatsData SenderId,
|
||||
_fileDownloads :: Int,
|
||||
_fileDownloadAcks :: Int,
|
||||
_filesCount :: Int,
|
||||
_filesSize :: Int64
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
newFileServerStats :: UTCTime -> STM FileServerStats
|
||||
newFileServerStats ts = do
|
||||
fromTime <- newTVar ts
|
||||
filesCreated <- newTVar 0
|
||||
fileRecipients <- newTVar 0
|
||||
filesUploaded <- 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}
|
||||
|
||||
getFileServerStatsData :: FileServerStats -> STM FileServerStatsData
|
||||
getFileServerStatsData s = do
|
||||
_fromTime <- readTVar $ fromTime (s :: FileServerStats)
|
||||
_filesCreated <- readTVar $ filesCreated s
|
||||
_fileRecipients <- readTVar $ fileRecipients s
|
||||
_filesUploaded <- readTVar $ filesUploaded 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}
|
||||
|
||||
setFileServerStats :: FileServerStats -> FileServerStatsData -> STM ()
|
||||
setFileServerStats s d = do
|
||||
writeTVar (fromTime (s :: FileServerStats)) $! _fromTime (d :: FileServerStatsData)
|
||||
writeTVar (filesCreated s) $! _filesCreated d
|
||||
writeTVar (fileRecipients s) $! _fileRecipients d
|
||||
writeTVar (filesUploaded s) $! _filesUploaded d
|
||||
writeTVar (filesDeleted s) $! _filesDeleted d
|
||||
setPeriodStats (filesDownloaded s) $! _filesDownloaded d
|
||||
writeTVar (fileDownloads s) $! _fileDownloads d
|
||||
writeTVar (fileDownloadAcks s) $! _fileDownloadAcks d
|
||||
writeTVar (filesCount s) $! _filesCount d
|
||||
writeTVar (filesSize s) $! _filesSize d
|
||||
|
||||
instance StrEncoding FileServerStatsData where
|
||||
strEncode FileServerStatsData {_fromTime, _filesCreated, _fileRecipients, _filesUploaded, _filesDeleted, _filesDownloaded, _fileDownloads, _fileDownloadAcks} =
|
||||
B.unlines
|
||||
[ "fromTime=" <> strEncode _fromTime,
|
||||
"filesCreated=" <> strEncode _filesCreated,
|
||||
"fileRecipients=" <> strEncode _fileRecipients,
|
||||
"filesUploaded=" <> strEncode _filesUploaded,
|
||||
"filesDeleted=" <> strEncode _filesDeleted,
|
||||
"filesDownloaded:",
|
||||
strEncode _filesDownloaded,
|
||||
"fileDownloads=" <> strEncode _fileDownloads,
|
||||
"fileDownloadAcks=" <> strEncode _fileDownloadAcks
|
||||
]
|
||||
strP = do
|
||||
_fromTime <- "fromTime=" *> strP <* A.endOfLine
|
||||
_filesCreated <- "filesCreated=" *> strP <* A.endOfLine
|
||||
_fileRecipients <- "fileRecipients=" *> strP <* A.endOfLine
|
||||
_filesUploaded <- "filesUploaded=" *> strP <* A.endOfLine
|
||||
_filesDeleted <- "filesDeleted=" *> strP <* A.endOfLine
|
||||
_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}
|
||||
@@ -0,0 +1,145 @@
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module Simplex.FileTransfer.Server.Store
|
||||
( FileStore (..),
|
||||
FileRec (..),
|
||||
FileRecipient (..),
|
||||
newFileStore,
|
||||
addFile,
|
||||
setFilePath,
|
||||
setFilePath',
|
||||
addRecipient,
|
||||
deleteFile,
|
||||
deleteRecipient,
|
||||
expiredFilePath,
|
||||
getFile,
|
||||
ackFile,
|
||||
)
|
||||
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 qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (RcvPublicVerifyKey, 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),
|
||||
usedStorage :: TVar Int64
|
||||
}
|
||||
|
||||
data FileRec = FileRec
|
||||
{ senderId :: SenderId,
|
||||
fileInfo :: FileInfo,
|
||||
filePath :: TVar (Maybe FilePath),
|
||||
recipientIds :: TVar (Set RecipientId),
|
||||
createdAt :: SystemTime
|
||||
}
|
||||
deriving (Eq)
|
||||
|
||||
data FileRecipient = FileRecipient RecipientId RcvPublicVerifyKey
|
||||
|
||||
instance StrEncoding FileRecipient where
|
||||
strEncode (FileRecipient rId rKey) = strEncode rId <> ":" <> strEncode rKey
|
||||
strP = FileRecipient <$> strP <* A.char ':' <*> strP
|
||||
|
||||
newFileStore :: STM FileStore
|
||||
newFileStore = do
|
||||
files <- TM.empty
|
||||
recipients <- TM.empty
|
||||
usedStorage <- newTVar 0
|
||||
pure FileStore {files, recipients, usedStorage}
|
||||
|
||||
addFile :: FileStore -> SenderId -> FileInfo -> SystemTime -> STM (Either XFTPErrorType ())
|
||||
addFile FileStore {files} sId fileInfo createdAt =
|
||||
ifM (TM.member sId files) (pure $ Left DUPLICATE_) $ do
|
||||
f <- newFileRec sId fileInfo createdAt
|
||||
TM.insert sId f files
|
||||
pure $ Right ()
|
||||
|
||||
newFileRec :: SenderId -> FileInfo -> SystemTime -> STM FileRec
|
||||
newFileRec senderId fileInfo createdAt = do
|
||||
recipientIds <- newTVar S.empty
|
||||
filePath <- newTVar Nothing
|
||||
pure FileRec {senderId, fileInfo, filePath, recipientIds, createdAt}
|
||||
|
||||
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))
|
||||
|
||||
addRecipient :: FileStore -> SenderId -> FileRecipient -> STM (Either XFTPErrorType ())
|
||||
addRecipient st@FileStore {recipients} senderId (FileRecipient rId rKey) =
|
||||
withFile st senderId $ \FileRec {recipientIds} -> do
|
||||
rIds <- readTVar recipientIds
|
||||
mem <- TM.member rId recipients
|
||||
if rId `S.member` rIds || mem
|
||||
then pure $ Left DUPLICATE_
|
||||
else do
|
||||
writeTVar recipientIds $! S.insert rId rIds
|
||||
TM.insert rId (senderId, rKey) recipients
|
||||
pure $ Right ()
|
||||
|
||||
-- this function must be called after the file is deleted from the file system
|
||||
deleteFile :: FileStore -> SenderId -> STM (Either XFTPErrorType ())
|
||||
deleteFile FileStore {files, recipients, usedStorage} senderId = do
|
||||
TM.lookupDelete senderId files >>= \case
|
||||
Just FileRec {fileInfo, recipientIds} -> do
|
||||
readTVar recipientIds >>= mapM_ (`TM.delete` recipients)
|
||||
modifyTVar' usedStorage $ subtract (fromIntegral $ size fileInfo)
|
||||
pure $ Right ()
|
||||
_ -> pure $ Left AUTH
|
||||
|
||||
deleteRecipient :: FileStore -> RecipientId -> FileRec -> STM ()
|
||||
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 st party fId = case party of
|
||||
SFSender -> withFile st fId $ pure . Right . (\f -> (f, sndKey $ fileInfo f))
|
||||
SFRecipient ->
|
||||
TM.lookup fId (recipients st) >>= \case
|
||||
Just (sId, rKey) -> withFile st sId $ pure . Right . (,rKey)
|
||||
_ -> pure $ Left AUTH
|
||||
|
||||
expiredFilePath :: FileStore -> XFTPFileId -> Int64 -> STM (Maybe FilePath)
|
||||
expiredFilePath FileStore {files} sId old =
|
||||
TM.lookup sId files
|
||||
$>>= \FileRec {filePath, createdAt} ->
|
||||
if systemSeconds createdAt < old
|
||||
then readTVar filePath
|
||||
else pure Nothing
|
||||
|
||||
ackFile :: FileStore -> RecipientId -> STM (Either XFTPErrorType ())
|
||||
ackFile st@FileStore {recipients} recipientId = do
|
||||
TM.lookupDelete recipientId recipients >>= \case
|
||||
Just (sId, _) ->
|
||||
withFile st sId $ \FileRec {recipientIds} -> do
|
||||
modifyTVar' recipientIds $ S.delete recipientId
|
||||
pure $ Right ()
|
||||
_ -> pure $ Left AUTH
|
||||
|
||||
withFile :: FileStore -> SenderId -> (FileRec -> STM (Either XFTPErrorType a)) -> STM (Either XFTPErrorType a)
|
||||
withFile FileStore {files} sId a =
|
||||
TM.lookup sId files >>= \case
|
||||
Just f -> a f
|
||||
_ -> pure $ Left AUTH
|
||||
@@ -0,0 +1,124 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Simplex.FileTransfer.Server.StoreLog
|
||||
( StoreLog,
|
||||
FileStoreLogRecord (..),
|
||||
closeStoreLog,
|
||||
readWriteFileStore,
|
||||
logAddFile,
|
||||
logPutFile,
|
||||
logAddRecipients,
|
||||
logDeleteFile,
|
||||
logAckFile,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad.Except
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Composition ((.:), (.:.))
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Time.Clock.System (SystemTime)
|
||||
import Simplex.FileTransfer.Protocol (FileInfo (..))
|
||||
import Simplex.FileTransfer.Server.Store
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (RcvPublicVerifyKey, RecipientId, SenderId)
|
||||
import Simplex.Messaging.Server.StoreLog
|
||||
import Simplex.Messaging.Util (bshow, whenM)
|
||||
import System.Directory (doesFileExist, renameFile)
|
||||
import System.IO
|
||||
|
||||
data FileStoreLogRecord
|
||||
= AddFile SenderId FileInfo SystemTime
|
||||
| PutFile SenderId FilePath
|
||||
| AddRecipients SenderId (NonEmpty FileRecipient)
|
||||
| DeleteFile SenderId
|
||||
| AckFile RecipientId
|
||||
|
||||
instance StrEncoding FileStoreLogRecord where
|
||||
strEncode = \case
|
||||
AddFile sId file createdAt -> strEncode (Str "FNEW", sId, file, createdAt)
|
||||
PutFile sId path -> strEncode (Str "FPUT", sId, path)
|
||||
AddRecipients sId rcps -> strEncode (Str "FADD", sId, rcps)
|
||||
DeleteFile sId -> strEncode (Str "FDEL", sId)
|
||||
AckFile rId -> strEncode (Str "FACK", rId)
|
||||
strP =
|
||||
A.choice
|
||||
[ "FNEW " *> (AddFile <$> strP_ <*> strP_ <*> strP),
|
||||
"FPUT " *> (PutFile <$> strP_ <*> strP),
|
||||
"FADD " *> (AddRecipients <$> strP_ <*> strP),
|
||||
"FDEL " *> (DeleteFile <$> strP),
|
||||
"FACK " *> (AckFile <$> strP)
|
||||
]
|
||||
|
||||
logFileStoreRecord :: StoreLog 'WriteMode -> FileStoreLogRecord -> IO ()
|
||||
logFileStoreRecord = writeStoreLogRecord
|
||||
|
||||
logAddFile :: StoreLog 'WriteMode -> SenderId -> FileInfo -> SystemTime -> IO ()
|
||||
logAddFile s = logFileStoreRecord s .:. AddFile
|
||||
|
||||
logPutFile :: StoreLog 'WriteMode -> SenderId -> FilePath -> IO ()
|
||||
logPutFile s = logFileStoreRecord s .: PutFile
|
||||
|
||||
logAddRecipients :: StoreLog 'WriteMode -> SenderId -> NonEmpty FileRecipient -> IO ()
|
||||
logAddRecipients s = logFileStoreRecord s .: AddRecipients
|
||||
|
||||
logDeleteFile :: StoreLog 'WriteMode -> SenderId -> IO ()
|
||||
logDeleteFile s = logFileStoreRecord s . DeleteFile
|
||||
|
||||
logAckFile :: StoreLog 'WriteMode -> RecipientId -> IO ()
|
||||
logAckFile s = logFileStoreRecord s . AckFile
|
||||
|
||||
readWriteFileStore :: FilePath -> FileStore -> IO (StoreLog 'WriteMode)
|
||||
readWriteFileStore f st = do
|
||||
whenM (doesFileExist f) $ do
|
||||
readFileStore f st
|
||||
renameFile f $ f <> ".bak"
|
||||
s <- openWriteStoreLog f
|
||||
writeFileStore s st
|
||||
pure s
|
||||
|
||||
readFileStore :: FilePath -> FileStore -> IO ()
|
||||
readFileStore f st = mapM_ addFileLogRecord . B.lines =<< B.readFile f
|
||||
where
|
||||
addFileLogRecord s = case strDecode s of
|
||||
Left e -> B.putStrLn $ "Log parsing error (" <> B.pack e <> "): " <> B.take 100 s
|
||||
Right lr ->
|
||||
atomically (addToStore lr) >>= \case
|
||||
Left e -> B.putStrLn $ "Log processing error (" <> bshow e <> "): " <> B.take 100 s
|
||||
_ -> pure ()
|
||||
addToStore = \case
|
||||
AddFile sId file createdAt -> addFile st sId file createdAt
|
||||
PutFile qId path -> setFilePath st qId path
|
||||
AddRecipients sId rcps -> runExceptT $ addRecipients sId rcps
|
||||
DeleteFile sId -> deleteFile st sId
|
||||
AckFile rId -> ackFile st rId
|
||||
addRecipients sId rcps = mapM_ (ExceptT . addRecipient st sId) rcps
|
||||
|
||||
writeFileStore :: StoreLog 'WriteMode -> FileStore -> IO ()
|
||||
writeFileStore s FileStore {files, recipients} = do
|
||||
allRcps <- readTVarIO recipients
|
||||
readTVarIO files >>= mapM_ (logFile allRcps)
|
||||
where
|
||||
logFile :: Map RecipientId (SenderId, RcvPublicVerifyKey) -> 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
|
||||
mapM_ (logAddRecipients s senderId) $ L.nonEmpty $ M.elems rcps
|
||||
mapM_ (B.putStrLn . ("Error storing log: " <>)) rcpErrs
|
||||
readTVarIO filePath >>= mapM_ (logPutFile s senderId)
|
||||
where
|
||||
getRcp rId = case M.lookup rId allRcps of
|
||||
Just (sndId, rKey)
|
||||
| sndId == senderId -> Right $ FileRecipient rId rKey
|
||||
| otherwise -> Left $ "sender ID for recipient ID " <> bshow rId <> " does not match FileRec"
|
||||
Nothing -> Left $ "recipient ID " <> bshow rId <> " not found"
|
||||
@@ -0,0 +1,98 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.FileTransfer.Transport
|
||||
( supportedFileServerVRange,
|
||||
XFTPRcvChunkSpec (..),
|
||||
ReceiveFileError (..),
|
||||
receiveFile,
|
||||
sendEncFile,
|
||||
receiveEncFile,
|
||||
receiveSbFile,
|
||||
)
|
||||
where
|
||||
|
||||
import qualified Control.Exception as E
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.IO.Class
|
||||
import Data.Bifunctor (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 qualified Simplex.Messaging.Crypto as C
|
||||
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||
import Simplex.Messaging.Transport.HTTP2.File
|
||||
import Simplex.Messaging.Version
|
||||
import System.IO (Handle, IOMode (..), withFile)
|
||||
|
||||
data XFTPRcvChunkSpec = XFTPRcvChunkSpec
|
||||
{ filePath :: FilePath,
|
||||
chunkSize :: Word32,
|
||||
chunkDigest :: ByteString
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
supportedFileServerVRange :: VersionRange
|
||||
supportedFileServerVRange = mkVersionRange 1 1
|
||||
|
||||
sendEncFile :: Handle -> (Builder -> IO ()) -> LC.SbState -> Word32 -> IO ()
|
||||
sendEncFile h send = go
|
||||
where
|
||||
go sbState 0 = do
|
||||
let authTag = BA.convert (LC.sbAuth sbState)
|
||||
send $ byteString authTag
|
||||
go sbState sz =
|
||||
getFileChunk h sz >>= \ch -> do
|
||||
let (encCh, sbState') = LC.sbEncryptChunk sbState ch
|
||||
send (byteString encCh) `E.catch` \(e :: E.SomeException) -> print e >> E.throwIO e
|
||||
go sbState' $ sz - fromIntegral (B.length ch)
|
||||
|
||||
receiveFile :: (Int -> IO ByteString) -> XFTPRcvChunkSpec -> ExceptT XFTPErrorType IO ()
|
||||
receiveFile getBody = receiveFile_ receive
|
||||
where
|
||||
receive h sz = hReceiveFile getBody h sz >>= \sz' -> pure $ if sz' == 0 then Right () else Left SIZE
|
||||
|
||||
receiveEncFile :: (Int -> IO ByteString) -> LC.SbState -> XFTPRcvChunkSpec -> ExceptT XFTPErrorType IO ()
|
||||
receiveEncFile getBody = receiveFile_ . receive
|
||||
where
|
||||
receive sbState h sz = first err <$> receiveSbFile getBody h sbState sz
|
||||
err RFESize = SIZE
|
||||
err RFECrypto = CRYPTO
|
||||
|
||||
data ReceiveFileError = RFESize | RFECrypto
|
||||
|
||||
receiveSbFile :: (Int -> IO ByteString) -> Handle -> LC.SbState -> Word32 -> IO (Either ReceiveFileError ())
|
||||
receiveSbFile getBody h = receive
|
||||
where
|
||||
receive sbState sz = do
|
||||
ch <- getBody fileBlockSize
|
||||
let chSize = fromIntegral $ B.length ch
|
||||
if
|
||||
| chSize > sz + authSz -> pure $ Left RFESize
|
||||
| chSize > 0 -> do
|
||||
let (ch', rest) = B.splitAt (fromIntegral sz) ch
|
||||
(decCh, sbState') = LC.sbDecryptChunk sbState ch'
|
||||
sz' = sz - fromIntegral (B.length ch')
|
||||
B.hPut h decCh
|
||||
if sz' > 0
|
||||
then receive sbState' sz'
|
||||
else do
|
||||
let tag' = B.take C.authTagSize rest
|
||||
tagSz = B.length tag'
|
||||
tag = LC.sbAuth sbState'
|
||||
tag'' <- if tagSz == C.authTagSize then pure tag' else (tag' <>) <$> getBody (C.authTagSize - tagSz)
|
||||
pure $ if BA.constEq tag'' tag then Right () else Left RFECrypto
|
||||
| otherwise -> pure $ Left RFESize
|
||||
authSz = fromIntegral C.authTagSize
|
||||
|
||||
receiveFile_ :: (Handle -> Word32 -> IO (Either XFTPErrorType ())) -> XFTPRcvChunkSpec -> ExceptT XFTPErrorType IO ()
|
||||
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
|
||||
@@ -0,0 +1,229 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Simplex.FileTransfer.Types where
|
||||
|
||||
import Data.Int (Int64)
|
||||
import Data.Word (Word32)
|
||||
import Database.SQLite.Simple.FromField (FromField (..))
|
||||
import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import Simplex.FileTransfer.Client (XFTPChunkSpec (..))
|
||||
import Simplex.FileTransfer.Description
|
||||
import Simplex.Messaging.Agent.Protocol (RcvFileId, SndFileId)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..))
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (fromTextField_)
|
||||
import Simplex.Messaging.Protocol
|
||||
import System.FilePath ((</>))
|
||||
|
||||
authTagSize :: Int64
|
||||
authTagSize = fromIntegral C.authTagSize
|
||||
|
||||
-- fileExtra is added to allow header extension in future versions
|
||||
data FileHeader = FileHeader
|
||||
{ fileName :: String,
|
||||
fileExtra :: Maybe String
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance Encoding FileHeader where
|
||||
smpEncode FileHeader {fileName, fileExtra} = smpEncode (fileName, fileExtra)
|
||||
smpP = do
|
||||
(fileName, fileExtra) <- smpP
|
||||
pure FileHeader {fileName, fileExtra}
|
||||
|
||||
type DBRcvFileId = Int64
|
||||
|
||||
data RcvFile = RcvFile
|
||||
{ rcvFileId :: DBRcvFileId,
|
||||
rcvFileEntityId :: RcvFileId,
|
||||
userId :: Int64,
|
||||
size :: FileSize Int64,
|
||||
digest :: FileDigest,
|
||||
key :: C.SbKey,
|
||||
nonce :: C.CbNonce,
|
||||
chunkSize :: FileSize Word32,
|
||||
chunks :: [RcvFileChunk],
|
||||
prefixPath :: FilePath,
|
||||
tmpPath :: Maybe FilePath,
|
||||
saveFile :: CryptoFile,
|
||||
status :: RcvFileStatus,
|
||||
deleted :: Bool
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data RcvFileStatus
|
||||
= RFSReceiving
|
||||
| RFSReceived
|
||||
| RFSDecrypting
|
||||
| RFSComplete
|
||||
| RFSError
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance FromField RcvFileStatus where fromField = fromTextField_ textDecode
|
||||
|
||||
instance ToField RcvFileStatus where toField = toField . textEncode
|
||||
|
||||
instance TextEncoding RcvFileStatus where
|
||||
textDecode = \case
|
||||
"receiving" -> Just RFSReceiving
|
||||
"received" -> Just RFSReceived
|
||||
"decrypting" -> Just RFSDecrypting
|
||||
"complete" -> Just RFSComplete
|
||||
"error" -> Just RFSError
|
||||
_ -> Nothing
|
||||
textEncode = \case
|
||||
RFSReceiving -> "receiving"
|
||||
RFSReceived -> "received"
|
||||
RFSDecrypting -> "decrypting"
|
||||
RFSComplete -> "complete"
|
||||
RFSError -> "error"
|
||||
|
||||
data RcvFileChunk = RcvFileChunk
|
||||
{ rcvFileId :: DBRcvFileId,
|
||||
rcvFileEntityId :: RcvFileId,
|
||||
userId :: Int64,
|
||||
rcvChunkId :: Int64,
|
||||
chunkNo :: Int,
|
||||
chunkSize :: FileSize Word32,
|
||||
digest :: FileDigest,
|
||||
replicas :: [RcvFileChunkReplica],
|
||||
fileTmpPath :: FilePath,
|
||||
chunkTmpPath :: Maybe FilePath
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data RcvFileChunkReplica = RcvFileChunkReplica
|
||||
{ rcvChunkReplicaId :: Int64,
|
||||
server :: XFTPServer,
|
||||
replicaId :: ChunkReplicaId,
|
||||
replicaKey :: C.APrivateSignKey,
|
||||
received :: Bool,
|
||||
delay :: Maybe Int64,
|
||||
retries :: Int
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- Sending files
|
||||
|
||||
type DBSndFileId = Int64
|
||||
|
||||
data SndFile = SndFile
|
||||
{ sndFileId :: DBSndFileId,
|
||||
sndFileEntityId :: SndFileId,
|
||||
userId :: Int64,
|
||||
numRecipients :: Int,
|
||||
digest :: Maybe FileDigest,
|
||||
key :: C.SbKey,
|
||||
nonce :: C.CbNonce,
|
||||
chunks :: [SndFileChunk],
|
||||
srcFile :: CryptoFile,
|
||||
prefixPath :: Maybe FilePath,
|
||||
status :: SndFileStatus,
|
||||
deleted :: Bool
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
sndFileEncPath :: FilePath -> FilePath
|
||||
sndFileEncPath prefixPath = prefixPath </> "xftp.encrypted"
|
||||
|
||||
data SndFileStatus
|
||||
= SFSNew -- db record created
|
||||
| SFSEncrypting -- encryption started
|
||||
| SFSEncrypted -- encryption complete
|
||||
| SFSUploading -- all chunk replicas are created on servers
|
||||
| SFSComplete -- all chunk replicas are uploaded
|
||||
| SFSError -- permanent error
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance FromField SndFileStatus where fromField = fromTextField_ textDecode
|
||||
|
||||
instance ToField SndFileStatus where toField = toField . textEncode
|
||||
|
||||
instance TextEncoding SndFileStatus where
|
||||
textDecode = \case
|
||||
"new" -> Just SFSNew
|
||||
"encrypting" -> Just SFSEncrypting
|
||||
"encrypted" -> Just SFSEncrypted
|
||||
"uploading" -> Just SFSUploading
|
||||
"complete" -> Just SFSComplete
|
||||
"error" -> Just SFSError
|
||||
_ -> Nothing
|
||||
textEncode = \case
|
||||
SFSNew -> "new"
|
||||
SFSEncrypting -> "encrypting"
|
||||
SFSEncrypted -> "encrypted"
|
||||
SFSUploading -> "uploading"
|
||||
SFSComplete -> "complete"
|
||||
SFSError -> "error"
|
||||
|
||||
data SndFileChunk = SndFileChunk
|
||||
{ sndFileId :: DBSndFileId,
|
||||
sndFileEntityId :: SndFileId,
|
||||
userId :: Int64,
|
||||
numRecipients :: Int,
|
||||
sndChunkId :: Int64,
|
||||
chunkNo :: Int,
|
||||
chunkSpec :: XFTPChunkSpec,
|
||||
filePrefixPath :: FilePath,
|
||||
digest :: FileDigest,
|
||||
replicas :: [SndFileChunkReplica]
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
sndChunkSize :: SndFileChunk -> Word32
|
||||
sndChunkSize SndFileChunk {chunkSpec = XFTPChunkSpec {chunkSize}} = chunkSize
|
||||
|
||||
data NewSndChunkReplica = NewSndChunkReplica
|
||||
{ server :: XFTPServer,
|
||||
replicaId :: ChunkReplicaId,
|
||||
replicaKey :: C.APrivateSignKey,
|
||||
rcvIdsKeys :: [(ChunkReplicaId, C.APrivateSignKey)]
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data SndFileChunkReplica = SndFileChunkReplica
|
||||
{ sndChunkReplicaId :: Int64,
|
||||
server :: XFTPServer,
|
||||
replicaId :: ChunkReplicaId,
|
||||
replicaKey :: C.APrivateSignKey,
|
||||
rcvIdsKeys :: [(ChunkReplicaId, C.APrivateSignKey)],
|
||||
replicaStatus :: SndFileReplicaStatus,
|
||||
delay :: Maybe Int64,
|
||||
retries :: Int
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data SndFileReplicaStatus
|
||||
= SFRSCreated
|
||||
| SFRSUploaded
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance FromField SndFileReplicaStatus where fromField = fromTextField_ textDecode
|
||||
|
||||
instance ToField SndFileReplicaStatus where toField = toField . textEncode
|
||||
|
||||
instance TextEncoding SndFileReplicaStatus where
|
||||
textDecode = \case
|
||||
"created" -> Just SFRSCreated
|
||||
"uploaded" -> Just SFRSUploaded
|
||||
_ -> Nothing
|
||||
textEncode = \case
|
||||
SFRSCreated -> "created"
|
||||
SFRSUploaded -> "uploaded"
|
||||
|
||||
data DeletedSndChunkReplica = DeletedSndChunkReplica
|
||||
{ deletedSndChunkReplicaId :: Int64,
|
||||
userId :: Int64,
|
||||
server :: XFTPServer,
|
||||
replicaId :: ChunkReplicaId,
|
||||
replicaKey :: C.APrivateSignKey,
|
||||
chunkDigest :: FileDigest,
|
||||
delay :: Maybe Int64,
|
||||
retries :: Int
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
@@ -0,0 +1,24 @@
|
||||
module Simplex.FileTransfer.Util
|
||||
( uniqueCombine,
|
||||
removePath,
|
||||
)
|
||||
where
|
||||
|
||||
import Simplex.Messaging.Util (ifM, whenM)
|
||||
import System.FilePath (splitExtensions, (</>))
|
||||
import UnliftIO
|
||||
import UnliftIO.Directory
|
||||
|
||||
uniqueCombine :: MonadIO m => FilePath -> String -> m FilePath
|
||||
uniqueCombine filePath fileName = tryCombine (0 :: Int)
|
||||
where
|
||||
tryCombine n =
|
||||
let (name, ext) = splitExtensions fileName
|
||||
suffix = if n == 0 then "" else "_" <> show n
|
||||
f = filePath </> (name <> suffix <> ext)
|
||||
in ifM (doesPathExist f) (tryCombine $ n + 1) (pure f)
|
||||
|
||||
removePath :: MonadIO m => FilePath -> m ()
|
||||
removePath p = do
|
||||
ifM (doesDirectoryExist p) (removeDirectoryRecursive p) $
|
||||
whenM (doesFileExist p) (removeFile p)
|
||||
+2315
-497
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user