Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29a242bfe1 | ||
|
|
f03cec7a58 | ||
|
|
118a8e89bb | ||
|
|
b6f551000f | ||
|
|
fb2d0a4c3d | ||
|
|
012c8cc104 | ||
|
|
8bd3193280 | ||
|
|
ef3339ae4f | ||
|
|
fd298ae328 | ||
|
|
64d5413694 | ||
|
|
1f173abf6d | ||
|
|
21f4597dad | ||
|
|
ba6af65c54 | ||
|
|
d2957ff318 | ||
|
|
858fac7f4f | ||
|
|
90432a44b4 | ||
|
|
1e1f897c79 | ||
|
|
0dc2940eff | ||
|
|
8833e5c1b5 | ||
|
|
95b17ada27 | ||
|
|
43cdf55f3b | ||
|
|
bc5ea42bec | ||
|
|
0933cbcb9c | ||
|
|
f2dafd983b | ||
|
|
3e68fa2b63 | ||
|
|
34c0909c1a | ||
|
|
99f9de71e5 | ||
|
|
97802a30fc | ||
|
|
b82cf7d001 | ||
|
|
9bc0c70fa0 | ||
|
|
fe30d69ec0 | ||
|
|
bd01e78b6a | ||
|
|
0741583f78 | ||
|
|
0ebea15506 | ||
|
|
7a3713faf3 | ||
|
|
f8f172f32f | ||
|
|
9c07ddff3c | ||
|
|
50b71d3e56 | ||
|
|
d930bbad14 | ||
|
|
a1b762992b | ||
|
|
3134d6206d | ||
|
|
c3a041a786 | ||
|
|
1e0093be9a | ||
|
|
909c974445 | ||
|
|
2012236f65 | ||
|
|
a54518afe1 | ||
|
|
e762e84f46 | ||
|
|
5f08457b7e | ||
|
|
1a12ee0a5a | ||
|
|
efcef2d1fd | ||
|
|
1a255f2e2f | ||
|
|
963d7b2f75 | ||
|
|
8f4274763b | ||
|
|
4e4e0a4f42 | ||
|
|
082a6c6f22 | ||
|
|
dc2921e4ce | ||
|
|
5a32e729e0 | ||
|
|
48eba591bc | ||
|
|
782cacfb3c | ||
|
|
328d3b941a | ||
|
|
b6b87a6323 | ||
|
|
b81670c414 | ||
|
|
98351cf097 | ||
|
|
01785d5de8 | ||
|
|
f745ce5ab6 | ||
|
|
583f4e059d | ||
|
|
3c5752383d | ||
|
|
33454444a4 | ||
|
|
3a25561c78 | ||
|
|
1449e6418c | ||
|
|
3a627a9599 | ||
|
|
d2d834ad16 | ||
|
|
c624a10e70 | ||
|
|
437cdde4a5 | ||
|
|
eed1bf14c6 | ||
|
|
313e96513c | ||
|
|
8518f6087b | ||
|
|
1d3057985c | ||
|
|
c6e3a4d80f | ||
|
|
aebc01ba58 | ||
|
|
0e67647e90 | ||
|
|
f3408d9bb6 | ||
|
|
3ecb901e3e | ||
|
|
f6aca47604 | ||
|
|
50ae1e1c3e | ||
|
|
483ac674fb | ||
|
|
3dba9c2b1e | ||
|
|
8fdc0703bc | ||
|
|
d7b90b8415 | ||
|
|
9346b85c3f | ||
|
|
3c5ec8d9a1 | ||
|
|
84e8b72ca3 | ||
|
|
d10e05b796 | ||
|
|
66cc06738e | ||
|
|
0f3b8a4a16 | ||
|
|
ac825b0ff3 | ||
|
|
a1596ed234 | ||
|
|
89b81d151f | ||
|
|
3e5b654109 | ||
|
|
502d923817 | ||
|
|
d908404143 | ||
|
|
ca26c69937 | ||
|
|
58212c421a | ||
|
|
1000107259 | ||
|
|
6aadcf1f3f | ||
|
|
07604a146f | ||
|
|
4c782d3191 | ||
|
|
c4b687ba64 | ||
|
|
a7b43b1a3e | ||
|
|
d6df769799 | ||
|
|
db4b27e88a | ||
|
|
9e813c26a7 | ||
|
|
bafdbc1dec | ||
|
|
5f73d1e629 | ||
|
|
70d1b99fb4 | ||
|
|
2ea98db9d8 | ||
|
|
bbe1c716e6 | ||
|
|
77ac452190 | ||
|
|
11ae20ea20 | ||
|
|
a1277bf6bf | ||
|
|
c8a72431c0 | ||
|
|
ea70575275 | ||
|
|
568500c6bc | ||
|
|
49e9ce1649 | ||
|
|
f5eb735551 | ||
|
|
83894074b0 | ||
|
|
2ca440dd2d | ||
|
|
35fe5ac9e4 | ||
|
|
2ea9a9a143 | ||
|
|
92a9579e69 | ||
|
|
ff7bdbcbeb | ||
|
|
cf9b7e5b6a | ||
|
|
38e899957f | ||
|
|
5e9b164f4e | ||
|
|
3ccf854865 | ||
|
|
1ca4677b28 | ||
|
|
3016b929b4 | ||
|
|
1ae3e8d0be | ||
|
|
1329fc726f | ||
|
|
234aeb81dd | ||
|
|
1dbc15b2e6 | ||
|
|
80aa56cbcc | ||
|
|
9cda20381f | ||
|
|
318ddf692a | ||
|
|
11a4859f8e | ||
|
|
a3d1a72eb0 | ||
|
|
17b71cf673 | ||
|
|
779222d1a7 | ||
|
|
538dcb6a4c | ||
|
|
98115b6db1 | ||
|
|
9854caca33 | ||
|
|
c8b551dcf7 | ||
|
|
99f40ae109 | ||
|
|
3a9381b966 | ||
|
|
54a2a6c905 | ||
|
|
7b7616ce7e | ||
|
|
b020a08ea0 | ||
|
|
2737e7d602 | ||
|
|
d3754b36bc | ||
|
|
112cd9d5f4 | ||
|
|
8a7991a376 | ||
|
|
6f4d0f5377 | ||
|
|
9cfdae3494 | ||
|
|
62183699db | ||
|
|
9be9a76b42 | ||
|
|
80f7be6dd7 | ||
|
|
83721240a4 | ||
|
|
6c66cf367a | ||
|
|
a137d01c90 | ||
|
|
bac6ea6e91 | ||
|
|
0c1030cf02 | ||
|
|
23aff6b0b1 | ||
|
|
3335cd5500 | ||
|
|
a4f049d8da | ||
|
|
8fea15245a | ||
|
|
42a2c1fc57 | ||
|
|
7e98b3103f | ||
|
|
2a61085f07 | ||
|
|
4386dd8b5a | ||
|
|
50ddd630be | ||
|
|
cb3250e7b4 | ||
|
|
0319addd2b | ||
|
|
77bf76e1f9 | ||
|
|
beafac1f73 | ||
|
|
a2d777bda0 | ||
|
|
e48bedeaf2 | ||
|
|
a2d35281b2 | ||
|
|
46035af9a3 | ||
|
|
4b7fc34fe3 | ||
|
|
96e8b4a146 | ||
|
|
2cedb66667 | ||
|
|
e345671c76 | ||
|
|
86fb2cddc5 | ||
|
|
931c533a3d | ||
|
|
79ba60e3ad | ||
|
|
fb477b24d7 | ||
|
|
9f263e8f3e | ||
|
|
db325cb81f | ||
|
|
b167d01f8a | ||
|
|
f4e7469f96 | ||
|
|
4647d69d4b | ||
|
|
9ab071d62c | ||
|
|
f4c09ac51f | ||
|
|
846be50f72 | ||
|
|
c0f357d817 | ||
|
|
40fc09a93d | ||
|
|
2a90a2c552 | ||
|
|
fc581bf729 | ||
|
|
b6ea025333 | ||
|
|
d3e2d9ac5b | ||
|
|
85a7fbf538 | ||
|
|
62733ef4c1 | ||
|
|
99e59b73a3 | ||
|
|
384a3352cf | ||
|
|
7a04fdff2e | ||
|
|
ba3c75e58c | ||
|
|
1062ccc5c3 | ||
|
|
36f05e272e | ||
|
|
660c4293f0 | ||
|
|
1b8613d767 | ||
|
|
bfa52c4ba5 | ||
|
|
c5eb66038b | ||
|
|
a46edd60f0 | ||
|
|
b4bcfd325b | ||
|
|
976bd3a389 | ||
|
|
bf27c846da | ||
|
|
455360205c | ||
|
|
79c67f2026 | ||
|
|
c8928626fc | ||
|
|
b47d28a22a | ||
|
|
c5b7d3c7af | ||
|
|
d950012530 | ||
|
|
27b1f48929 | ||
|
|
3d62a383d5 | ||
|
|
6ac7101f4f | ||
|
|
65cc19842c | ||
|
|
656f290660 | ||
|
|
643c3c3b3e | ||
|
|
da37384335 | ||
|
|
1658048c2c | ||
|
|
27d38518e1 | ||
|
|
cf8088ac6a | ||
|
|
1e82104224 | ||
|
|
46ff37c362 | ||
|
|
3df2425162 | ||
|
|
5241f5fe5e | ||
|
|
8e86c97a13 | ||
|
|
90e8c3adf6 | ||
|
|
56851365b1 | ||
|
|
a9814bb6d3 | ||
|
|
3ad8bd15a6 | ||
|
|
4c33d8ac43 | ||
|
|
a94ca62624 | ||
|
|
53b72469b6 | ||
|
|
f80ed32a06 | ||
|
|
07eaf9157b | ||
|
|
56ea2fdd56 | ||
|
|
ffecd4a17a | ||
|
|
dae649fb87 | ||
|
|
57a77f75c1 | ||
|
|
18e73b8aa7 | ||
|
|
af9ca59e51 | ||
|
|
d352d518c2 | ||
|
|
f0dc600016 | ||
|
|
f44ea0a6d8 | ||
|
|
f7d31d4c02 | ||
|
|
b90e25a3a5 | ||
|
|
cf4b9f669d | ||
|
|
e417d35cce | ||
|
|
deaec3cce2 | ||
|
|
7bbd99644a | ||
|
|
cb59a449dd | ||
|
|
a632eea75b | ||
|
|
3d10c9bf9e | ||
|
|
2f0cdc40af | ||
|
|
0a3d014f5d | ||
|
|
7d0115daec | ||
|
|
f024ab1c3f | ||
|
|
f4bc1f0926 | ||
|
|
42dbb887f7 | ||
|
|
850d2fa423 | ||
|
|
08b84deba4 | ||
|
|
ec5a60430d | ||
|
|
e4d4b51c59 | ||
|
|
6b60f8bab6 | ||
|
|
aa9b93eee5 | ||
|
|
afb338a41a | ||
|
|
1e29f7c811 | ||
|
|
d11be15295 | ||
|
|
305f79d2a6 | ||
|
|
af988f774e | ||
|
|
82e389298a | ||
|
|
c784d5ce0c | ||
|
|
c2c4730953 | ||
|
|
ccdd8e1775 | ||
|
|
307a784174 | ||
|
|
9abc0fa88d | ||
|
|
76aad61f00 | ||
|
|
37ce109009 | ||
|
|
a66163dc46 | ||
|
|
bdf8bf093c | ||
|
|
b7a9542213 | ||
|
|
3a3f9fd51e | ||
|
|
6dc9d76ed3 | ||
|
|
a2a4b80af4 | ||
|
|
7ec0ae3bb5 | ||
|
|
2c5530c9f0 | ||
|
|
94ee3ceced | ||
|
|
dd67de4d71 | ||
|
|
285fd93c32 | ||
|
|
56bec06856 | ||
|
|
04cbed90fb | ||
|
|
c1a6647f19 | ||
|
|
1dd677eec2 | ||
|
|
7636bc7491 | ||
|
|
79adb83782 | ||
|
|
b83d897650 | ||
|
|
0c3b25706a | ||
|
|
c3f57beafd | ||
|
|
aace3fd2fb | ||
|
|
2e67ed9c4c | ||
|
|
614fa2b163 | ||
|
|
903e96bdfa | ||
|
|
5c0adcbbff | ||
|
|
b2f16eeff4 | ||
|
|
6db79808aa | ||
|
|
f4b55bfc0c | ||
|
|
fe64d42db1 | ||
|
|
1b5a9f3b0c | ||
|
|
fdf8bd7ee2 | ||
|
|
019a32a623 | ||
|
|
d44f09d111 | ||
|
|
6b5de2c51b | ||
|
|
9410fb6f16 | ||
|
|
7b42aaa132 | ||
|
|
e4b9aa9746 | ||
|
|
aa26a55df4 | ||
|
|
6e505f5c0b | ||
|
|
a491a1d878 | ||
|
|
36f5539b9a | ||
|
|
1a2afe8bfd | ||
|
|
9fece9ce3d | ||
|
|
2e2ede5968 | ||
|
|
205d4ead1c | ||
|
|
80a070a8ea | ||
|
|
172540984c | ||
|
|
4dc40bd795 | ||
|
|
f9d7b1eebc | ||
|
|
ffbc733d58 | ||
|
|
2286726d72 | ||
|
|
1b8110a332 | ||
|
|
dad7e1b60c | ||
|
|
72c2ddcf57 | ||
|
|
a75e138965 | ||
|
|
fa319d798a | ||
|
|
c192339af9 | ||
|
|
fa67d128d1 | ||
|
|
55ff581655 | ||
|
|
560b257af7 | ||
|
|
7ac80bffcb | ||
|
|
0d8a1a2879 | ||
|
|
bd97cb0449 | ||
|
|
a58d3540ad | ||
|
|
5dbe6337ea | ||
|
|
b633f89c1a | ||
|
|
944a22a2fb | ||
|
|
ce24f83b64 | ||
|
|
45373e7f1f | ||
|
|
efe71cd598 | ||
|
|
ae41717b9b | ||
|
|
e78ab60c97 | ||
|
|
b3c8358a43 | ||
|
|
be81fe1f74 | ||
|
|
817f5e1737 | ||
|
|
eda9e36c82 | ||
|
|
2318975375 | ||
|
|
298666380f | ||
|
|
6a9075141f | ||
|
|
488c7082f3 | ||
|
|
fdde9863cd | ||
|
|
9404a3ab63 | ||
|
|
dadf6ec5b6 | ||
|
|
c528ea4f31 | ||
|
|
3d4e0b06c0 | ||
|
|
9d9ec8cd0b | ||
|
|
992b42e922 | ||
|
|
3cf9dacbc0 | ||
|
|
cfde5932fd | ||
|
|
69fb9a9e0e | ||
|
|
0a118f5631 | ||
|
|
0b986c1158 | ||
|
|
184a95cd2a | ||
|
|
b7a8df40c4 | ||
|
|
bf28902327 | ||
|
|
426bf68763 | ||
|
|
e557dc86e7 | ||
|
|
f5cef2556b | ||
|
|
cf66aadc20 | ||
|
|
77a5ed2ec6 | ||
|
|
4d640c16aa | ||
|
|
0a827307d6 | ||
|
|
64149599de | ||
|
|
07be469f49 | ||
|
|
79e9447b73 | ||
|
|
9893935e7c | ||
|
|
966b9990e0 | ||
|
|
bef1e38295 | ||
|
|
4b43cb8054 | ||
|
|
38ad3c046e | ||
|
|
b2afe6f40c | ||
|
|
601620bdde | ||
|
|
97104988a3 | ||
|
|
45333bd340 | ||
|
|
bbcb1abfda | ||
|
|
a64c1aa2c4 | ||
|
|
21fbbf9106 | ||
|
|
17a0be10fa | ||
|
|
3017d14392 | ||
|
|
d3275cef48 | ||
|
|
0fd4aa1e23 | ||
|
|
7c21945569 | ||
|
|
ffecf200d4 | ||
|
|
58d3c160ef | ||
|
|
326cf08efa | ||
|
|
d49dc201c3 | ||
|
|
6ff4c3f1ce | ||
|
|
5940514f40 | ||
|
|
916150742e | ||
|
|
1735b619e8 | ||
|
|
6b61d101dc | ||
|
|
56ca7bf30e | ||
|
|
2a218675b2 | ||
|
|
8b9eb49b57 | ||
|
|
3860341584 | ||
|
|
9078118a6d | ||
|
|
a7f5297ad1 | ||
|
|
870a60fc5d | ||
|
|
a8471eed5b | ||
|
|
a3351ad060 | ||
|
|
2322f5bf59 | ||
|
|
c9c075fd49 | ||
|
|
1484523164 | ||
|
|
dc17402896 | ||
|
|
0414ea59f0 | ||
|
|
967afaf802 | ||
|
|
77ee9bea38 | ||
|
|
e0cd28a65a | ||
|
|
7b6c86c6c1 | ||
|
|
1d0a418011 | ||
|
|
9253f5647a | ||
|
|
c41bfe831d | ||
|
|
c9af7e327e | ||
|
|
8870442830 | ||
|
|
9ad26c97a1 | ||
|
|
e1201f0967 | ||
|
|
ca2202e3c8 | ||
|
|
f24ee70138 | ||
|
|
8981cae3c2 | ||
|
|
b8971a31bc | ||
|
|
e01398a31a | ||
|
|
f871f20172 | ||
|
|
80d3518d55 | ||
|
|
49935473ae | ||
|
|
0705d05078 | ||
|
|
97d45fb2a6 | ||
|
|
50e4abf836 | ||
|
|
b130ea439a | ||
|
|
0e921d3258 | ||
|
|
d0f65329fb | ||
|
|
61b2b9df1a | ||
|
|
04e4a37d85 | ||
|
|
0ba3e69872 | ||
|
|
7488923fc3 | ||
|
|
cf559ee43f | ||
|
|
2ab0c2a7c6 | ||
|
|
243548631a | ||
|
|
da79d544cf | ||
|
|
e808825c95 | ||
|
|
a9576935cf | ||
|
|
9cf6c97137 | ||
|
|
0739f7b702 | ||
|
|
e12710fa55 | ||
|
|
2a120dfe57 | ||
|
|
3c18c4b66a | ||
|
|
21eee2b548 | ||
|
|
03168b9fbf | ||
|
|
41e1b2bd39 | ||
|
|
1afa0206a5 | ||
|
|
81fcdf8ac9 | ||
|
|
1f67b403de | ||
|
|
8e7f3f7b27 | ||
|
|
e6e9f1d5a5 | ||
|
|
4268b90763 | ||
|
|
856137b763 | ||
|
|
d0ee505c3a | ||
|
|
7dcac19a67 | ||
|
|
22260cd719 | ||
|
|
bef11e4cbe | ||
|
|
d5a65656f5 | ||
|
|
309ef3766c | ||
|
|
bde7b97f6a | ||
|
|
398dbeedea | ||
|
|
fa772af6c6 | ||
|
|
9ebdd2b0ea | ||
|
|
bec4e5e038 | ||
|
|
ea67b34ba7 | ||
|
|
e247f691cb | ||
|
|
896b2425a4 | ||
|
|
30e15b881f | ||
|
|
24ded9e5a0 | ||
|
|
f5e666ae4f | ||
|
|
62133ceb24 | ||
|
|
3b50e1fb7d | ||
|
|
7c25b3b1e0 | ||
|
|
a70bd02c67 | ||
|
|
990dcec348 | ||
|
|
75712641ee | ||
|
|
946e16339e | ||
|
|
092ed088ca | ||
|
|
7000431249 | ||
|
|
dab1980d79 | ||
|
|
344a295845 | ||
|
|
67d38090ed | ||
|
|
a9e8d02593 | ||
|
|
d859f27999 | ||
|
|
e86338d555 | ||
|
|
137ebc1cad | ||
|
|
d84a49b85a | ||
|
|
d5efe3406a | ||
|
|
d559a66145 | ||
|
|
ce6777b68d | ||
|
|
655e7ad7d5 | ||
|
|
9596a03139 | ||
|
|
2e7e476f81 | ||
|
|
2c9ad74599 | ||
|
|
aa60bd6770 | ||
|
|
16cf5c8628 | ||
|
|
56986f82c8 | ||
|
|
53e829a21c | ||
|
|
ab4acadcc1 | ||
|
|
325e679bc4 | ||
|
|
d81cae712e | ||
|
|
1d22608f86 | ||
|
|
39f3cf3082 | ||
|
|
5be81c562f | ||
|
|
2bdda1ff1f | ||
|
|
ac930dff30 | ||
|
|
1cbf8c0015 | ||
|
|
571d148bdf | ||
|
|
32a150eb40 | ||
|
|
5ad6e5f2f3 | ||
|
|
f229e135e3 | ||
|
|
d15d87c111 | ||
|
|
0dd52dc69f | ||
|
|
47dd0c4c64 | ||
|
|
7762b4d364 | ||
|
|
7d8457263b | ||
|
|
fc6b9c0e1b | ||
|
|
a76e15fd77 | ||
|
|
a017047c52 | ||
|
|
f4d1a33ed8 | ||
|
|
03ea151be5 | ||
|
|
83f8622b23 | ||
|
|
3753379ae4 | ||
|
|
2de16cfae8 | ||
|
|
5fa3c149e9 | ||
|
|
9093c7b120 | ||
|
|
47ae921b86 | ||
|
|
c605156302 | ||
|
|
051bf38bc7 | ||
|
|
8423c636a8 | ||
|
|
cc5732f41f | ||
|
|
7565ddd91c | ||
|
|
0de596dbcf | ||
|
|
e59a098e66 | ||
|
|
8d56b0ba85 | ||
|
|
b40d55c358 | ||
|
|
784d36d691 | ||
|
|
1bdfc8ae00 | ||
|
|
291039159f | ||
|
|
d4fa0af350 | ||
|
|
492d2f86bc | ||
|
|
8dd54ced0e | ||
|
|
e56bd0b47b | ||
|
|
ff2b00a029 | ||
|
|
017469b2de | ||
|
|
26979ff6b5 | ||
|
|
21abc5cabe | ||
|
|
6e76221e07 | ||
|
|
743676421d | ||
|
|
9d0774a58e | ||
|
|
ce732c0efb | ||
|
|
1ac0068d22 | ||
|
|
ae8e1c5e9a | ||
|
|
f392ce0a93 | ||
|
|
aa1d8d6c8b | ||
|
|
ae4325d0e7 | ||
|
|
da5f669133 | ||
|
|
5c839b40c9 | ||
|
|
a50e2e74a5 | ||
|
|
26cfad5e88 | ||
|
|
a99ce6122c | ||
|
|
a6b542b301 | ||
|
|
6a54a58a0d | ||
|
|
9ee684b0f4 | ||
|
|
c788692687 | ||
|
|
9e7e0d102d | ||
|
|
d47c099ac9 | ||
|
|
e371a4a62d | ||
|
|
8a3b72458f | ||
|
|
f616cb7b3e | ||
|
|
4bbadffa37 | ||
|
|
388d77b61a | ||
|
|
6597f6f0ed | ||
|
|
7008b08031 | ||
|
|
369efc24c2 |
@@ -0,0 +1,44 @@
|
||||
name: 'Set Swap Space'
|
||||
description: 'Add moar swap'
|
||||
branding:
|
||||
icon: 'crop'
|
||||
color: 'orange'
|
||||
inputs:
|
||||
swap-size-gb:
|
||||
description: 'Swap space to create, in Gigabytes.'
|
||||
required: false
|
||||
default: '10'
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Swap space report before modification
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Memory and swap:"
|
||||
free -h
|
||||
echo
|
||||
swapon --show
|
||||
echo
|
||||
- name: Set Swap
|
||||
shell: bash
|
||||
run: |
|
||||
export SWAP_FILE=$(swapon --show=NAME | tail -n 1)
|
||||
echo "Swap file: $SWAP_FILE"
|
||||
if [ -z "$SWAP_FILE" ]; then
|
||||
SWAP_FILE=/opt/swapfile
|
||||
else
|
||||
sudo swapoff $SWAP_FILE
|
||||
sudo rm $SWAP_FILE
|
||||
fi
|
||||
sudo fallocate -l ${{ inputs.swap-size-gb }}G $SWAP_FILE
|
||||
sudo chmod 600 $SWAP_FILE
|
||||
sudo mkswap $SWAP_FILE
|
||||
sudo swapon $SWAP_FILE
|
||||
- name: Swap space report after modification
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Memory and swap:"
|
||||
free -h
|
||||
echo
|
||||
swapon --show
|
||||
echo
|
||||
@@ -10,62 +10,25 @@ on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build-${{ matrix.os }}-${{ matrix.ghc }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-20.04
|
||||
platform_name: 20_04-x86-64
|
||||
ghc: "8.10.7"
|
||||
- os: ubuntu-20.04
|
||||
platform_name: 20_04-x86-64
|
||||
ghc: "9.6.3"
|
||||
- os: ubuntu-22.04
|
||||
platform_name: 22_04-x86-64
|
||||
ghc: "9.6.3"
|
||||
|
||||
# =============================
|
||||
# Create release
|
||||
# =============================
|
||||
|
||||
# Create release, but only if it's triggered by tag push.
|
||||
# On pull requests/commits push, this job will always complete.
|
||||
|
||||
maybe-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone project
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Haskell
|
||||
uses: haskell-actions/setup@v2
|
||||
with:
|
||||
ghc-version: ${{ matrix.ghc }}
|
||||
cabal-version: "3.10.1.0"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cabal/store
|
||||
dist-newstyle
|
||||
key: ${{ matrix.os }}-${{ hashFiles('cabal.project', 'simplexmq.cabal') }}
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: cabal build --enable-tests
|
||||
|
||||
- name: Test
|
||||
timeout-minutes: 40
|
||||
shell: bash
|
||||
run: cabal test --test-show-details=direct
|
||||
|
||||
- name: Prepare binaries
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
shell: bash
|
||||
run: |
|
||||
mv $(cabal list-bin smp-server) smp-server-ubuntu-${{ matrix.platform_name}}
|
||||
mv $(cabal list-bin ntf-server) ntf-server-ubuntu-${{ matrix.platform_name}}
|
||||
mv $(cabal list-bin xftp-server) xftp-server-ubuntu-${{ matrix.platform_name}}
|
||||
mv $(cabal list-bin xftp) xftp-ubuntu-${{ matrix.platform_name}}
|
||||
|
||||
- name: Build changelog
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-22.04'
|
||||
id: build_changelog
|
||||
uses: mikepenz/release-changelog-builder-action@v1
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: simplex-chat/release-changelog-builder-action@v5
|
||||
with:
|
||||
configuration: .github/changelog_conf.json
|
||||
failOnError: true
|
||||
@@ -75,8 +38,8 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create release
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.ghc != '8.10.7'
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: simplex-chat/action-gh-release@v2
|
||||
with:
|
||||
body: |
|
||||
See full changelog [here](https://github.com/simplex-chat/simplexmq/blob/master/CHANGELOG.md).
|
||||
@@ -86,10 +49,251 @@ jobs:
|
||||
prerelease: true
|
||||
files: |
|
||||
LICENSE
|
||||
smp-server-ubuntu-${{ matrix.platform_name}}
|
||||
ntf-server-ubuntu-${{ matrix.platform_name}}
|
||||
xftp-server-ubuntu-${{ matrix.platform_name}}
|
||||
xftp-ubuntu-${{ matrix.platform_name}}
|
||||
fail_on_unmatched_files: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# =============================
|
||||
# Main build job
|
||||
# =============================
|
||||
|
||||
build:
|
||||
name: "ubuntu-${{ matrix.os }}-${{ matrix.arch }}, GHC: ${{ matrix.ghc }}"
|
||||
needs: maybe-release
|
||||
env:
|
||||
apps: "smp-server xftp-server ntf-server xftp"
|
||||
runs-on: ${{ matrix.runner }}
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
env:
|
||||
POSTGRES_HOST_AUTH_METHOD: trust # Allows passwordless access
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
# Maps tcp port 5432 on service container to the host
|
||||
- 5432:5432
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 22.04
|
||||
os_underscore: 22_04
|
||||
arch: x86-64
|
||||
runner: "ubuntu-22.04"
|
||||
ghc: "8.10.7"
|
||||
should_run: ${{ !(github.ref == 'refs/heads/stable' || startsWith(github.ref, 'refs/tags/v')) }}
|
||||
- os: 22.04
|
||||
os_underscore: 22_04
|
||||
arch: x86-64
|
||||
runner: "ubuntu-22.04"
|
||||
ghc: "9.6.3"
|
||||
should_run: true
|
||||
- os: 24.04
|
||||
os_underscore: 24_04
|
||||
arch: x86-64
|
||||
runner: "ubuntu-24.04"
|
||||
ghc: "9.6.3"
|
||||
should_run: true
|
||||
- os: 22.04
|
||||
os_underscore: 22_04
|
||||
arch: aarch64
|
||||
runner: "ubuntu-22.04-arm"
|
||||
ghc: "9.6.3"
|
||||
should_run: true
|
||||
- os: 24.04
|
||||
os_underscore: 24_04
|
||||
arch: aarch64
|
||||
runner: "ubuntu-24.04-arm"
|
||||
ghc: "9.6.3"
|
||||
should_run: true
|
||||
steps:
|
||||
- name: Clone project
|
||||
if: matrix.should_run == true
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: matrix.should_run == true
|
||||
uses: simplex-chat/docker-setup-buildx-action@v3
|
||||
|
||||
- name: Setup swap
|
||||
if: matrix.ghc == '8.10.7' && matrix.should_run == true
|
||||
uses: ./.github/actions/swap
|
||||
with:
|
||||
swap-size-gb: 20
|
||||
|
||||
- name: Install PostgreSQL 15 client tools
|
||||
if: matrix.os == '22.04' && matrix.should_run == true
|
||||
shell: bash
|
||||
run: |
|
||||
# Import the repository signing key
|
||||
sudo install -d /usr/share/postgresql-common/pgdg
|
||||
sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc
|
||||
# Add the PostgreSQL APT repository
|
||||
sudo sh -c 'echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
||||
# Update repository and install postgresql tools
|
||||
sudo apt update
|
||||
sudo apt -y install postgresql-client-15
|
||||
|
||||
- name: Build and cache Docker image
|
||||
if: matrix.should_run == true
|
||||
uses: simplex-chat/docker-build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
load: true
|
||||
file: Dockerfile.build
|
||||
tags: build/${{ matrix.os }}:latest
|
||||
build-args: |
|
||||
TAG=${{ matrix.os }}
|
||||
GHC=${{ matrix.ghc }}
|
||||
|
||||
- name: Cache dependencies
|
||||
if: matrix.should_run == true
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cabal/store
|
||||
dist-newstyle
|
||||
key: ubuntu-${{ matrix.os }}-${{ matrix.arch }}-ghc${{ matrix.ghc }}-${{ hashFiles('cabal.project', 'simplexmq.cabal') }}
|
||||
|
||||
- name: Start container
|
||||
if: matrix.should_run == true
|
||||
shell: bash
|
||||
run: |
|
||||
docker run -t -d \
|
||||
--device /dev/fuse \
|
||||
--cap-add SYS_ADMIN \
|
||||
--security-opt apparmor:unconfined \
|
||||
--name builder \
|
||||
-v ~/.cabal:/root/.cabal \
|
||||
-v /home/runner/work/_temp:/home/runner/work/_temp \
|
||||
-v ${{ github.workspace }}:/project \
|
||||
build/${{ matrix.os }}:latest
|
||||
|
||||
- name: Build smp-server, xftp-server (postgresql) and tests
|
||||
if: matrix.should_run == true
|
||||
shell: docker exec -t builder sh -eu {0}
|
||||
run: |
|
||||
chmod -fR 777 ~/.cabal ./dist-newstyle || :; git config --global --add safe.directory '*'
|
||||
cabal clean
|
||||
cabal update
|
||||
cabal build --jobs=$(nproc) --enable-tests -fserver_postgres
|
||||
mkdir -p /out
|
||||
for i in smp-server xftp-server simplexmq-test; do
|
||||
bin=$(find /project/dist-newstyle -name "$i" -type f -executable)
|
||||
chmod +x "$bin"
|
||||
mv "$bin" /out/
|
||||
done
|
||||
strip /out/smp-server /out/xftp-server
|
||||
|
||||
- name: Copy simplexmq-test from container
|
||||
if: matrix.should_run == true
|
||||
shell: bash
|
||||
run: |
|
||||
docker cp builder:/out/simplexmq-test .
|
||||
|
||||
- name: Copy smp-server, xftp-server (postgresql) from container and prepare it
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true
|
||||
id: prepare-postgres
|
||||
shell: bash
|
||||
run: |
|
||||
printf 'bins<<EOF\n' > bins.output
|
||||
printf 'hashes<<EOF\n' > hashes.output
|
||||
|
||||
for i in smp-server xftp-server; do
|
||||
name="${i}-postgres-ubuntu-${{ matrix.os_underscore }}-${{ matrix.arch }}"
|
||||
docker cp builder:/out/$i $name
|
||||
|
||||
path="${{ github.workspace }}/$name"
|
||||
hash="SHA2-256($name)= $(openssl sha256 $path | cut -d' ' -f 2)"
|
||||
|
||||
printf '%s\n' "$path" >> bins.output
|
||||
printf '%s\n\n' "$hash" >> hashes.output
|
||||
done
|
||||
printf 'EOF\n' >> bins.output
|
||||
printf 'EOF\n' >> hashes.output
|
||||
|
||||
cat bins.output >> "$GITHUB_OUTPUT"
|
||||
cat hashes.output >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build everything else (standard)
|
||||
if: matrix.should_run == true
|
||||
shell: docker exec -t builder sh -eu {0}
|
||||
run: |
|
||||
cabal build --jobs=$(nproc)
|
||||
mkdir -p /out
|
||||
for i in ${{ env.apps }}; do
|
||||
bin=$(find /project/dist-newstyle -name "$i" -type f -executable)
|
||||
strip "$bin"
|
||||
chmod +x "$bin"
|
||||
mv "$bin" /out/
|
||||
done
|
||||
|
||||
- name: Copy binaries from container and prepare them
|
||||
id: prepare-regular
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true
|
||||
shell: bash
|
||||
run: |
|
||||
docker cp builder:/out .
|
||||
|
||||
printf 'bins<<EOF\n' > bins.output
|
||||
printf 'hashes<<EOF\n' > hashes.output
|
||||
for i in ${{ env.apps }}; do
|
||||
name="$i-ubuntu-${{ matrix.os_underscore }}-${{ matrix.arch }}"
|
||||
|
||||
mv ./out/$i ./$name
|
||||
|
||||
path="${{ github.workspace }}/$name"
|
||||
hash="SHA2-256($name)= $(openssl sha256 $path | cut -d' ' -f 2)"
|
||||
|
||||
printf '%s\n' "$path" >> bins.output
|
||||
printf '%s\n\n' "$hash" >> hashes.output
|
||||
done
|
||||
printf 'EOF\n' >> bins.output
|
||||
printf 'EOF\n' >> hashes.output
|
||||
|
||||
cat bins.output >> "$GITHUB_OUTPUT"
|
||||
cat hashes.output >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload binaries
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true
|
||||
uses: simplex-chat/action-gh-release@v2
|
||||
with:
|
||||
append_body: true
|
||||
prerelease: true
|
||||
fail_on_unmatched_files: true
|
||||
body: |
|
||||
${{ steps.prepare-regular.outputs.hashes }}
|
||||
${{ steps.prepare-postgres.outputs.hashes }}
|
||||
files: |
|
||||
${{ steps.prepare-regular.outputs.bins }}
|
||||
${{ steps.prepare-postgres.outputs.bins }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Test
|
||||
if: matrix.should_run == true && matrix.arch == 'x86-64'
|
||||
timeout-minutes: 120
|
||||
shell: bash
|
||||
env:
|
||||
PGHOST: localhost
|
||||
run: |
|
||||
i=1
|
||||
attempts=1
|
||||
${{ (github.ref == 'refs/heads/stable' || startsWith(github.ref, 'refs/tags/v')) }} && attempts=3
|
||||
while [ "$i" -le "$attempts" ]; do
|
||||
if ./simplexmq-test; then
|
||||
break
|
||||
else
|
||||
echo "Attempt $i failed, retrying..."
|
||||
i=$((i + 1))
|
||||
sleep 1
|
||||
fi
|
||||
done
|
||||
if [ "$i" -gt "$attempts" ]; then
|
||||
echo "All "$attempts" attempts failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -14,22 +14,22 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- app: smp-server
|
||||
app_port: 5223
|
||||
app_port: "443 5223"
|
||||
- app: xftp-server
|
||||
app_port: 443
|
||||
app_port: 443
|
||||
steps:
|
||||
- name: Clone project
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: simplex-chat/docker-login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Extract metadata for Docker image
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: simplex-chat/docker-metadata-action@v5
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.app }}
|
||||
flavor: |
|
||||
@@ -39,9 +39,17 @@ jobs:
|
||||
type=semver,pattern=v{{major}}.{{minor}}
|
||||
type=semver,pattern=v{{major}}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
uses: simplex-chat/docker-build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
build-args: |
|
||||
APP=${{ matrix.app }}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
name: Reproduce latest release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # every day at 02:00 night
|
||||
|
||||
jobs:
|
||||
reproduce:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get latest release
|
||||
shell: bash
|
||||
run: |
|
||||
curl --proto '=https' \
|
||||
--tlsv1.2 \
|
||||
-sSf -L \
|
||||
'https://api.github.com/repos/simplex-chat/simplexmq/releases/latest' \
|
||||
2>/dev/null | \
|
||||
grep -i "tag_name" | \
|
||||
awk -F \" '{print "TAG="$4}' >> $GITHUB_ENV
|
||||
|
||||
- name: Execute reproduce script
|
||||
run: |
|
||||
${GITHUB_WORKSPACE}/scripts/simplexmq-reproduce-builds.sh "$TAG" || :
|
||||
|
||||
- name: Check if build has been reproduced
|
||||
env:
|
||||
url: ${{ secrets.STATUS_SIMPLEX_WEBHOOK_URL }}
|
||||
user: ${{ secrets.STATUS_SIMPLEX_WEBHOOK_USER }}
|
||||
pass: ${{ secrets.STATUS_SIMPLEX_WEBHOOK_PASS }}
|
||||
run: |
|
||||
if [ -f "${GITHUB_WORKSPACE}/${TAG}-simplexmq/_sha256sums" ]; then
|
||||
exit 0
|
||||
else
|
||||
curl --proto '=https' --tlsv1.2 -sSf \
|
||||
-u "${user}:${pass}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"title": "👾 GitHub: Runner", "description": "⛔️ '"$TAG"' did not reproduce."}' \
|
||||
"$url"
|
||||
exit 1
|
||||
fi
|
||||
@@ -11,3 +11,4 @@ cabal.project.local~
|
||||
.hpc/
|
||||
*.tix
|
||||
.coverage
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# XFTPClientAgent Pattern
|
||||
|
||||
## TOC
|
||||
1. Executive Summary
|
||||
2. Changes: client.ts
|
||||
3. Changes: agent.ts
|
||||
4. Changes: test/browser.test.ts
|
||||
5. Verification
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Add `XFTPClientAgent` — a per-server connection pool matching the Haskell pattern. The agent caches `XFTPClient` instances by server URL. All orchestration functions (`uploadFile`, `downloadFile`, `deleteFile`) take `agent` as first parameter and use `getXFTPServerClient(agent, server)` instead of calling `connectXFTP` directly. Connections stay open on success; the caller creates and closes the agent.
|
||||
|
||||
`connectXFTP` and `closeXFTP` stay exported (used by `XFTPWebTests.hs` Haskell tests). The `browserClients` hack, per-function `connections: Map`, and `getOrConnect` are deleted.
|
||||
|
||||
## Changes: client.ts
|
||||
|
||||
**Add** after types section: `XFTPClientAgent` interface, `newXFTPAgent`, `getXFTPServerClient`, `closeXFTPServerClient`, `closeXFTPAgent`.
|
||||
|
||||
**Delete**: `browserClients` Map and all `isNode` browser-cache checks in `connectXFTP` and `closeXFTP`.
|
||||
|
||||
**Revert `closeXFTP`** to unconditional `c.transport.close()` (browser transport.close() is already a no-op).
|
||||
|
||||
`connectXFTP` stays exported (backward compat) but becomes a raw low-level function — no caching.
|
||||
|
||||
## Changes: agent.ts
|
||||
|
||||
**Imports**: replace `connectXFTP`/`closeXFTP` with `getXFTPServerClient`/`closeXFTPAgent` etc.
|
||||
|
||||
**Re-export** from agent.ts: `newXFTPAgent`, `closeXFTPAgent`, `XFTPClientAgent`.
|
||||
|
||||
**`uploadFile`**: add `agent: XFTPClientAgent` as first param. Replace `connectXFTP` → `getXFTPServerClient`. Remove `finally { closeXFTP }`. Pass `agent` to `uploadRedirectDescription`.
|
||||
|
||||
**`uploadRedirectDescription`**: change from `(client, server, innerFd)` to `(agent, server, innerFd)`. Get client via `getXFTPServerClient`.
|
||||
|
||||
**`downloadFile`**: add `agent` param. Delete local `connections: Map`. Replace `getOrConnect` → `getXFTPServerClient`. Remove finally cleanup. Pass `agent` to `downloadWithRedirect`.
|
||||
|
||||
**`downloadWithRedirect`**: add `agent` param. Same replacements. Remove try/catch cleanup. Recursive call passes `agent`.
|
||||
|
||||
**`deleteFile`**: add `agent` param. Same pattern.
|
||||
|
||||
**Delete**: `getOrConnect` function entirely.
|
||||
|
||||
## Changes: test/browser.test.ts
|
||||
|
||||
Create agent before operations, pass to upload/download, close in finally.
|
||||
|
||||
## Verification
|
||||
|
||||
1. `npx vitest --run` — browser round-trip test passes
|
||||
2. No remaining `browserClients`, `getOrConnect`, or per-function `connections: Map` locals
|
||||
3. `connectXFTP` and `closeXFTP` still exported (XFTPWebTests.hs compat)
|
||||
4. All orchestration functions take `agent` as first param
|
||||
@@ -1,3 +1,321 @@
|
||||
# 6.5.1
|
||||
|
||||
Version 6.5.1.0
|
||||
|
||||
XFTP client:
|
||||
- backwards compatible file header decoding.
|
||||
|
||||
# 6.5.0
|
||||
|
||||
Version 6.5.0.17
|
||||
|
||||
SMP agent:
|
||||
- improve subscriptions
|
||||
- reduce memory usage and retries during initial subscription (#1758)
|
||||
- fix race resulting in pending subscriptions never subscribed (#1756)
|
||||
- batch processing of subscription results and errors (#1652)
|
||||
- reduce memory usage of active subscriptions.
|
||||
- drop message after N reception attempts (#1762)
|
||||
- fix possible deadlocks of queue overloading when processing messages (#1713)
|
||||
- improved APIs for short link management and creation.
|
||||
- support multiple link owners in link data (#1701)
|
||||
|
||||
SMP server:
|
||||
- store messages in PostgreSQL (#1622).
|
||||
- reduce memory usage with PostgreSQL database - do not use queue cache (#1637)
|
||||
- fix in-memory server not restoring queue/service associations after 2+ restarts (#1618)
|
||||
|
||||
XFTP server:
|
||||
- support PostgreSQL database.
|
||||
- add server page.
|
||||
- support uploads from web clients.
|
||||
|
||||
Servers:
|
||||
- better socket leak prevention during TLS handshake, NetworkError type to bette diagnose connection errors (#1619)
|
||||
- use "=" as default INI key-value separator (#1767)
|
||||
|
||||
# 6.4.4
|
||||
|
||||
Servers:
|
||||
- fix server pages when source code is not specified.
|
||||
- include commit SHA in printed version and in web page (#1608).
|
||||
|
||||
SMP server:
|
||||
- support short SimpleX addresses in server information page (#1600).
|
||||
- wrap all queries in transactions (#1603).
|
||||
|
||||
SMP agent:
|
||||
- chat relay address type for short links (#1602).
|
||||
- extend xrcp certificate validity 1 hour in the past, to allow out of sync clocks (#1601).
|
||||
|
||||
# 6.4.3
|
||||
|
||||
SMP agent:
|
||||
- fix some connection errors by updating contact request server hosts to match server in short link (#1597).
|
||||
|
||||
SMP server:
|
||||
- support short link URI as queue identifier in control port commands (#1596).
|
||||
|
||||
# 6.4.2
|
||||
|
||||
SMP server:
|
||||
- fix memory leak when connection interrupts straight after client connects.
|
||||
- do not include repeated queue blocking into stats/quota.
|
||||
|
||||
XFTP server:
|
||||
- prometheus metrics
|
||||
|
||||
# 6.4.1
|
||||
|
||||
SMP protocol:
|
||||
- create notification credentials via NEW command that creates the queue (#1586)
|
||||
|
||||
SMP server:
|
||||
- control port session improvements (#1591)
|
||||
- additional stat counter for ntf credentials created together with the queue (#1589)
|
||||
|
||||
# 6.4.0
|
||||
|
||||
SMP protocol (server/client):
|
||||
- support associated queue data and short connection links (see [RFC](./rfcs/2025-03-16-smp-queues.md)).
|
||||
- service certificates to optimize subscriptions.
|
||||
|
||||
SMP agent:
|
||||
- support retries for interactive connection handshakes.
|
||||
- use web port 443 by default for preset servers.
|
||||
- use static RNG function to avoid creating dynamic C stubs when generating sntrup keys (it was detected as Dynamic Code Loading in GrapheneOS).
|
||||
- different timeouts for interactive and background operations.
|
||||
|
||||
Ntf server:
|
||||
- PostgreSQL storage.
|
||||
- Prometheus metrics.
|
||||
- use service certificates.
|
||||
- fix repeat token registration.
|
||||
|
||||
# 6.3.2
|
||||
|
||||
Servers:
|
||||
- enable store log by default (#1501).
|
||||
|
||||
SMP server:
|
||||
- reduce memory usage (#1498)
|
||||
|
||||
SMP agent:
|
||||
- handle client/agent version downgrades after connection was established (#1508).
|
||||
|
||||
# 6.3.1
|
||||
|
||||
Servers:
|
||||
- handle ECONNABORTED error on client connections.
|
||||
- reproducible builds.
|
||||
- blocking records for content moderation.
|
||||
- update script (simplex-servers-update) downloads scripts from the specified or the latest stable tag.
|
||||
|
||||
SMP server:
|
||||
- support for PostgreSQL database for queue records for higher traffic servers.
|
||||
- fix old clients sending messages to new servers (#1443)
|
||||
- remove empty journals when opening message queues and expiring idle queues (#1456, #1458).
|
||||
- additional start options (#1465):
|
||||
- `maintenance` to run all start/stop operations without starting server.
|
||||
- `skip-warnings` to ignore the last corrupted line in store log (can happen on abnormal termination).
|
||||
|
||||
Ntf server:
|
||||
- record date of last token activity, to allow expiring inactive tokens.
|
||||
- additional token invalidation reasons in logs.
|
||||
|
||||
SMP agent:
|
||||
- store message sent to multiple connections only once, to reduce storage when sending to groups (#1453).
|
||||
- encrypt messages on delivery, to reduce database writes (#1446).
|
||||
- don't block method calls on congested sockets for better concurrency (#1454).
|
||||
- check notification token status on client connection.
|
||||
- option to skip SQLite vacuum on migrations.
|
||||
|
||||
# 6.3.0
|
||||
|
||||
SMP agent: fix joining connection after failure by using the same ratchet.
|
||||
|
||||
# 6.2.2
|
||||
|
||||
SMP server:
|
||||
- add optional Prometheus metrics (#1411).
|
||||
|
||||
Build:
|
||||
- remove three modules from client library.
|
||||
|
||||
# 6.2.0
|
||||
|
||||
Version 6.2.0.7
|
||||
|
||||
Build:
|
||||
- client_library flag to build only used modules in the clients, remove package yaml
|
||||
|
||||
SMP server:
|
||||
- journal storage for messages (BETA).
|
||||
- prevent race condition when deleting queue and to avoid "orphan" messages (#1395).
|
||||
|
||||
SMP agent:
|
||||
- support SMP and XFTP server roles (storage/proxy) and operators (#1343).
|
||||
- treat blocked STM and other critical errors that offer restart as temporary for message delivery (#1405).
|
||||
- fix inconsistent state after app restart while accepting contact request (#1412).
|
||||
|
||||
# 6.1.3
|
||||
|
||||
SMP server: fix restoring notification credentials.
|
||||
|
||||
# 6.1.2
|
||||
|
||||
Servers: more reliable restoring of state.
|
||||
|
||||
SMP server: reduced memory usage and faster start.
|
||||
|
||||
Notifications: compensate for iOS notifications being dropped by Apple while device is offline (#1378):
|
||||
- Ntf server: send multiple SMP notifications in one iOS notification.
|
||||
- Agent: get multiple messages for one iOS notification.
|
||||
|
||||
# 6.1.1
|
||||
|
||||
SMP:
|
||||
- stop server faster (#1371)
|
||||
- add STORE error (#1372)
|
||||
|
||||
# 6.1.0
|
||||
|
||||
Version 6.1.0.7
|
||||
|
||||
SMP server and client:
|
||||
- transport block encryption (#1317).
|
||||
|
||||
Agent:
|
||||
- batch and optimize iOS notifications processing (#1308, #1311, #1313, #1316, #1330, #1331, #1333, #1337, #1346).
|
||||
- allow receiving multiple messages from single iOS notification (#1355, #1362).
|
||||
- prepare connection to accept to avoid race condition with events (#1365).
|
||||
- transport isolation mode "Session" (default) to use new SOCKS credentials when client restarts or SOCKS proxy configuration changes (#1321).
|
||||
|
||||
Ntf server:
|
||||
- control port (#1354).
|
||||
- enable pings on ntf subscriptions, to resubscribe on reconnection (#1353).
|
||||
|
||||
SMP server:
|
||||
- support multiple server ports (#1319).
|
||||
- support serving HTTPS and SMP transport on the same port (#1326, #1327).
|
||||
- persist iOS notifications to avoid losing them when Ntf server is offline (#1336, #1339, #1350).
|
||||
- fix lost notification subscriptions (#1347).
|
||||
- reject SKEY with different key earlier, at verification step (#1366).
|
||||
- pass server information via CLI during server initialization (#1356).
|
||||
- show version on server page (#1341).
|
||||
- explicit graceful shutdown on SIGINT (#1360).
|
||||
|
||||
XRCP (remote access protocol):
|
||||
- use SHA3-256 in hybrid key agreement (#1302).
|
||||
- session encryption with forward secrecy (#1328).
|
||||
|
||||
# 6.0.5
|
||||
|
||||
SMP agent:
|
||||
- support generic SOCKS proxy (without isolate-by-auth).
|
||||
- reduce max message sizes
|
||||
|
||||
# 6.0.4
|
||||
|
||||
SMP server:
|
||||
- better performance/memory: fewer map updates on re-subscriptions (#1297), split and reduce STM transactions (#1294)
|
||||
- send DELD when subscribed queue is deleted (#1312)
|
||||
- add created/updated/used date to queues to manage expiration (#1306)
|
||||
|
||||
XFTP server: truncate file creation time to 1 hour (#1310)
|
||||
|
||||
Servers:
|
||||
- bind control port only to 127.0.0.1 for better security in case of firewall misconfiguration (#1280)
|
||||
- reduce memory used for period stats (#1298)
|
||||
|
||||
Agent: process last notification from list (#1307)
|
||||
- report receive file error with redirected file ID, when redirect is present (#1304)
|
||||
- special error when deleted user record is not in database (#1303)
|
||||
- fix race when sending a message to the deleted connection (#1296)
|
||||
- support for multiple messages in a single notification
|
||||
|
||||
Ntf server:
|
||||
- only use SOCKS proxy for servers without public address (#1314)
|
||||
|
||||
# 6.0.3
|
||||
|
||||
Agent:
|
||||
- fix possible stuck queue rotation (#1290).
|
||||
|
||||
SMP server:
|
||||
- batch END responses when subscribed client switches to reduce server and client traffic.
|
||||
- reduce STM transactions for better performance.
|
||||
- add stats for END events and for SUB/DEL event batches.
|
||||
- remove "expensive" stats to save memory.
|
||||
|
||||
# 6.0.2
|
||||
|
||||
SMP agent:
|
||||
- fix stuck connection commands when a server is not responding.
|
||||
- store query errors, reduce slow query threshold to 1ms.
|
||||
|
||||
Notification server:
|
||||
- reduce PING interval to 1 minute.
|
||||
- fix subscriptions disabled on race condition (only mark subscriptions with END status when received via the active connection).
|
||||
|
||||
# 6.0.1
|
||||
|
||||
SMP agent:
|
||||
- support changing user of the new connection.
|
||||
- do not start delivery workers when there are no messages to deliver.
|
||||
- enable notifications for all connections.
|
||||
- combine database transactions when subscribing.
|
||||
|
||||
SMP server:
|
||||
- safe compacting of store log.
|
||||
- fix possible race when creating client that might lead to memory leak.
|
||||
|
||||
Dependencies: upgrade tls to 1.9
|
||||
|
||||
# 6.0.0
|
||||
|
||||
Version 6.0.0.8
|
||||
|
||||
Agent:
|
||||
- enabled fast handshake support.
|
||||
- batch-send multiple messages in each connection.
|
||||
- resume subscriptions as soon as agent moves to foreground or as network connection resumes.
|
||||
- "known" servers to determine whether to use SMP proxy.
|
||||
- retry on SMP proxy NO_SESSION error.
|
||||
- fixes to notification subscriptions.
|
||||
- persistent server statistics.
|
||||
- better concurrency.
|
||||
|
||||
SMP server:
|
||||
- reduce threads usage.
|
||||
- additional statistics.
|
||||
- improve disabling inactive clients.
|
||||
- additional control port commands for monitoring.
|
||||
|
||||
Notification server:
|
||||
- support onion-only SMP servers.
|
||||
|
||||
# 5.8.2
|
||||
|
||||
Agent:
|
||||
- fast handshake support (disabled).
|
||||
- new statistics api.
|
||||
|
||||
SMP server:
|
||||
- fast handshake support (SKEY command).
|
||||
- minor changes to reduce memory usage.
|
||||
|
||||
# 5.8.1
|
||||
|
||||
Agent:
|
||||
- API to reconnect one server.
|
||||
- Better error handling of file errors and remote control connection errors.
|
||||
- Only start uploading file once all chunks were registered on the servers.
|
||||
|
||||
SMP server:
|
||||
- additional stats for sent message notifications.
|
||||
- fix server page layout.
|
||||
|
||||
# 5.8.0
|
||||
|
||||
Version 5.8.0.10
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
ARG TAG=22.04
|
||||
# syntax=docker/dockerfile:1.7.0-labs
|
||||
ARG TAG=24.04
|
||||
|
||||
FROM ubuntu:${TAG} AS build
|
||||
|
||||
### Build stage
|
||||
|
||||
# Install curl and git and simplexmq dependencies
|
||||
RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev llvm-12 llvm-12-dev libnuma-dev libssl-dev
|
||||
RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev llvm-18 llvm-18-dev libnuma-dev libssl-dev
|
||||
|
||||
# Specify bootstrap Haskell versions
|
||||
ENV BOOTSTRAP_HASKELL_GHC_VERSION=9.6.3
|
||||
ENV BOOTSTRAP_HASKELL_CABAL_VERSION=3.10.1.0
|
||||
ENV BOOTSTRAP_HASKELL_CABAL_VERSION=3.12.1.0
|
||||
|
||||
# Do not install Stack
|
||||
ENV BOOTSTRAP_HASKELL_INSTALL_NO_STACK=true
|
||||
ENV BOOTSTRAP_HASKELL_INSTALL_NO_STACK_HOOK=true
|
||||
|
||||
# Install ghcup
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 sh
|
||||
@@ -21,26 +26,42 @@ ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH"
|
||||
RUN ghcup set ghc "${BOOTSTRAP_HASKELL_GHC_VERSION}" && \
|
||||
ghcup set cabal "${BOOTSTRAP_HASKELL_CABAL_VERSION}"
|
||||
|
||||
COPY . /project
|
||||
# Copy only the source code
|
||||
COPY apps /project/apps/
|
||||
COPY cbits /project/cbits/
|
||||
COPY src /project/src/
|
||||
|
||||
COPY cabal.project Setup.hs simplexmq.cabal LICENSE /project
|
||||
|
||||
WORKDIR /project
|
||||
|
||||
# Debug
|
||||
#ARG CACHEBUST=1
|
||||
|
||||
#ADD --chmod=755 https://github.com/MShekow/directory-checksum/releases/download/v1.4.6/directory-checksum_1.4.6_linux_amd64 /usr/local/bin/directory-checksum
|
||||
#RUN directory-checksum --max-depth 2 .
|
||||
|
||||
# Set build arguments and check if they exist
|
||||
ARG APP
|
||||
ARG APP_PORT
|
||||
RUN if [ -z "$APP" ] || [ -z "$APP_PORT" ]; then printf "Please spcify \$APP and \$APP_PORT build-arg.\n"; exit 1; fi
|
||||
RUN if [ -z "$APP" ]; then printf "Please spcify \$APP build-arg.\n"; exit 1; fi
|
||||
|
||||
# Compile app
|
||||
RUN cabal update
|
||||
RUN cabal build exe:$APP
|
||||
|
||||
# Copy scripts
|
||||
COPY scripts /project/scripts/
|
||||
|
||||
# Create new path containing all files needed
|
||||
RUN mkdir /final
|
||||
WORKDIR /final
|
||||
|
||||
# Strip the binary from debug symbols to reduce size
|
||||
RUN bin=$(find /project/dist-newstyle -name "$APP" -type f -executable) && \
|
||||
RUN bin="$(find /project/dist-newstyle -name "$APP" -type f -executable)" && \
|
||||
mv "$bin" ./ && \
|
||||
strip ./"$APP" &&\
|
||||
mv /project/scripts/docker/entrypoint-"$APP" ./entrypoint
|
||||
mv /project/scripts/docker/entrypoint-"$APP" ./entrypoint &&\
|
||||
mv /project/scripts/main/simplex-servers-stopscript ./simplex-servers-stopscript
|
||||
|
||||
### Final stage
|
||||
FROM ubuntu:${TAG}
|
||||
@@ -53,6 +74,8 @@ COPY --from=build /final /usr/local/bin/
|
||||
|
||||
# Open app listening port
|
||||
ARG APP_PORT
|
||||
RUN if [ -z "$APP_PORT" ]; then printf "Please spcify \$APP_PORT build-arg.\n"; exit 1; fi
|
||||
|
||||
EXPOSE $APP_PORT
|
||||
|
||||
# simplexmq requires using SIGINT to correctly preserve undelivered messages and restore them on restart
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# syntax=docker/dockerfile:1.7.0-labs
|
||||
ARG TAG=24.04
|
||||
FROM ubuntu:${TAG} AS build
|
||||
|
||||
### Build stage
|
||||
|
||||
ARG GHC=9.6.3
|
||||
ARG CABAL=3.14.1.1
|
||||
|
||||
# Install curl, git and and simplexmq dependencies
|
||||
RUN apt-get update && apt-get install -y curl libpq-dev git sqlite3 libsqlite3-dev build-essential libgmp3-dev zlib1g-dev llvm llvm-dev libnuma-dev libssl-dev
|
||||
|
||||
# Specify bootstrap Haskell versions
|
||||
ENV BOOTSTRAP_HASKELL_GHC_VERSION=${GHC}
|
||||
ENV BOOTSTRAP_HASKELL_CABAL_VERSION=${CABAL}
|
||||
|
||||
# Do not install Stack
|
||||
ENV BOOTSTRAP_HASKELL_INSTALL_NO_STACK=true
|
||||
ENV BOOTSTRAP_HASKELL_INSTALL_NO_STACK_HOOK=true
|
||||
|
||||
# Install ghcup
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 sh
|
||||
|
||||
# Adjust PATH
|
||||
ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH"
|
||||
|
||||
# Set both as default
|
||||
RUN ghcup set ghc "${GHC}" && \
|
||||
ghcup set cabal "${CABAL}"
|
||||
|
||||
WORKDIR /project
|
||||
@@ -1,6 +1,6 @@
|
||||
# SimpleXMQ
|
||||
|
||||
[](https://github.com/simplex-chat/simplexmq/actions?query=workflow%3Abuild)
|
||||
[](https://github.com/simplex-chat/simplexmq/actions/workflows/build.yml)
|
||||
[](https://github.com/simplex-chat/simplexmq/releases)
|
||||
|
||||
📢 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).
|
||||
@@ -33,7 +33,7 @@ To initialize the server use `smp-server init -n <fqdn>` (or `smp-server init --
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -116,7 +116,7 @@ On Linux, you can deploy smp and xftp server using Docker. This will download im
|
||||
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 \
|
||||
@@ -129,7 +129,7 @@ On Linux, you can deploy smp and xftp server using Docker. This will download im
|
||||
```
|
||||
|
||||
- `xftp-server`
|
||||
|
||||
|
||||
You must change **your_ip_or_domain** and **maximum_storage**.
|
||||
```sh
|
||||
docker run -d \
|
||||
@@ -149,8 +149,15 @@ On Linux, you can deploy smp and xftp server using Docker. This will download im
|
||||
You can install and setup servers automatically using our script:
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/install.sh -o simplex-server-install.sh \
|
||||
&& if echo 'b8cf2be103f21f9461d9a500bcd3db06ab7d01d68871b07f4bd245195cbead1d simplex-server-install.sh' | sha256sum -c; then chmod +x ./simplex-server-install.sh && ./simplex-server-install.sh; rm ./simplex-server-install.sh; else echo "SHA-256 checksum is incorrect!" && rm ./simplex-server-install.sh; fi
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/install.sh -o simplex-server-install.sh &&\
|
||||
if echo '53fcdb4ceab324316e2c4cda7e84dbbb344f32550a65975a7895425e5a1be757 simplex-server-install.sh' | sha256sum -c; then
|
||||
chmod +x ./simplex-server-install.sh
|
||||
./simplex-server-install.sh
|
||||
rm ./simplex-server-install.sh
|
||||
else
|
||||
echo "SHA-256 checksum is incorrect!"
|
||||
rm ./simplex-server-install.sh
|
||||
fi
|
||||
```
|
||||
|
||||
### Build from source
|
||||
@@ -180,7 +187,7 @@ On Linux, you can build smp server using Docker.
|
||||
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 \
|
||||
@@ -193,7 +200,7 @@ On Linux, you can build smp server using Docker.
|
||||
```
|
||||
|
||||
- `xftp-server`
|
||||
|
||||
|
||||
You must change **your_ip_or_domain** and **maximum_storage**.
|
||||
```sh
|
||||
docker run -d \
|
||||
@@ -208,14 +215,17 @@ On Linux, you can build smp server using Docker.
|
||||
|
||||
#### Using your distribution
|
||||
|
||||
1. Install [Haskell GHCup](https://www.haskell.org/ghcup/), GHC 8.10.7 and cabal:
|
||||
1. Install dependencies and build tools (`GHC`, `cabal` and dev libs):
|
||||
|
||||
```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
|
||||
# On Ubuntu. Depending on your distribution, use your package manager to determine package names.
|
||||
sudo apt-get update && apt-get install -y build-essential curl libffi-dev libffi7 libgmp3-dev libgmp10 libncurses-dev libncurses5 libtinfo5 pkg-config zlib1g-dev libnuma-dev libssl-dev
|
||||
export BOOTSTRAP_HASKELL_GHC_VERSION=9.6.3
|
||||
export BOOTSTRAP_HASKELL_CABAL_VERSION=3.10.3.0
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 sh
|
||||
ghcup set ghc "${BOOTSTRAP_HASKELL_GHC_VERSION}"
|
||||
ghcup set cabal "${BOOTSTRAP_HASKELL_CABAL_VERSION}"
|
||||
source ~/.ghcup/env
|
||||
```
|
||||
|
||||
2. Build the project:
|
||||
@@ -224,10 +234,20 @@ On Linux, you can build smp server using Docker.
|
||||
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
|
||||
cabal build exe:smp-server exe:xftp-server
|
||||
```
|
||||
|
||||
3. List compiled binaries:
|
||||
|
||||
`smp-server`
|
||||
```sh
|
||||
cabal list-bin exe:smp-server
|
||||
```
|
||||
|
||||
`xftp-server`
|
||||
```sh
|
||||
cabal list-bin exe:xftp-server
|
||||
```
|
||||
|
||||
- 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.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
module Web.Embedded where
|
||||
|
||||
import Data.FileEmbed (embedDir, embedFile)
|
||||
import Simplex.Messaging.Server.Web (EmbeddedContent (..))
|
||||
|
||||
embeddedContent :: EmbeddedContent
|
||||
embeddedContent =
|
||||
EmbeddedContent
|
||||
{ indexHtml = $(embedFile "apps/common/Web/static/index.html"),
|
||||
linkHtml = $(embedFile "apps/common/Web/static/link.html"),
|
||||
mediaContent = $(embedDir "apps/common/Web/static/media/"),
|
||||
wellKnown = $(embedDir "apps/common/Web/static/.well-known/")
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"applinks": {
|
||||
"details": [
|
||||
{
|
||||
"appIDs": [
|
||||
"5NN7GUYB6T.chat.simplex.app"
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"/": "/contact/*"
|
||||
},
|
||||
{
|
||||
"/": "/contact"
|
||||
},
|
||||
{
|
||||
"/": "/invitation/*"
|
||||
},
|
||||
{
|
||||
"/": "/invitation"
|
||||
},
|
||||
{
|
||||
"/": "/a/*"
|
||||
},
|
||||
{
|
||||
"/": "/a"
|
||||
},
|
||||
{
|
||||
"/": "/c/*"
|
||||
},
|
||||
{
|
||||
"/": "/c"
|
||||
},
|
||||
{
|
||||
"/": "/g/*"
|
||||
},
|
||||
{
|
||||
"/": "/g"
|
||||
},
|
||||
{
|
||||
"/": "/i/*"
|
||||
},
|
||||
{
|
||||
"/": "/i"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[
|
||||
{
|
||||
"relation": [
|
||||
"delegate_permission/common.handle_all_urls"
|
||||
],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "chat.simplex.app",
|
||||
"sha256_cert_fingerprints": [
|
||||
"5E:3E:DC:C2:00:FB:A8:D5:F4:88:F3:CA:4C:32:5B:05:78:C5:6A:9C:03:A1:CC:B5:92:9C:D7:5C:7E:57:E2:4D",
|
||||
"3C:52:C4:FD:3C:AD:1C:07:C9:B0:0A:70:80:E3:58:FA:B9:FE:FC:B8:AF:5A:EC:14:77:65:F1:6D:0F:21:AD:85",
|
||||
"AE:C1:95:DC:FD:46:14:BD:3A:91:EC:26:D1:D5:14:C8:75:71:C5:CC:8D:CF:48:08:3F:92:83:14:3C:A2:B9:A6"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -15,6 +15,12 @@
|
||||
<link rel="stylesheet" href="/media/style.css">
|
||||
|
||||
<style>
|
||||
body,
|
||||
html {
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
table#config {
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 4px;
|
||||
@@ -42,6 +48,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 440px) {
|
||||
table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
tr td:not(:last-child) {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
|
||||
.dark tr td:not(:last-child) {
|
||||
color: #fff;
|
||||
}
|
||||
@@ -88,6 +105,13 @@
|
||||
class="text-[16px] leading-[26px] tracking-[0.01em] nav-link-text text-black dark:text-white before:bg-black dark:before:bg-white">Server
|
||||
information</span></a>
|
||||
</li>
|
||||
<!-- <x-xftpConfig>
|
||||
<li class="nav-link relative"><a href="/file"
|
||||
class="flex items-center justify-between gap-2 lg:py-5 whitespace-nowrap"><span
|
||||
class="text-[16px] leading-[26px] tracking-[0.01em] nav-link-text text-black dark:text-white before:bg-black dark:before:bg-white">File
|
||||
transfer</span></a>
|
||||
</li>
|
||||
</x-xftpConfig> -->
|
||||
</ul><a target="_blank" href="https://github.com/simplex-chat/simplex-chat#help-us-with-donations"
|
||||
class="whitespace-nowrap flex items-center gap-1 self-center text-white dark:text-black text-[16px] font-medium tracking-[0.02em] rounded-[34px] bg-primary-light dark:bg-primary-dark py-3 lg:py-2 px-20 lg:px-5 mb-16 lg:mb-0">Donate</a>
|
||||
</div>
|
||||
@@ -204,9 +228,16 @@
|
||||
Public information
|
||||
</h2>
|
||||
<table id="public-info">
|
||||
<tr class="text-grey-black dark:text-white text-base">
|
||||
<td>Server version:</td>
|
||||
<td>${version}<x-commit> / <a href="${commitSourceCode}/commit/${commit}" target="_blank">${shortCommit}</a></x-commit></td>
|
||||
</tr>
|
||||
<tr class="text-grey-black dark:text-white text-base">
|
||||
<td>Source code:</td>
|
||||
<td><a href="${sourceCode}" target="_blank">${sourceCode}</a></td>
|
||||
<td>
|
||||
<x-sourceCode><a href="${sourceCode}" target="_blank">${sourceCode}</a></x-sourceCode>
|
||||
<x-noSourceCode>add to ${iniFileName} (required by <a href="https://github.com/simplex-chat/simplexmq/blob/stable/LICENSE" target="_blank">AGPLv3</a>)</x-noSourceCode>
|
||||
</td>
|
||||
</tr>
|
||||
<x-website>
|
||||
<tr class="text-grey-black dark:text-white text-base">
|
||||
@@ -274,6 +305,12 @@
|
||||
<td>${hostingEntity} (${hostingCountry})</td>
|
||||
</tr>
|
||||
</x-hosting>
|
||||
<x-hostingType>
|
||||
<tr class="text-grey-black dark:text-white text-base">
|
||||
<td>Hosting type:</td>
|
||||
<td>${hostingType}</td>
|
||||
</tr>
|
||||
</x-hostingType>
|
||||
<x-serverCountry>
|
||||
<tr class="text-grey-black dark:text-white text-base">
|
||||
<td>Server country:</td>
|
||||
@@ -287,6 +324,7 @@
|
||||
<h2 class="text-[30px] mb-[20px] leading-[28px] text-[#606C71] dark:text-white font-bold max-w-[475px]">
|
||||
Configuration</h2>
|
||||
<table id="config">
|
||||
<x-smpConfig>
|
||||
<tr class="text-grey-black dark:text-white text-base">
|
||||
<td>Persistence:</td>
|
||||
<td>${persistence}</td>
|
||||
@@ -307,6 +345,25 @@
|
||||
<td>Basic auth enabled:</td>
|
||||
<td>${basicAuthEnabled}</td>
|
||||
</tr>
|
||||
</x-smpConfig>
|
||||
<x-xftpConfig>
|
||||
<tr class="text-grey-black dark:text-white text-base">
|
||||
<td>File expiration:</td>
|
||||
<td>${fileExpiration}</td>
|
||||
</tr>
|
||||
<tr class="text-grey-black dark:text-white text-base">
|
||||
<td>Stats enabled:</td>
|
||||
<td>${statsEnabled}</td>
|
||||
</tr>
|
||||
<tr class="text-grey-black dark:text-white text-base">
|
||||
<td>New uploads allowed:</td>
|
||||
<td>${newUploadsAllowed}</td>
|
||||
</tr>
|
||||
<tr class="text-grey-black dark:text-white text-base">
|
||||
<td>Basic auth enabled:</td>
|
||||
<td>${basicAuthEnabled}</td>
|
||||
</tr>
|
||||
</x-xftpConfig>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -142,8 +142,7 @@
|
||||
class="hidden xl:block h-screen pt-[66px] bg-white dark:bg-gradient-radial-mobile dark:lg:bg-gradient-radial">
|
||||
<div class="container m-auto h-full flex items-center justify-between px-5">
|
||||
<div class="flex flex-col items-start justify-center w-full">
|
||||
<h1 class="text-[38px] leading-[43px] font-bold max-w-[500px] mb-[30px] primary-header-contact">You received a
|
||||
1-time link to connect on SimpleX Chat</h1>
|
||||
<h1 class="text-[38px] leading-[43px] font-bold max-w-[500px] mb-[30px] primary-header-contact">This is a one-time link of the SimpleX network user</h1>
|
||||
<h2
|
||||
class="text-[20px] leading-[28px] text-[#606C71] dark:text-white font-bold max-w-[475px] mb-[80px] secondary-header-contact">
|
||||
Scan the QR code with the SimpleX Chat app on your phone or tablet.</h2>
|
||||
@@ -184,10 +183,8 @@
|
||||
class="block xl:hidden pt-[106px] py-[90px] bg-white dark:bg-gradient-radial-mobile dark:lg:bg-gradient-radial">
|
||||
<div class="container m-auto px-5">
|
||||
<div class="flex flex-col items-center">
|
||||
<h1 class="text-[28px] font-bold text-center max-w-[602px] mb-[40px] primary-header-contact">You received a
|
||||
1-time link to connect on SimpleX Chat</h1>
|
||||
<p class="text-[20px] leading-[28px] text-grey-black dark:text-white font-medium mb-[30px]">To make a
|
||||
connection:</p>
|
||||
<h1 class="text-[28px] font-bold text-center max-w-[602px] mb-[40px] primary-header-contact">This is a one-time link of the SimpleX network user</h1>
|
||||
<p class="text-[20px] leading-[28px] text-grey-black dark:text-white font-medium mb-[30px]">To make a connection:</p>
|
||||
<div
|
||||
class="flex flex-col justify-center items-center p-4 w-full max-w-[468px] min-h-[131px] rounded-[30px] border-[1px] border-[#A8B0B4] dark:border-white border-opacity-60 mb-6 relative">
|
||||
<p class="text-xl font-medium text-grey-black dark:text-white mb-4">Install SimpleX app</p>
|
||||
@@ -506,13 +503,17 @@
|
||||
const url = window.location.href
|
||||
const messageElements = document.getElementsByClassName('primary-header-contact')
|
||||
|
||||
if (url.includes('/invitation')) {
|
||||
for (let element of messageElements) {
|
||||
element.textContent = 'You received a 1-time link to connect on SimpleX Chat'
|
||||
}
|
||||
} else {
|
||||
for (let element of messageElements) {
|
||||
element.textContent = 'You received an address to connect on SimpleX Chat'
|
||||
for (let element of messageElements) {
|
||||
if (url.includes('/g') || url.includes('&data=%7B%22groupLinkId%22%3A')) {
|
||||
element.innerHTML = 'This is a public group address on SimpleX network'
|
||||
} else if (url.includes('/a') || url.includes('/contact')) {
|
||||
element.innerHTML = 'This is a public address of the SimpleX network user'
|
||||
} else if (url.includes('/i') || url.includes('/invitation')) {
|
||||
element.innerHTML = 'This is a one-time link of the SimpleX network user'
|
||||
} else if (url.includes('/c')) {
|
||||
element.innerHTML = 'This is a public channel address on SimpleX network'
|
||||
} else if (url.includes('/r')) {
|
||||
element.innerHTML = 'This is a chat relay address on SimpleX network'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@@ -20,7 +20,18 @@
|
||||
parsedURI.pathname = "/" + action
|
||||
connURI = parsedURI.toString()
|
||||
console.log("connection URI: ", connURI)
|
||||
mobileConnURIanchor.href = "simplex:" + parsedURI.pathname + parsedURI.hash
|
||||
const hash = parsedURI.hash
|
||||
const hostname = parsedURI.hostname
|
||||
let appURI = "simplex:" + parsedURI.pathname
|
||||
appURI += action.length > 1 // not short link
|
||||
? hash
|
||||
: !hash.includes("?") // otherwise add server hostname
|
||||
? hash + "?h=" + hostname // no parameters
|
||||
: !hash.includes("?h=") && !hash.includes("&h=")
|
||||
? hash + "&h=" + hostname // no "h" parameter
|
||||
: hash.replace(/([?&])h=([^&]+)/, `$1h=${hostname},$2`) // add as the first hostname to "h" parameter
|
||||
mobileConnURIanchor.href = appURI
|
||||
console.log("app URI: ", appURI)
|
||||
connURIel.innerText = "/c " + connURI
|
||||
for (const connQRCode of connQRCodes) {
|
||||
try {
|
||||
|
Before Width: | Height: | Size: 289 KiB After Width: | Height: | Size: 289 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 632 B After Width: | Height: | Size: 632 B |
@@ -50,6 +50,10 @@ img {
|
||||
/* For Internet Explorer and Edge */
|
||||
}
|
||||
|
||||
a{
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* NEW SITE */
|
||||
.container,
|
||||
.container-fluid,
|
||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
@@ -15,7 +15,6 @@ logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
setLogLevel LogDebug -- change to LogError in production
|
||||
cfgPath <- getEnvPath "NTF_SERVER_CFG_PATH" defaultCfgPath
|
||||
logPath <- getEnvPath "NTF_SERVER_LOG_PATH" defaultLogPath
|
||||
withGlobalLogging logCfg $ ntfServerCLI cfgPath logPath
|
||||
|
||||
@@ -2,8 +2,9 @@ module Main where
|
||||
|
||||
import Control.Logger.Simple
|
||||
import Simplex.Messaging.Server.CLI (getEnvPath)
|
||||
import Simplex.Messaging.Server.Main
|
||||
import qualified Static
|
||||
import Simplex.Messaging.Server.Main (smpServerCLI_)
|
||||
import Simplex.Messaging.Server.Web (serveStaticFiles, attachStaticFiles)
|
||||
import SMPWeb (smpGenerateSite)
|
||||
|
||||
defaultCfgPath :: FilePath
|
||||
defaultCfgPath = "/etc/opt/simplex"
|
||||
@@ -16,7 +17,6 @@ logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
setLogLevel LogDebug
|
||||
cfgPath <- getEnvPath "SMP_SERVER_CFG_PATH" defaultCfgPath
|
||||
logPath <- getEnvPath "SMP_SERVER_LOG_PATH" defaultLogPath
|
||||
withGlobalLogging logCfg $ smpServerCLI_ Static.generateSite Static.serveStaticFiles cfgPath logPath
|
||||
withGlobalLogging logCfg $ smpServerCLI_ smpGenerateSite serveStaticFiles attachStaticFiles cfgPath logPath
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module SMPWeb
|
||||
( smpGenerateSite,
|
||||
serverInformation,
|
||||
) where
|
||||
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.String (fromString)
|
||||
import Web.Embedded (embeddedContent)
|
||||
import Simplex.Messaging.Encoding.String (strEncode)
|
||||
import Simplex.Messaging.Server.Information
|
||||
import Simplex.Messaging.Server.Main (simplexmqSource)
|
||||
import qualified Simplex.Messaging.Server.Web as Web
|
||||
import Simplex.Messaging.Server.Web (render, serverInfoSubsts, timedTTLText)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
|
||||
smpGenerateSite :: ServerInformation -> Maybe TransportHost -> FilePath -> IO ()
|
||||
smpGenerateSite si onionHost path =
|
||||
Web.generateSite embeddedContent (serverInformation si onionHost) smpLinkPages path
|
||||
|
||||
smpLinkPages :: [String]
|
||||
smpLinkPages = ["contact", "invitation", "a", "c", "g", "r", "i"]
|
||||
|
||||
serverInformation :: ServerInformation -> Maybe TransportHost -> ByteString
|
||||
serverInformation ServerInformation {config, information} onionHost = render (Web.indexHtml embeddedContent) substs
|
||||
where
|
||||
substs = [("smpConfig", Just "y"), ("xftpConfig", Nothing)] <> substConfig <> serverInfoSubsts simplexmqSource information <> [("onionHost", strEncode <$> onionHost), ("iniFileName", Just "smp-server.ini")]
|
||||
substConfig =
|
||||
[ ( "persistence",
|
||||
Just $ case persistence config of
|
||||
SPMMemoryOnly -> "In-memory only"
|
||||
SPMQueues -> "Queues"
|
||||
SPMMessages -> "Queues and messages"
|
||||
),
|
||||
("messageExpiration", Just $ maybe "Never" (fromString . timedTTLText) $ messageExpiration config),
|
||||
("statsEnabled", Just . yesNo $ statsEnabled config),
|
||||
("newQueuesAllowed", Just . yesNo $ newQueuesAllowed config),
|
||||
("basicAuthEnabled", Just . yesNo $ basicAuthEnabled config)
|
||||
]
|
||||
yesNo True = "Yes"
|
||||
yesNo False = "No"
|
||||
@@ -1 +0,0 @@
|
||||
../link.html
|
||||
@@ -1 +0,0 @@
|
||||
../link.html
|
||||
@@ -1,176 +0,0 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Static where
|
||||
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as B
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.String (fromString)
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Network.Wai.Application.Static as S
|
||||
import Network.Wai.Handler.Warp as W
|
||||
import qualified Network.Wai.Handler.WarpTLS as W
|
||||
import Simplex.Messaging.Encoding.String (strEncode)
|
||||
import Simplex.Messaging.Server.Information
|
||||
import Simplex.Messaging.Server.Main (EmbeddedWebParams (..), WebHttpsParams (..))
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
import Simplex.Messaging.Util (tshow)
|
||||
import Static.Embedded as E
|
||||
import System.Directory (createDirectoryIfMissing)
|
||||
import System.FilePath
|
||||
import UnliftIO.Concurrent (forkFinally)
|
||||
|
||||
serveStaticFiles :: EmbeddedWebParams -> IO ()
|
||||
serveStaticFiles EmbeddedWebParams {webStaticPath, webHttpPort, webHttpsParams} = do
|
||||
forM_ webHttpPort $ \port -> flip forkFinally (\e -> logError $ "HTTP server crashed: " <> tshow e) $ do
|
||||
logInfo $ "Serving static site on port " <> tshow port
|
||||
W.runSettings (mkSettings port) (S.staticApp $ S.defaultFileServerSettings webStaticPath)
|
||||
forM_ webHttpsParams $ \WebHttpsParams {port, cert, key} -> flip forkFinally (\e -> logError $ "HTTPS server crashed: " <> tshow e) $ do
|
||||
logInfo $ "Serving static site on port " <> tshow port <> " (TLS)"
|
||||
W.runTLS (W.tlsSettings cert key) (mkSettings port) (S.staticApp $ S.defaultFileServerSettings webStaticPath)
|
||||
where
|
||||
mkSettings port = setPort port defaultSettings
|
||||
|
||||
generateSite :: ServerInformation -> Maybe TransportHost -> FilePath -> IO ()
|
||||
generateSite si onionHost sitePath = do
|
||||
createDirectoryIfMissing True sitePath
|
||||
B.writeFile (sitePath </> "index.html") $ serverInformation si onionHost
|
||||
createDirectoryIfMissing True $ sitePath </> "media"
|
||||
forM_ E.mediaContent $ \(path, bs) -> B.writeFile (sitePath </> "media" </> path) bs
|
||||
createDirectoryIfMissing True $ sitePath </> "contact"
|
||||
B.writeFile (sitePath </> "contact" </> "index.html") E.linkHtml
|
||||
createDirectoryIfMissing True $ sitePath </> "invitation"
|
||||
B.writeFile (sitePath </> "invitation" </> "index.html") E.linkHtml
|
||||
logInfo $ "Generated static site contents at " <> tshow sitePath
|
||||
|
||||
serverInformation :: ServerInformation -> Maybe TransportHost -> ByteString
|
||||
serverInformation ServerInformation {config, information} onionHost = render E.indexHtml substs
|
||||
where
|
||||
substs = substConfig <> maybe [] substInfo information <> [("onionHost", strEncode <$> onionHost)]
|
||||
substConfig =
|
||||
[ ( "persistence",
|
||||
Just $ case persistence config of
|
||||
SPMMemoryOnly -> "In-memory only"
|
||||
SPMQueues -> "Queues"
|
||||
SPMMessages -> "Queues and messages"
|
||||
),
|
||||
("messageExpiration", Just $ maybe "Never" (fromString . timedTTLText) $ messageExpiration config),
|
||||
("statsEnabled", Just . yesNo $ statsEnabled config),
|
||||
("newQueuesAllowed", Just . yesNo $ newQueuesAllowed config),
|
||||
("basicAuthEnabled", Just . yesNo $ basicAuthEnabled config)
|
||||
]
|
||||
yesNo True = "Yes"
|
||||
yesNo False = "No"
|
||||
substInfo spi =
|
||||
concat
|
||||
[ basic,
|
||||
maybe [("usageConditions", Nothing), ("usageAmendments", Nothing)] conds (usageConditions spi),
|
||||
maybe [("operator", Nothing)] operatorE (operator spi),
|
||||
maybe [("admin", Nothing)] admin (adminContacts spi),
|
||||
maybe [("complaints", Nothing)] complaints (complaintsContacts spi),
|
||||
maybe [("hosting", Nothing)] hostingE (hosting spi),
|
||||
server
|
||||
]
|
||||
where
|
||||
basic =
|
||||
[ ("sourceCode", Just . encodeUtf8 $ sourceCode spi),
|
||||
("website", encodeUtf8 <$> website spi)
|
||||
]
|
||||
conds ServerConditions {conditions, amendments} =
|
||||
[ ("usageConditions", Just $ encodeUtf8 conditions),
|
||||
("usageAmendments", encodeUtf8 <$> amendments)
|
||||
]
|
||||
operatorE Entity {name, country} =
|
||||
[ ("operator", Just ""),
|
||||
("operatorEntity", Just $ encodeUtf8 name),
|
||||
("operatorCountry", encodeUtf8 <$> country)
|
||||
]
|
||||
admin ServerContactAddress {simplex, email, pgp} =
|
||||
[ ("admin", Just ""),
|
||||
("adminSimplex", strEncode <$> simplex),
|
||||
("adminEmail", encodeUtf8 <$> email),
|
||||
("adminPGP", encodeUtf8 . pkURI <$> pgp),
|
||||
("adminPGPFingerprint", encodeUtf8 . pkFingerprint <$> pgp)
|
||||
]
|
||||
complaints ServerContactAddress {simplex, email, pgp} =
|
||||
[ ("complaints", Just ""),
|
||||
("complaintsSimplex", strEncode <$> simplex),
|
||||
("complaintsEmail", encodeUtf8 <$> email),
|
||||
("complaintsPGP", encodeUtf8 . pkURI <$> pgp),
|
||||
("complaintsPGPFingerprint", encodeUtf8 . pkFingerprint <$> pgp)
|
||||
]
|
||||
hostingE Entity {name, country} =
|
||||
[ ("hosting", Just ""),
|
||||
("hostingEntity", Just $ encodeUtf8 name),
|
||||
("hostingCountry", encodeUtf8 <$> country)
|
||||
]
|
||||
server =
|
||||
[ ("serverCountry", fmap encodeUtf8 $ serverCountry =<< information)
|
||||
]
|
||||
|
||||
-- Copy-pasted from simplex-chat Simplex.Chat.Types.Preferences
|
||||
{-# INLINE timedTTLText #-}
|
||||
timedTTLText :: (Integral i, Show i) => i -> String
|
||||
timedTTLText 0 = "0 sec"
|
||||
timedTTLText ttl = do
|
||||
let (m', s) = ttl `quotRem` 60
|
||||
(h', m) = m' `quotRem` 60
|
||||
(d', h) = h' `quotRem` 24
|
||||
(mm, d) = d' `quotRem` 30
|
||||
unwords $
|
||||
[mms mm | mm /= 0]
|
||||
<> [ds d | d /= 0]
|
||||
<> [hs h | h /= 0]
|
||||
<> [ms m | m /= 0]
|
||||
<> [ss s | s /= 0]
|
||||
where
|
||||
ss s = show s <> " sec"
|
||||
ms m = show m <> " min"
|
||||
hs 1 = "1 hour"
|
||||
hs h = show h <> " hours"
|
||||
ds 1 = "1 day"
|
||||
ds 7 = "1 week"
|
||||
ds 14 = "2 weeks"
|
||||
ds d = show d <> " days"
|
||||
mms 1 = "1 month"
|
||||
mms mm = show mm <> " months"
|
||||
|
||||
-- | Rewrite source with provided substitutions
|
||||
render :: ByteString -> [(ByteString, Maybe ByteString)] -> ByteString
|
||||
render src = \case
|
||||
[] -> src
|
||||
(label, content') : rest -> render (section_ label content' src) rest
|
||||
|
||||
-- | Rewrite section content inside @<x-label>...</x-label>@ markers.
|
||||
-- Markers are always removed when found. Closing marker is mandatory.
|
||||
-- If content is absent, whole section is removed.
|
||||
-- Section content is delegated to `item_`. If no sections found, the whole source is delegated.
|
||||
section_ :: ByteString -> Maybe ByteString -> ByteString -> ByteString
|
||||
section_ label content' src =
|
||||
case B.breakSubstring startMarker src of
|
||||
(_, "") -> item_ label (fromMaybe "" content') src -- no section, just replace items
|
||||
(before, afterStart') ->
|
||||
-- found section start, search for end too
|
||||
case B.breakSubstring endMarker $ B.drop (B.length startMarker) afterStart' of
|
||||
(_, "") -> error $ "missing section end: " <> show endMarker
|
||||
(inside, next') ->
|
||||
let next = B.drop (B.length endMarker) next'
|
||||
in case content' of
|
||||
Nothing -> before <> next -- collapse section
|
||||
Just content -> before <> item_ label content inside <> section_ label content' next
|
||||
where
|
||||
startMarker = "<x-" <> label <> ">"
|
||||
endMarker = "</x-" <> label <> ">"
|
||||
|
||||
-- | Replace all occurences of @${label}@ with provided content.
|
||||
item_ :: ByteString -> ByteString -> ByteString -> ByteString
|
||||
item_ label content' src =
|
||||
case B.breakSubstring marker src of
|
||||
(done, "") -> done
|
||||
(before, after') -> before <> content' <> item_ label content' (B.drop (B.length marker) after')
|
||||
where
|
||||
marker = "${" <> label <> "}"
|
||||
@@ -1,15 +0,0 @@
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
module Static.Embedded where
|
||||
|
||||
import Data.FileEmbed (embedDir, embedFile)
|
||||
import Data.ByteString (ByteString)
|
||||
|
||||
indexHtml :: ByteString
|
||||
indexHtml = $(embedFile "apps/smp-server/static/index.html")
|
||||
|
||||
linkHtml :: ByteString
|
||||
linkHtml = $(embedFile "apps/smp-server/static/link.html")
|
||||
|
||||
mediaContent :: [(FilePath, ByteString)]
|
||||
mediaContent = $(embedDir "apps/smp-server/static/media/")
|
||||
@@ -1,8 +1,10 @@
|
||||
module Main where
|
||||
|
||||
import Control.Logger.Simple
|
||||
import Simplex.FileTransfer.Server.Main (xftpServerCLI_)
|
||||
import Simplex.Messaging.Server.CLI (getEnvPath)
|
||||
import Simplex.FileTransfer.Server.Main
|
||||
import Simplex.Messaging.Server.Web (serveStaticFiles)
|
||||
import XFTPWeb (xftpGenerateSite)
|
||||
|
||||
defaultCfgPath :: FilePath
|
||||
defaultCfgPath = "/etc/opt/simplex-xftp"
|
||||
@@ -18,4 +20,4 @@ main = do
|
||||
setLogLevel LogDebug -- change to LogError in production
|
||||
cfgPath <- getEnvPath "XFTP_SERVER_CFG_PATH" defaultCfgPath
|
||||
logPath <- getEnvPath "XFTP_SERVER_LOG_PATH" defaultLogPath
|
||||
withGlobalLogging logCfg $ xftpServerCLI cfgPath logPath
|
||||
withGlobalLogging logCfg $ xftpServerCLI_ xftpGenerateSite serveStaticFiles cfgPath logPath
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
module XFTPWeb
|
||||
( xftpGenerateSite,
|
||||
xftpServerInformation,
|
||||
) where
|
||||
|
||||
import Control.Monad (forM_)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.FileEmbed (embedDir, embedFile)
|
||||
import Data.Maybe (isJust)
|
||||
import Data.String (fromString)
|
||||
import Web.Embedded (embeddedContent)
|
||||
import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..))
|
||||
import Simplex.Messaging.Encoding.String (strEncode)
|
||||
import Simplex.Messaging.Server.Expiration (ExpirationConfig (..))
|
||||
import Simplex.Messaging.Server.Information (ServerPublicInfo)
|
||||
import Simplex.Messaging.Server.Main (simplexmqSource)
|
||||
import qualified Simplex.Messaging.Server.Web as Web
|
||||
import Simplex.Messaging.Server.Web (render, serverInfoSubsts, timedTTLText)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
import System.Directory (createDirectoryIfMissing)
|
||||
import System.FilePath ((</>))
|
||||
|
||||
xftpWebContent :: [(FilePath, ByteString)]
|
||||
xftpWebContent = $(embedDir "apps/xftp-server/static/xftp-web-bundle/")
|
||||
|
||||
xftpMediaContent :: [(FilePath, ByteString)]
|
||||
xftpMediaContent = $(embedDir "apps/xftp-server/static/media/")
|
||||
|
||||
xftpFilePageHtml :: ByteString
|
||||
xftpFilePageHtml = $(embedFile "apps/xftp-server/static/file.html")
|
||||
|
||||
xftpGenerateSite :: XFTPServerConfig s -> Maybe ServerPublicInfo -> Maybe TransportHost -> FilePath -> IO ()
|
||||
xftpGenerateSite cfg info onionHost path = do
|
||||
let substs = xftpSubsts cfg info onionHost
|
||||
Web.generateSite embeddedContent (render (Web.indexHtml embeddedContent) substs) [] path
|
||||
let xftpDir = path </> "xftp-web-bundle"
|
||||
mediaDir = path </> "media"
|
||||
fileDir = path </> "file"
|
||||
filePage xftpDir xftpWebContent
|
||||
filePage mediaDir xftpMediaContent
|
||||
createDirectoryIfMissing True fileDir
|
||||
B.writeFile (fileDir </> "index.html") $ render xftpFilePageHtml substs
|
||||
where
|
||||
filePage dir content_ = do
|
||||
createDirectoryIfMissing True dir
|
||||
forM_ content_ $ \(fp, content) -> B.writeFile (dir </> fp) content
|
||||
|
||||
xftpServerInformation :: XFTPServerConfig s -> Maybe ServerPublicInfo -> Maybe TransportHost -> ByteString
|
||||
xftpServerInformation cfg info onionHost = render (Web.indexHtml embeddedContent) (xftpSubsts cfg info onionHost)
|
||||
|
||||
xftpSubsts :: XFTPServerConfig s -> Maybe ServerPublicInfo -> Maybe TransportHost -> [(ByteString, Maybe ByteString)]
|
||||
xftpSubsts XFTPServerConfig {fileExpiration, logStatsInterval, allowNewFiles, newFileBasicAuth} information onionHost =
|
||||
[("smpConfig", Nothing), ("xftpConfig", Just "y")] <> substConfig <> serverInfoSubsts simplexmqSource information <> [("onionHost", strEncode <$> onionHost), ("iniFileName", Just "file-server.ini")]
|
||||
where
|
||||
substConfig =
|
||||
[ ("fileExpiration", Just $ maybe "Never" (fromString . timedTTLText . ttl) fileExpiration),
|
||||
("statsEnabled", Just . yesNo $ isJust logStatsInterval),
|
||||
("newUploadsAllowed", Just . yesNo $ allowNewFiles),
|
||||
("basicAuthEnabled", Just . yesNo $ isJust newFileBasicAuth)
|
||||
]
|
||||
yesNo True = "Yes"
|
||||
yesNo False = "No"
|
||||
@@ -0,0 +1,115 @@
|
||||
<svg width="440" height="520" viewBox="-20 0 440 520" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Sender browser -->
|
||||
<rect x="120" y="16" width="160" height="56" rx="10" stroke="#70F0F9" stroke-width="1.5"/>
|
||||
<text x="200" y="40" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#70F0F9">Sender's browser</text>
|
||||
<text x="200" y="56" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="rgba(112,240,249,0.7)">encrypts file</text>
|
||||
|
||||
<!-- Arrow down from sender to chunks -->
|
||||
<line x1="200" y1="72" x2="200" y2="120" stroke="#70F0F9" stroke-width="1.5" marker-end="url(#arrowC)"/>
|
||||
|
||||
<!-- Chunks row -->
|
||||
<rect x="112" y="120" width="176" height="40" rx="8" fill="none" stroke="#70F0F9" stroke-width="1" stroke-dasharray="4 3"/>
|
||||
<text x="200" y="145" text-anchor="middle" font-family="system-ui, sans-serif" font-size="12" fill="#70F0F9">encrypted chunks</text>
|
||||
|
||||
<!-- Arrows from chunks to routers -->
|
||||
<line x1="152" y1="160" x2="80" y2="220" stroke="#70F0F9" stroke-width="1.5" marker-end="url(#arrowC)"/>
|
||||
<line x1="200" y1="160" x2="200" y2="220" stroke="#70F0F9" stroke-width="1.5" marker-end="url(#arrowC)"/>
|
||||
<line x1="248" y1="160" x2="320" y2="220" stroke="#70F0F9" stroke-width="1.5" marker-end="url(#arrowC)"/>
|
||||
|
||||
<!-- Router 1 (SimpleX) -->
|
||||
<rect x="20" y="220" width="120" height="56" rx="6" fill="none" stroke="#70F0F9" stroke-width="1.5"/>
|
||||
<g transform="translate(28, 227)">
|
||||
<rect width="14" height="4" rx="1" fill="rgba(112,240,249,0.5)"/>
|
||||
<rect y="6" width="14" height="4" rx="1" fill="rgba(112,240,249,0.5)"/>
|
||||
<rect y="12" width="14" height="4" rx="1" fill="rgba(112,240,249,0.5)"/>
|
||||
<circle cx="11" cy="2" r="1" fill="#70F0F9"/>
|
||||
<circle cx="11" cy="8" r="1" fill="#70F0F9"/>
|
||||
<circle cx="11" cy="14" r="1" fill="#70F0F9"/>
|
||||
</g>
|
||||
<text x="80" y="244" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" font-weight="600" fill="#70F0F9">SimpleX</text>
|
||||
<text x="80" y="258" text-anchor="middle" font-family="system-ui, sans-serif" font-size="9" fill="rgba(112,240,249,0.7)">XFTP router</text>
|
||||
|
||||
<!-- Router 2 (Flux) -->
|
||||
<rect x="155" y="220" width="90" height="56" rx="6" fill="none" stroke="#70F0F9" stroke-width="1.5"/>
|
||||
<g transform="translate(163, 227)">
|
||||
<rect width="14" height="4" rx="1" fill="rgba(112,240,249,0.5)"/>
|
||||
<rect y="6" width="14" height="4" rx="1" fill="rgba(112,240,249,0.5)"/>
|
||||
<rect y="12" width="14" height="4" rx="1" fill="rgba(112,240,249,0.5)"/>
|
||||
<circle cx="11" cy="2" r="1" fill="#70F0F9"/>
|
||||
<circle cx="11" cy="8" r="1" fill="#70F0F9"/>
|
||||
<circle cx="11" cy="14" r="1" fill="#70F0F9"/>
|
||||
</g>
|
||||
<text x="200" y="244" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" font-weight="600" fill="#70F0F9">Flux</text>
|
||||
<text x="200" y="258" text-anchor="middle" font-family="system-ui, sans-serif" font-size="9" fill="rgba(112,240,249,0.7)">XFTP router</text>
|
||||
|
||||
<!-- Router 3 (SimpleX) -->
|
||||
<rect x="260" y="220" width="120" height="56" rx="6" fill="none" stroke="#70F0F9" stroke-width="1.5"/>
|
||||
<g transform="translate(268, 227)">
|
||||
<rect width="14" height="4" rx="1" fill="rgba(112,240,249,0.5)"/>
|
||||
<rect y="6" width="14" height="4" rx="1" fill="rgba(112,240,249,0.5)"/>
|
||||
<rect y="12" width="14" height="4" rx="1" fill="rgba(112,240,249,0.5)"/>
|
||||
<circle cx="11" cy="2" r="1" fill="#70F0F9"/>
|
||||
<circle cx="11" cy="8" r="1" fill="#70F0F9"/>
|
||||
<circle cx="11" cy="14" r="1" fill="#70F0F9"/>
|
||||
</g>
|
||||
<text x="320" y="244" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" font-weight="600" fill="#70F0F9">SimpleX</text>
|
||||
<text x="320" y="258" text-anchor="middle" font-family="system-ui, sans-serif" font-size="9" fill="rgba(112,240,249,0.7)">XFTP router</text>
|
||||
|
||||
<!-- Arrows from routers down -->
|
||||
<line x1="80" y1="276" x2="152" y2="336" stroke="#70F0F9" stroke-width="1.5" marker-end="url(#arrowC)"/>
|
||||
<line x1="200" y1="276" x2="200" y2="336" stroke="#70F0F9" stroke-width="1.5" marker-end="url(#arrowC)"/>
|
||||
<line x1="320" y1="276" x2="248" y2="336" stroke="#70F0F9" stroke-width="1.5" marker-end="url(#arrowC)"/>
|
||||
|
||||
<!-- Re-encrypt label -->
|
||||
<text x="330" y="310" text-anchor="start" font-family="system-ui, sans-serif" font-size="10" fill="rgba(112,240,249,0.7)">re-encrypted</text>
|
||||
<text x="330" y="322" text-anchor="start" font-family="system-ui, sans-serif" font-size="10" fill="rgba(112,240,249,0.7)">per recipient</text>
|
||||
|
||||
<!-- Chunks row (download) -->
|
||||
<rect x="112" y="336" width="176" height="40" rx="8" fill="none" stroke="#70F0F9" stroke-width="1" stroke-dasharray="4 3"/>
|
||||
<text x="200" y="361" text-anchor="middle" font-family="system-ui, sans-serif" font-size="12" fill="#70F0F9">encrypted chunks</text>
|
||||
|
||||
<!-- Arrow down to recipient -->
|
||||
<line x1="200" y1="376" x2="200" y2="424" stroke="#70F0F9" stroke-width="1.5" marker-end="url(#arrowC)"/>
|
||||
|
||||
<!-- Recipient browser -->
|
||||
<rect x="120" y="424" width="160" height="56" rx="10" stroke="#70F0F9" stroke-width="1.5"/>
|
||||
<text x="200" y="448" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#70F0F9">Recipient's browser</text>
|
||||
<text x="200" y="464" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="rgba(112,240,249,0.7)">decrypts file</text>
|
||||
|
||||
<!-- Key path (dashed, side) -->
|
||||
<path d="M120 44 L8 44 L8 452 L120 452" stroke="#70F0F9" stroke-width="1.5" stroke-dasharray="6 4" fill="none" marker-end="url(#arrowC)"/>
|
||||
<text x="-6" y="240" text-anchor="middle" font-family="system-ui, sans-serif" font-size="10" fill="#70F0F9" transform="rotate(-90 -6 240)">key in URL fragment - never sent to page server or data router</text>
|
||||
|
||||
|
||||
<!-- Closed padlock: encryption (between sender and chunks) -->
|
||||
<g transform="translate(192, 88)">
|
||||
<path d="M4,7 V4 C4,1.2 12,1.2 12,4 V7" stroke="#60a5fa" stroke-width="1.5" fill="none" stroke-linecap="round"/>
|
||||
<rect x="2" y="7" width="12" height="9" rx="2" fill="#60a5fa"/>
|
||||
<circle cx="8" cy="12" r="1.2" fill="#0B2A59"/>
|
||||
</g>
|
||||
|
||||
<!-- Open padlock: decryption (between chunks and recipient) -->
|
||||
<g transform="translate(192, 392)">
|
||||
<path d="M4,7 V4 C4,1.2 12,1.2 12,4 V2" stroke="#60a5fa" stroke-width="1.5" fill="none" stroke-linecap="round"/>
|
||||
<rect x="2" y="7" width="12" height="9" rx="2" fill="#60a5fa"/>
|
||||
<circle cx="8" cy="12" r="1.2" fill="#0B2A59"/>
|
||||
</g>
|
||||
|
||||
<!-- Key icon on dashed line -->
|
||||
<g transform="translate(8, 410)">
|
||||
<circle cx="0" cy="0" r="6" stroke="#FBBF24" stroke-width="2" fill="#FBBF24"/>
|
||||
<circle cx="0" cy="0" r="2" fill="#0B2A59"/>
|
||||
<line x1="6" y1="0" x2="16" y2="0" stroke="#FBBF24" stroke-width="2"/>
|
||||
<line x1="14" y1="0" x2="14" y2="4" stroke="#FBBF24" stroke-width="2"/>
|
||||
<line x1="11" y1="0" x2="11" y2="3.5" stroke="#FBBF24" stroke-width="2"/>
|
||||
</g>
|
||||
|
||||
<!-- Annotation: no shared IDs -->
|
||||
<text x="200" y="510" text-anchor="middle" font-family="system-ui, sans-serif" font-size="10" fill="rgba(112,240,249,0.7)">Each file fragment uses unique anonymous credentials - no shared identifiers</text>
|
||||
|
||||
<defs>
|
||||
<marker id="arrowC" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#70F0F9"/>
|
||||
</marker>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.2 KiB |
@@ -0,0 +1,130 @@
|
||||
<svg width="440" height="520" viewBox="-20 0 440 520" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Sender browser -->
|
||||
<rect x="120" y="16" width="160" height="56" rx="10" fill="url(#gBox)" stroke="#606C71" stroke-width="1.5"/>
|
||||
<text x="200" y="40" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#fff">Sender's browser</text>
|
||||
<text x="200" y="56" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="rgba(255,255,255,0.8)">encrypts file</text>
|
||||
|
||||
<!-- Arrow down from sender to chunks -->
|
||||
<line x1="200" y1="72" x2="200" y2="120" stroke="#606C71" stroke-width="1.5" marker-end="url(#arrowG)"/>
|
||||
|
||||
<!-- Chunks row -->
|
||||
<rect x="112" y="120" width="176" height="40" rx="8" fill="#f0f7ff" stroke="#0053D0" stroke-width="1" stroke-dasharray="4 3"/>
|
||||
<text x="200" y="145" text-anchor="middle" font-family="system-ui, sans-serif" font-size="12" fill="#0053D0">encrypted chunks</text>
|
||||
|
||||
<!-- Arrows from chunks to routers -->
|
||||
<line x1="152" y1="160" x2="80" y2="220" stroke="#606C71" stroke-width="1.5" marker-end="url(#arrowG)"/>
|
||||
<line x1="200" y1="160" x2="200" y2="220" stroke="#606C71" stroke-width="1.5" marker-end="url(#arrowG)"/>
|
||||
<line x1="248" y1="160" x2="320" y2="220" stroke="#606C71" stroke-width="1.5" marker-end="url(#arrowG)"/>
|
||||
|
||||
<!-- Router 1 (SimpleX) -->
|
||||
<rect x="20" y="220" width="120" height="56" rx="6" fill="#f0f4f8" stroke="#606C71" stroke-width="1.5"/>
|
||||
<g transform="translate(28, 227)">
|
||||
<rect width="14" height="4" rx="1" fill="#606C71"/>
|
||||
<rect y="6" width="14" height="4" rx="1" fill="#606C71"/>
|
||||
<rect y="12" width="14" height="4" rx="1" fill="#606C71"/>
|
||||
<circle cx="11" cy="2" r="1" fill="#53C1FF"/>
|
||||
<circle cx="11" cy="8" r="1" fill="#53C1FF"/>
|
||||
<circle cx="11" cy="14" r="1" fill="#53C1FF"/>
|
||||
</g>
|
||||
<text x="80" y="244" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" font-weight="600" fill="#3F484B">SimpleX</text>
|
||||
<text x="80" y="258" text-anchor="middle" font-family="system-ui, sans-serif" font-size="9" fill="#606C71">XFTP router</text>
|
||||
|
||||
<!-- Router 2 (Flux) -->
|
||||
<rect x="155" y="220" width="90" height="56" rx="6" fill="#f0f4f8" stroke="#606C71" stroke-width="1.5"/>
|
||||
<g transform="translate(163, 227)">
|
||||
<rect width="14" height="4" rx="1" fill="#606C71"/>
|
||||
<rect y="6" width="14" height="4" rx="1" fill="#606C71"/>
|
||||
<rect y="12" width="14" height="4" rx="1" fill="#606C71"/>
|
||||
<circle cx="11" cy="2" r="1" fill="#53C1FF"/>
|
||||
<circle cx="11" cy="8" r="1" fill="#53C1FF"/>
|
||||
<circle cx="11" cy="14" r="1" fill="#53C1FF"/>
|
||||
</g>
|
||||
<text x="200" y="244" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" font-weight="600" fill="#3F484B">Flux</text>
|
||||
<text x="200" y="258" text-anchor="middle" font-family="system-ui, sans-serif" font-size="9" fill="#606C71">XFTP router</text>
|
||||
|
||||
<!-- Router 3 (SimpleX) -->
|
||||
<rect x="260" y="220" width="120" height="56" rx="6" fill="#f0f4f8" stroke="#606C71" stroke-width="1.5"/>
|
||||
<g transform="translate(268, 227)">
|
||||
<rect width="14" height="4" rx="1" fill="#606C71"/>
|
||||
<rect y="6" width="14" height="4" rx="1" fill="#606C71"/>
|
||||
<rect y="12" width="14" height="4" rx="1" fill="#606C71"/>
|
||||
<circle cx="11" cy="2" r="1" fill="#53C1FF"/>
|
||||
<circle cx="11" cy="8" r="1" fill="#53C1FF"/>
|
||||
<circle cx="11" cy="14" r="1" fill="#53C1FF"/>
|
||||
</g>
|
||||
<text x="320" y="244" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" font-weight="600" fill="#3F484B">SimpleX</text>
|
||||
<text x="320" y="258" text-anchor="middle" font-family="system-ui, sans-serif" font-size="9" fill="#606C71">XFTP router</text>
|
||||
|
||||
<!-- Arrows from routers down -->
|
||||
<line x1="80" y1="276" x2="152" y2="336" stroke="#606C71" stroke-width="1.5" marker-end="url(#arrowG)"/>
|
||||
<line x1="200" y1="276" x2="200" y2="336" stroke="#606C71" stroke-width="1.5" marker-end="url(#arrowG)"/>
|
||||
<line x1="320" y1="276" x2="248" y2="336" stroke="#606C71" stroke-width="1.5" marker-end="url(#arrowG)"/>
|
||||
|
||||
<!-- Re-encrypt label -->
|
||||
<text x="330" y="310" text-anchor="start" font-family="system-ui, sans-serif" font-size="10" fill="#606C71">re-encrypted</text>
|
||||
<text x="330" y="322" text-anchor="start" font-family="system-ui, sans-serif" font-size="10" fill="#606C71">per recipient</text>
|
||||
|
||||
<!-- Chunks row (download) -->
|
||||
<rect x="112" y="336" width="176" height="40" rx="8" fill="#f0f7ff" stroke="#0053D0" stroke-width="1" stroke-dasharray="4 3"/>
|
||||
<text x="200" y="361" text-anchor="middle" font-family="system-ui, sans-serif" font-size="12" fill="#0053D0">encrypted chunks</text>
|
||||
|
||||
<!-- Arrow down to recipient -->
|
||||
<line x1="200" y1="376" x2="200" y2="424" stroke="#606C71" stroke-width="1.5" marker-end="url(#arrowG)"/>
|
||||
|
||||
<!-- Recipient browser -->
|
||||
<rect x="120" y="424" width="160" height="56" rx="10" fill="url(#gBox)" stroke="#606C71" stroke-width="1.5"/>
|
||||
<text x="200" y="448" text-anchor="middle" font-family="system-ui, sans-serif" font-size="13" font-weight="600" fill="#fff">Recipient's browser</text>
|
||||
<text x="200" y="464" text-anchor="middle" font-family="system-ui, sans-serif" font-size="11" fill="rgba(255,255,255,0.8)">decrypts file</text>
|
||||
|
||||
<!-- Key path (dashed, side) -->
|
||||
<path d="M120 44 L8 44 L8 452 L120 452" stroke="#0053D0" stroke-width="1.5" stroke-dasharray="6 4" fill="none" marker-end="url(#arrowB)"/>
|
||||
<text x="-6" y="240" text-anchor="middle" font-family="system-ui, sans-serif" font-size="10" fill="#0053D0" transform="rotate(-90 -6 240)">key in URL fragment - never sent to page server or data router</text>
|
||||
|
||||
|
||||
<!-- Closed padlock: encryption (between sender and chunks) -->
|
||||
<g transform="translate(192, 88)">
|
||||
<path d="M4,7 V4 C4,1.2 12,1.2 12,4 V7" stroke="#0053D0" stroke-width="1.5" fill="none" stroke-linecap="round"/>
|
||||
<rect x="2" y="7" width="12" height="9" rx="2" fill="#0053D0"/>
|
||||
<circle cx="8" cy="12" r="1.2" fill="#fff"/>
|
||||
</g>
|
||||
|
||||
<!-- Open padlock: decryption (between chunks and recipient) -->
|
||||
<g transform="translate(192, 392)">
|
||||
<path d="M4,7 V4 C4,1.2 12,1.2 12,4 V2" stroke="#0053D0" stroke-width="1.5" fill="none" stroke-linecap="round"/>
|
||||
<rect x="2" y="7" width="12" height="9" rx="2" fill="#0053D0"/>
|
||||
<circle cx="8" cy="12" r="1.2" fill="#fff"/>
|
||||
</g>
|
||||
|
||||
<!-- Key icon on dashed line -->
|
||||
<g transform="translate(8, 410)">
|
||||
<circle cx="0" cy="0" r="6" stroke="#D97706" stroke-width="2" fill="#D97706"/>
|
||||
<circle cx="0" cy="0" r="2" fill="#fff"/>
|
||||
<line x1="6" y1="0" x2="16" y2="0" stroke="#D97706" stroke-width="2"/>
|
||||
<line x1="14" y1="0" x2="14" y2="4" stroke="#D97706" stroke-width="2"/>
|
||||
<line x1="11" y1="0" x2="11" y2="3.5" stroke="#D97706" stroke-width="2"/>
|
||||
</g>
|
||||
|
||||
<!-- Annotation: no shared IDs -->
|
||||
<text x="200" y="510" text-anchor="middle" font-family="system-ui, sans-serif" font-size="10" fill="#606C71">Each file fragment uses unique anonymous credentials - no shared identifiers</text>
|
||||
|
||||
<defs>
|
||||
<linearGradient id="gBox" x1="120" y1="16" x2="280" y2="72" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0053D0"/>
|
||||
<stop offset="1" stop-color="#53C1FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="gSrv1" x1="20" y1="220" x2="140" y2="276" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0053D0"/>
|
||||
<stop offset="1" stop-color="#53C1FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="gSrv2" x1="155" y1="220" x2="245" y2="276" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0053D0"/>
|
||||
<stop offset="1" stop-color="#53C1FF"/>
|
||||
</linearGradient>
|
||||
<marker id="arrowG" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#606C71"/>
|
||||
</marker>
|
||||
<marker id="arrowB" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0053D0"/>
|
||||
</marker>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.8 KiB |
@@ -0,0 +1,145 @@
|
||||
#app, [data-xftp-app] {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
color: #333;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
--xftp-ring-fg: #3b82f6;
|
||||
}
|
||||
|
||||
:is(#app, [data-xftp-app]) .card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 32px 24px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:is(#app, [data-xftp-app]) h1 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:is(#app, [data-xftp-app]) .stage { margin-top: 16px; }
|
||||
|
||||
/* Drop zone */
|
||||
:is(#app, [data-xftp-app]) .drop-zone {
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 32px 16px;
|
||||
transition: border-color .15s, background .15s;
|
||||
}
|
||||
:is(#app, [data-xftp-app]) .drop-zone.drag-over {
|
||||
border-color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
:is(#app, [data-xftp-app]) .btn {
|
||||
display: inline-block;
|
||||
padding: 10px 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
font-size: .9rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background .15s;
|
||||
}
|
||||
:is(#app, [data-xftp-app]) .btn:hover { background: #2563eb; }
|
||||
:is(#app, [data-xftp-app]) .btn-secondary { background: #6b7280; }
|
||||
:is(#app, [data-xftp-app]) .btn-secondary:hover { background: #4b5563; }
|
||||
|
||||
/* Hints */
|
||||
:is(#app, [data-xftp-app]) .hint { color: #999; font-size: .85rem; margin-top: 8px; }
|
||||
:is(#app, [data-xftp-app]) .expiry { margin-top: 12px; }
|
||||
|
||||
/* Progress */
|
||||
:is(#app, [data-xftp-app]) .progress-ring { display: block; margin: 0 auto 12px; }
|
||||
:is(#app, [data-xftp-app]) #upload-status,
|
||||
:is(#app, [data-xftp-app]) #dl-status { font-size: .9rem; color: #666; margin-bottom: 12px; }
|
||||
|
||||
/* Share link row */
|
||||
:is(#app, [data-xftp-app]) .link-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
:is(#app, [data-xftp-app]) .link-row input {
|
||||
flex: 1;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6px;
|
||||
font-size: .85rem;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
/* Upload link */
|
||||
:is(#app, [data-xftp-app]) .upload-link {
|
||||
margin-top: 12px;
|
||||
color: #3b82f6;
|
||||
font-size: .9rem;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
:is(#app, [data-xftp-app]) .upload-link:not([hidden]) {
|
||||
display: inline-block;
|
||||
}
|
||||
:is(#app, [data-xftp-app]) .upload-link:hover { text-decoration: underline; }
|
||||
|
||||
/* Messages */
|
||||
:is(#app, [data-xftp-app]) .success { color: #16a34a; font-weight: 600; }
|
||||
:is(#app, [data-xftp-app]) .error { color: #dc2626; font-weight: 500; margin-bottom: 12px; }
|
||||
|
||||
/* Security note */
|
||||
:is(#app, [data-xftp-app]) .security-note {
|
||||
margin-top: 20px;
|
||||
padding: 12px;
|
||||
background: #f0fdf4;
|
||||
border-radius: 6px;
|
||||
font-size: .8rem;
|
||||
color: #555;
|
||||
text-align: left;
|
||||
}
|
||||
:is(#app, [data-xftp-app]) .security-note p + p { margin-top: 6px; }
|
||||
:is(#app, [data-xftp-app]) .security-note a { color: #3b82f6; text-decoration: none; }
|
||||
:is(#app, [data-xftp-app]) .security-note a:hover { text-decoration: underline; }
|
||||
|
||||
/* ── Dark mode ─────────────────────────────────── */
|
||||
.dark :is(#app, [data-xftp-app]) {
|
||||
color: #e5e7eb;
|
||||
--xftp-ring-bg: #374151;
|
||||
--xftp-ring-fg: #60a5fa;
|
||||
--xftp-ring-text: #e5e7eb;
|
||||
--xftp-ring-done: #4ade80;
|
||||
}
|
||||
.dark :is(#app, [data-xftp-app]) .card {
|
||||
background: #1f2937;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,.4);
|
||||
}
|
||||
.dark :is(#app, [data-xftp-app]) .drop-zone { border-color: #4b5563; }
|
||||
.dark :is(#app, [data-xftp-app]) .drop-zone.drag-over {
|
||||
border-color: #60a5fa;
|
||||
background: rgba(59,130,246,.15);
|
||||
}
|
||||
.dark :is(#app, [data-xftp-app]) .btn-secondary { background: #4b5563; }
|
||||
.dark :is(#app, [data-xftp-app]) .btn-secondary:hover { background: #374151; }
|
||||
.dark :is(#app, [data-xftp-app]) .hint { color: #9ca3af; }
|
||||
.dark :is(#app, [data-xftp-app]) #upload-status,
|
||||
.dark :is(#app, [data-xftp-app]) #dl-status { color: #9ca3af; }
|
||||
.dark :is(#app, [data-xftp-app]) .link-row input {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
.dark :is(#app, [data-xftp-app]) .success { color: #4ade80; }
|
||||
.dark :is(#app, [data-xftp-app]) .error { color: #f87171; }
|
||||
.dark :is(#app, [data-xftp-app]) .security-note {
|
||||
background: rgba(34,197,94,.1);
|
||||
color: #d1d5db;
|
||||
}
|
||||
.dark :is(#app, [data-xftp-app]) .upload-link { color: #60a5fa; }
|
||||
.dark :is(#app, [data-xftp-app]) .security-note a { color: #60a5fa; }
|
||||
@@ -4,6 +4,15 @@ packages: .
|
||||
-- packages: . ../http2
|
||||
-- packages: . ../network-transport
|
||||
|
||||
-- uncomment two sections below to run tests with coverage
|
||||
-- package *
|
||||
-- coverage: True
|
||||
-- library-coverage: True
|
||||
|
||||
-- package attoparsec
|
||||
-- coverage: False
|
||||
-- library-coverage: False
|
||||
|
||||
index-state: 2023-12-12T00:00:00Z
|
||||
|
||||
package cryptostore
|
||||
@@ -28,3 +37,17 @@ source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/sqlcipher-simple.git
|
||||
tag: a46bd361a19376c5211f1058908fc0ae6bf42446
|
||||
|
||||
-- waiting for published warp-tls-3.4.7
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/yesodweb/wai.git
|
||||
tag: ec5e017d896a78e787a5acea62b37a4e677dec2e
|
||||
subdir: warp-tls
|
||||
|
||||
-- backported fork due http-5.0
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/wai.git
|
||||
tag: 2f6e5aa5f05ba9140ac99e195ee647b4f7d926b0
|
||||
subdir: warp
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
# Coding and building
|
||||
|
||||
This file provides guidance on coding style and approaches and on building the code.
|
||||
|
||||
## Code Security
|
||||
|
||||
When designing code and planning implementations:
|
||||
- Apply adversarial thinking, and consider what may happen if one of the communicating parties is malicious.
|
||||
- Formulate an explicit threat model for each change - who can do which undesirable things and under which circumstances.
|
||||
|
||||
## Code Quality Standards
|
||||
|
||||
Haskell client and server code serves as system specification, not just implementation — we use type-driven design to reflect the business domain in types. Quality, conciseness, and clarity of Haskell code are critical.
|
||||
|
||||
## Code Style, Formatting and Approaches
|
||||
|
||||
The project uses **fourmolu** for Haskell code formatting. Configuration is in `fourmolu.yaml`.
|
||||
|
||||
**Key formatting rules:**
|
||||
- 2-space indentation
|
||||
- Trailing function arrows, commas, and import/export style
|
||||
- Record brace without space: `{field = value}`
|
||||
- Single newline between declarations
|
||||
- Never use unicode symbols
|
||||
- Inline `let` style with right-aligned `in`
|
||||
|
||||
**Format code before committing:**
|
||||
|
||||
```bash
|
||||
# Format a single file
|
||||
fourmolu -i src/Simplex/Messaging/Protocol.hs
|
||||
```
|
||||
|
||||
Some files that use CPP language extension cannot be formatted as a whole, so individual code fragments need to be formatted.
|
||||
|
||||
**Follow existing code patterns:**
|
||||
- Match the style of surrounding code
|
||||
- Use qualified imports with short aliases (e.g., `import qualified Data.ByteString.Char8 as B`)
|
||||
- Use record syntax for types with multiple fields
|
||||
- Prefer explicit pattern matching over partial functions
|
||||
|
||||
**Comments policy:**
|
||||
- Avoid redundant comments that restate what the code already says
|
||||
- Only comment on non-obvious design decisions or tricky implementation details
|
||||
- Function names and type signatures should be self-documenting
|
||||
- Do not add comments like "wire format encoding" (Encoding class is always wire format) or "check if X" when the function name already says that
|
||||
- Assume a competent Haskell reader
|
||||
|
||||
**Diff and refactoring:**
|
||||
- Avoid unnecessary changes and code movements
|
||||
- Never do refactoring unless it substantially reduces cost of solving the current problem, including the cost of refactoring
|
||||
- Aim to minimize the code changes - do what is minimally required to solve users' problems
|
||||
|
||||
**Document and code structure:**
|
||||
- **Never move existing code or sections around** - add new content at appropriate locations without reorganizing existing structure.
|
||||
- When adding new sections to documents, continue the existing numbering scheme.
|
||||
- Minimize diff size - prefer small, targeted changes over reorganization.
|
||||
|
||||
**Code analysis and review:**
|
||||
- Trace data flows end-to-end: from origin, through storage/parameters, to consumption. Flag values that are discarded and reconstructed from partial data (e.g. extracted from a URI missing original fields) — this is usually a bug.
|
||||
- Read implementations of called functions, not just signatures — if duplication involves a called function, check whether decomposing it resolves the duplication.
|
||||
- Do not save time on analysis. Read every function in the data flow even when the interface seems clear — wrong assumptions about internals are the main source of missed bugs.
|
||||
|
||||
### Haskell Extensions
|
||||
- `StrictData` enabled by default
|
||||
- Use STM for safe concurrency
|
||||
- Assume concurrency in PostgreSQL queries
|
||||
- Comprehensive warning flags with strict pattern matching
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
# Standard build
|
||||
cabal build
|
||||
|
||||
# Fast build
|
||||
cabal build --ghc-options -O0
|
||||
|
||||
# Build specific executables
|
||||
cabal build exe:smp-server exe:xftp-server exe:ntf-server exe:xftp
|
||||
|
||||
# Build with PostgreSQL server support
|
||||
cabal build -fserver_postgres
|
||||
|
||||
# Client-only library build (no server code)
|
||||
cabal build -fclient_library
|
||||
|
||||
# Find binary location
|
||||
cabal list-bin exe:smp-server
|
||||
```
|
||||
|
||||
### Cabal Flags
|
||||
|
||||
- `swift`: Enable Swift JSON format
|
||||
- `client_library`: Build without server code
|
||||
- `client_postgres`: Use PostgreSQL instead of SQLite for agent persistence
|
||||
- `server_postgres`: PostgreSQL support for server queue/notification store
|
||||
|
||||
## External Dependencies
|
||||
|
||||
Custom forks specified in `cabal.project`:
|
||||
- `aeson`, `hs-socks` (SimpleX forks)
|
||||
- `direct-sqlcipher`, `sqlcipher-simple` (encrypted SQLite)
|
||||
- `warp`, `warp-tls` (HTTP server)
|
||||
@@ -0,0 +1,105 @@
|
||||
# SimpleXMQ repository
|
||||
|
||||
This file provides guidance on the project structure to help working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
SimpleXMQ is a Haskell message broker implementing unidirectional (simplex) queues for privacy-preserving messaging.
|
||||
|
||||
Key components:
|
||||
|
||||
- **SimpleX Messaging Protocol**: SMP protocol definition and encodings ([code](../src/Simplex/Messaging/Protocol.hs), [transport code](../src/Simplex/Messaging/Transport.hs), [spec](../protocol/simplex-messaging.md)).
|
||||
- **SMP Server**: Message broker with TLS, in-memory queues, optional persistence ([main code](../src/Simplex/Messaging/Server.hs), [all code files](../src/Simplex/Messaging/Server/), [executable](../apps/smp-server/)). For proxying SMP commands the server uses [lightweight SMP client](../src/Simplex/Messaging/Client/Agent.hs).
|
||||
- **SMP Client**: Functional API with STM-based message delivery ([code](../src/Simplex/Messaging/Client.hs)).
|
||||
- **SMP Agent**: High-level duplex connections via multiple simplex queues with E2E encryption ([code](../src/Simplex/Messaging/Agent.hs)). Implements Agent-to-agent protocol ([code](../src/Simplex/Messaging/Agent/Protocol.hs), [spec](../protocol/agent-protocol.md)) via intermediary agent client ([code](../src/Simplex/Messaging/Agent/Client.hs)).
|
||||
- **XFTP**: SimpleX File Transfer Protocol, server and CLI client ([code](../src/Simplex/FileTransfer/), [spec](../protocol/xftp.md)).
|
||||
- **XRCP**: SimpleX Remote Control Protocol ([code](`../src/Simplex/RemoteControl/`), [spec](../protocol/xrcp.md)).
|
||||
- **Notifications**: Push notifications server requires PostgreSQL ([code](../src/Simplex/Messaging/Notifications), [executable](../apps/ntf-server/)). Client protocol is used for clients to communicate with the server ([code](../src/Simplex/Messaging/Notifications/Protocol.hs), [spec](../protocol/push-notifications.md)). For subscribing to SMP notifications the server uses [lightweight SMP client](../src/Simplex/Messaging/Client/Agent.hs).
|
||||
|
||||
## Architecture
|
||||
|
||||
For general overview see `../protocol/overview-tjr.md`.
|
||||
|
||||
SMP Protocol Layers:
|
||||
|
||||
```
|
||||
TLS Transport → SMP Protocol → Agent Protocol → Application protocol
|
||||
```
|
||||
|
||||
XFTP Protocol Layers:
|
||||
|
||||
```
|
||||
TLS Transport (HTTP2 encoding) → XFTP Protocol → Out-of-band file descriptions
|
||||
```
|
||||
|
||||
## Key Patterns
|
||||
|
||||
1. **Persistence**: All queue state managed via Software Transactional Memory or via PostgreSQL
|
||||
- `Simplex.Messaging.Server.MsgStore.STM` - in-memory messages
|
||||
- `Simplex.Messaging.Server.QueueStore.STM` - in-memory queue state
|
||||
- `Simplex.Messaging.Server.MsgStore.Postgres` - message storage
|
||||
- `Simplex.Messaging.Server.QueueStore.Postgres` - queue storage
|
||||
|
||||
2. **Append-Only Store Log**: Optional persistence via journal for in-memory storage
|
||||
- `Simplex.Messaging.Server.StoreLog` - queue creation log
|
||||
- Compacted on restart
|
||||
|
||||
3. **Agent Storage**:
|
||||
- SQLite (default) or PostgreSQL
|
||||
- Migrations in `src/Simplex/Messaging/Agent/Store/{SQLite,Postgres}/Migrations/`
|
||||
|
||||
4. **Protocol Versioning**: All layers support version negotiation
|
||||
- `Simplex.Messaging.Version` - version range utilities
|
||||
|
||||
5. **Double Ratchet E2E**: Per-connection encryption
|
||||
- `Simplex.Messaging.Crypto.Ratchet`
|
||||
- SNTRUP761 post-quantum KEM (`src/Simplex/Messaging/Crypto/SNTRUP761/`)
|
||||
|
||||
## Source Layout
|
||||
|
||||
```
|
||||
src/Simplex/
|
||||
├── Messaging/
|
||||
│ ├── Agent.hs # Main agent (~210KB)
|
||||
│ ├── Server.hs # SMP server (~130KB)
|
||||
│ ├── Client.hs # Client API (~65KB)
|
||||
│ ├── Protocol.hs # Protocol types (~77KB)
|
||||
│ ├── Crypto.hs # E2E encryption (~52KB)
|
||||
│ ├── Transport.hs # Transport encoding over TLS
|
||||
│ ├── Agent/Store/ # SQLite/Postgres persistence
|
||||
│ ├── Server/ # Server internals (QueueStore, MsgStore, Control)
|
||||
│ └── Notifications/ # Push notification system
|
||||
├── FileTransfer/ # XFTP implementation for file transfers
|
||||
└── RemoteControl/ # XRCP implementation for device discovery & control
|
||||
```
|
||||
|
||||
## Protocol Documentation
|
||||
|
||||
- `protocol/overview-tjr.md`: SMP protocols stack overview
|
||||
- `protocol/simplex-messaging.md`: SMP protocol spec (v19)
|
||||
- `protocol/agent-protocol.md`: Agent protocol spec (v7)
|
||||
- `protocol/xftp.md`: File transfer protocol
|
||||
- `protocol/xrcp.md`: Remote control protocol
|
||||
- `rfcs/`: Design RFCs for features
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cabal test --test-show-details=streaming
|
||||
|
||||
# Run specific test group (uses HSpec)
|
||||
cabal test --test-option=--match="/Core tests/Encryption tests/"
|
||||
|
||||
# Run single test
|
||||
cabal test --test-option=--match="/SMP client agent/functional API/"
|
||||
```
|
||||
|
||||
Tests require PostgreSQL running on `localhost:5432` when using `-fserver_postgres` or `-fclient_postgres`.
|
||||
|
||||
Test files are in `tests/` with structure:
|
||||
- `Test.hs`: Main runner
|
||||
- `AgentTests/`: Agent protocol and connection tests
|
||||
- `CoreTests/`: Crypto, encoding, storage tests
|
||||
- `ServerTests.hs`: SMP server tests
|
||||
- `XFTPServerTests.hs`: File transfer tests
|
||||
@@ -0,0 +1,23 @@
|
||||
# Contributing to SimpleX repositories
|
||||
|
||||
## Focus on user problems
|
||||
|
||||
We do not make code changes to improve code - any change must address a specific user problem or request.
|
||||
|
||||
## Discuss the plans as early as possible
|
||||
|
||||
Please discuss the problem you want to solve and your detailed implementation plan with the project team prior to contributing, to avoid wasted time and additional changes. Acceptance of your contribution depends on your willingness and ability to iterate the proposed contribution to achieve the required quality level, coding style, test coverage, and alignment with user requirements as they are understood by the project team.
|
||||
|
||||
## Follow project structure, coding style and approaches
|
||||
|
||||
./PROJECT.md has information about the structure of this `simplexmq` repository.
|
||||
|
||||
./CODE.md has details about general requirements common for `simplexmq` and `simplex-chat` repositories.
|
||||
|
||||
This files can be used with LLM prompts, e.g. if you use Claude Code you can create CLAUDE.md file in project root importing content from these files:
|
||||
|
||||
```markdown
|
||||
@README.md
|
||||
@contributing/PROJECT.md
|
||||
@contributing/CODE.md
|
||||
```
|
||||
@@ -2,10 +2,6 @@
|
||||
set -eu
|
||||
|
||||
# Links to scripts/configs
|
||||
bin="https://github.com/simplex-chat/simplexmq/releases/latest/download"
|
||||
bin_smp="$bin/smp-server-ubuntu-20_04-x86-64"
|
||||
bin_xftp="$bin/xftp-server-ubuntu-20_04-x86-64"
|
||||
|
||||
scripts="https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/scripts/main"
|
||||
scripts_systemd_smp="$scripts/smp-server.service"
|
||||
scripts_systemd_xftp="$scripts/xftp-server.service"
|
||||
@@ -26,6 +22,8 @@ path_conf_var="/var/opt"
|
||||
path_conf_smp="$path_conf_etc/simplex $path_conf_var/simplex"
|
||||
path_conf_xftp="$path_conf_etc/simplex-xftp $path_conf_var/simplex-xftp /srv/xftp"
|
||||
|
||||
path_conf_info="$path_conf_etc/simplex-info"
|
||||
|
||||
path_systemd="/etc/systemd/system"
|
||||
path_systemd_smp="$path_systemd/smp-server.service"
|
||||
path_systemd_xftp="$path_systemd/xftp-server.service"
|
||||
@@ -56,7 +54,7 @@ ${GRN}1.${NC} Install latest binaries from GitHub releases:
|
||||
${GRN}2.${NC} Create server directories:
|
||||
- smp: ${YLW}${path_conf_smp}${NC}
|
||||
- xftp: ${YLW}${path_conf_xftp}${NC}
|
||||
${GRN}3.${NC} Setup user for each server:
|
||||
${GRN}3.${NC} Setup user for server:
|
||||
- xmp: ${YLW}${user_smp}${NC}
|
||||
- xftp: ${YLW}${user_xftp}${NC}
|
||||
${GRN}4.${NC} Create systemd services:
|
||||
@@ -65,7 +63,12 @@ ${GRN}4.${NC} Create systemd services:
|
||||
${GRN}5.${NC} Install stopscript (systemd), update and uninstallation script:
|
||||
- all: ${YLW}${path_bin_update}${NC}, ${YLW}${path_bin_uninstall}${NC}, ${YLW}${path_bin_stopscript}${NC}
|
||||
|
||||
Press ${GRN}ENTER${NC} to continue or ${RED}Ctrl+C${NC} to cancel installation"
|
||||
Press:
|
||||
- ${GRN}1${NC} to install smp server
|
||||
- ${GRN}2${NC} to install xftp server
|
||||
- ${RED}Ctrl+C${NC} to cancel installation
|
||||
|
||||
Selection: "
|
||||
|
||||
end="Installtion is complete!
|
||||
|
||||
@@ -76,27 +79,79 @@ Please checkout our server guides:
|
||||
To uninstall with full clean-up, simply run: ${YLW}sudo /usr/local/bin/simplex-servers-uninstall${NC}
|
||||
"
|
||||
|
||||
set_version() {
|
||||
ver="${VER:-latest}"
|
||||
|
||||
case "$ver" in
|
||||
latest)
|
||||
bin="https://github.com/simplex-chat/simplexmq/releases/latest/download"
|
||||
remote_version="$(curl --proto '=https' --tlsv1.2 -sSf -L https://api.github.com/repos/simplex-chat/simplexmq/releases/latest | grep -i "tag_name" | awk -F \" '{print $4}')"
|
||||
;;
|
||||
*)
|
||||
bin="https://github.com/simplex-chat/simplexmq/releases/download/${ver}"
|
||||
remote_version="${ver}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
os_test() {
|
||||
. /etc/os-release
|
||||
|
||||
case "$VERSION_ID" in
|
||||
20.04|22.04) : ;;
|
||||
24.04) VERSION_ID='22.04' ;;
|
||||
*) printf "${RED}Unsupported Ubuntu version!${NC}\nPlease file Github issue with request to support Ubuntu %s: https://github.com/simplex-chat/simplexmq/issues/new\n" "$VERSION_ID" && exit 1 ;;
|
||||
esac
|
||||
|
||||
version="$(printf '%s' "$VERSION_ID" | tr '.' '_')"
|
||||
arch="$(uname -p)"
|
||||
|
||||
case "$arch" in
|
||||
x86_64) arch="$(printf '%s' "$arch" | tr '_' '-')" ;;
|
||||
*) printf "${RED}Unsupported architecture!${NC}\nPlease file Github issue with request to support %s architecture: https://github.com/simplex-chat/simplexmq/issues/new" "$arch" && exit 1 ;;
|
||||
esac
|
||||
|
||||
bin_smp="$bin/smp-server-ubuntu-${version}-${arch}"
|
||||
bin_xftp="$bin/xftp-server-ubuntu-${version}-${arch}"
|
||||
}
|
||||
|
||||
setup_bins() {
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_smp" -o "$path_bin_smp" && chmod +x "$path_bin_smp"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_xftp" -o "$path_bin_xftp" && chmod +x "$path_bin_xftp"
|
||||
eval "bin=\$bin_${1}"
|
||||
eval "path=\$path_bin_${1}"
|
||||
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$bin" -o "$path" && chmod +x "$path"
|
||||
|
||||
unset bin path
|
||||
}
|
||||
|
||||
setup_users() {
|
||||
useradd -M "$user_smp" 2> /dev/null || true
|
||||
useradd -M "$user_xftp" 2> /dev/null || true
|
||||
eval "user=\$user_${1}"
|
||||
|
||||
useradd -M "$user" 2> /dev/null || true
|
||||
|
||||
unset user
|
||||
}
|
||||
|
||||
setup_dirs() {
|
||||
# Unquoted varibles, so field splitting can occur
|
||||
mkdir -p $path_conf_smp
|
||||
chown "$user_smp":"$user_smp" $path_conf_smp
|
||||
mkdir -p $path_conf_xftp
|
||||
chown "$user_xftp":"$user_xftp" $path_conf_xftp
|
||||
eval "path_conf=\$path_conf_${1}"
|
||||
eval "user=\$user_${1}"
|
||||
|
||||
mkdir -p $path_conf
|
||||
mkdir -p $path_conf_info
|
||||
printf "local_version_%s='%s'\n" "$1" "$remote_version" >> "$path_conf_info/release"
|
||||
chown -R "$user":"$user" $path_conf
|
||||
|
||||
unset path_conf user
|
||||
}
|
||||
|
||||
setup_systemd() {
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd_smp" -o "$path_systemd_smp"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd_xftp" -o "$path_systemd_xftp"
|
||||
eval "scripts_systemd=\$scripts_systemd_${1}"
|
||||
eval "path_systemd=\$path_systemd_${1}"
|
||||
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd" -o "$path_systemd"
|
||||
|
||||
unset scripts_systemd path_systemd
|
||||
}
|
||||
|
||||
setup_scripts() {
|
||||
@@ -110,32 +165,61 @@ checks() {
|
||||
printf "This script is intended to be run with root privileges. Please re-run script using sudo."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set_version
|
||||
os_test
|
||||
|
||||
mkdir -p $path_conf_info
|
||||
}
|
||||
|
||||
main() {
|
||||
checks
|
||||
|
||||
printf "%b\n%b\n" "${BLU}$logo${NC}" "$welcome"
|
||||
printf "%b\n%b" "${BLU}$logo${NC}" "$welcome"
|
||||
read ans
|
||||
|
||||
case "$ans" in
|
||||
1) setup='smp' ;;
|
||||
2) setup='xftp' ;;
|
||||
*) printf 'Installation aborted.\n' && exit 0 ;;
|
||||
esac
|
||||
|
||||
printf "Installing binaries..."
|
||||
setup_bins
|
||||
|
||||
for i in $setup; do
|
||||
setup_bins "$i"
|
||||
done
|
||||
|
||||
printf "${GRN} Done!${NC}\n"
|
||||
|
||||
printf "Creating users..."
|
||||
setup_users
|
||||
|
||||
for i in $setup; do
|
||||
setup_users "$i"
|
||||
done
|
||||
|
||||
printf "${GRN} Done!${NC}\n"
|
||||
|
||||
printf "Creating directories..."
|
||||
setup_dirs
|
||||
|
||||
for i in $setup; do
|
||||
setup_dirs "$i"
|
||||
done
|
||||
|
||||
printf "${GRN} Done!${NC}\n"
|
||||
|
||||
printf "Creating systemd services..."
|
||||
setup_systemd
|
||||
|
||||
for i in $setup; do
|
||||
setup_systemd "$i"
|
||||
done
|
||||
|
||||
printf "${GRN} Done!${NC}\n"
|
||||
|
||||
printf "Installing stopscript, update and uninstallation script..."
|
||||
|
||||
setup_scripts
|
||||
|
||||
printf "${GRN} Done!${NC}\n"
|
||||
|
||||
printf "%b" "$end"
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
common:
|
||||
corrId - random BS, used as CbNonce
|
||||
entityId - p2r tlsUniq
|
||||
|
||||
# setup
|
||||
s->p: "proxy", uri, auth?
|
||||
# unless connected
|
||||
p->r: "p_handshake"
|
||||
p<-r: "r_key", tls-signed dh pub
|
||||
s<-r: "r_key", tls-signed dh pub # reply entityId contains tlsUniq
|
||||
|
||||
# working
|
||||
s ; generate random dh priv, make shared secret
|
||||
s->p: s2r("forward", random dh pub, SEND command blob)
|
||||
p->r: p2r("forward", random dh pub, s2r("forward", ...)))
|
||||
r->c@ "msg", ...
|
||||
p<-r: p2r("r_res", s2r("ok" / "error", error))
|
||||
s<-p@ s2r("ok" / "error", error)
|
||||
|
||||
# expired
|
||||
p<-r@ p2r("error", "key expired")
|
||||
s<-p@ "error", "key expired"
|
||||
s ; reconnect
|
||||
@@ -1,211 +0,0 @@
|
||||
name: simplexmq
|
||||
version: 5.8.0.10
|
||||
synopsis: SimpleXMQ message broker
|
||||
description: |
|
||||
This package includes <./docs/Simplex-Messaging-Server.html server>,
|
||||
<./docs/Simplex-Messaging-Client.html client> and
|
||||
<./docs/Simplex-Messaging-Agent.html agent> for SMP protocols:
|
||||
.
|
||||
* <https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md SMP protocol>
|
||||
* <https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md SMP agent protocol>
|
||||
.
|
||||
See <https://github.com/simplex-chat/simplex-chat terminal chat prototype> built with SimpleXMQ broker.
|
||||
|
||||
homepage: https://github.com/simplex-chat/simplexmq#readme
|
||||
license: AGPL-3
|
||||
author: simplex.chat
|
||||
maintainer: chat@simplex.chat
|
||||
copyright: 2020-2022 simplex.chat
|
||||
category: Chat, Network, Web, System, Cryptography
|
||||
extra-source-files:
|
||||
- README.md
|
||||
- CHANGELOG.md
|
||||
- cbits/sha512.h
|
||||
- cbits/sntrup761.h
|
||||
- apps/smp-server/static/*.html
|
||||
- apps/smp-server/static/media/*
|
||||
|
||||
dependencies:
|
||||
- aeson == 2.2.*
|
||||
- ansi-terminal >= 0.10 && < 0.12
|
||||
- asn1-encoding == 0.9.*
|
||||
- asn1-types == 0.3.*
|
||||
- async == 2.2.*
|
||||
- attoparsec == 0.14.*
|
||||
- base >= 4.14 && < 5
|
||||
- base64-bytestring >= 1.0 && < 1.3
|
||||
- case-insensitive == 1.2.*
|
||||
- composition == 1.0.*
|
||||
- constraints >= 0.12 && < 0.14
|
||||
- containers == 0.6.*
|
||||
- crypton == 0.34.*
|
||||
- crypton-x509 == 1.7.*
|
||||
- crypton-x509-store == 1.6.*
|
||||
- crypton-x509-validation == 1.6.*
|
||||
- cryptostore == 0.3.*
|
||||
- data-default == 0.7.*
|
||||
- direct-sqlcipher == 2.3.*
|
||||
- directory == 1.3.*
|
||||
- filepath == 1.4.*
|
||||
- hourglass == 0.2.*
|
||||
- http-types == 0.12.*
|
||||
- http2 >= 4.2.2 && < 4.3
|
||||
- ini == 0.4.1
|
||||
- iproute == 1.7.*
|
||||
- iso8601-time == 0.1.*
|
||||
- memory == 0.18.*
|
||||
- mtl >= 2.3.1 && < 3.0
|
||||
- network >= 3.1.2.7 && < 3.2
|
||||
- network-info >= 0.2 && < 0.3
|
||||
- network-transport == 0.5.6
|
||||
- network-udp >= 0.0 && < 0.1
|
||||
- optparse-applicative >= 0.15 && < 0.17
|
||||
- process == 1.6.*
|
||||
- random >= 1.1 && < 1.3
|
||||
- simple-logger == 0.1.*
|
||||
- socks == 0.6.*
|
||||
- sqlcipher-simple == 0.4.*
|
||||
- stm == 2.5.*
|
||||
- temporary == 1.3.*
|
||||
- time == 1.12.*
|
||||
- time-manager == 0.0.*
|
||||
- tls >= 1.7.0 && < 1.8
|
||||
- transformers == 0.6.*
|
||||
- unliftio == 0.2.*
|
||||
- unliftio-core == 0.2.*
|
||||
- websockets == 0.12.*
|
||||
- yaml == 0.11.*
|
||||
- zstd == 0.1.3.*
|
||||
|
||||
flags:
|
||||
swift:
|
||||
description: Enable swift JSON format
|
||||
manual: True
|
||||
default: False
|
||||
use_crypton:
|
||||
description: Use crypton etc. in cryptostore
|
||||
manual: True
|
||||
default: True
|
||||
|
||||
# cpp-options:
|
||||
# - -Dslow_servers
|
||||
|
||||
when:
|
||||
- condition: flag(swift)
|
||||
cpp-options:
|
||||
- -DswiftJSON
|
||||
- condition: impl(ghc >= 9.6.2)
|
||||
dependencies:
|
||||
- bytestring == 0.11.*
|
||||
- template-haskell == 2.20.*
|
||||
- text >= 2.0.1 && < 2.2
|
||||
- condition: impl(ghc < 9.6.2)
|
||||
dependencies:
|
||||
- bytestring == 0.10.*
|
||||
- template-haskell == 2.16.*
|
||||
- text >= 1.2.3.0 && < 1.3
|
||||
|
||||
library:
|
||||
source-dirs: src
|
||||
c-sources:
|
||||
- cbits/sha512.c
|
||||
- cbits/sntrup761.c
|
||||
include-dirs: cbits
|
||||
extra-libraries: crypto
|
||||
|
||||
executables:
|
||||
smp-server:
|
||||
source-dirs:
|
||||
- apps/smp-server
|
||||
- apps/smp-server/web
|
||||
main: Main.hs
|
||||
dependencies:
|
||||
- file-embed
|
||||
- simplexmq
|
||||
- wai-app-static
|
||||
- warp
|
||||
- warp-tls
|
||||
ghc-options:
|
||||
- -threaded
|
||||
- -rtsopts
|
||||
|
||||
ntf-server:
|
||||
source-dirs: apps/ntf-server
|
||||
main: Main.hs
|
||||
dependencies:
|
||||
- simplexmq
|
||||
ghc-options:
|
||||
- -threaded
|
||||
- -rtsopts
|
||||
|
||||
xftp-server:
|
||||
source-dirs: apps/xftp-server
|
||||
main: Main.hs
|
||||
dependencies:
|
||||
- simplexmq
|
||||
ghc-options:
|
||||
- -threaded
|
||||
- -rtsopts
|
||||
|
||||
xftp:
|
||||
source-dirs: apps/xftp
|
||||
main: Main.hs
|
||||
dependencies:
|
||||
- simplexmq
|
||||
ghc-options:
|
||||
- -threaded
|
||||
- -rtsopts
|
||||
|
||||
tests:
|
||||
simplexmq-test:
|
||||
source-dirs: tests
|
||||
main: Test.hs
|
||||
dependencies:
|
||||
- simplexmq
|
||||
- deepseq == 1.4.*
|
||||
- generic-random == 1.5.*
|
||||
- hspec == 2.11.*
|
||||
- hspec-core == 2.11.*
|
||||
- HUnit == 1.6.*
|
||||
- QuickCheck == 2.14.*
|
||||
- silently == 1.2.*
|
||||
- main-tester == 0.2.*
|
||||
- timeit == 2.0.*
|
||||
ghc-options:
|
||||
- -threaded
|
||||
- -rtsopts
|
||||
- -with-rtsopts=-A64M
|
||||
- -with-rtsopts=-N1
|
||||
|
||||
ghc-options:
|
||||
# - -haddock
|
||||
- -Weverything
|
||||
- -Wno-missing-exported-signatures
|
||||
- -Wno-missing-import-lists
|
||||
- -Wno-missed-specialisations
|
||||
- -Wno-all-missed-specialisations
|
||||
- -Wno-unsafe
|
||||
- -Wno-safe
|
||||
- -Wno-missing-local-signatures
|
||||
- -Wno-missing-kind-signatures
|
||||
- -Wno-missing-deriving-strategies
|
||||
- -Wno-monomorphism-restriction
|
||||
- -Wno-prepositive-qualified-module
|
||||
- -Wno-unused-packages
|
||||
- -Wno-implicit-prelude
|
||||
- -Wno-missing-safe-haskell-mode
|
||||
- -Wno-missing-export-lists
|
||||
- -Wno-partial-fields
|
||||
- -Wcompat
|
||||
- -Werror=incomplete-record-updates
|
||||
- -Werror=incomplete-patterns
|
||||
- -Werror=incomplete-uni-patterns
|
||||
- -Werror=missing-methods
|
||||
- -Werror=tabs
|
||||
- -Wredundant-constraints
|
||||
- -Wincomplete-record-updates
|
||||
- -Wunused-type-patterns
|
||||
- -O2
|
||||
|
||||
default-extensions:
|
||||
- StrictData
|
||||
@@ -0,0 +1,472 @@
|
||||
# XFTP Server PostgreSQL Backend
|
||||
|
||||
## Overview
|
||||
|
||||
Add PostgreSQL backend support to xftp-server, following the SMP server pattern. Supports bidirectional migration between STM (in-memory with StoreLog) and PostgreSQL backends.
|
||||
|
||||
## Goals
|
||||
|
||||
- PostgreSQL-backed file metadata storage as an alternative to STM + StoreLog
|
||||
- Polymorphic server code via `FileStoreClass` typeclass with IO-based methods (following `QueueStoreClass` pattern)
|
||||
- Bidirectional migration: StoreLog <-> PostgreSQL via CLI commands
|
||||
- Shared `server_postgres` cabal flag (same flag enables both SMP and XFTP Postgres support)
|
||||
- INI-based backend selection at runtime
|
||||
|
||||
## Architecture
|
||||
|
||||
### FileStoreClass Typeclass
|
||||
|
||||
IO-based typeclass following the `QueueStoreClass` pattern — each method is a self-contained IO action, with the implementation responsible for its own atomicity (STM backend wraps in `atomically`, Postgres backend uses database transactions):
|
||||
|
||||
```haskell
|
||||
class FileStoreClass s where
|
||||
type FileStoreConfig s
|
||||
|
||||
-- Lifecycle
|
||||
newFileStore :: FileStoreConfig s -> IO s
|
||||
closeFileStore :: s -> IO ()
|
||||
|
||||
-- File operations
|
||||
addFile :: s -> SenderId -> FileInfo -> RoundedFileTime -> ServerEntityStatus -> IO (Either XFTPErrorType ())
|
||||
setFilePath :: s -> SenderId -> FilePath -> IO (Either XFTPErrorType ())
|
||||
addRecipient :: s -> SenderId -> FileRecipient -> IO (Either XFTPErrorType ())
|
||||
getFile :: s -> SFileParty p -> XFTPFileId -> IO (Either XFTPErrorType (FileRec, C.APublicAuthKey))
|
||||
deleteFile :: s -> SenderId -> IO (Either XFTPErrorType ())
|
||||
blockFile :: s -> SenderId -> BlockingInfo -> Bool -> IO (Either XFTPErrorType ())
|
||||
deleteRecipient :: s -> RecipientId -> FileRec -> IO ()
|
||||
ackFile :: s -> RecipientId -> IO (Either XFTPErrorType ())
|
||||
|
||||
-- Expiration (with LIMIT for Postgres; called in a loop until empty)
|
||||
expiredFiles :: s -> Int64 -> Int -> IO [(SenderId, Maybe FilePath, Word32)]
|
||||
|
||||
-- Storage and stats (for init-time computation)
|
||||
getUsedStorage :: s -> IO Int64
|
||||
getFileCount :: s -> IO Int
|
||||
```
|
||||
|
||||
- STM backend: each method wraps its STM transaction in `atomically` internally.
|
||||
- Postgres backend: each method runs its query via `withDB` / database connection internally.
|
||||
|
||||
No polymorphic monad or `runStore` dispatcher needed — unlike `MsgStoreClass`, XFTP file operations are individually atomic and don't require grouping multiple operations into backend-dependent transactions.
|
||||
|
||||
### PostgresFileStore Data Type
|
||||
|
||||
```haskell
|
||||
data PostgresFileStore = PostgresFileStore
|
||||
{ dbStore :: DBStore,
|
||||
dbStoreLog :: Maybe (StoreLog 'WriteMode)
|
||||
}
|
||||
```
|
||||
|
||||
- `dbStore` — connection pool created via `createDBStore`, runs schema migrations on init.
|
||||
- `dbStoreLog` — optional parallel log file (enabled by `db_store_log` INI setting). When present, every mutation (`addFile`, `setFilePath`, `deleteFile`, `blockFile`, `addRecipient`, `ackFile`) also writes to this log via a `withLog` wrapper. `withLog` is called AFTER the DB operation succeeds (so the log reflects committed state only). Log write failures are non-fatal (logged as warnings, do not fail the DB operation). This provides an audit trail and enables recovery via export.
|
||||
|
||||
`closeFileStore` for Postgres calls `closeDBStore` (closes connection pool) then `mapM_ closeStoreLog dbStoreLog` (flushes and closes the parallel log). For STM, it closes the storeLog. Called from a `finally` block during server shutdown, matching SMP's `stopServer` → `closeMsgStore` → `closeQueueStore` pattern.
|
||||
|
||||
### STMFileStore Type
|
||||
|
||||
After extracting from current `Store.hs`, `STMFileStore` retains the file and recipient maps but no longer owns `usedStorage` (moved to `XFTPEnv`):
|
||||
|
||||
```haskell
|
||||
data STMFileStore = STMFileStore
|
||||
{ files :: TMap SenderId FileRec,
|
||||
recipients :: TMap RecipientId (SenderId, RcvPublicAuthKey)
|
||||
}
|
||||
```
|
||||
|
||||
`closeFileStore` for STM is a no-op (TMaps are garbage-collected; the env-level `storeLog` is closed separately by the server).
|
||||
|
||||
### Error Handling
|
||||
|
||||
Postgres operations follow SMP's `withDB` / `handleDuplicate` pattern:
|
||||
|
||||
```haskell
|
||||
withDB :: Text -> PostgresFileStore -> (DB.Connection -> IO (Either XFTPErrorType a)) -> ExceptT XFTPErrorType IO a
|
||||
withDB op st action =
|
||||
ExceptT $ E.try (withTransaction (dbStore st) action) >>= either logErr pure
|
||||
where
|
||||
logErr :: E.SomeException -> IO (Either XFTPErrorType a)
|
||||
logErr e = logError ("STORE: " <> err) $> Left INTERNAL
|
||||
where
|
||||
err = op <> ", withDB, " <> tshow e
|
||||
|
||||
handleDuplicate :: SqlError -> IO (Either XFTPErrorType a)
|
||||
handleDuplicate e = case constraintViolation e of
|
||||
Just (UniqueViolation _) -> pure $ Left DUPLICATE_
|
||||
_ -> E.throwIO e
|
||||
```
|
||||
|
||||
- All DB operations wrapped in `withDB` — catches exceptions, logs, returns `INTERNAL`.
|
||||
- Unique constraint violations caught by `handleDuplicate` and mapped to `DUPLICATE_`.
|
||||
- UPDATE operations verified with `assertUpdated` — returns `AUTH` if 0 rows affected (matching SMP pattern, prevents silent failures when WHERE clause doesn't match).
|
||||
- Critical sections (DB write + TVar update) wrapped in `uninterruptibleMask_` to prevent async exceptions from leaving inconsistent state between DB and TVars.
|
||||
|
||||
### FileRec and TVar Fields
|
||||
|
||||
`FileRec` retains its `TVar` fields (matching SMP's `PostgresQueue` pattern):
|
||||
|
||||
```haskell
|
||||
data FileRec = FileRec
|
||||
{ senderId :: SenderId,
|
||||
fileInfo :: FileInfo,
|
||||
filePath :: TVar (Maybe FilePath),
|
||||
recipientIds :: TVar (Set RecipientId),
|
||||
createdAt :: RoundedFileTime,
|
||||
fileStatus :: TVar ServerEntityStatus
|
||||
}
|
||||
```
|
||||
|
||||
- **STM backend**: TVars are the source of truth, as currently.
|
||||
- **Postgres backend**: `getFile` reads from DB and creates a `FileRec` with fresh TVars populated from the DB row (matching SMP's `mkQ` pattern — `newTVarIO` per load). Mutation methods (`setFilePath`, `blockFile`, etc.) update both the DB (persistence) and the TVars (in-session consistency). The `recipientIds` TVar is initialized to `S.empty` — no subquery needed because no server code reads `recipientIds` directly; all recipient operations go through the typeclass methods (`addRecipient`, `deleteRecipient`, `ackFile`), which query the `recipients` table for Postgres.
|
||||
|
||||
### usedStorage Ownership
|
||||
|
||||
`usedStorage :: TVar Int64` moves from the store to `XFTPEnv`. The store typeclass does **not** manage `usedStorage` — it only provides `getUsedStorage` for init-time computation.
|
||||
|
||||
- **STM init**: StoreLog replay calls `setFilePath` (which only sets the filePath TVar — the STM `setFilePath` implementation is changed to **not** update `usedStorage`). Similarly, STM `deleteFile` (Store.hs line 117) and `blockFile` (line 125) are changed to **not** update `usedStorage` — the server handles all `usedStorage` adjustments externally. After replay, `getUsedStorage` computes the sum over all file sizes (matching current `countUsedStorage` behavior).
|
||||
- **Postgres init**: `getUsedStorage` executes `SELECT COALESCE(SUM(file_size), 0) FROM files`.
|
||||
- **Runtime**: Server manages `usedStorage` TVar directly for reserve/commit/rollback during uploads, and adjusts after `deleteFile`/`blockFile` calls.
|
||||
|
||||
**Note on `getUsedStorage` semantics**: The current STM `countUsedStorage` sums all file sizes unconditionally (including files without `filePath` set, i.e., created but not yet uploaded). The Postgres `getUsedStorage` matches this: `SELECT SUM(file_size) FROM files` (no `WHERE file_path IS NOT NULL`). In practice, orphaned files (created but never uploaded) are rare and short-lived (expired within 48h), so the difference is negligible. A future improvement could filter by `file_path IS NOT NULL` in both backends to reflect actual disk usage more accurately.
|
||||
|
||||
### Server.hs Refactoring
|
||||
|
||||
`Server.hs` becomes polymorphic over `FileStoreClass s`. Since all typeclass methods are IO, call sites replace `atomically` with direct IO calls to the store.
|
||||
|
||||
**Call sites requiring changes** (exhaustive list):
|
||||
|
||||
1. **`receiveServerFile`** (line 563): `atomically $ writeTVar filePath (Just fPath)` → `setFilePath store senderId fPath`. The `reserve` logic (line 551-555) stays as direct TVar manipulation on `usedStorage` from `XFTPEnv`.
|
||||
|
||||
2. **`verifyXFTPTransmission`** (line 453): `atomically $ verify =<< getFile st party fId` — the `getFile` call and subsequent `readTVar fileStatus` are in a single `atomically` block. Refactored to: `getFile st party fId` (IO), then `readTVarIO (fileStatus fr)` from the returned `FileRec` (safe for both backends — STM TVar is the source of truth, Postgres TVar is a fresh snapshot from DB).
|
||||
|
||||
3. **`retryAdd`** (line 516): Signature `XFTPFileId -> STM (Either XFTPErrorType a)` → `XFTPFileId -> IO (Either XFTPErrorType a)`. The `atomically` call (line 520) replaced with `liftIO`.
|
||||
|
||||
4. **`deleteOrBlockServerFile_`** (line 620): Parameter `FileStore -> STM (Either XFTPErrorType ())` → `FileStoreClass s => s -> IO (Either XFTPErrorType ())`. The `atomically` call (line 626) removed — the store method is already IO. After the store action, server adjusts `usedStorage` TVar in `XFTPEnv` based on `fileInfo.size`.
|
||||
|
||||
5. **`ackFileReception`** (line 605): `atomically $ deleteRecipient st rId fr` → `deleteRecipient st rId fr`.
|
||||
|
||||
6. **Control port `CPDelete`/`CPBlock`** (lines 371, 377): `atomically $ getFile fs SFRecipient fileId` → `getFile fs SFRecipient fileId`.
|
||||
|
||||
7. **`expireServerFiles`** (line 636): Replace per-file `expiredFilePath` iteration with batched `expiredFiles st old batchSize`, which returns `[(SenderId, Maybe FilePath, Word32)]` — the `Word32` file size is needed so the server can adjust the `usedStorage` TVar after each deletion. Called in a loop until the returned list is empty. The `itemDelay` between files applies to the deletion loop over each batch, not the query itself. STM backend ignores the batch size limit (returns all expired files from TMap scan); Postgres uses `LIMIT`.
|
||||
|
||||
8. **`restoreServerStats`** (line 694): `FileStore {files, usedStorage} <- asks store` accesses store fields directly. Refactored to: `usedStorage` from `XFTPEnv` via `asks usedStorage`, file count via `getFileCount store`. STM: `M.size <$> readTVarIO files`. Postgres: `SELECT COUNT(*) FROM files`.
|
||||
|
||||
### Store Config Selection
|
||||
|
||||
GADT in `Env.hs`:
|
||||
|
||||
```haskell
|
||||
data XFTPStoreConfig s where
|
||||
XSCMemory :: Maybe FilePath -> XFTPStoreConfig STMFileStore
|
||||
#if defined(dbServerPostgres)
|
||||
XSCDatabase :: PostgresFileStoreCfg -> XFTPStoreConfig PostgresFileStore
|
||||
#endif
|
||||
```
|
||||
|
||||
`XFTPEnv` becomes polymorphic:
|
||||
|
||||
```haskell
|
||||
data XFTPEnv s = XFTPEnv
|
||||
{ config :: XFTPServerConfig,
|
||||
store :: s,
|
||||
usedStorage :: TVar Int64,
|
||||
storeLog :: Maybe (StoreLog 'WriteMode),
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
The `M` monad (`ReaderT (XFTPEnv s) IO`) and all functions in `Server.hs` gain `FileStoreClass s =>` constraints.
|
||||
|
||||
**StoreLog lifecycle per backend:**
|
||||
|
||||
- **STM mode**: `storeLog = Just sl` (current behavior — append-only log for persistence and recovery).
|
||||
- **Postgres mode**: `storeLog = Nothing` (main storeLog disabled — Postgres is the source of truth). The optional parallel `dbStoreLog` inside `PostgresFileStore` provides audit/recovery if enabled via `db_store_log` INI setting.
|
||||
|
||||
The existing `withFileLog` pattern in Server.hs continues to work unchanged — it maps over `Maybe (StoreLog 'WriteMode)`, which is `Nothing` in Postgres mode so the calls become no-ops.
|
||||
|
||||
### Main.hs Store Type Dispatch
|
||||
|
||||
The `Start` CLI command gains a `--confirm-migrations` flag (default `MCConsole` — manual prompt, matching SMP's `StartOptions`). For automated deployments, `--confirm-migrations up` auto-applies forward migrations. The import command uses `MCYesUp` (always auto-apply).
|
||||
|
||||
Following SMP's existential dispatch pattern (`AStoreType` + `run`), `Main.hs` selects the store type from INI config and dispatches to the polymorphic server:
|
||||
|
||||
```haskell
|
||||
runServer ini = do
|
||||
let storeType = fromRight "memory" $ lookupValue "STORE_LOG" "store_files" ini
|
||||
case storeType of
|
||||
"memory" -> run $ XSCMemory (enableStoreLog $> storeLogFilePath)
|
||||
"database" ->
|
||||
#if defined(dbServerPostgres)
|
||||
run $ XSCDatabase PostgresFileStoreCfg {..}
|
||||
#else
|
||||
exitError "server not compiled with Postgres support"
|
||||
#endif
|
||||
_ -> exitError $ "Invalid store_files value: " <> storeType
|
||||
where
|
||||
run :: FileStoreClass s => XFTPStoreConfig s -> IO ()
|
||||
run storeCfg = do
|
||||
env <- newXFTPServerEnv storeCfg config
|
||||
runReaderT (xftpServer config) env
|
||||
```
|
||||
|
||||
**`newXFTPServerEnv` refactored signature:**
|
||||
|
||||
```haskell
|
||||
newXFTPServerEnv :: FileStoreClass s => XFTPStoreConfig s -> XFTPServerConfig -> IO (XFTPEnv s)
|
||||
newXFTPServerEnv storeCfg config = do
|
||||
(store, storeLog) <- case storeCfg of
|
||||
XSCMemory storeLogPath -> do
|
||||
st <- newFileStore ()
|
||||
sl <- mapM (`readWriteFileStore` st) storeLogPath
|
||||
pure (st, sl)
|
||||
XSCDatabase dbCfg -> do
|
||||
st <- newFileStore dbCfg
|
||||
pure (st, Nothing) -- main storeLog disabled for Postgres
|
||||
usedStorage <- newTVarIO =<< getUsedStorage store
|
||||
...
|
||||
pure XFTPEnv {config, store, usedStorage, storeLog, ...}
|
||||
```
|
||||
|
||||
### Startup Config Validation
|
||||
|
||||
Following SMP's `checkMsgStoreMode` pattern, `Main.hs` validates config before starting:
|
||||
|
||||
- **`store_files=database` + StoreLog file exists** (without `db_store_log=on`): Error — "StoreLog file present but store_files is `database`. Use `xftp-server database import` to migrate, or set `db_store_log: on`."
|
||||
- **`store_files=database` + schema doesn't exist**: Error — "Create schema in PostgreSQL or use `xftp-server database import`."
|
||||
- **`store_files=memory` + Postgres schema exists**: Warning — "Postgres schema exists but store_files is `memory`. Data in Postgres will not be used."
|
||||
- **Binary compiled without `server_postgres` + `store_files=database`**: Error — "Server not compiled with Postgres support."
|
||||
|
||||
## Module Structure
|
||||
|
||||
```
|
||||
src/Simplex/FileTransfer/Server/
|
||||
Store.hs -- FileStoreClass typeclass + shared types (FileRec, FileRecipient, etc.)
|
||||
Store/
|
||||
STM.hs -- STMFileStore (extracted from current Store.hs)
|
||||
Postgres.hs -- PostgresFileStore [CPP-guarded]
|
||||
Postgres/
|
||||
Migrations.hs -- Schema migrations [CPP-guarded]
|
||||
Config.hs -- PostgresFileStoreCfg [CPP-guarded]
|
||||
StoreLog.hs -- Unchanged (interchange format for both backends + migration)
|
||||
Env.hs -- XFTPStoreConfig GADT, polymorphic XFTPEnv
|
||||
Main.hs -- Store selection, migration CLI commands
|
||||
Server.hs -- Polymorphic over FileStoreClass
|
||||
```
|
||||
|
||||
## PostgreSQL Schema
|
||||
|
||||
Initial migration (`20260325_initial`):
|
||||
|
||||
```sql
|
||||
CREATE TABLE files (
|
||||
sender_id BYTEA NOT NULL PRIMARY KEY,
|
||||
file_size INT4 NOT NULL,
|
||||
file_digest BYTEA NOT NULL,
|
||||
sender_key BYTEA NOT NULL,
|
||||
file_path TEXT,
|
||||
created_at INT8 NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active'
|
||||
);
|
||||
|
||||
CREATE TABLE recipients (
|
||||
recipient_id BYTEA NOT NULL PRIMARY KEY,
|
||||
sender_id BYTEA NOT NULL REFERENCES files ON DELETE CASCADE,
|
||||
recipient_key BYTEA NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_recipients_sender_id ON recipients (sender_id);
|
||||
CREATE INDEX idx_files_created_at ON files (created_at);
|
||||
```
|
||||
|
||||
- `file_size` is `INT4` matching `Word32` in `FileInfo.size`
|
||||
- `sender_key` and `recipient_key` stored as `BYTEA` using binary encoding via `C.encodePubKey` / `C.decodePubKey` (matching SMP's `ToField`/`FromField` instances for `APublicAuthKey` — includes algorithm type tag in the binary format)
|
||||
- `file_path` nullable (set after upload completes via `setFilePath`)
|
||||
- `ON DELETE CASCADE` for recipients when file is hard-deleted
|
||||
- `created_at` stores rounded epoch seconds (1-hour precision, `RoundedFileTime`)
|
||||
- `status` as TEXT via `StrEncoding` (`ServerEntityStatus`: `EntityActive`, `EntityBlocked info`, `EntityOff`)
|
||||
- Hard deletes (no `deleted_at` column)
|
||||
- No PL/pgSQL functions needed; `setFilePath` uses `WHERE file_path IS NULL` to prevent duplicate uploads (the `UPDATE` itself acquires a row-level lock)
|
||||
- `used_storage` computed on startup: `SELECT COALESCE(SUM(file_size), 0) FROM files` (matches STM `countUsedStorage` — all files, see usedStorage Ownership section)
|
||||
|
||||
### Migrations Module
|
||||
|
||||
Following SMP's `QueueStore/Postgres/Migrations.hs` pattern:
|
||||
|
||||
```haskell
|
||||
module Simplex.FileTransfer.Server.Store.Postgres.Migrations
|
||||
( xftpServerMigrations,
|
||||
)
|
||||
where
|
||||
|
||||
import Data.List (sortOn)
|
||||
import Data.Text (Text)
|
||||
import Simplex.Messaging.Agent.Store.Shared
|
||||
import Text.RawString.QQ (r)
|
||||
|
||||
xftpSchemaMigrations :: [(String, Text, Maybe Text)]
|
||||
xftpSchemaMigrations =
|
||||
[ ("20260325_initial", m20260325_initial, Nothing)
|
||||
]
|
||||
|
||||
xftpServerMigrations :: [Migration]
|
||||
xftpServerMigrations = sortOn name $ map migration xftpSchemaMigrations
|
||||
where
|
||||
migration (name, up, down) = Migration {name, up, down = down}
|
||||
|
||||
m20260325_initial :: Text
|
||||
m20260325_initial =
|
||||
[r|
|
||||
CREATE TABLE files (
|
||||
sender_id BYTEA NOT NULL PRIMARY KEY,
|
||||
...
|
||||
);
|
||||
|]
|
||||
```
|
||||
|
||||
The `Migration` type (from `Simplex.Messaging.Agent.Store.Shared`) has fields `{name :: String, up :: Text, down :: Maybe Text}`. Initial migration has `Nothing` for `down`. Future migrations should include `Just down_migration` for rollback support. Called via `createDBStore dbOpts xftpServerMigrations (MigrationConfig confirmMigrations Nothing)`.
|
||||
|
||||
### Postgres Operations
|
||||
|
||||
Key query patterns:
|
||||
|
||||
- **`addFile`**: `INSERT INTO files (...) VALUES (...)`, return `DUPLICATE_` on unique violation.
|
||||
- **`setFilePath`**: `UPDATE files SET file_path = ? WHERE sender_id = ? AND file_path IS NULL`, verified with `assertUpdated` (returns `AUTH` if 0 rows affected — file not found or already uploaded). The `WHERE file_path IS NULL` prevents duplicate uploads; the `UPDATE` acquires a row lock implicitly. Only persists the path; `usedStorage` managed by server.
|
||||
- **`addRecipient`**: `INSERT INTO recipients (...)`, plus check for duplicates. No need for `recipientIds` TVar update — Postgres derives it from the table.
|
||||
- **`getFile`** (sender): `SELECT ... FROM files WHERE sender_id = ?`, returns auth key from `sender_key` column.
|
||||
- **`getFile`** (recipient): `SELECT f.*, r.recipient_key FROM recipients r JOIN files f ON ... WHERE r.recipient_id = ?`.
|
||||
- **`deleteFile`**: `DELETE FROM files WHERE sender_id = ?` (recipients cascade).
|
||||
- **`blockFile`**: `UPDATE files SET status = ? WHERE sender_id = ?`. When `deleted = True`, the server adjusts `usedStorage` externally (matching current STM behavior where `blockFile` only updates status and storage, not `filePath`).
|
||||
- **`expiredFiles`**: `SELECT sender_id, file_path, file_size FROM files WHERE created_at + ? < ? LIMIT ?` — batched query replaces per-file iteration, includes `file_size` for `usedStorage` adjustment. Called in a loop until no rows returned.
|
||||
|
||||
## INI Configuration
|
||||
|
||||
New keys in `[STORE_LOG]` section:
|
||||
|
||||
```ini
|
||||
[STORE_LOG]
|
||||
enable: on
|
||||
store_files: memory # memory | database
|
||||
db_connection: postgresql://xftp@/xftp_server_store
|
||||
db_schema: xftp_server
|
||||
db_pool_size: 10
|
||||
db_store_log: off
|
||||
expire_files_hours: 48
|
||||
```
|
||||
|
||||
`store_files` selects the backend (`store_files` rather than `store_queues` because XFTP stores files, not queues):
|
||||
- `memory` -> `XSCMemory` (current behavior)
|
||||
- `database` -> `XSCDatabase` (requires `server_postgres` build flag)
|
||||
|
||||
### INI Template Generation (`xftp-server init`)
|
||||
|
||||
The `iniFileContent` function in `Main.hs` must be updated to generate the new keys in the `[STORE_LOG]` section. Following SMP's `iniDbOpts` pattern with `optDisabled'` (prefixes `"# "` when value equals default), Postgres keys are generated commented out by default:
|
||||
|
||||
```ini
|
||||
[STORE_LOG]
|
||||
enable: on
|
||||
|
||||
# File storage mode: `memory` or `database` (PostgreSQL).
|
||||
store_files: memory
|
||||
|
||||
# Database connection settings for PostgreSQL database (`store_files: database`).
|
||||
# db_connection: postgresql://xftp@/xftp_server_store
|
||||
# db_schema: xftp_server
|
||||
# db_pool_size: 10
|
||||
|
||||
# Write database changes to store log file
|
||||
# db_store_log: off
|
||||
|
||||
expire_files_hours: 48
|
||||
```
|
||||
|
||||
Reuses `iniDBOptions` from `Simplex.Messaging.Server.CLI` for runtime parsing (falls back to defaults when keys are commented out or missing). `enableDbStoreLog'` pattern (`settingIsOn "STORE_LOG" "db_store_log"`) controls `dbStoreLogPath`.
|
||||
|
||||
### PostgresFileStoreCfg
|
||||
|
||||
```haskell
|
||||
data PostgresFileStoreCfg = PostgresFileStoreCfg
|
||||
{ dbOpts :: DBOpts,
|
||||
dbStoreLogPath :: Maybe FilePath,
|
||||
confirmMigrations :: MigrationConfirmation
|
||||
}
|
||||
```
|
||||
|
||||
No `deletedTTL` (hard deletes).
|
||||
|
||||
### Default DB Options
|
||||
|
||||
```haskell
|
||||
defaultXFTPDBOpts :: DBOpts
|
||||
defaultXFTPDBOpts =
|
||||
DBOpts
|
||||
{ connstr = "postgresql://xftp@/xftp_server_store",
|
||||
schema = "xftp_server",
|
||||
poolSize = 10,
|
||||
createSchema = False
|
||||
}
|
||||
```
|
||||
|
||||
## Migration CLI
|
||||
|
||||
Bidirectional migration via StoreLog as interchange format:
|
||||
|
||||
```
|
||||
xftp-server database import [--database DB_CONN] [--schema DB_SCHEMA] [--pool-size N]
|
||||
xftp-server database export [--database DB_CONN] [--schema DB_SCHEMA] [--pool-size N]
|
||||
```
|
||||
|
||||
No `--table` flag needed (unlike SMP which has queues/messages/all) — XFTP has a single entity type (files + recipients, always migrated together).
|
||||
|
||||
CLI options reuse `dbOptsP` parser from `Simplex.Messaging.Server.CLI`.
|
||||
|
||||
### Import (StoreLog -> PostgreSQL)
|
||||
|
||||
1. Confirm: prompt user with database connection details and StoreLog path
|
||||
2. Read and replay StoreLog into temporary `STMFileStore`
|
||||
3. Connect to PostgreSQL, run schema migrations (`createSchema = True`, `confirmMigrations = MCYesUp`)
|
||||
4. Batch-insert file records into `files` table using PostgreSQL COPY protocol (matching SMP's `batchInsertQueues` pattern for performance). Progress reported every 10k files.
|
||||
5. Batch-insert recipient records into `recipients` table using COPY protocol
|
||||
6. Verify counts: `SELECT COUNT(*) FROM files` / `recipients` — warn if mismatch
|
||||
7. Rename StoreLog to `.bak` (prevents accidental re-import, preserves original for rollback)
|
||||
8. Report counts
|
||||
|
||||
### Export (PostgreSQL -> StoreLog)
|
||||
|
||||
1. Confirm: prompt user with database connection details and output path. Fail if output file already exists.
|
||||
2. Connect to PostgreSQL
|
||||
3. Open new StoreLog file for writing
|
||||
4. Fold over all file records, writing per file (in this order, matching existing `writeFileStore`): `AddFile` (with `ServerEntityStatus` — this preserves `EntityBlocked` state), `AddRecipients`, then `PutFile` (if `file_path` is set)
|
||||
5. Report counts
|
||||
|
||||
Note: `AddFile` carries `ServerEntityStatus` which includes `EntityBlocked info`, so blocking state is preserved through export/import without needing separate `BlockFile` log entries.
|
||||
|
||||
File data on disk is untouched by migration — only metadata moves between backends.
|
||||
|
||||
## Cabal Integration
|
||||
|
||||
Shared `server_postgres` flag. New Postgres modules added to existing conditional block:
|
||||
|
||||
```cabal
|
||||
if flag(server_postgres)
|
||||
cpp-options: -DdbServerPostgres
|
||||
exposed-modules:
|
||||
...existing SMP modules...
|
||||
Simplex.FileTransfer.Server.Store.Postgres
|
||||
Simplex.FileTransfer.Server.Store.Postgres.Migrations
|
||||
Simplex.FileTransfer.Server.Store.Postgres.Config
|
||||
```
|
||||
|
||||
CPP guards (`#if defined(dbServerPostgres)`) in:
|
||||
- `Store.hs` — Postgres `FromField`/`ToField` instances for XFTP-specific types if needed
|
||||
- `Env.hs` — `XSCDatabase` constructor
|
||||
- `Main.hs` — database CLI commands, store selection for `database` mode, Postgres imports
|
||||
- `Server.hs` — Postgres-specific imports if needed
|
||||
|
||||
## Testing
|
||||
|
||||
- **Parameterized server tests**: Existing `xftpServerTests` refactored to accept a store type parameter (following SMP's `SpecWith (ASrvTransport, AStoreType)` pattern). The same server tests run against both STM and Postgres backends — STM tests run unconditionally, Postgres tests added under `#if defined(dbServerPostgres)` with `postgressBracket` for database lifecycle (drop → create → test → drop).
|
||||
- **Unit tests**: `PostgresFileStore` operations — add/get/delete/block/expire, duplicate detection, auth errors
|
||||
- **Migration round-trip**: STM store → export to StoreLog → import to Postgres → export back → verify StoreLog equality (including blocked file status)
|
||||
- **Tests location**: in `tests/` alongside existing XFTP tests, guarded by `server_postgres` CPP flag
|
||||
- **Test database**: PostgreSQL on `localhost:5432`, using a dedicated `xftp_server_test` schema (dropped and recreated per test run via `postgressBracket`, following SMP's test database lifecycle pattern)
|
||||
- **Test fixtures**: `testXFTPStoreDBOpts :: DBOpts` with `createSchema = True`, `confirmMigrations = MCYesUp`, in `tests/XFTPClient.hs`
|
||||
@@ -0,0 +1,648 @@
|
||||
# XFTP PostgreSQL Backend — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers-extended-cc:subagent-driven-development (if subagents available) or superpowers-extended-cc:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add PostgreSQL backend support to xftp-server as an alternative to STM + StoreLog, with bidirectional migration.
|
||||
|
||||
**Architecture:** Introduce `FileStoreClass` typeclass (IO-based, following `QueueStoreClass` pattern). Extract current STM store into `Store/STM.hs`, make `Server.hs` polymorphic, then add `Store/Postgres.hs` behind `server_postgres` CPP flag. `usedStorage` moves from store to `XFTPEnv` so the server manages quota tracking externally.
|
||||
|
||||
**Tech Stack:** Haskell, postgresql-simple, STM, fourmolu, cabal with CPP flags
|
||||
|
||||
**Design spec:** `plans/2026-03-25-xftp-postgres-backend-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
**Existing files modified:**
|
||||
- `src/Simplex/FileTransfer/Server/Store.hs` — rewritten: becomes typeclass + shared types
|
||||
- `src/Simplex/FileTransfer/Server/Env.hs` — polymorphic `XFTPEnv s`, `XFTPStoreConfig` GADT
|
||||
- `src/Simplex/FileTransfer/Server.hs` — polymorphic over `FileStoreClass s`
|
||||
- `src/Simplex/FileTransfer/Server/StoreLog.hs` — update for IO store functions
|
||||
- `src/Simplex/FileTransfer/Server/Main.hs` — INI config, dispatch, CLI commands
|
||||
- `simplexmq.cabal` — new modules
|
||||
- `tests/XFTPClient.hs` — Postgres test fixtures
|
||||
- `tests/Test.hs` — Postgres test group
|
||||
|
||||
**New files created:**
|
||||
- `src/Simplex/FileTransfer/Server/Store/STM.hs` — `STMFileStore` (extracted from current `Store.hs`)
|
||||
- `src/Simplex/FileTransfer/Server/Store/Postgres.hs` — `PostgresFileStore` [CPP-guarded]
|
||||
- `src/Simplex/FileTransfer/Server/Store/Postgres/Config.hs` — `PostgresFileStoreCfg` [CPP-guarded]
|
||||
- `src/Simplex/FileTransfer/Server/Store/Postgres/Migrations.hs` — schema SQL [CPP-guarded]
|
||||
- `tests/CoreTests/XFTPStoreTests.hs` — Postgres store unit tests [CPP-guarded]
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Move `usedStorage` from `FileStore` to `XFTPEnv`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Simplex/FileTransfer/Server/Store.hs`
|
||||
- Modify: `src/Simplex/FileTransfer/Server/Env.hs`
|
||||
- Modify: `src/Simplex/FileTransfer/Server.hs`
|
||||
|
||||
- [ ] **Step 1: Remove `usedStorage` from `FileStore` in `Store.hs`**
|
||||
|
||||
1. Remove `usedStorage :: TVar Int64` field from `FileStore` record (line 47).
|
||||
2. Remove `usedStorage <- newTVarIO 0` from `newFileStore` (line 75) and drop the field from the record construction (line 76).
|
||||
3. In `setFilePath` (line 92-97): remove `modifyTVar' (usedStorage st) (+ fromIntegral (size fileInfo))` — keep only `writeTVar filePath (Just fPath)`. Change pattern from `\FileRec {fileInfo, filePath}` to `\FileRec {filePath}` (fileInfo is now unused — `-Wunused-matches` error).
|
||||
4. In `deleteFile` (line 112-119): remove `modifyTVar' usedStorage $ subtract (fromIntegral $ size fileInfo)`. Change outer pattern match from `FileStore {files, recipients, usedStorage}` to `FileStore {files, recipients}`. Change inner pattern from `Just FileRec {fileInfo, recipientIds}` to `Just FileRec {recipientIds}` (`fileInfo` is now unused — `-Wunused-matches` error).
|
||||
5. In `blockFile` (line 122-127): remove `when deleted $ modifyTVar' usedStorage $ subtract (fromIntegral $ size fileInfo)`. Change pattern match from `st@FileStore {usedStorage}` to `st`. The `deleted` parameter and `fileInfo` in the inner pattern become unused — prefix with `_` or remove from pattern to avoid `-Wunused-matches`.
|
||||
|
||||
- [ ] **Step 2: Add `usedStorage` to `XFTPEnv` in `Env.hs`**
|
||||
|
||||
1. Add `usedStorage :: TVar Int64` field to `XFTPEnv` record (between `store` and `storeLog`, line 93).
|
||||
2. In `newXFTPServerEnv` (line 112-126): replace lines 117-118:
|
||||
```
|
||||
used <- countUsedStorage <$> readTVarIO (files store)
|
||||
atomically $ writeTVar (usedStorage store) used
|
||||
```
|
||||
with:
|
||||
```
|
||||
usedStorage <- newTVarIO =<< countUsedStorage <$> readTVarIO (files store)
|
||||
```
|
||||
3. Add `usedStorage` to the `pure XFTPEnv {..}` construction.
|
||||
|
||||
- [ ] **Step 3: Update all `usedStorage` access sites in `Server.hs`**
|
||||
|
||||
1. Line 552: `us <- asks $ usedStorage . store` → `us <- asks usedStorage`.
|
||||
2. Line 569: `us <- asks $ usedStorage . store` → `us <- asks usedStorage`.
|
||||
3. Line 639: `usedStart <- readTVarIO $ usedStorage st` → `usedStart <- readTVarIO =<< asks usedStorage`.
|
||||
4. Line 647: `usedEnd <- readTVarIO $ usedStorage st` → `usedEnd <- readTVarIO =<< asks usedStorage`.
|
||||
5. Line 694: `FileStore {files, usedStorage} <- asks store` → split into `FileStore {files} <- asks store` and `usedStorage <- asks usedStorage`.
|
||||
6. In `deleteOrBlockServerFile_` (line 620): after `void $ atomically $ storeAction st`, add usedStorage adjustment — `us <- asks usedStorage` then `atomically $ modifyTVar' us $ subtract (fromIntegral $ size fileInfo)` when file had a path (check `path` from `readTVarIO filePath` earlier in the function).
|
||||
|
||||
- [ ] **Step 4: Build and verify**
|
||||
|
||||
Run: `cabal build`
|
||||
|
||||
- [ ] **Step 5: Run existing tests**
|
||||
|
||||
Run: `cabal test --test-show-details=streaming --test-option=--match="/XFTP/"`
|
||||
|
||||
- [ ] **Step 6: Format and commit**
|
||||
|
||||
```bash
|
||||
fourmolu -i src/Simplex/FileTransfer/Server/Store.hs src/Simplex/FileTransfer/Server/Env.hs src/Simplex/FileTransfer/Server.hs
|
||||
git add src/Simplex/FileTransfer/Server/Store.hs src/Simplex/FileTransfer/Server/Env.hs src/Simplex/FileTransfer/Server.hs
|
||||
git commit -m "refactor(xftp): move usedStorage from FileStore to XFTPEnv"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Add `getUsedStorage`, `getFileCount`, `expiredFiles` functions
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Simplex/FileTransfer/Server/Store.hs`
|
||||
- Modify: `src/Simplex/FileTransfer/Server/Env.hs`
|
||||
- Modify: `src/Simplex/FileTransfer/Server.hs`
|
||||
|
||||
- [ ] **Step 1: Add three new functions to `Store.hs`**
|
||||
|
||||
1. Add to exports: `getUsedStorage`, `getFileCount`, `expiredFiles`.
|
||||
2. Remove `expiredFilePath` from exports AND delete the function definition (dead code → `-Wunused-binds` error). Also remove `($>>=)` from import `Simplex.Messaging.Util (ifM, ($>>=))` → `Simplex.Messaging.Util (ifM)` — `$>>=` was only used by `expiredFilePath`.
|
||||
3. Add import: `qualified Data.Map.Strict as M` (needed for `M.foldl'` in `getUsedStorage` and `M.toList` in `expiredFiles`).
|
||||
4. Implement:
|
||||
```haskell
|
||||
getUsedStorage :: FileStore -> IO Int64
|
||||
getUsedStorage FileStore {files} =
|
||||
M.foldl' (\acc FileRec {fileInfo = FileInfo {size}} -> acc + fromIntegral size) 0 <$> readTVarIO files
|
||||
|
||||
getFileCount :: FileStore -> IO Int
|
||||
getFileCount FileStore {files} = M.size <$> readTVarIO files
|
||||
|
||||
expiredFiles :: FileStore -> Int64 -> Int -> IO [(SenderId, Maybe FilePath, Word32)]
|
||||
expiredFiles FileStore {files} old _limit = do
|
||||
fs <- readTVarIO files
|
||||
fmap catMaybes . forM (M.toList fs) $ \(sId, FileRec {fileInfo = FileInfo {size}, filePath, createdAt = RoundedSystemTime createdAt}) ->
|
||||
if createdAt + fileTimePrecision < old
|
||||
then do
|
||||
path <- readTVarIO filePath
|
||||
pure $ Just (sId, path, size)
|
||||
else pure Nothing
|
||||
```
|
||||
5. Add imports: `Data.Maybe (catMaybes)`, `Data.Word (Word32)` (note: `qualified Data.Map.Strict as M` already added in item 3).
|
||||
|
||||
- [ ] **Step 2: Replace `countUsedStorage` in `Env.hs`**
|
||||
|
||||
1. Replace `countUsedStorage <$> readTVarIO (files store)` with `getUsedStorage store` in `newXFTPServerEnv`.
|
||||
2. Remove `countUsedStorage` function definition and its export.
|
||||
3. Remove `qualified Data.Map.Strict as M` import if no longer used.
|
||||
|
||||
- [ ] **Step 3: Update `restoreServerStats` in `Server.hs` to use `getFileCount`**
|
||||
|
||||
In `restoreServerStats` (line 694-696): replace `FileStore {files} <- asks store` and `_filesCount <- M.size <$> readTVarIO files` with `st <- asks store` and `_filesCount <- liftIO $ getFileCount st` (eliminates the `FileStore` pattern match — `files` binding no longer needed).
|
||||
|
||||
- [ ] **Step 4: Replace `expireServerFiles` iteration in `Server.hs`**
|
||||
|
||||
1. Replace the body of `expireServerFiles` (lines 636-660). Remove `files' <- readTVarIO (files st)` and the `forM_ (M.keys files')` loop.
|
||||
2. New body: call `expiredFiles st old 10000` in a loop. For each `(sId, filePath_, fileSize)` in returned list: apply `itemDelay`, remove disk file if present, call `atomically $ deleteFile st sId`, adjust `usedStorage` TVar by `fileSize`, increment `filesExpired` stat. Loop until `expiredFiles` returns `[]`.
|
||||
3. Remove `Data.Map.Strict` import from Server.hs if no longer needed (was used for `M.size` and `M.keys` — now replaced by `getFileCount` and `expiredFiles`).
|
||||
|
||||
- [ ] **Step 5: Build and verify**
|
||||
|
||||
Run: `cabal build`
|
||||
|
||||
- [ ] **Step 6: Run existing tests**
|
||||
|
||||
Run: `cabal test --test-show-details=streaming --test-option=--match="/XFTP/"`
|
||||
|
||||
- [ ] **Step 7: Format and commit**
|
||||
|
||||
```bash
|
||||
fourmolu -i src/Simplex/FileTransfer/Server/Store.hs src/Simplex/FileTransfer/Server/Env.hs src/Simplex/FileTransfer/Server.hs
|
||||
git add src/Simplex/FileTransfer/Server/Store.hs src/Simplex/FileTransfer/Server/Env.hs src/Simplex/FileTransfer/Server.hs
|
||||
git commit -m "refactor(xftp): add getUsedStorage, getFileCount, expiredFiles store functions"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Change `Store.hs` functions from STM to IO
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Simplex/FileTransfer/Server/Store.hs`
|
||||
- Modify: `src/Simplex/FileTransfer/Server.hs`
|
||||
- Modify: `src/Simplex/FileTransfer/Server/StoreLog.hs`
|
||||
|
||||
- [ ] **Step 1: Change all Store.hs function signatures from STM to IO**
|
||||
|
||||
For each of: `addFile`, `setFilePath`, `addRecipient`, `getFile`, `deleteFile`, `blockFile`, `deleteRecipient`, `ackFile`:
|
||||
1. Change return type from `STM (Either XFTPErrorType ...)` to `IO (Either XFTPErrorType ...)` (or `STM ()` to `IO ()` for `deleteRecipient`).
|
||||
2. Wrap the function body in `atomically $ do ...`.
|
||||
3. Keep `withFile` and `newFileRec` as internal STM helpers (called inside the `atomically` blocks).
|
||||
|
||||
- [ ] **Step 2: Update Server.hs call sites — remove `atomically` wrappers**
|
||||
|
||||
1. Line 563 (`receiveServerFile`): change `atomically $ writeTVar filePath (Just fPath)` → add `st <- asks store` then `void $ liftIO $ setFilePath st senderId fPath` (design call site #1 — `store` is not in scope in `receiveServerFile`'s `receive` helper, so bind via `asks`; `void` avoids `-Wunused-do-bind` warning on the `Either` result).
|
||||
2. Line 453 (`verifyXFTPTransmission`): split `atomically $ verify =<< getFile st party fId` into: `liftIO (getFile st party fId)` (IO→M lift), then pattern match on result, use `readTVarIO (fileStatus fr)` instead of `readTVar`.
|
||||
3. Lines 371, 377 (control port `CPDelete`/`CPBlock`): change `ExceptT $ atomically $ getFile fs SFRecipient fileId` → `ExceptT $ liftIO $ getFile fs SFRecipient fileId` (inside `unliftIO u $ do` block which runs in M monad — `liftIO` required to lift IO into M).
|
||||
4. Line 508 (`addFile` in `createFile`): the `ExceptT $ addFile st sId file ts EntityActive` — `addFile` is now IO, `ExceptT` wraps IO directly. Remove any `atomically`.
|
||||
5. Line 514 (`addRecipient`): same — `ExceptT . addRecipient st sId` works directly in IO.
|
||||
6. Line 516 (`retryAdd`): change parameter type from `(XFTPFileId -> STM (Either XFTPErrorType a))` to `(XFTPFileId -> IO (Either XFTPErrorType a))`. Line 520: change `atomically (add fId)` to `liftIO (add fId)`.
|
||||
7. Line 605 (`ackFileReception`): change `atomically $ deleteRecipient st rId fr` to `liftIO $ deleteRecipient st rId fr`.
|
||||
8. Line 620 (`deleteOrBlockServerFile_`): change third parameter type from `(FileStore -> STM (Either XFTPErrorType ()))` to `(FileStore -> IO (Either XFTPErrorType ()))`. Line 626: change `void $ atomically $ storeAction st` to `void $ liftIO $ storeAction st`.
|
||||
9. `expireServerFiles` `delete` helper: change `atomically $ deleteFile st sId` to `liftIO $ deleteFile st sId` (deleteFile is now IO; `liftIO` required because the helper runs in M monad, not IO).
|
||||
|
||||
- [ ] **Step 3: Update `StoreLog.hs` — remove `atomically` from replay**
|
||||
|
||||
In `readFileStore` (line 93), function `addToStore`:
|
||||
1. Change `atomically (addToStore lr)` to `addToStore lr` — store functions are now IO.
|
||||
2. The `addToStore` body calls `addFile`, `setFilePath`, `deleteFile`, `blockFile`, `ackFile` — all IO now, no `atomically` needed.
|
||||
3. For `AddRecipients`: `runExceptT $ mapM_ (ExceptT . addRecipient st sId) rcps` — `addRecipient` returns `IO (Either ...)`, so `ExceptT . addRecipient st sId` works directly.
|
||||
|
||||
- [ ] **Step 4: Build and verify**
|
||||
|
||||
Run: `cabal build`
|
||||
|
||||
- [ ] **Step 5: Run existing tests**
|
||||
|
||||
Run: `cabal test --test-show-details=streaming --test-option=--match="/XFTP/"`
|
||||
|
||||
- [ ] **Step 6: Format and commit**
|
||||
|
||||
```bash
|
||||
fourmolu -i src/Simplex/FileTransfer/Server/Store.hs src/Simplex/FileTransfer/Server.hs src/Simplex/FileTransfer/Server/StoreLog.hs
|
||||
git add src/Simplex/FileTransfer/Server/Store.hs src/Simplex/FileTransfer/Server.hs src/Simplex/FileTransfer/Server/StoreLog.hs
|
||||
git commit -m "refactor(xftp): change file store operations from STM to IO"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Extract `FileStoreClass` typeclass, move STM impl to `Store/STM.hs`
|
||||
|
||||
**Files:**
|
||||
- Rewrite: `src/Simplex/FileTransfer/Server/Store.hs`
|
||||
- Create: `src/Simplex/FileTransfer/Server/Store/STM.hs`
|
||||
- Modify: `src/Simplex/FileTransfer/Server/StoreLog.hs`
|
||||
- Modify: `src/Simplex/FileTransfer/Server/Env.hs`
|
||||
- Modify: `src/Simplex/FileTransfer/Server.hs`
|
||||
- Modify: `simplexmq.cabal`
|
||||
|
||||
- [ ] **Step 1: Create `Store/STM.hs` — move all implementation code**
|
||||
|
||||
1. Create directory `src/Simplex/FileTransfer/Server/Store/`.
|
||||
2. Create `src/Simplex/FileTransfer/Server/Store/STM.hs`.
|
||||
3. Move from `Store.hs`: `FileStore` data type (rename to `STMFileStore`), all function implementations, internal helpers (`withFile`, `newFileRec`), all STM-specific imports.
|
||||
4. Rename all `FileStore` references to `STMFileStore` in the new file.
|
||||
5. Module declaration: `module Simplex.FileTransfer.Server.Store.STM` exporting only `STMFileStore (..)` — do NOT export standalone functions (`addFile`, `setFilePath`, etc.) to avoid name collisions with the typeclass methods from `Store.hs`.
|
||||
|
||||
- [ ] **Step 2: Rewrite `Store.hs` as the typeclass module**
|
||||
|
||||
1. Add `{-# LANGUAGE TypeFamilies #-}` pragma to `Store.hs` (required for `type FileStoreConfig s` associated type).
|
||||
2. Keep in `Store.hs`: `FileRec (..)`, `FileRecipient (..)`, `RoundedFileTime`, `fileTimePrecision` definitions and their `StrEncoding` instance.
|
||||
3. Add `FileStoreClass` typeclass:
|
||||
```haskell
|
||||
class FileStoreClass s where
|
||||
type FileStoreConfig s
|
||||
|
||||
-- Lifecycle
|
||||
newFileStore :: FileStoreConfig s -> IO s
|
||||
closeFileStore :: s -> IO ()
|
||||
|
||||
-- File operations
|
||||
addFile :: s -> SenderId -> FileInfo -> RoundedFileTime -> ServerEntityStatus -> IO (Either XFTPErrorType ())
|
||||
setFilePath :: s -> SenderId -> FilePath -> IO (Either XFTPErrorType ())
|
||||
addRecipient :: s -> SenderId -> FileRecipient -> IO (Either XFTPErrorType ())
|
||||
getFile :: s -> SFileParty p -> XFTPFileId -> IO (Either XFTPErrorType (FileRec, C.APublicAuthKey))
|
||||
deleteFile :: s -> SenderId -> IO (Either XFTPErrorType ())
|
||||
blockFile :: s -> SenderId -> BlockingInfo -> Bool -> IO (Either XFTPErrorType ())
|
||||
deleteRecipient :: s -> RecipientId -> FileRec -> IO ()
|
||||
ackFile :: s -> RecipientId -> IO (Either XFTPErrorType ())
|
||||
|
||||
-- Expiration
|
||||
expiredFiles :: s -> Int64 -> Int -> IO [(SenderId, Maybe FilePath, Word32)]
|
||||
|
||||
-- Stats
|
||||
getUsedStorage :: s -> IO Int64
|
||||
getFileCount :: s -> IO Int
|
||||
```
|
||||
4. Do NOT re-export from `Store/STM.hs` — this would create a circular module dependency (Store.hs imports Store/STM.hs, Store/STM.hs imports Store.hs). Consumers must import `Store.STM` directly where they need `STMFileStore`.
|
||||
5. Remove all STM-specific imports that are no longer needed.
|
||||
|
||||
- [ ] **Step 3: Add `FileStoreClass` instance in `Store/STM.hs`**
|
||||
|
||||
1. Import `FileStoreClass` from `Simplex.FileTransfer.Server.Store`.
|
||||
2. Inline all implementations directly in the instance body (do NOT delegate to standalone functions — the standalone names collide with typeclass method names, causing ambiguous occurrences for importers):
|
||||
```haskell
|
||||
instance FileStoreClass STMFileStore where
|
||||
type FileStoreConfig STMFileStore = ()
|
||||
newFileStore () = do
|
||||
files <- TM.emptyIO
|
||||
recipients <- TM.emptyIO
|
||||
pure STMFileStore {files, recipients}
|
||||
closeFileStore _ = pure ()
|
||||
addFile st sId fileInfo createdAt status = atomically $ ...
|
||||
setFilePath st sId fPath = atomically $ ...
|
||||
-- ... (each method's body is the existing function body, inlined)
|
||||
```
|
||||
3. Remove the standalone top-level function definitions — they are now instance methods. Keep only `withFile` and `newFileRec` as internal helpers used by the instance methods.
|
||||
|
||||
- [ ] **Step 4: Update importers**
|
||||
|
||||
1. `Env.hs`: add `import Simplex.FileTransfer.Server.Store.STM (STMFileStore (..))`. Change `FileStore` → `STMFileStore` in `XFTPEnv` type and `newXFTPServerEnv`. Change `store <- newFileStore` to `store <- newFileStore ()` (typeclass method now takes `FileStoreConfig STMFileStore` which is `()`). Keep `import Simplex.FileTransfer.Server.Store` for `FileRec`, `FileRecipient`, `FileStoreClass`, etc.
|
||||
2. `Server.hs`: add `import Simplex.FileTransfer.Server.Store.STM`. Change `FileStore` → `STMFileStore` in any explicit type annotations. Import `FileStoreClass` from `Simplex.FileTransfer.Server.Store`.
|
||||
3. `StoreLog.hs`: add `import Simplex.FileTransfer.Server.Store.STM` to access concrete `STMFileStore` type and store functions used during log replay. Change `FileStore` → `STMFileStore` in `readWriteFileStore` and `writeFileStore` parameter types.
|
||||
|
||||
- [ ] **Step 5: Update cabal file**
|
||||
|
||||
Add `Simplex.FileTransfer.Server.Store.STM` to `exposed-modules` in the `!flag(client_library)` section, alongside existing XFTP server modules.
|
||||
|
||||
- [ ] **Step 6: Build and verify**
|
||||
|
||||
Run: `cabal build`
|
||||
|
||||
- [ ] **Step 7: Run existing tests**
|
||||
|
||||
Run: `cabal test --test-show-details=streaming --test-option=--match="/XFTP/"`
|
||||
|
||||
- [ ] **Step 8: Format and commit**
|
||||
|
||||
```bash
|
||||
fourmolu -i src/Simplex/FileTransfer/Server/Store.hs src/Simplex/FileTransfer/Server/Store/STM.hs src/Simplex/FileTransfer/Server/Env.hs src/Simplex/FileTransfer/Server.hs src/Simplex/FileTransfer/Server/StoreLog.hs
|
||||
git add src/Simplex/FileTransfer/Server/Store.hs src/Simplex/FileTransfer/Server/Store/STM.hs src/Simplex/FileTransfer/Server/Env.hs src/Simplex/FileTransfer/Server.hs src/Simplex/FileTransfer/Server/StoreLog.hs simplexmq.cabal
|
||||
git commit -m "refactor(xftp): extract FileStoreClass typeclass, move STM impl to Store.STM"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Make `XFTPEnv` and `Server.hs` polymorphic over `FileStoreClass`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Simplex/FileTransfer/Server/Env.hs`
|
||||
- Modify: `src/Simplex/FileTransfer/Server.hs`
|
||||
- Modify: `src/Simplex/FileTransfer/Server/Main.hs`
|
||||
- Modify: `tests/XFTPClient.hs` (if it calls `runXFTPServerBlocking` directly)
|
||||
|
||||
- [ ] **Step 1: Make `XFTPEnv` polymorphic in `Env.hs`**
|
||||
|
||||
1. Add `XFTPStoreConfig` GADT: `data XFTPStoreConfig s where XSCMemory :: Maybe FilePath -> XFTPStoreConfig STMFileStore`.
|
||||
2. Change `data XFTPEnv` to `data XFTPEnv s` — field `store :: FileStore` becomes `store :: s`.
|
||||
3. Change `newXFTPServerEnv :: XFTPServerConfig -> IO XFTPEnv` to `newXFTPServerEnv :: FileStoreClass s => XFTPStoreConfig s -> XFTPServerConfig -> IO (XFTPEnv s)`.
|
||||
4. Pattern match on `XSCMemory storeLogPath` in `newXFTPServerEnv` body. Create store via `newFileStore ()`, storeLog via `mapM (`readWriteFileStore` st) storeLogPath`.
|
||||
|
||||
- [ ] **Step 2: Make `Server.hs` polymorphic**
|
||||
|
||||
1. Change `type M a = ReaderT XFTPEnv IO a` to `type M s a = ReaderT (XFTPEnv s) IO a`.
|
||||
2. Add `FileStoreClass s =>` constraint to all functions using `M s a`. Use `forall s.` in signatures of functions that have `where`-block bindings with `M s` type annotations — `ScopedTypeVariables` requires explicit `forall` to bring `s` into scope for inner type signatures (matching SMP's `smpServer :: forall s. MsgStoreClass s => ...` pattern). Full list: `xftpServer`, `processRequest`, `verifyXFTPTransmission`, `processXFTPRequest` and all its `where`-bound functions (`createFile`, `addRecipients`, `receiveServerFile`, `sendServerFile`, `deleteServerFile`, `ackFileReception`, `retryAdd`, `addFileRetry`, `addRecipientRetry`), `deleteServerFile_`, `blockServerFile`, `deleteOrBlockServerFile_`, `expireServerFiles`, `randomId`, `getFileId`, `withFileLog`, `incFileStat`, `saveServerStats`, `restoreServerStats`, `randomDelay` (inside `#ifdef slow_servers` CPP block). Also update `encodeXftp` (line 236) and `runCPClient` (line 339) which use explicit `ReaderT XFTPEnv IO` instead of the `M` alias — change to `ReaderT (XFTPEnv s) IO`.
|
||||
3. Change `runXFTPServerBlocking` and `runXFTPServer` to take `XFTPStoreConfig s` parameter.
|
||||
4. Add `closeFileStore store` call to the server shutdown path (in the `finally` block or `stopServer` equivalent — after saving stats, before logging "Server stopped"). This ensures Postgres connection pool and `dbStoreLog` are properly closed. For STM this is a no-op.
|
||||
|
||||
- [ ] **Step 3: Update `Main.hs` dispatch**
|
||||
|
||||
1. In `runServer`: construct `XSCMemory (enableStoreLog $> storeLogFilePath)`.
|
||||
2. Add dispatch function that calls the updated `runXFTPServer` (which creates `started` internally):
|
||||
```haskell
|
||||
run :: FileStoreClass s => XFTPStoreConfig s -> IO ()
|
||||
run storeCfg = runXFTPServer storeCfg serverConfig
|
||||
```
|
||||
3. Call `run` with the `XSCMemory` config.
|
||||
|
||||
- [ ] **Step 4: Update test helper if needed**
|
||||
|
||||
If `tests/XFTPClient.hs` calls `runXFTPServerBlocking` directly, update the call to pass an `XSCMemory` config. Check the `withXFTPServer` / `serverBracket` helper.
|
||||
|
||||
- [ ] **Step 5: Build and verify**
|
||||
|
||||
Run: `cabal build && cabal build test:simplexmq-test`
|
||||
|
||||
- [ ] **Step 6: Run existing tests**
|
||||
|
||||
Run: `cabal test --test-show-details=streaming --test-option=--match="/XFTP/"`
|
||||
|
||||
- [ ] **Step 7: Format and commit**
|
||||
|
||||
```bash
|
||||
fourmolu -i src/Simplex/FileTransfer/Server/Env.hs src/Simplex/FileTransfer/Server.hs src/Simplex/FileTransfer/Server/Main.hs
|
||||
git add src/Simplex/FileTransfer/Server/Env.hs src/Simplex/FileTransfer/Server.hs src/Simplex/FileTransfer/Server/Main.hs tests/XFTPClient.hs simplexmq.cabal
|
||||
git commit -m "refactor(xftp): make XFTPEnv and server polymorphic over FileStoreClass"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Add Postgres config, migrations, and store skeleton
|
||||
|
||||
**Files:**
|
||||
- Create: `src/Simplex/FileTransfer/Server/Store/Postgres/Config.hs`
|
||||
- Create: `src/Simplex/FileTransfer/Server/Store/Postgres/Migrations.hs`
|
||||
- Create: `src/Simplex/FileTransfer/Server/Store/Postgres.hs`
|
||||
- Modify: `src/Simplex/FileTransfer/Server/Env.hs`
|
||||
- Modify: `simplexmq.cabal`
|
||||
|
||||
- [ ] **Step 1: Create `Store/Postgres/Config.hs`**
|
||||
|
||||
```haskell
|
||||
module Simplex.FileTransfer.Server.Store.Postgres.Config
|
||||
( PostgresFileStoreCfg (..),
|
||||
defaultXFTPDBOpts,
|
||||
)
|
||||
where
|
||||
|
||||
import Simplex.Messaging.Agent.Store.Postgres.Options (DBOpts (..))
|
||||
import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation)
|
||||
|
||||
data PostgresFileStoreCfg = PostgresFileStoreCfg
|
||||
{ dbOpts :: DBOpts,
|
||||
dbStoreLogPath :: Maybe FilePath,
|
||||
confirmMigrations :: MigrationConfirmation
|
||||
}
|
||||
|
||||
defaultXFTPDBOpts :: DBOpts
|
||||
defaultXFTPDBOpts =
|
||||
DBOpts
|
||||
{ connstr = "postgresql://xftp@/xftp_server_store",
|
||||
schema = "xftp_server",
|
||||
poolSize = 10,
|
||||
createSchema = False
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create `Store/Postgres/Migrations.hs`**
|
||||
|
||||
Full migration module with `xftpServerMigrations :: [Migration]` and `m20260325_initial` containing CREATE TABLE SQL for `files` and `recipients` tables plus indexes. Follow SMP's `QueueStore/Postgres/Migrations.hs` pattern exactly: tuple list → `sortOn name . map migration`.
|
||||
|
||||
- [ ] **Step 3: Create `Store/Postgres.hs` with stub instance**
|
||||
|
||||
1. Define `PostgresFileStore` with `dbStore :: DBStore` and `dbStoreLog :: Maybe (StoreLog 'WriteMode)`.
|
||||
2. `instance FileStoreClass PostgresFileStore` with `error "not implemented"` for all methods except `newFileStore` (calls `createDBStore` + opens `dbStoreLog`) and `closeFileStore` (closes both). `type FileStoreConfig PostgresFileStore = PostgresFileStoreCfg`.
|
||||
3. Add `withDB`, `handleDuplicate`, `assertUpdated`, `withLog` helpers.
|
||||
|
||||
- [ ] **Step 4: Add `XSCDatabase` GADT constructor in `Env.hs` (CPP-guarded)**
|
||||
|
||||
```haskell
|
||||
#if defined(dbServerPostgres)
|
||||
import Simplex.FileTransfer.Server.Store.Postgres (PostgresFileStore)
|
||||
import Simplex.FileTransfer.Server.Store.Postgres.Config (PostgresFileStoreCfg)
|
||||
#endif
|
||||
|
||||
data XFTPStoreConfig s where
|
||||
XSCMemory :: Maybe FilePath -> XFTPStoreConfig STMFileStore
|
||||
#if defined(dbServerPostgres)
|
||||
XSCDatabase :: PostgresFileStoreCfg -> XFTPStoreConfig PostgresFileStore
|
||||
#endif
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Update cabal**
|
||||
|
||||
Add to existing `if flag(server_postgres)` block:
|
||||
```
|
||||
Simplex.FileTransfer.Server.Store.Postgres
|
||||
Simplex.FileTransfer.Server.Store.Postgres.Config
|
||||
Simplex.FileTransfer.Server.Store.Postgres.Migrations
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Build both ways**
|
||||
|
||||
Run: `cabal build && cabal build -fserver_postgres`
|
||||
|
||||
- [ ] **Step 7: Format and commit**
|
||||
|
||||
```bash
|
||||
fourmolu -i src/Simplex/FileTransfer/Server/Store/Postgres.hs src/Simplex/FileTransfer/Server/Store/Postgres/Config.hs src/Simplex/FileTransfer/Server/Env.hs
|
||||
git add src/Simplex/FileTransfer/Server/Store/Postgres.hs src/Simplex/FileTransfer/Server/Store/Postgres/Config.hs src/Simplex/FileTransfer/Server/Store/Postgres/Migrations.hs src/Simplex/FileTransfer/Server/Env.hs simplexmq.cabal
|
||||
git commit -m "feat(xftp): add PostgreSQL store skeleton with schema migration"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Implement `PostgresFileStore` operations
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Simplex/FileTransfer/Server/Store/Postgres.hs`
|
||||
|
||||
- [ ] **Step 1: Implement `addFile`**
|
||||
|
||||
`INSERT INTO files (sender_id, file_size, file_digest, sender_key, file_path, created_at, status) VALUES (?,?,?,?,NULL,?,?)`. Catch unique violation with `handleDuplicate` → `DUPLICATE_`. Call `withLog "addFile"` after.
|
||||
|
||||
- [ ] **Step 2: Implement `getFile`**
|
||||
|
||||
For `SFSender`: `SELECT ... FROM files WHERE sender_id = ?`. Construct `FileRec` with `newTVarIO` per TVar field. `recipientIds = S.empty`.
|
||||
For `SFRecipient`: `SELECT f.*, r.recipient_key FROM recipients r JOIN files f ON r.sender_id = f.sender_id WHERE r.recipient_id = ?`.
|
||||
|
||||
- [ ] **Step 3: Implement `setFilePath`**
|
||||
|
||||
`UPDATE files SET file_path = ? WHERE sender_id = ? AND file_path IS NULL`. Use `assertUpdated`. Call `withLog "setFilePath"`.
|
||||
|
||||
- [ ] **Step 4: Implement `addRecipient`**
|
||||
|
||||
`INSERT INTO recipients (recipient_id, sender_id, recipient_key) VALUES (?,?,?)`. `handleDuplicate` → `DUPLICATE_`. Call `withLog "addRecipient"`.
|
||||
|
||||
- [ ] **Step 5: Implement `deleteFile`, `blockFile`**
|
||||
|
||||
`deleteFile`: `DELETE FROM files WHERE sender_id = ?` (CASCADE). `withLog "deleteFile"`.
|
||||
`blockFile`: `UPDATE files SET status = ? WHERE sender_id = ?`. `assertUpdated`. `withLog "blockFile"`.
|
||||
|
||||
- [ ] **Step 6: Implement `deleteRecipient`, `ackFile`**
|
||||
|
||||
`deleteRecipient`: `DELETE FROM recipients WHERE recipient_id = ?`. `withLog "deleteRecipient"`.
|
||||
`ackFile`: same + return `Left AUTH` if 0 rows.
|
||||
|
||||
- [ ] **Step 7: Implement `expiredFiles`, `getUsedStorage`, `getFileCount`**
|
||||
|
||||
`expiredFiles`: `SELECT sender_id, file_path, file_size FROM files WHERE created_at + ? < ? LIMIT ?`.
|
||||
`getUsedStorage`: `SELECT COALESCE(SUM(file_size), 0) FROM files`.
|
||||
`getFileCount`: `SELECT COUNT(*) FROM files`.
|
||||
|
||||
- [ ] **Step 8: Add `ToField`/`FromField` instances**
|
||||
|
||||
For `RoundedFileTime` (Int64 wrapper), `ServerEntityStatus` (Text via StrEncoding), `C.APublicAuthKey` (Binary via `encodePubKey`/`decodePubKey`). Check SMP's `QueueStore/Postgres.hs` for existing instances to import.
|
||||
|
||||
- [ ] **Step 9: Wrap mutation operations in `uninterruptibleMask_`**
|
||||
|
||||
Operations that combine a DB write with a TVar update (e.g., `getFile` constructs `FileRec` with `newTVarIO`) must be wrapped in `E.uninterruptibleMask_` to prevent async exceptions from leaving inconsistent state. Follow SMP's `addQueue_`, `deleteStoreQueue` pattern.
|
||||
|
||||
- [ ] **Step 10: Build**
|
||||
|
||||
Run: `cabal build -fserver_postgres`
|
||||
|
||||
- [ ] **Step 11: Format and commit**
|
||||
|
||||
```bash
|
||||
fourmolu -i src/Simplex/FileTransfer/Server/Store/Postgres.hs
|
||||
git add src/Simplex/FileTransfer/Server/Store/Postgres.hs
|
||||
git commit -m "feat(xftp): implement PostgresFileStore operations"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Add INI config, Main.hs dispatch, startup validation
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Simplex/FileTransfer/Server/Main.hs`
|
||||
- Modify: `src/Simplex/FileTransfer/Server/Env.hs`
|
||||
|
||||
- [ ] **Step 1: Update `iniFileContent` in `Main.hs`**
|
||||
|
||||
Add to `[STORE_LOG]` section: `store_files: memory`, commented-out `db_connection`, `db_schema`, `db_pool_size`, `db_store_log` keys. Follow SMP's `optDisabled'` pattern for commented defaults.
|
||||
|
||||
- [ ] **Step 2: Add `StartOptions` and `--confirm-migrations` flag**
|
||||
|
||||
```haskell
|
||||
data StartOptions = StartOptions
|
||||
{ confirmMigrations :: MigrationConfirmation
|
||||
}
|
||||
```
|
||||
Add to `Start` command parser with default `MCConsole`. Thread through to `runServer`.
|
||||
|
||||
- [ ] **Step 3: Add store_files INI parsing and CPP-guarded Postgres dispatch**
|
||||
|
||||
In `runServer`: read `store_files` from INI (`fromRight "memory" $ lookupValue "STORE_LOG" "store_files" ini`). Add `"database"` branch (CPP-guarded) that constructs `PostgresFileStoreCfg` using `iniDBOptions ini defaultXFTPDBOpts` and `enableDbStoreLog'` pattern. Non-postgres build: `exitError`.
|
||||
|
||||
- [ ] **Step 4: Add `XSCDatabase` branch in `newXFTPServerEnv` (`Env.hs`)**
|
||||
|
||||
CPP-guarded pattern match on `XSCDatabase dbCfg`: `newFileStore dbCfg`, `storeLog = Nothing`.
|
||||
|
||||
- [ ] **Step 5: Add startup config validation**
|
||||
|
||||
Add `checkFileStoreMode` (CPP-guarded) before `run`: validate conflicting storeLog file + database mode, missing schema, etc. per design doc.
|
||||
|
||||
- [ ] **Step 6: Build both ways**
|
||||
|
||||
Run: `cabal build && cabal build -fserver_postgres`
|
||||
|
||||
- [ ] **Step 7: Format and commit**
|
||||
|
||||
```bash
|
||||
fourmolu -i src/Simplex/FileTransfer/Server/Main.hs src/Simplex/FileTransfer/Server/Env.hs
|
||||
git add src/Simplex/FileTransfer/Server/Main.hs src/Simplex/FileTransfer/Server/Env.hs
|
||||
git commit -m "feat(xftp): add PostgreSQL INI config, store dispatch, startup validation"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Add database import/export CLI commands
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Simplex/FileTransfer/Server/Main.hs`
|
||||
|
||||
- [ ] **Step 1: Add `Database` CLI command (CPP-guarded)**
|
||||
|
||||
Add `Database StoreCmd DBOpts` constructor to `CliCommand`. Add `database` subcommand parser with `import`/`export` subcommands + `dbOptsP defaultXFTPDBOpts`.
|
||||
|
||||
- [ ] **Step 2: Implement `importFileStoreToDatabase`**
|
||||
|
||||
1. `confirmOrExit` with database details.
|
||||
2. Create temporary `STMFileStore`, replay StoreLog via `readWriteFileStore`.
|
||||
3. Create `PostgresFileStore` with `createSchema = True`, `confirmMigrations = MCYesUp`.
|
||||
4. Batch-insert files using PostgreSQL COPY protocol. Progress every 10k.
|
||||
5. Batch-insert recipients using COPY protocol.
|
||||
6. Verify counts: `SELECT COUNT(*)` — warn on mismatch.
|
||||
7. Rename StoreLog to `.bak`.
|
||||
8. Report counts.
|
||||
|
||||
- [ ] **Step 3: Implement `exportDatabaseToStoreLog`**
|
||||
|
||||
1. `confirmOrExit`. Fail if output file exists.
|
||||
2. Create `PostgresFileStore` from config.
|
||||
3. Open StoreLog for writing.
|
||||
4. Fold over file records: write `AddFile` (with status), `AddRecipients`, `PutFile` per file.
|
||||
5. Close StoreLog, report counts.
|
||||
|
||||
- [ ] **Step 4: Build**
|
||||
|
||||
Run: `cabal build -fserver_postgres`
|
||||
|
||||
- [ ] **Step 5: Format and commit**
|
||||
|
||||
```bash
|
||||
fourmolu -i src/Simplex/FileTransfer/Server/Main.hs
|
||||
git add src/Simplex/FileTransfer/Server/Main.hs
|
||||
git commit -m "feat(xftp): add database import/export CLI commands"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 10: Add Postgres tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/XFTPClient.hs`
|
||||
- Modify: `tests/Test.hs`
|
||||
- Create: `tests/CoreTests/XFTPStoreTests.hs`
|
||||
|
||||
- [ ] **Step 1: Add test fixtures in `tests/XFTPClient.hs`**
|
||||
|
||||
```haskell
|
||||
testXFTPStoreDBOpts :: DBOpts
|
||||
testXFTPStoreDBOpts =
|
||||
DBOpts
|
||||
{ connstr = "postgresql://test_xftp_server_user@/test_xftp_server_db",
|
||||
schema = "xftp_server_test",
|
||||
poolSize = 10,
|
||||
createSchema = True
|
||||
}
|
||||
```
|
||||
Add `testXFTPDBConnectInfo :: ConnectInfo` matching the connection string.
|
||||
|
||||
- [ ] **Step 2: Add Postgres server test group in `tests/Test.hs`**
|
||||
|
||||
CPP-guarded block that runs existing `xftpServerTests` with Postgres store config, wrapped in `postgressBracket testXFTPDBConnectInfo`. Parameterize `withXFTPServer` to accept store config if needed.
|
||||
|
||||
- [ ] **Step 3: Create `tests/CoreTests/XFTPStoreTests.hs` — unit tests**
|
||||
|
||||
Test `PostgresFileStore` operations directly:
|
||||
- `addFile` + `getFile SFSender` round-trip.
|
||||
- `addFile` duplicate → `DUPLICATE_`.
|
||||
- `getFile` nonexistent → `AUTH`.
|
||||
- `setFilePath` + verify `WHERE file_path IS NULL` guard.
|
||||
- `addRecipient` + `getFile SFRecipient` round-trip.
|
||||
- `deleteFile` cascades recipients.
|
||||
- `blockFile` + verify status.
|
||||
- `expiredFiles` batch semantics.
|
||||
- `getUsedStorage`, `getFileCount` correctness.
|
||||
|
||||
- [ ] **Step 4: Add migration round-trip test**
|
||||
|
||||
Create `STMFileStore` with test data (files + recipients + blocked status) → export to StoreLog → import to Postgres → export back → compare StoreLog files byte-for-byte.
|
||||
|
||||
- [ ] **Step 5: Build and run tests**
|
||||
|
||||
```bash
|
||||
cabal build -fserver_postgres test:simplexmq-test
|
||||
cabal test --test-show-details=streaming --test-option=--match="/XFTP/" -fserver_postgres
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Format and commit**
|
||||
|
||||
```bash
|
||||
fourmolu -i tests/CoreTests/XFTPStoreTests.hs tests/XFTPClient.hs
|
||||
git add tests/CoreTests/XFTPStoreTests.hs tests/XFTPClient.hs tests/Test.hs
|
||||
git commit -m "test(xftp): add PostgreSQL backend tests"
|
||||
```
|
||||
@@ -0,0 +1,152 @@
|
||||
# Server: batched SUB command processing
|
||||
|
||||
Implementation plan for Part 1 of [RFC 2026-03-28-subscription-performance](../rfcs/2026-03-28-subscription-performance.md).
|
||||
|
||||
## Current state
|
||||
|
||||
When a batch of ~135 SUB commands arrives, the server already batches:
|
||||
- Queue record lookups (`getQueueRecs` in `receive`, Server.hs:1151)
|
||||
- Command verification (`verifyLoadedQueue`, Server.hs:1152)
|
||||
|
||||
But command processing is per-command (`foldrM process` in `client`, Server.hs:1372-1375). Each SUB calls `subscribeQueueAndDeliver` which calls `tryPeekMsg` - one DB query per queue. For Postgres, that's ~135 individual `SELECT ... FROM messages WHERE recipient_id = ? ORDER BY message_id ASC LIMIT 1` queries per batch.
|
||||
|
||||
## Goal
|
||||
|
||||
Replace ~135 individual message peek queries with 1 batched query per batch. No protocol changes.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Step 1: Add `tryPeekMsgs` to MsgStoreClass
|
||||
|
||||
File: `src/Simplex/Messaging/Server/MsgStore/Types.hs`
|
||||
|
||||
Add to `MsgStoreClass`:
|
||||
|
||||
```haskell
|
||||
tryPeekMsgs :: s -> [StoreQueue s] -> ExceptT ErrorType IO (Map RecipientId Message)
|
||||
```
|
||||
|
||||
Returns a map from recipient ID to earliest pending message for each queue that has one. Queues with no messages are absent from the map.
|
||||
|
||||
### Step 2: Parameterize `deliver` to accept pre-fetched message
|
||||
|
||||
File: `src/Simplex/Messaging/Server.hs`
|
||||
|
||||
Currently `deliver` (inside `subscribeQueueAndDeliver`, line 1641) calls `tryPeekMsg ms q`. Add a parameter for an optional pre-fetched message:
|
||||
|
||||
```haskell
|
||||
deliver :: Maybe Message -> (Bool, Maybe Sub) -> M s ResponseAndMessage
|
||||
deliver prefetchedMsg (hasSub, sub_) = do
|
||||
stats <- asks serverStats
|
||||
fmap (either ((,Nothing) . err) id) $ liftIO $ runExceptT $ do
|
||||
msg_ <- maybe (tryPeekMsg ms q) (pure . Just) prefetchedMsg
|
||||
...
|
||||
```
|
||||
|
||||
When `Nothing` is passed, falls back to individual `tryPeekMsg` (existing behavior). When `Just msg` is passed, uses it directly (batched path).
|
||||
|
||||
### Step 3: Pre-fetch messages before the processing loop
|
||||
|
||||
File: `src/Simplex/Messaging/Server.hs`
|
||||
|
||||
Currently (lines 1372-1375):
|
||||
|
||||
```haskell
|
||||
forever $
|
||||
atomically (readTBQueue rcvQ)
|
||||
>>= foldrM process ([], [])
|
||||
>>= \(rs_, msgs) -> ...
|
||||
```
|
||||
|
||||
Add a pre-fetch step before the existing loop:
|
||||
|
||||
```haskell
|
||||
forever $ do
|
||||
batch <- atomically (readTBQueue rcvQ)
|
||||
msgMap <- prefetchMsgs batch
|
||||
foldrM (process msgMap) ([], []) batch
|
||||
>>= \(rs_, msgs) -> ...
|
||||
```
|
||||
|
||||
`prefetchMsgs` scans the batch, collects queues from SUB commands that have a verified queue (`q_ = Just (q, _)`), calls `tryPeekMsgs` once, returns the map. For batches with no SUBs it returns an empty map (no DB call).
|
||||
|
||||
`process` passes the looked-up message (or Nothing) through to `processCommand` and down to `deliver`.
|
||||
|
||||
The `foldrM process` loop, `processCommand`, `subscribeQueueAndDeliver`, and all other command handlers stay structurally the same. Only `deliver` gains one parameter, and the `client` loop gains one pre-fetch call.
|
||||
|
||||
### Step 4: Review
|
||||
|
||||
Review the typeclass signature and server usage. Confirm the interface has the right shape before implementing store backends.
|
||||
|
||||
### Step 5: Implement for each store backend
|
||||
|
||||
#### Postgres
|
||||
|
||||
File: `src/Simplex/Messaging/Server/MsgStore/Postgres.hs`
|
||||
|
||||
Single query using `DISTINCT ON`:
|
||||
|
||||
```sql
|
||||
SELECT DISTINCT ON (recipient_id)
|
||||
recipient_id, msg_id, msg_ts, msg_quota, msg_ntf_flag, msg_body
|
||||
FROM messages
|
||||
WHERE recipient_id IN ?
|
||||
ORDER BY recipient_id, message_id ASC
|
||||
```
|
||||
|
||||
Build `Map RecipientId Message` from results.
|
||||
|
||||
#### STM
|
||||
|
||||
File: `src/Simplex/Messaging/Server/MsgStore/STM.hs`
|
||||
|
||||
Loop over queues, call `tryPeekMsg` for each, collect into map.
|
||||
|
||||
#### Journal
|
||||
|
||||
File: `src/Simplex/Messaging/Server/MsgStore/Journal.hs`
|
||||
|
||||
Loop over queues, call `tryPeekMsg` for each, collect into map.
|
||||
|
||||
### Step 6: Handle edge cases
|
||||
|
||||
1. **Mixed batches**: `prefetchMsgs` collects only SUB queues. Non-SUB commands get Nothing for the pre-fetched message and process unchanged.
|
||||
|
||||
2. **Already-subscribed queues**: Include in pre-fetch - `deliver` is called for re-SUBs too (delivers pending message).
|
||||
|
||||
3. **Service subscriptions**: The pre-fetch doesn't care about service state. `sharedSubscribeQueue` handles service association in STM; message peek is the same.
|
||||
|
||||
4. **Error queues**: Verification errors from `receive` are Left values in the batch. `prefetchMsgs` only looks at Right values with SUB commands.
|
||||
|
||||
5. **Empty pre-fetch**: If batch has no SUBs (e.g., all ACKs), `prefetchMsgs` returns empty map, no DB call made.
|
||||
|
||||
### Step 7: Batch other commands (future, not in scope)
|
||||
|
||||
The same pattern (pre-fetch before loop, parameterize handler) can extend to:
|
||||
- `ACK` with `tryDelPeekMsg` - batch delete+peek
|
||||
- `GET` with `tryPeekMsg` - same map lookup
|
||||
|
||||
Lower priority since these don't have the N-at-once pattern of subscriptions.
|
||||
|
||||
## File changes summary
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/Simplex/Messaging/Server/MsgStore/Types.hs` | Add `tryPeekMsgs` to typeclass |
|
||||
| `src/Simplex/Messaging/Server/MsgStore/Postgres.hs` | Implement `tryPeekMsgs` with batch SQL |
|
||||
| `src/Simplex/Messaging/Server/MsgStore/STM.hs` | Implement `tryPeekMsgs` as loop |
|
||||
| `src/Simplex/Messaging/Server/MsgStore/Journal.hs` | Implement `tryPeekMsgs` as loop |
|
||||
| `src/Simplex/Messaging/Server.hs` | Add `prefetchMsgs`, parameterize `deliver` |
|
||||
|
||||
## Testing
|
||||
|
||||
1. Existing server tests must pass unchanged (correctness preserved).
|
||||
2. Add a test that subscribes a batch of queues (some with pending messages, some without) and verifies all get correct SOK + MSG responses.
|
||||
3. Prometheus metrics: existing `qSub` stat should still increment correctly.
|
||||
|
||||
## Performance expectation
|
||||
|
||||
For 300K queues across ~2200 batches:
|
||||
- Before: ~300K individual DB queries
|
||||
- After: ~2200 batched DB queries (one per batch of ~135)
|
||||
- ~136x reduction in DB round-trips
|
||||
@@ -0,0 +1,126 @@
|
||||
# Server: batch queue service associations
|
||||
|
||||
When a batch of SUB or NSUB commands arrives from a service client, each command that needs a new or removed service association calls `setQueueService` individually - one DB write per command. For 135 commands per batch, that's 135 individual `UPDATE msg_queues` queries.
|
||||
|
||||
## Goal
|
||||
|
||||
Reduce to at most 2 DB queries per batch (one for rcv associations, one for ntf associations), using `UPDATE ... RETURNING recipient_id` to identify which queues were actually updated.
|
||||
|
||||
Also fuse message pre-fetch and association batching into a single batch preparation step with a clean contract.
|
||||
|
||||
## Contract
|
||||
|
||||
```haskell
|
||||
prepareBatch :: Maybe ServiceId -> NonEmpty (VerifiedTransmission s) -> M s (Either ErrorType (Map RecipientId (Maybe Message, Maybe (Either ErrorType ()))))
|
||||
```
|
||||
|
||||
`Left e` = batch-level failure (message pre-fetch or association query failed entirely). All SUBs/NSUBs in the batch get this error.
|
||||
|
||||
`Right map` = per-queue results as a tuple:
|
||||
- `Maybe Message` - pre-fetched message for SUB queues, `Nothing` for NSUB or no message
|
||||
- `Maybe (Either ErrorType ())` - association result. `Nothing` = no update needed. `Just (Right ())` = update succeeded. `Just (Left e)` = update failed for this queue.
|
||||
|
||||
One map, one lookup per queue. `processCommand` passes both values to `subscribeQueueAndDeliver` / `subscribeNotifications` -> `sharedSubscribeQueue`.
|
||||
|
||||
Queues not in the map (non-SUB/NSUB commands, failed verification) are not affected.
|
||||
|
||||
## prepareBatch implementation
|
||||
|
||||
One accumulating fold over the batch, collecting three lists:
|
||||
- `subMsgQs :: [StoreQueue s]` - SUB queues for message pre-fetch
|
||||
- `rcvAssocQs :: [StoreQueue s]` - SUB queues needing `rcv_service_id` update (`clntServiceId /= rcvServiceId qr`)
|
||||
- `ntfAssocQs :: [StoreQueue s]` - NSUB queues needing `ntf_service_id` update (`clntServiceId /= ntfServiceId` from `NtfCreds`)
|
||||
|
||||
Classification reads from the already-loaded `QueueRec` in `VerifiedTransmission` - no extra DB query.
|
||||
|
||||
Then three store calls (each skipped if its list is empty):
|
||||
1. `tryPeekMsgs ms subMsgQs` -> `Map RecipientId Message`
|
||||
2. `setRcvQueueServices (queueStore ms) clntServiceId rcvAssocQs` -> `Set RecipientId`
|
||||
3. `setNtfQueueServices (queueStore ms) clntServiceId ntfAssocQs` -> `Set RecipientId`
|
||||
|
||||
Then one pass to merge results into `Map RecipientId (Maybe Message, Maybe (Either ErrorType ()))`:
|
||||
- For each SUB queue: `(M.lookup rId msgMap, assocResult rId rcvUpdated rcvAssocQs)`
|
||||
- For each NSUB queue: `(Nothing, assocResult rId ntfUpdated ntfAssocQs)`
|
||||
|
||||
Where `assocResult rId updated assocQs` = if the queue was in `assocQs` (needed update), then `Just (Right ())` if `rId` is in `updated`, else `Just (Left AUTH)`. If not in `assocQs` (no update needed), `Nothing`.
|
||||
|
||||
If any of the three calls fails entirely, return `Left e`.
|
||||
|
||||
## Store interface
|
||||
|
||||
Replace the polymorphic `setQueueServices` with two plain functions in `QueueStoreClass`:
|
||||
|
||||
```haskell
|
||||
setRcvQueueServices :: s -> Maybe ServiceId -> [q] -> IO (Set RecipientId)
|
||||
setNtfQueueServices :: s -> Maybe ServiceId -> [q] -> IO (Set RecipientId)
|
||||
```
|
||||
|
||||
No `SParty p` polymorphism. Each function knows its column.
|
||||
|
||||
### Postgres implementation
|
||||
|
||||
`setRcvQueueServices`:
|
||||
```sql
|
||||
UPDATE msg_queues SET rcv_service_id = ?
|
||||
WHERE recipient_id IN ? AND deleted_at IS NULL
|
||||
RETURNING recipient_id
|
||||
```
|
||||
|
||||
`setNtfQueueServices`:
|
||||
```sql
|
||||
UPDATE msg_queues SET ntf_service_id = ?
|
||||
WHERE recipient_id IN ? AND notifier_id IS NOT NULL AND deleted_at IS NULL
|
||||
RETURNING recipient_id
|
||||
```
|
||||
|
||||
After each batch query, for each queue in the returned set:
|
||||
1. Read QueueRec TVar, update with new serviceId
|
||||
2. Write store log entry
|
||||
|
||||
### STM implementation
|
||||
|
||||
Loop over queues, call existing per-item logic, collect succeeded `RecipientId`s into a Set.
|
||||
|
||||
## Downstream changes in Server.hs
|
||||
|
||||
### processCommand
|
||||
|
||||
Gains one parameter: `Map RecipientId (Maybe Message, Maybe (Either ErrorType ()))`.
|
||||
|
||||
SUB case: `M.lookup entId prepared` gives `Just (msg_, assocResult)` or `Nothing`. Pass both to `subscribeQueueAndDeliver`.
|
||||
|
||||
NSUB case: `M.lookup entId prepared` gives `Just (Nothing, assocResult)` or `Nothing`. Pass `assocResult` to `subscribeNotifications`.
|
||||
|
||||
Forwarded commands: pass `M.empty`.
|
||||
|
||||
### subscribeQueueAndDeliver
|
||||
|
||||
Takes `Maybe Message` and `Maybe (Either ErrorType ())` as before. No change in how it uses them.
|
||||
|
||||
### sharedSubscribeQueue
|
||||
|
||||
Takes `Maybe (Either ErrorType ())`. On paths needing association update:
|
||||
- `Just (Left e)` -> return error
|
||||
- `Just (Right ())` -> skip `setQueueService`, proceed with STM work
|
||||
- `Nothing` -> no update needed, proceed with existing logic
|
||||
|
||||
## Implementation order (top-down)
|
||||
|
||||
1. Define the `prepareBatch` contract and thread one map through `processCommand` -> `subscribeQueueAndDeliver` / `subscribeNotifications` -> `sharedSubscribeQueue` (Server.hs)
|
||||
2. Implement `prepareBatch` with the fold, three calls, and merge (Server.hs)
|
||||
3. Add `setRcvQueueServices` and `setNtfQueueServices` to `QueueStoreClass` (Types.hs)
|
||||
4. Implement for Postgres with batch `UPDATE ... RETURNING` (Postgres.hs)
|
||||
5. Implement for STM as loop (STM.hs)
|
||||
6. Implement for Journal as delegation (Journal.hs)
|
||||
|
||||
At step 2, store functions can initially be stubs returning empty sets. Steps 3-6 fill in the real implementations.
|
||||
|
||||
## Files changed
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/Simplex/Messaging/Server.hs` | `prepareBatch` with fold + merge; one map parameter through `processCommand` -> `subscribeQueueAndDeliver` / `subscribeNotifications` -> `sharedSubscribeQueue` |
|
||||
| `src/Simplex/Messaging/Server/QueueStore/Types.hs` | Add `setRcvQueueServices`, `setNtfQueueServices` to `QueueStoreClass` |
|
||||
| `src/Simplex/Messaging/Server/QueueStore/Postgres.hs` | Implement with batch `UPDATE ... RETURNING` + per-item TVar/log updates |
|
||||
| `src/Simplex/Messaging/Server/QueueStore/STM.hs` | Implement as loop |
|
||||
| `src/Simplex/Messaging/Server/MsgStore/Journal.hs` | Delegate to underlying store |
|
||||
@@ -0,0 +1,180 @@
|
||||
|
||||
# SMP router message storage
|
||||
|
||||
## Problem
|
||||
|
||||
Currently SMP routers store all queues in router memory. As the traffic grows, so does the number of undelivered messages. What is worse, Haskell is not avoiding heap fragmentation when messages are allocated and then de-allocated - undelivered messages use ByteString and GC cannot move them around, as they use pinned memory.
|
||||
|
||||
## Possible solutions
|
||||
|
||||
### Solution 1: solve only GC fragmentation problem
|
||||
|
||||
Move from ByteString to some other primitive to store messages in memory long term, e.g. ShortByteString, or manage allocation/de-allocation of stored messages manually in some other way.
|
||||
|
||||
Pros: the simplest solution that avoids substantial re-engineering of the router.
|
||||
|
||||
Cons:
|
||||
- not a long term solution, as memory growth still has limits.
|
||||
- may be ineffective, as it introduces additional copying of bytes.
|
||||
|
||||
### Solution 2: move message storage to hard drive
|
||||
|
||||
Use files or RocksDB to store messages.
|
||||
|
||||
Pros:
|
||||
- much lower memory usage.
|
||||
- no message loss in case of abnormal router termination (important until clients have delivery redundancy).
|
||||
- this is a long term solution, and at some point it might need to be done anyway.
|
||||
|
||||
Cons:
|
||||
- substantial re-engineering costs and risks.
|
||||
- metadata privacy. Currently we only save undelivered messages when router is restarted, with this approach all messages will be stored for some time. this argument is limited, as hosting providers of VMs can make memory snapshots too, on the other hand they are harder to analyze than files. On another hand, with this approach messages will be stored for a shorter time.
|
||||
|
||||
#### RocksDB and other key-value stores
|
||||
|
||||
The downside of any key-value stores is that they don't seem to have efficient primitives for sequential delivery. While sequential delivery can be modelled with linked lists, they would require 1 key insert (on send), 3 key updates (1 update to update queue data on send, 1 update of the last message to point to the next, 1 update on delivery or message expiration) and 1 key deletion (on delivery or message expiration) for each delivered message.
|
||||
|
||||
This might result in substantial write amplification and compacting costs.
|
||||
|
||||
In general, tree structures that are efficient for quick lookups and updates, given approximately fixed value size, are inefficient for modelling queues.
|
||||
|
||||
#### Files
|
||||
|
||||
The upside of files is that they are well suited for sequential delivery and don't result in the same churn, with careful design, as trees do.
|
||||
|
||||
The downside of filesystem is that it does not scale well with the large number of files in a folder, so queues will have to be spread across multiple folders, following tree-like structure.
|
||||
|
||||
I could not find an available library that efficiently models sequential delivery in highly concurrent environment.
|
||||
|
||||
A possible design could be the following.
|
||||
|
||||
##### Queue folder and files
|
||||
|
||||
Each message queue is stored in its own folder (see below on folder locations). Folder would contain these files:
|
||||
|
||||
- `messages.abcd.log` - the file that is used to read messages from, sequentially
|
||||
- `messages.efgh.log` - the optional file that is used to write messages, in case it is different from read file.
|
||||
- `queue.log` - append-only file where the last line represents the current queue state
|
||||
- `queue.timestamp.log` - previous states of queue.log file
|
||||
|
||||
Each line in "queue.log" file has this syntax
|
||||
|
||||
```abnf
|
||||
queueLogLine =
|
||||
%s"read_file=" base64
|
||||
%s"read_msg=" digits
|
||||
%s"read_byte=" digits
|
||||
%s"write_file=" base64
|
||||
%s"write_msg=" digits
|
||||
```
|
||||
|
||||
When queue is first requested by the router:
|
||||
|
||||
```c
|
||||
if queue folder exists:
|
||||
read queue state from last line of queue.log
|
||||
if queue.log contained more than one line: // compaction
|
||||
copy queue.log to queue.timestamp.log
|
||||
write one line queue state to queue.log
|
||||
else:
|
||||
create queue folder
|
||||
create messages.abcd.log (abcd is some random string)
|
||||
read_msg = 0
|
||||
read_byte = 0
|
||||
create queue.log with one line: "read_file=abcd read_msg=0 read_byte=0 write_files=abcd write_msg=0"
|
||||
open read_file in ReadMode and seek to read_byte position
|
||||
nextReadByte = read_byte
|
||||
nextReadMsg = read_msg
|
||||
open write_file in AppendMode
|
||||
```
|
||||
|
||||
When message is added to the queue (assumes that queue state is loaded to router memory, if not the previous section will be done first):
|
||||
|
||||
```c
|
||||
if write_msg > max_queue_messages:
|
||||
return quota error
|
||||
else if write_msg = max_queue_messages:
|
||||
add quota_exceeded message to write_file
|
||||
update queue state: write_msg += 1
|
||||
append updated queue state to queue.log
|
||||
else
|
||||
// It is required that `max_queue_messages < max_file_messages`,
|
||||
// so that we never need more than one additional write file.
|
||||
if write_msg >= max_file_messages: // queue file rotation
|
||||
create messages.efgh.log // efgh is some random string
|
||||
update queue state: write_file=efgh write_msg=0 // read file remains the same as it was
|
||||
append updated queue state to queue.log
|
||||
copy queue.log to queue.timestamp.log
|
||||
// `old` needs to be defined to limit the number and storage duration,
|
||||
// preserving not more than N files, and not more than M days files, "and then some"
|
||||
// (that is if the queue has high churn, we have file from M days before in any case, for any debugging).
|
||||
delete `old` `queue.timestamp.log` files
|
||||
write one line queue state to queue.log // compaction
|
||||
|
||||
add message to write_file
|
||||
update queue state: write_msg += 1
|
||||
append updated queue state to queue.log
|
||||
```
|
||||
|
||||
The above algorithm assumes `max_queue_messages < than max_file_messages`, so that we never need more than one write file.
|
||||
|
||||
When message is delivered, it is simply read from the read queue, queue state does not change yet:
|
||||
|
||||
```c
|
||||
if nextReadMsg > read_msg:
|
||||
deliver cached message, no need to read it again
|
||||
else
|
||||
read message from read_file handle
|
||||
nextReadMsg = read_msg + 1
|
||||
nextReadByte = current position in file
|
||||
```
|
||||
|
||||
When message delivery is acknowledged, the read queue needs to be advanced, and possibly switched to read from the current write queue:
|
||||
|
||||
```c
|
||||
if nextReadByte == read_byte:
|
||||
return error // nothing was delivered
|
||||
else if nextReadByte = EOF:
|
||||
// end of file is reached, possibly some other condition,
|
||||
// but it should allow changing max_file_messages on server restart
|
||||
currReadFile = read_file
|
||||
read_file = write_file
|
||||
read_msg = 0
|
||||
read_byte = 0
|
||||
append updated queue state to queue.log
|
||||
delete currReadFile
|
||||
else
|
||||
read_msg += 1
|
||||
read_byte = nextReadByte
|
||||
// `seek` should not be necessary, as the handle is already at nextReadByte position here
|
||||
// seek to read_byte
|
||||
append updated queue state to queue.log
|
||||
```
|
||||
|
||||
The above algorithm delegates the problem of compaction and fragmentation management to file system, that is very optimized for such scenarios.
|
||||
|
||||
Also, read and write files will grow to almost a constant size, so the space they used may be re-used.
|
||||
|
||||
An important consideration is that writes to queue.log and message.log files and queue state modifications have to be sequential, without concurrency - it can be managed with the usual locks.
|
||||
|
||||
##### Queue folders structure
|
||||
|
||||
Most Linux systems use EXT4 filesystem where the file lookup time scales linearly to the number of files. While alternatives with logarithmic lookup time exist (XFS), they may be very complex to configure on the existing systems.
|
||||
|
||||
So storing all queue folders in one folder won't scale.
|
||||
|
||||
To solve this problem we could use recipient queue ID in base64url format not as a folder name, but as a folder path, splitting it to path fragments of some length. The number of fragments can be configurable and migration to a different fragment size can be supported as the number of queues on a given router grows.
|
||||
|
||||
Currently, queue ID is 24 bytes random number, thus allowing 2^192 possible queue IDs. If we assume that a router must hold 1b queues, it means that we have ~2^162 possible addresses for each existing queue. 24 bytes in base64 is 32 characters that can be split into say 8 fragments with 4 characters each, so that queue folder path for queue with ID `abcdefghijklmnopqrstuvwxyz012345` would be:
|
||||
|
||||
`/var/opt/simplex/messages/abcd/efgh/ijkl/mnop/qrst/uvwx/yz01/2345`
|
||||
|
||||
The maximum theoretic number of the folders on the 1st level is 64^4, or 2^24 ~ 16m - this is probably still a large number of subfolders for EXT4. Given that addresses are random, all the possible combinations in the first folder can be used with a large number of queues.
|
||||
|
||||
So we could use an unequal split of path, two letters each and the last being long:
|
||||
|
||||
`/var/opt/simplex/messages/ab/cd/ef/ghijklmnopqrstuvwxyz012345`
|
||||
|
||||
The first three levels in this case can have 4096 subfolders each, and it gives 68b possible subfolders (64^2^3), so the last level will be sparse in case of 1b queues on the router. So we could make it 4 levels with 2 letters to never think about it, accounting for a large variance of the random numbers distribution:
|
||||
|
||||
`/var/opt/simplex/messages/ab/cd/ef/gh/ijklmnopqrstuvwxyz012345`
|
||||
@@ -0,0 +1,162 @@
|
||||
|
||||
# Sharing protocol ports with HTTPS
|
||||
|
||||
Some networks block all ports other than web ports, including port 5223 used for SMP protocol by default. Running SMP routers on a common web port 443 would allow them to work on more networks. The routers would need to provide an HTTPS page for browsers (and probes).
|
||||
|
||||
## Problem
|
||||
|
||||
Browsers and tools rely on system CA bundles instead of certificate pinning.
|
||||
The crypto parameters used by HTTPS are different from what the protocols use.
|
||||
Public certificate providers like LetsEncrypt can only sign specific types of keys and Ed25519 isn't one of them.
|
||||
|
||||
This means a router should distinguish browser and protocol clients and adjust its behavior to match.
|
||||
|
||||
## Solution
|
||||
|
||||
`tls` package has a server hook that allows producing a different set of `TLS.Credentials` according to a client-provided "Server Name Indication" extension.
|
||||
|
||||
Since LE certificates are only handed out to domain names, TLS client will be sending the SNI.
|
||||
However client transports are constructed over connected sockets and the SNI wouldn't be present unless explicitly requested.
|
||||
When a client sends SNI, then it's a browser and web credentials should be used.
|
||||
Otherwise it's a protocol client to be offered the self-signed ca, cert and key.
|
||||
|
||||
When a transport colocated with a HTTPS, its ALPN list should be extended with `h2 http/1.1`.
|
||||
The browsers will send it, and it should be checked before running transport client.
|
||||
If HTTP ALPN is detected, then the client connection is served with HTTP `Application` instead (the same "router information" page).
|
||||
|
||||
If some client connects to router IP, doesn't send SNI and doesn't send ALPN, it will look like a pre-handshake client.
|
||||
In that case a router will send its handshake first.
|
||||
This can be mitigated by delaying its handshake and letting the probe to issue its HTTP request.
|
||||
|
||||
## Implementation plan
|
||||
|
||||
An unmodified client should be able to use protocols on port 443 right away.
|
||||
|
||||
The switchover happens inside `runTransportServerState` before `runClient`:
|
||||
|
||||
```haskell
|
||||
runServer (tcpPort, ATransport t) = do
|
||||
-- ...
|
||||
runTransportServerState_ ss started tcpPort serverParams tCfg $ \socket h -> do -- expose raw socket for warp-tls internals to attach
|
||||
negotiated <- getSessionALPN
|
||||
if allowHTTP t && isHTTP negotiated -- only attempt the switch for the TLS transport
|
||||
then runHTTP socket (tlsContext h)-- ... collect data and produce values needed to run WAI Application
|
||||
else runClient serverSignKey t h `runReaderT` env -- performs serverHandshake etc as usual
|
||||
```
|
||||
|
||||
The web app and router live outside, so `runHttp` has to be provided by the `runSMPServer` caller.
|
||||
Additonally, Warp is using its `InternalInfo` object that's scoped to `withII` bracket.
|
||||
|
||||
```haskell
|
||||
runServer ini = do
|
||||
-- ...
|
||||
|
||||
runWebServer ini ServerInformation {config, information} $ if sharedHttps then Nothing else webHttpsParams -- suppress serving https
|
||||
if sharedHttps
|
||||
then withRunHTTP staticFilesPath \attachStatic -> runSMPServer cfg (Just attachStatic) -- provide wrapped application runner
|
||||
else runSMPServer cfg Nothing
|
||||
```
|
||||
|
||||
### Upstream
|
||||
|
||||
The implementation relies on a few modification to upstream code:
|
||||
- `warp-tls`: The library provides `httpOverTls`, but it wants to do handshake itself.
|
||||
Since we have to do the handshake to switch on ALPN, the setup function has to be split.
|
||||
This is a resonable change that may be upstreamed and nothing blocks us from using the recent version.
|
||||
- `warp`: Only the re-export of `serveConnection` is needed.
|
||||
Unfortunately the most recent `warp` version can't be used right away due to dependency cascade around `http-5` and `auto-update-2`.
|
||||
So a fork containing the backported re-export has to be used until the dependencies are refreshed.
|
||||
### TLS.ServerParams
|
||||
|
||||
When a router has port sharing enabled, a new set of TLS params is loaded and combined with transport params:
|
||||
|
||||
```haskell
|
||||
newEnv config = do
|
||||
-- ...
|
||||
tlsServerParams <- loadTLSServerParams caCertificateFile certificateFile privateKeyFile (alpn transportConfig)
|
||||
sharedServerParams <- forM ((,) <$> sharedHttpsCredentials config <*> alpn transportConfig) $ \((chain, key), alpn) ->
|
||||
let ca = Nothing -- It is possible to provide CA certificate, but it is typical for web server to use combined certificate chains
|
||||
loadHTTPSServerParams tlsServerParams ca chain key alpn
|
||||
```
|
||||
|
||||
`loadHTTPSServerParams` extends params with:
|
||||
1. `onALPNClientSuggest` hook gets `["h2", "http/1.1"]` added to the ALPN list which is now required.
|
||||
2. `onServerNameIndication` hook added, which upon detecting client SNI prepends the web credentials.
|
||||
3. `sharedCredentials = T.Credentials []` should be done to prevent transport credentials confusing browsers.
|
||||
But that aborts key exchange somewhere in tls internals, so disabled for now.
|
||||
As a workaround, another set of dummy credentials can be provided in the hope that any sane browser would reject them.
|
||||
Like, RC4 ciphers, "impossible" digest combination, etc.
|
||||
|
||||
### supportedParameters
|
||||
|
||||
TLS certificate chains provided by LetsEncrypt use ECDSA/P256 and that requires extending `supportedParameters` with things disabled in transports:
|
||||
|
||||
```haskell
|
||||
browserCiphers =
|
||||
[ TE.cipher_TLS13_AES128CCM8_SHA256
|
||||
, TE.cipher_ECDHE_ECDSA_AES128CCM8_SHA256
|
||||
, TE.cipher_ECDHE_ECDSA_AES256CCM8_SHA256
|
||||
]
|
||||
browserGroups =
|
||||
[ T.P256
|
||||
]
|
||||
browserSigs =
|
||||
[ (T.HashSHA256, T.SignatureECDSA),
|
||||
(T.HashSHA384, T.SignatureECDSA)
|
||||
]
|
||||
```
|
||||
|
||||
This may not be enough for other certificate providers.
|
||||
|
||||
## Configuration
|
||||
|
||||
> XXX: This is for the current implementation and should be updated.
|
||||
|
||||
Web certificate chain is picked up from the WEB section:
|
||||
|
||||
```ini
|
||||
[TRANSPORT]
|
||||
port: 443
|
||||
|
||||
[WEB]
|
||||
https: 443
|
||||
cert: /etc/opt/simplex/web.cert
|
||||
key: /etc/opt/simplex/web.key
|
||||
|
||||
# Alternatively, with a proper access configuration, the paths can point to the LE creds directly:
|
||||
# cert: /etc/letsencrypt/live/smp.hostname.tld/fullchain.pem
|
||||
# key: /etc/letsencrypt/live/smp.hostname.tld/privkey.pem
|
||||
```
|
||||
|
||||
When `TRANSPORT.port` matches `WEB.https` the transport router becomes shared.
|
||||
|
||||
Perhaps a more desirable option would be explicit configuration resulting in additional transported to run:
|
||||
|
||||
```ini
|
||||
[TRANSPORT]
|
||||
port: 5223 ; pure protocol transport
|
||||
# control_port: 5224
|
||||
shared_port: 443 ; variant 1: register in TRANSPORT
|
||||
|
||||
[WEB]
|
||||
https: 443
|
||||
cert: /etc/opt/simplex/web.cert
|
||||
key: /etc/opt/simplex/web.key
|
||||
# transport: on ; variant 2:
|
||||
```
|
||||
|
||||
## Caveats
|
||||
|
||||
Serving static files and the protocols together may pose a problem for those who currently use dedicated web servers as they should switch to embedded http handlers.
|
||||
|
||||
As before, using embedded HTTP server is increasing attack surface.
|
||||
|
||||
Users who want to run everything on a single host will have to add an extra IP address and bind routers to specific IPs instead of 0.0.0.0.
|
||||
An amalgamated router binary can be provided that would contain both SMP and XFTP routers, where transport will dispatch connections by handshake ALPN.
|
||||
|
||||
## Alternative: Use transports routable with reverse-proxies
|
||||
|
||||
An "industrial" reverse proxy may do the ALPN routing, serving HTTP by itself and delegating `smp` and `xftp` to protocol servers.
|
||||
Same with the `websockets`.
|
||||
|
||||
Since this in effect does TLS termination, the protocol routers will have to rely on credentials from protocol handshakes.
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
# Expiring messages in journal storage
|
||||
|
||||
## Problem
|
||||
|
||||
The journal storage routers recently migrated to do not delete delivered or expired messages, they only update pointers to journal file lines. The messages are actually deleted when the whole journal file is deleted (when fully deleted or fully expired).
|
||||
|
||||
The problem is that in case the queue stops receiving the new messages then writing of messages won't switch to the new journal file, and the current journal file containing delivered or expired messages would never be deleted.
|
||||
|
||||
## Solution
|
||||
|
||||
Remove current journal file and update queue_state.log during message expiration of "idle" queue (that is, without any new messages received or delivered within 3 hours) in case when:
|
||||
- the queue is "empty" after the expiration
|
||||
- the queue contains only quota marker(s), in which case move them to a new journal file and update the queue_state accordingly. Quota markers can be kept indefinitely to prevent writing the new messages to the dormant queues that reached capacity, so it's important to handle this case.
|
||||
|
||||
Also remove current journal file when the queue is opened in case it is empty (as it would not be ever expired in case it remains empty), and also update queue_state.log
|
||||
@@ -0,0 +1,416 @@
|
||||
|
||||
# Fix subQ deadlock: blocking writeTBQueue inside connLock
|
||||
|
||||
## Problem
|
||||
|
||||
Users report that message reception silently and permanently stops across all connections, with no error alerts. The app appears functional but no messages arrive. Recovery requires restart.
|
||||
|
||||
Root cause: a deadlock between worker threads holding `connLock` and the `agentSubscriber` (sole `subQ` reader).
|
||||
|
||||
### The deadlock mechanism
|
||||
|
||||
`subQ` (`TBQueue ATransmission`, capacity 4096 on mobile / 1024 on desktop) is the single pipeline between the agent layer and the chat layer. The `agentSubscriber` thread (`Commands.hs:4373`) is its **sole reader**.
|
||||
|
||||
Three code sites hold `connLock` and call blocking `writeTBQueue subQ` without a fullness check. When `subQ` is full, these block while holding the lock. If `agentSubscriber` simultaneously needs the same `connLock` (via `sendMessagesB_` → `withConnLocks`), it blocks too — creating a circular wait:
|
||||
|
||||
- **Worker**: holds `connLock(X)`, waits for `subQ` space (needs `agentSubscriber` to read)
|
||||
- **agentSubscriber**: sole `subQ` reader, waits for `connLock(X)` (needs worker to release)
|
||||
- **Result**: permanent silent deadlock — no exception, no alert, all connections blocked
|
||||
|
||||
### Confirmed deadlock scenarios
|
||||
|
||||
**Scenario 1**: Delivery worker during queue rotation test
|
||||
|
||||
```
|
||||
Delivery worker: agentSubscriber (sole subQ reader):
|
||||
withConnLock(X) [2187] readTBQueue subQ → processAgentMessageConn
|
||||
...DB operations... → sendPendingGroupMessages (on CON/SENT/QCONT)
|
||||
notify → writeTBQueue subQ [2238] → batchSendConnMessages → deliverMessagesB
|
||||
[BLOCKED — subQ full] → withAgent sendMessagesB [synchronous]
|
||||
→ sendMessagesB_ → withConnLocks({..X..}) [1708]
|
||||
[BLOCKED — connLock(X) held]
|
||||
```
|
||||
|
||||
**Scenario 2**: Async command worker during message ACK with notification
|
||||
|
||||
```
|
||||
Async cmd worker: agentSubscriber (sole subQ reader):
|
||||
tryWithLock "ICAck" [1930→1824] readTBQueue subQ → processAgentMessageConn
|
||||
→ withConnLock(X) → sendPendingGroupMessages
|
||||
→ ack → ackQueueMessage [1899] → sendMessagesB_ → withConnLocks({..X..})
|
||||
→ sendMsgNtf [2381] [BLOCKED — connLock(X) held]
|
||||
→ writeTBQueue subQ [2386]
|
||||
[BLOCKED — subQ full]
|
||||
```
|
||||
|
||||
**Scenario 3**: Synchronous `ackMessage'` API (same mechanism as Scenario 2 but from external API caller)
|
||||
|
||||
```
|
||||
ackMessage' caller: agentSubscriber (sole subQ reader):
|
||||
withConnLock(X) [2254] → sendMessagesB_ → withConnLocks({..X..})
|
||||
→ ack → ackQueueMessage [2267] [BLOCKED — connLock(X) held]
|
||||
→ sendMsgNtf [2381]
|
||||
→ writeTBQueue subQ [2386]
|
||||
[BLOCKED — subQ full]
|
||||
```
|
||||
|
||||
### ConnId overlap verified
|
||||
|
||||
No guard prevents a connection undergoing queue rotation (AM_QTEST_) or ACK processing from being included in `sendMessagesB_`'s batch. During these operations, the connection has `connStatus == ConnReady`, passing all filters in `memberSendAction`.
|
||||
|
||||
### Cascade amplification
|
||||
|
||||
Once any single deadlock triggers, `subQ` never drains. ALL other threads that attempt `writeTBQueue subQ` block progressively — their locks are held forever too. The entire threading system freezes within seconds.
|
||||
|
||||
### Affected code sites (blocking `writeTBQueue subQ` inside `connLock`)
|
||||
|
||||
| Site | File | Lock line | Write line | Events written |
|
||||
|------|------|-----------|------------|----------------|
|
||||
| `runSmpQueueMsgDelivery::notify` | Agent.hs | 2187 | 2238 | SWITCH SPCompleted, ERR INTERNAL |
|
||||
| `runSmpQueueMsgDelivery::internalErr/notifyDel` | Agent.hs | 2187 | 2238 (via notifyDel→notify) | ERR INTERNAL + delMsg |
|
||||
| `ackQueueMessage::sendMsgNtf` | Agent.hs | 2254 or 1930 | 2386 | MSGNTF |
|
||||
|
||||
### Safe patterns that already exist in the codebase
|
||||
|
||||
1. **`isFullTBQueue` + pending TVar** (used at `runCommandProcessing` lines 1782-1784/1937, and `runProcessSMP` lines 3027-3029/3216):
|
||||
```haskell
|
||||
-- Before processing (e.g. line 1782):
|
||||
pending <- newTVarIO []
|
||||
-- During processing — safe notify (e.g. line 1937):
|
||||
notify cmd =
|
||||
let t = (corrId, connId, AEvt (sAEntity @e) cmd)
|
||||
in atomically $ ifM (isFullTBQueue subQ) (modifyTVar' pendingCmds (t :)) (writeTBQueue subQ t)
|
||||
-- After processing — flush (e.g. line 1784):
|
||||
mapM_ (atomically . writeTBQueue subQ) . reverse =<< readTVarIO pending
|
||||
```
|
||||
|
||||
2. **`nonBlockingWriteTBQueue`** (used at Client.hs:789, NtfSubSupervisor.hs:507):
|
||||
```haskell
|
||||
nonBlockingWriteTBQueue q x = do
|
||||
sent <- atomically $ tryWriteTBQueue q x
|
||||
unless sent $ void $ forkIO $ atomically $ writeTBQueue q x
|
||||
```
|
||||
Note: `nonBlockingWriteTBQueue` does NOT preserve ordering — the spawned background thread may complete out of order relative to subsequent direct writes from the same calling thread.
|
||||
|
||||
### Exhaustive proof: no other deadlock scenarios exist
|
||||
|
||||
All 15 `withConnLock` sites in Agent.hs were analyzed. Only 3 write to `subQ`:
|
||||
|
||||
| withConnLock site | Writes subQ? | Safe? |
|
||||
|-------------------|-------------|-------|
|
||||
| switchConnectionAsync' (899) | No | ✓ |
|
||||
| setConnShortLinkAsync' (995) | No | ✓ |
|
||||
| setConnShortLink' (1031) | No | ✓ |
|
||||
| deleteConnShortLink' (1075) | No | ✓ |
|
||||
| allowConnection' (1407) | No | ✓ |
|
||||
| acceptContact' (1417) | No | ✓ |
|
||||
| sendMessagesB_ (1708, `withConnLocks`) | No | ✓ |
|
||||
| tryWithLock/runSmpCommand (1930) | Yes (1937) | ✓ — `isFullTBQueue` check |
|
||||
| tryMoveableWithLock/runSmpCommand (1931) | Yes (1937) | ✓ — `isFullTBQueue` check |
|
||||
| **runSmpQueueMsgDelivery AM_QTEST_ (2187)** | **Yes (2238)** | **✗ — DEADLOCK** |
|
||||
| **ackMessage' (2254)** | **Yes (2386)** | **✗ — DEADLOCK** |
|
||||
| switchConnection' (2298) | No | ✓ |
|
||||
| abortConnectionSwitch' (2328) | No | ✓ |
|
||||
| synchronizeRatchet' (2351) | No | ✓ |
|
||||
| suspendConnection' (2390) | No | ✓ |
|
||||
| **processSMP (3037)** | Yes (3216) | ✓ — `isFullTBQueue` check |
|
||||
|
||||
Note: `processSMP` (line 3037) holds `connLock` and its local `notify` (line 3216) writes to `subQ`, but it uses the safe `isFullTBQueue` pattern. Its `ack` (line 3196) uses `enqueueCmd` (DB-only), NOT `ackQueueMessage`. The actual `ackQueueMessage` runs later from the async command worker via ICAck/ICAckDel.
|
||||
|
||||
Other lock pairs checked — no circular dependencies:
|
||||
- `connLock × DB MVar`: DB never acquires connLock
|
||||
- `entityLock × connLock`: consistent ordering (entity first in chat, conn in agent)
|
||||
- `connLock(X) × connLock(Y)`: single agentSubscriber thread, one `withConnLocks` at a time
|
||||
|
||||
---
|
||||
|
||||
## Deadlock call graph: agentSubscriber → connLock
|
||||
|
||||
All deadlock paths require `agentSubscriber` to synchronously acquire `connLock`. Exhaustive analysis shows that **every such path converges on a single agent function**: `sendMessagesB_` → `withConnLocks` (Agent.hs:1708). No other agent API function called synchronously from the agentSubscriber acquires connLock.
|
||||
|
||||
Verified (FACT): `ackMessageAsync` → `enqueueCommand` only (no connLock). `toggleConnectionNtfs` → no lock. `deleteConnectionAsync` → `deleteLock` not `connLock`. `joinConnectionAsync` → `withInvLock` not `connLock`.
|
||||
|
||||
Also verified (FACT): `Lock = TMVar Text` (Lock.hs:24) is **non-reentrant** — double acquisition on the same thread deadlocks.
|
||||
|
||||
### All 22 trigger paths
|
||||
|
||||
Every path goes through `deliverMessage`/`deliverMessages`/`deliverMessagesB` → `withAgent sendMessagesB` → `sendMessagesB_` → `withConnLocks`:
|
||||
|
||||
| # | Trigger | Chat function | ConnIds locked | Risk |
|
||||
|---|---------|--------------|----------------|------|
|
||||
| 1 | Group CON (Invitee) | `introduceToAll` → broadcast XGrpMemNew | **ALL member connIds** | **HIGHEST** |
|
||||
| 2 | Group MSG XGrpLinkAcpt | `introduceToRemaining` → broadcast | **ALL member connIds** | **HIGHEST** |
|
||||
| 3 | Group CON (Invitee) | `sendIntroductions` → batch intros to new member | new member connId | Medium |
|
||||
| 4 | Group CON (Invitee) | `sendHistory` → batch to new member | new member connId | Medium |
|
||||
| 5 | Group CON | `sendPendingGroupMessages` | member connId | Medium |
|
||||
| 6 | Group SENT | `sendPendingGroupMessages` | member connId | Medium |
|
||||
| 7 | Group QCONT | `sendPendingGroupMessages` | member connId | Medium |
|
||||
| 8 | Group CON (PendingReview) | `introduceToModerators` → to moderators | moderator connIds | Medium |
|
||||
| 9 | Group CON (PreMember) | `sendXGrpMemCon` → to host | host connId | Low |
|
||||
| 10 | Group CON (PreMember) | `probeMatchingMemberContact` → probes + hashes | member + N matching connIds | Medium |
|
||||
| 11 | Direct CON | `probeMatchingMembers` → probes + hashes | contact + N matching connIds | Medium |
|
||||
| 12 | Direct JOINED | `sendAutoReply` | contact connId | Low |
|
||||
| 13 | Group JOINED | `sendGroupAutoReply` | member connId | Low |
|
||||
| 14 | Group INV | `sendXGrpMemInv` → to host | host connId | Low |
|
||||
| 15 | Group INV (legacy) | `sendGrpInvitation` → to contact | contact connId | Low |
|
||||
| 16 | Group MSG XGrpMemInv | `xGrpMemInv` → `sendGroupMemberMessage` | re-member connId | Low |
|
||||
| 17 | Group MSG XGrpMemDel | `forwardToMember` | deleted member connId | Low |
|
||||
| 18 | Group MSG XGrpLinkMem | `probeMatchingMemberContact` | member + N matching connIds | Medium |
|
||||
| 19 | Group MSG (dup relay) | `saveGroupRcvMsg` error → `sendDirectMemberMessage` | forwarder connId | Low |
|
||||
| 20 | SFDONE | `sendFileDescriptions` → to recipients | recipient connIds | Medium |
|
||||
| 21 | Group MSG XGrpLinkAcpt | `sendHistory` → to accepted member | accepted member connId | Medium |
|
||||
| 22 | Direct MSG (autoAccept) | `autoAcceptFile` → inline accept reply | contact connId | Low (test-only config) |
|
||||
|
||||
### Key observations
|
||||
|
||||
1. **Single bottleneck**: All 22 paths converge on `sendMessagesB_` → `withConnLocks` (Agent.hs:1708). The deadlock is between this lock acquisition and any worker thread holding `connLock` + blocking on `writeTBQueue subQ`.
|
||||
|
||||
2. **Highest-risk paths** (#1, #2): Broadcasting to ALL group members in `introduceToAll` / `introduceToRemaining` acquires `withConnLocks` on ALL member connIds in a single batch. For large groups, this holds the agentSubscriber thread for a long time, during which subQ fills, which causes worker threads holding connLock on any of those connIds to deadlock.
|
||||
|
||||
3. **Medium-risk paths** (#5-7): `sendPendingGroupMessages` fires on every CON/SENT/QCONT. These are frequent and lock the member's connId, which is the SAME connId that a delivery worker or ACK worker may hold while writing to subQ.
|
||||
|
||||
---
|
||||
|
||||
## Analysis: `withConnLocks` in `sendMessagesB_`
|
||||
|
||||
### FACT: the lock protects ratchet encryption state
|
||||
|
||||
`sendMessagesB_` (Agent.hs:1708-1713) acquires `withConnLocks` and executes:
|
||||
|
||||
1. **`getConn_`** — reads connection metadata, send queues from DB
|
||||
2. **`setConnPQSupport`** — updates PQ encryption flag per connection
|
||||
3. **`enqueueMessagesB`** → `enqueueMessageB` → `storeSentMsg_` which calls:
|
||||
- **`updateSndIds`** (AgentStore.hs:899) — increments `internalSndId` (sequential send counter)
|
||||
- **`agentRatchetEncryptHeader`** (Agent.hs:3698) — reads current ratchet via `getRatchetForUpdate`, encrypts message header via `rcEncryptHeader`, writes advanced ratchet state via `updateRatchet`
|
||||
- **`createSndMsg`** + **`createSndMsgDelivery`** — inserts message and delivery records
|
||||
|
||||
All operations run within `unsafeWithStore` → `withTransaction` (single DB transaction per batch).
|
||||
|
||||
### FACT: the lock CANNOT be removed
|
||||
|
||||
Without `withConnLocks`, concurrent `sendMessagesB_` calls targeting the same connection would:
|
||||
- Read the same ratchet state, both encrypt, one overwrite the other → **ratchet desync** (unrecoverable)
|
||||
- Get duplicate `internalSndId` values → **message ID collision**
|
||||
- Race on `setConnPQSupport` → **PQ state inconsistency**
|
||||
|
||||
The lock serializes ALL operations on the connection's encryption state. Removing it would introduce data corruption.
|
||||
|
||||
Note: `sendMessage` (singular, line 530) uses the same `sendMessagesB_` function — there is no lock-free send path.
|
||||
|
||||
### Eliminated strategies
|
||||
|
||||
- **Strategy C (remove lock)**: The lock protects ratchet encryption. Removing it causes unrecoverable ratchet desync. Eliminated.
|
||||
- **Strategy A (async dispatch)**: All 22 chat-layer callers use `deliverMessagesB` return values (delivery IDs, PQ state) synchronously. `forkIO` loses results. Eliminated.
|
||||
- **Strategy W (isFullTBQueue + pending TVar)**: The existing pattern (lines 1937, 3216) buffers events in a local TVar and flushes after lock release. Between lock release and flush, another thread can acquire the same connLock and write events to subQ — reordering events within the same connection. This trades a visible deadlock for invisible ordering bugs. Eliminated.
|
||||
- **Strategy O (per-connection overflow queues)**: Bounded overflow queues with "drop when full" were analyzed. Drop consequences are unacceptable at 5 of 6 write sites — CONF, INFO, CON cause permanent connection failure after ACK; INV loses connection invitations; SENT/MERR leave messages stuck forever. Unbounded overflow defeats backpressure. Eliminated.
|
||||
|
||||
---
|
||||
|
||||
## Solution: move subQ writes outside connLock
|
||||
|
||||
### Root cause
|
||||
|
||||
The `writeTBQueue subQ` calls at the 3 deadlock sites are inside `connLock` by accident of code structure, not necessity. `connLock` protects ratchet encryption state and DB consistency. The `notify` calls write informational events to `subQ` — they do not modify any state that `connLock` protects.
|
||||
|
||||
Moving the writes outside the lock scope eliminates the deadlock: blocking `writeTBQueue subQ` without holding `connLock` is safe — agentSubscriber is free to acquire the lock, process events, and drain `subQ`.
|
||||
|
||||
### Why reordering doesn't matter at these sites
|
||||
|
||||
The chat layer handlers for the 3 deadlock site events do NOT advance the ratchet:
|
||||
|
||||
| Event | Chat handler | Calls sendMessagesB_? |
|
||||
|-------|-------------|----------------------|
|
||||
| SWITCH SPCompleted | Creates internal chat item, updates UI | **No** |
|
||||
| ERR INTERNAL | Logs error to view | **No** |
|
||||
| MSGNTF | `toView CEvtNtfMessage` → empty output | **No** |
|
||||
|
||||
Events that DO trigger ratchet advances (CON, SENT, QCONT → `sendPendingGroupMessages` → `sendMessagesB_`) are all already written OUTSIDE `connLock` in the current code.
|
||||
|
||||
Ratchet state lives in the DB, not in subQ events. agentSubscriber processes events sequentially regardless of arrival order. The SENT-before-SWITCH race already exists in the current code (new queue worker writes SENT outside connLock while old queue worker writes SWITCH inside connLock).
|
||||
|
||||
### Fix: Site 1 — `runSmpQueueMsgDelivery` AM_QTEST_ (line 2187)
|
||||
|
||||
Restructure `withConnLock` to return the event, write outside.
|
||||
|
||||
**Current code** (Agent.hs:2187-2214):
|
||||
```haskell
|
||||
AM_QTEST_ -> withConnLock c connId "runSmpQueueMsgDelivery AM_QTEST_" $ do
|
||||
withStore' c $ \db -> setSndQueueStatus db sq Active
|
||||
SomeConn _ conn <- withStore c (`getConn` connId)
|
||||
case conn of
|
||||
DuplexConnection cData' rqs sqs -> do
|
||||
let addr = qAddress sq
|
||||
case findQ addr sqs of
|
||||
Just SndQueue {dbReplaceQueueId = Just replacedId, primary} ->
|
||||
case removeQP (\sq' -> dbQId sq' == replacedId && not (sameQueue addr sq')) sqs of
|
||||
Nothing -> internalErr msgId "sent QTEST: queue not found in connection"
|
||||
Just (sq', sq'' : sqs') -> do
|
||||
checkSQSwchStatus sq' SSSendingQTEST
|
||||
atomically $ TM.delete (qAddress sq') $ smpDeliveryWorkers c
|
||||
withStore' c $ \db -> do
|
||||
when primary $ setSndQueuePrimary db connId sq
|
||||
deletePendingMsgs db connId sq'
|
||||
deleteConnSndQueue db connId sq'
|
||||
let sqs'' = sq'' :| sqs'
|
||||
conn' = DuplexConnection cData' rqs sqs''
|
||||
cStats <- connectionStats c conn'
|
||||
notify $ SWITCH QDSnd SPCompleted cStats -- DEADLOCK
|
||||
_ -> internalErr msgId "sent QTEST: ..." -- DEADLOCK (via notifyDel → notify)
|
||||
_ -> internalErr msgId "sent QTEST: ..." -- DEADLOCK
|
||||
_ -> internalErr msgId "QTEST sent not in duplex ..." -- DEADLOCK
|
||||
```
|
||||
|
||||
**New code:**
|
||||
```haskell
|
||||
AM_QTEST_ -> do
|
||||
evt_ <- withConnLock c connId "runSmpQueueMsgDelivery AM_QTEST_" $ do
|
||||
withStore' c $ \db -> setSndQueueStatus db sq Active
|
||||
SomeConn _ conn <- withStore c (`getConn` connId)
|
||||
case conn of
|
||||
DuplexConnection cData' rqs sqs -> do
|
||||
let addr = qAddress sq
|
||||
case findQ addr sqs of
|
||||
Just SndQueue {dbReplaceQueueId = Just replacedId, primary} ->
|
||||
case removeQP (\sq' -> dbQId sq' == replacedId && not (sameQueue addr sq')) sqs of
|
||||
Nothing -> pure $ Left "sent QTEST: queue not found in connection"
|
||||
Just (sq', sq'' : sqs') -> do
|
||||
checkSQSwchStatus sq' SSSendingQTEST
|
||||
atomically $ TM.delete (qAddress sq') $ smpDeliveryWorkers c
|
||||
withStore' c $ \db -> do
|
||||
when primary $ setSndQueuePrimary db connId sq
|
||||
deletePendingMsgs db connId sq'
|
||||
deleteConnSndQueue db connId sq'
|
||||
let sqs'' = sq'' :| sqs'
|
||||
conn' = DuplexConnection cData' rqs sqs''
|
||||
cStats <- connectionStats c conn'
|
||||
pure $ Right $ SWITCH QDSnd SPCompleted cStats
|
||||
_ -> pure $ Left "sent QTEST: there is only one queue in connection"
|
||||
_ -> pure $ Left "sent QTEST: queue not in connection or not replacing another queue"
|
||||
_ -> pure $ Left "QTEST sent not in duplex connection"
|
||||
-- subQ write is now OUTSIDE connLock — blocking writeTBQueue is safe
|
||||
case evt_ of
|
||||
Right evt -> notify evt
|
||||
Left err -> internalErr msgId err
|
||||
```
|
||||
|
||||
All DB operations remain inside the lock. Only `notify`/`internalErr` (which write to subQ) move outside. `internalErr` calls `notifyDel` = `notify >> delMsg` — both `notify` (subQ write) and `delMsg` (`deleteSndMsgDelivery`, keyed on unique msgId) are safe outside the lock. The existing double-delete pattern (`delMsg` inside `internalErr` + `delMsgKeep` at line 2216) is preserved.
|
||||
|
||||
### Fix: Sites 2 & 3 — `ackQueueMessage::sendMsgNtf` (line 2386)
|
||||
|
||||
Change `ackQueueMessage` to return the MSGNTF event instead of writing it to subQ. Callers write to subQ after releasing connLock.
|
||||
|
||||
**Current code** (Agent.hs:2371-2386):
|
||||
```haskell
|
||||
ackQueueMessage :: AgentClient -> RcvQueue -> SMP.MsgId -> AM ()
|
||||
ackQueueMessage c rq@RcvQueue {userId, connId, server} srvMsgId = do
|
||||
atomically $ incSMPServerStat c userId server ackAttempts
|
||||
tryAllErrors (sendAck c rq srvMsgId) >>= \case
|
||||
Right _ -> sendMsgNtf ackMsgs
|
||||
Left (SMP _ SMP.NO_MSG) -> sendMsgNtf ackNoMsgErrs
|
||||
Left e -> ...
|
||||
where
|
||||
sendMsgNtf stat = do
|
||||
atomically $ incSMPServerStat c userId server stat
|
||||
whenM (liftIO $ hasGetLock c rq) $ do
|
||||
atomically $ releaseGetLock c rq
|
||||
brokerTs_ <- eitherToMaybe <$> tryAllErrors (withStore c $ \db -> getRcvMsgBrokerTs db connId srvMsgId)
|
||||
atomically $ writeTBQueue (subQ c) ("", connId, AEvt SAEConn $ MSGNTF srvMsgId brokerTs_)
|
||||
```
|
||||
|
||||
**New code** — return `Maybe ATransmission` instead of writing:
|
||||
```haskell
|
||||
ackQueueMessage :: AgentClient -> RcvQueue -> SMP.MsgId -> AM (Maybe ATransmission)
|
||||
ackQueueMessage c rq@RcvQueue {userId, connId, server} srvMsgId = do
|
||||
atomically $ incSMPServerStat c userId server ackAttempts
|
||||
tryAllErrors (sendAck c rq srvMsgId) >>= \case
|
||||
Right _ -> sendMsgNtf ackMsgs
|
||||
Left (SMP _ SMP.NO_MSG) -> sendMsgNtf ackNoMsgErrs
|
||||
Left e -> ... >> pure Nothing
|
||||
where
|
||||
sendMsgNtf stat = do
|
||||
atomically $ incSMPServerStat c userId server stat
|
||||
ifM (liftIO $ hasGetLock c rq)
|
||||
(do atomically $ releaseGetLock c rq
|
||||
brokerTs_ <- eitherToMaybe <$> tryAllErrors (withStore c $ \db -> getRcvMsgBrokerTs db connId srvMsgId)
|
||||
pure $ Just ("", connId, AEvt SAEConn $ MSGNTF srvMsgId brokerTs_))
|
||||
(pure Nothing)
|
||||
```
|
||||
|
||||
**Caller 1: `ackMessage'`** (Agent.hs:2253-2267) — return event from `withConnLock`, write after:
|
||||
```haskell
|
||||
ackMessage' c connId msgId rcptInfo_ = do
|
||||
t_ <- withConnLock c connId "ackMessage" $ do
|
||||
SomeConn _ conn <- withStore c (`getConn` connId)
|
||||
case conn of
|
||||
DuplexConnection {} -> do
|
||||
t_ <- ack
|
||||
sendRcpt conn
|
||||
del
|
||||
pure t_
|
||||
RcvConnection {} -> do
|
||||
t_ <- ack
|
||||
del
|
||||
pure t_
|
||||
SndConnection {} -> throwE $ CONN SIMPLEX "ackMessage"
|
||||
ContactConnection {} -> throwE $ CMD PROHIBITED "ackMessage: ContactConnection"
|
||||
NewConnection _ -> throwE $ CMD PROHIBITED "ackMessage: NewConnection"
|
||||
-- subQ write is OUTSIDE connLock
|
||||
case t_ of
|
||||
Just t -> atomically $ writeTBQueue (subQ c) t
|
||||
Nothing -> pure ()
|
||||
```
|
||||
|
||||
**Caller 2: `ICAck` / `ICAckDel`** (Agent.hs:1823-1824) — inline `tryWithLock` as `tryCommand` + `withConnLock`, write subQ between the two scopes:
|
||||
|
||||
`tryWithLock name = tryCommand . withConnLock c connId name` — by inlining, the subQ write can be placed outside `withConnLock` but inside `tryCommand` (retaining retry/error handling).
|
||||
|
||||
```haskell
|
||||
ICAck rId srvMsgId -> withServer $ \srv ->
|
||||
tryCommand $ do
|
||||
t_ <- withConnLock c connId "ICAck" $ ack srv rId srvMsgId
|
||||
-- subQ write is OUTSIDE connLock — cannot deadlock with agentSubscriber
|
||||
forM_ t_ $ atomically . writeTBQueue subQ
|
||||
|
||||
ICAckDel rId srvMsgId msgId -> withServer $ \srv ->
|
||||
tryCommand $ do
|
||||
t_ <- withConnLock c connId "ICAckDel" $ do
|
||||
t_ <- ack srv rId srvMsgId
|
||||
withStore' c (\db -> deleteMsg db connId msgId)
|
||||
pure t_
|
||||
-- subQ write is OUTSIDE connLock — cannot deadlock with agentSubscriber
|
||||
forM_ t_ $ atomically . writeTBQueue subQ
|
||||
```
|
||||
|
||||
Where `ack` now returns `AM (Maybe ATransmission)`:
|
||||
```haskell
|
||||
ack srv rId srvMsgId = do
|
||||
rq <- withStore c $ \db -> getRcvQueue db connId srv rId
|
||||
ackQueueMessage c rq srvMsgId
|
||||
```
|
||||
|
||||
All subQ writes for MSGNTF are now outside connLock. FIFO ordering is preserved — no `nonBlockingWriteTBQueue`, no forked threads. The same thread that held the lock writes to subQ sequentially after releasing it.
|
||||
|
||||
### Race analysis
|
||||
|
||||
Window between connLock release and subQ write at Site 1:
|
||||
|
||||
| Thread | Can acquire connLock(X)? | Writes subQ? | Consequence |
|
||||
|--------|-------------------------|-------------|-------------|
|
||||
| agentSubscriber via sendMessagesB_ | Yes | **No** (encrypts only) | No race |
|
||||
| processSMP for connId X | Yes | Yes (pending flush) | MSG before SWITCH — cosmetic |
|
||||
| runCommandProcessing for connId X | Yes | Yes (pending flush) | Command response before SWITCH — cosmetic |
|
||||
| New queue delivery worker | No (SENT outside lock) | Yes | SENT before SWITCH — cosmetic, **already exists in current code** |
|
||||
|
||||
All races are cosmetic UI ordering. None affect ratchet state, protocol correctness, or message delivery.
|
||||
|
||||
### Summary of changes
|
||||
|
||||
| File | Change | Lines affected |
|
||||
|------|--------|---------------|
|
||||
| Agent.hs | Restructure AM_QTEST_ to return event from `withConnLock`, write outside | ~2187-2214 |
|
||||
| Agent.hs | Change `ackQueueMessage` return type to `AM (Maybe ATransmission)`, return event instead of writing | ~2371-2386 |
|
||||
| Agent.hs | `ackMessage'`: return event from `withConnLock`, write outside | ~2253-2267 |
|
||||
| Agent.hs | `ICAck`/`ICAckDel`: inline `tryCommand` + `withConnLock`, write subQ between scopes | ~1823-1824 |
|
||||
| Agent.hs | `ack` helper: propagate new return type | ~1899-1901 |
|
||||
|
||||
No new data structures. No new modules. No changes to other write sites (1937, 3216 — already safe). ~25 lines changed total.
|
||||
@@ -0,0 +1,65 @@
|
||||
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: createConnection
|
||||
|
||||
note over AA, AS: 2. create Alice's SMP queue
|
||||
AA ->> AS: NEW: create SMP queue<br>allow sender to secure
|
||||
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: joinConnection:<br>via invitation info
|
||||
note over BA: status: NONE/NEW
|
||||
|
||||
note over BA, AS: 5. secure Alice's SMP queue
|
||||
BA ->> AS: SKEY: secure queue (this command needs to be proxied)
|
||||
note over BA: status: NONE/SECURED
|
||||
|
||||
note over BA, BS: 6. create Bob's SMP queue
|
||||
BA ->> BS: NEW: create SMP queue<br>allow sender to secure
|
||||
BS ->> BA: IDS: SMP queue IDs
|
||||
note over BA: status: NEW/SECURED
|
||||
|
||||
note over BA, AA: 7. confirm Alice's SMP queue
|
||||
BA ->> AS: SEND: Bob's info without sender's key (SMP confirmation with reply queues)
|
||||
note over BA: status: NEW/CONFIRMED
|
||||
|
||||
AS ->> AA: MSG: Bob's info without<br>sender server key
|
||||
note over AA: status: CONFIRMED/NEW
|
||||
AA ->> AS: ACK: confirm message
|
||||
AA ->> A: CONF: connection request ID<br>and Bob's info
|
||||
A -> AA: allowConnection: accept connection request,<br>send Alice's info
|
||||
|
||||
note over AA, BS: 8. secure Bob's SMP queue
|
||||
AA ->> BS: SKEY: secure queue (this command needs to be proxied)
|
||||
note over BA: status: CONFIRMED/SECURED
|
||||
|
||||
AA ->> BS: SEND: Alice's info without sender's server key (SMP confirmation without reply queues)
|
||||
note over AA: status: CONFIRMED/CONFIRMED
|
||||
|
||||
note over AA, A: 9. notify Alice<br>about connection success<br>(no HELLO needed in v6)
|
||||
AA ->> A: CON: connected
|
||||
note over AA: status: ACTIVE/ACTIVE
|
||||
|
||||
note over BA, B: 10. notify Bob<br>about connection success
|
||||
BS ->> BA: MSG: Alice's info without<br>sender's server key
|
||||
note over BA: status: CONFIRMED/CONFIRMED
|
||||
BA ->> B: INFO: Alice's info
|
||||
BA ->> BS: ACK: confirm message
|
||||
|
||||
BA ->> B: CON: connected
|
||||
note over BA: status: ACTIVE/ACTIVE
|
||||
|
After Width: | Height: | Size: 40 KiB |
@@ -1,71 +0,0 @@
|
||||
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
|
||||
@@ -8,8 +8,8 @@ sequenceDiagram
|
||||
|
||||
note over AA, BA: status (receive/send): NONE/NONE
|
||||
|
||||
note over A, AA: 1. request connection from agent
|
||||
A ->> AA: NEW: create<br>duplex connection
|
||||
note over A, AA: 1. request connection<br>from agent
|
||||
A ->> AA: createConnection
|
||||
|
||||
note over AA, AS: 2. create Alice's SMP queue
|
||||
AA ->> AS: NEW: create SMP queue
|
||||
@@ -17,63 +17,58 @@ sequenceDiagram
|
||||
note over AA: status: NEW/NONE
|
||||
|
||||
AA ->> A: INV: invitation<br>to connect
|
||||
note over AA: status: PENDING/NONE
|
||||
|
||||
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
|
||||
B ->> BA: joinConnection:<br>via invitation info
|
||||
note over BA: status: NONE/NEW
|
||||
|
||||
note over BA, AA: 5. establish Alice's SMP queue
|
||||
BA ->> AS: SEND: Bob's info and sender server key (SMP confirmation)
|
||||
note over BA: status: NONE/CONFIRMED
|
||||
activate BA
|
||||
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. confirm 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
|
||||
A ->> AA: allowConnection: accept connection request,<br>send Alice's info
|
||||
AA ->> AS: KEY: secure queue
|
||||
note over AA: status: SECURED/NONE
|
||||
|
||||
BA ->> AS: SEND: HELLO: try sending until successful
|
||||
deactivate BA
|
||||
note over BA: status: NONE/ACTIVE
|
||||
AS ->> AA: MSG: HELLO: Alice's agent<br>knows Bob can send
|
||||
note over AA: status: ACTIVE/NONE
|
||||
AA ->> AS: ACK: confirm message
|
||||
AA ->> BS: SEND: Alice's info and sender's server key (SMP confirmation without reply queues)
|
||||
note over AA: status: SECURED/CONFIRMED
|
||||
|
||||
note over BA, BS: 6. create Bob's SMP queue
|
||||
BA ->> BS: NEW: create SMP queue
|
||||
BS ->> BA: IDS: SMP queue IDs
|
||||
note over BA: status: NEW/ACTIVE
|
||||
|
||||
note over AA, BA: 7. establish Bob's SMP queue
|
||||
BA ->> AS: SEND: REPLY: invitation to the connect
|
||||
note over BA: status: PENDING/ACTIVE
|
||||
AS ->> AA: MSG: REPLY: invitation<br>to connect
|
||||
note over AA: status: ACTIVE/NEW
|
||||
AA ->> AS: ACK: confirm message
|
||||
|
||||
AA ->> BS: SEND: Alice's info and sender's server key
|
||||
note over AA: status: ACTIVE/CONFIRMED
|
||||
activate AA
|
||||
note over BA, AA: 7. confirm Bob's SMP queue
|
||||
BS ->> BA: MSG: Alice's info and<br>sender's server key
|
||||
note over BA: status: CONFIRMED/ACTIVE
|
||||
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 message
|
||||
|
||||
note over BA: status: SECURED/ACTIVE
|
||||
|
||||
AA ->> BS: SEND: HELLO: try sending until successful
|
||||
deactivate AA
|
||||
AS ->> AA: MSG: HELLO: Alice's agent<br>knows Bob can send
|
||||
note over AA: status: SECURED/ACTIVE
|
||||
AA ->> AS: ACK: confirm message
|
||||
AA ->> BS: SEND: HELLO
|
||||
|
||||
note over A, AA: 8. notify Alice<br>about connection success
|
||||
AA ->> A: CON: connected
|
||||
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
|
||||
|
||||
note over A, B: 8. notify users about connection success
|
||||
AA ->> A: CON: connected
|
||||
note over BA, B: 9. notify Bob<br>about connection success
|
||||
BA ->> B: CON: connected
|
||||
|
||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,17 @@
|
||||
sequenceDiagram
|
||||
participant A as Alice
|
||||
participant R as Current server<br>that has A's<br>receive queue
|
||||
participant R' as New server<br>that has the new A's<br>receive queue
|
||||
participant S as Server<br>that has A's send queue<br>(B's receive queue)
|
||||
participant B as Bob
|
||||
|
||||
A ->> R': NEW: create new queue<br>(allow SKEY)
|
||||
A ->> S: SEND: QADD (R'): send address<br>of the new queue(s)
|
||||
S ->> B: MSG: QADD (R')
|
||||
B ->> R': SKEY: secure new queue
|
||||
B ->> R': SEND: QTEST
|
||||
R' ->> A: MSG: QTEST
|
||||
A ->> R: DEL: delete the old queue
|
||||
B ->> R': SEND: send messages to the new queue
|
||||
R' ->> A: MSG: receive messages from the new queue
|
||||
|
||||
|
After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,21 @@
|
||||
sequenceDiagram
|
||||
participant A as Alice
|
||||
participant R as Current server<br>that has A's<br>receive queue
|
||||
participant R' as New server<br>that has the new A's<br>receive queue
|
||||
participant S as Server<br>that has A's send queue<br>(B's receive queue)
|
||||
participant B as Bob
|
||||
|
||||
A ->> R': NEW: create new queue
|
||||
A ->> S: SEND: QADD (R'): send address<br>of the new queue(s)
|
||||
S ->> B: MSG: QADD (R')
|
||||
B ->> R: SEND: QKEY (R'): sender's key<br>for the new queue(s)
|
||||
R ->> A: MSG: QKEY(R')
|
||||
A ->> R': KEY: secure new queue
|
||||
A ->> S: SEND: QUSE (R'): instruction to use new queue(s)
|
||||
S ->> B: MSG: QUSE (R')
|
||||
B ->> R': SEND: QTEST
|
||||
R' ->> A: MSG: QTEST
|
||||
A ->> R: DEL: delete the old queue
|
||||
B ->> R': SEND: send messages to the new queue
|
||||
R' ->> A: MSG: receive messages from the new queue
|
||||
|
||||
|
After Width: | Height: | Size: 28 KiB |
@@ -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
|
||||
@@ -1,30 +1,26 @@
|
||||
sequenceDiagram
|
||||
participant M as mobile app
|
||||
participant C as chat core
|
||||
participant C as client app
|
||||
participant A as agent
|
||||
participant P as push server
|
||||
participant APN as APN
|
||||
participant P as SimpleX<br>Notification<br>Server
|
||||
participant APN as Apple<br>Push Notifications<br>Server
|
||||
|
||||
note over M, APN: get device token
|
||||
M ->> APN: registerForRemoteNotifications()
|
||||
APN ->> M: device token
|
||||
note over C, APN: get device token
|
||||
C ->> APN: registerForRemoteNotifications()
|
||||
APN ->> C: device token
|
||||
|
||||
note over M, P: register device token with push server
|
||||
M ->> C: /_ntf register <token>
|
||||
C ->> A: registerNtfToken(<token>)
|
||||
note over C, P: register device token with push server
|
||||
C ->> A: registerToken
|
||||
A ->> P: TNEW
|
||||
P ->> A: ID (tokenId)
|
||||
A ->> C: registered
|
||||
C ->> M: registered
|
||||
|
||||
note over M, APN: verify device token
|
||||
note over C, 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>)
|
||||
APN ->> C: deliver background notification with e2ee verification token
|
||||
C ->> A: verifyToken<br>(<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
|
||||
note over C, APN: now token ID can be used
|
||||
|
||||
|
After Width: | Height: | Size: 28 KiB |
@@ -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>...
|
||||
@@ -1,17 +1,16 @@
|
||||
sequenceDiagram
|
||||
participant M as mobile app
|
||||
participant C as chat core
|
||||
participant C as client app
|
||||
participant A as agent
|
||||
participant S as SMP server
|
||||
participant N as NTF server
|
||||
participant APN as APN
|
||||
|
||||
note over M, APN: register subscription
|
||||
note over C, APN: register subscription
|
||||
|
||||
alt register existing
|
||||
M -->> A: on /_ntf register, for subscribed queues
|
||||
C -->> A: registerToken
|
||||
else create new connection
|
||||
A -->> S: NEW / JOIN
|
||||
A -->> S: create/joinConnection
|
||||
note over A, S: ...<br>Connection handshake<br>...
|
||||
S -->> A: CON
|
||||
end
|
||||
@@ -20,21 +19,20 @@ sequenceDiagram
|
||||
A ->> N: SNEW tknId dhKey (smpServer, nId, nKey)
|
||||
N ->> A: ID subId dhKey
|
||||
N ->> S: NSUB nId
|
||||
S ->> N: OK [/ NMSG]
|
||||
S ->> N: OK / NMSG:<br>confirm subscription
|
||||
|
||||
note over M, APN: notify about message
|
||||
note over C, 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
|
||||
APN ->> C: UNMutableNotificationContent
|
||||
note over C, S: ...<br>Client awaken, message is received<br>...
|
||||
S ->> C: message
|
||||
note over C: show notification
|
||||
|
||||
note over M, APN: change APN token
|
||||
note over C, APN: change APN token
|
||||
|
||||
APN ->> M: new device token
|
||||
M -->> C: /_ntf_sub update tkn
|
||||
C -->> A: updateNtfToken()
|
||||
APN ->> C: new device token
|
||||
C -->> A: updateToken()
|
||||
A -->> N: TUPD tknId newDeviceToken
|
||||
note over M, N: ...<br>Verify token<br>...
|
||||
note over C, N: ...<br>Verify token<br>...
|
||||
|
||||
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,23 @@
|
||||
sequenceDiagram
|
||||
participant B as Bob (sender)
|
||||
participant S as server (queue RID)
|
||||
participant A as Alice (recipient)
|
||||
|
||||
note over A: creating queue<br>("public" key RK<br>for msg retrieval)
|
||||
A ->> S: 1. create queue ("NEW")
|
||||
S ->> A: respond with queue RID and SID ("IDS")
|
||||
|
||||
note over A: out-of-band msg<br>(sender's queue SID<br>and "public" key EK<br>to encrypt msgs)
|
||||
A -->> B: 2. send out-of-band message
|
||||
|
||||
note over B: secure queue<br>(with "public" key SK for<br>sending messages)
|
||||
B ->> S: 3. confirm queue ("SKEY" command authorized with SK)
|
||||
|
||||
note over B: confirm queue<br>(public key<br>for e2e encryption<br>and any optional<br>encrypted info.)
|
||||
B ->> S: 4. confirm queue ("SEND" command authorized with SK)
|
||||
|
||||
S ->> A: 5. deliver Bob's message (MSG)
|
||||
note over A: decrypt message<br>("private" key EK)
|
||||
A ->> S: acknowledge message (ACK)
|
||||
|
||||
note over S: 6. simplex<br>queue RID<br>is ready to use!
|
||||
|
After Width: | Height: | Size: 27 KiB |
@@ -10,11 +10,13 @@ sequenceDiagram
|
||||
note over A: out-of-band msg<br>(sender's queue SID<br>and "public" key EK<br>to encrypt msgs)
|
||||
A -->> B: 2. send out-of-band message
|
||||
|
||||
note over B: confirm queue<br>("public" key SK for<br>sending messages<br>and any optional<br>info encrypted with<br>"public" key EK)
|
||||
note over B: confirm queue<br>("public" key SK for<br>sending messages,<br>public key for<br>e2e encryption<br>and any optional<br>encrypted info)
|
||||
B ->> S: 3. confirm queue ("SEND" command not signed)
|
||||
|
||||
S ->> A: 4. deliver Bob's message
|
||||
S ->> A: 4. deliver Bob's message (MSG)
|
||||
note over A: decrypt message<br>("private" key EK)
|
||||
A ->> S: acknowledge message (ACK)
|
||||
|
||||
A ->> S: 5. secure queue ("KEY", RK-signed)
|
||||
|
||||
note over S: 6. simplex<br>queue RID<br>is ready to use!
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,18 @@
|
||||
sequenceDiagram
|
||||
participant B as Bob (recipient)
|
||||
participant S as XFTP server(s)
|
||||
|
||||
note over B: having received file description<br>from sender
|
||||
|
||||
loop for each chunk
|
||||
B ->> S: 1a. download chunk ("FGET")
|
||||
S ->> B: send chunk body ("FILE")
|
||||
|
||||
opt
|
||||
B ->> S: 1b. acknowledge chunk reception ("FACK")
|
||||
note over S: delete recipient ID
|
||||
S ->> B: respond with ok ("OK")
|
||||
end
|
||||
end
|
||||
|
||||
note over B: 2. combine chunks into a file<br>3. decrypt file using key from file description<br>4. extract file name and unpad the file<br>5. validate file digest with the file description
|
||||
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,23 @@
|
||||
sequenceDiagram
|
||||
participant A as Alice (sender)
|
||||
participant S as XFTP server(s)
|
||||
participant B as recipient(s)
|
||||
|
||||
note over A: 1. prepare file:<br>encrypt,<br>split into chunks,<br>generate recipient<br>keys, etc.
|
||||
|
||||
loop for each chunk
|
||||
A ->> S: 2a. register chunk ("FNEW")
|
||||
S ->> A: respond with sender's and recipients' chunk IDs ("SIDS")
|
||||
|
||||
opt
|
||||
A ->> S: 2b. request additional recipient IDs ("FADD")
|
||||
S ->> A: respond with added recipients' chunk IDs ("RIDS")
|
||||
end
|
||||
|
||||
A ->> S: 2c. upload chunk to chosen server ("FPUT")
|
||||
S ->> A: respond with ok ("OK")
|
||||
end
|
||||
|
||||
note over A: 3. prepare file description(s)
|
||||
|
||||
A -->> B: 4. send file description(s) out-of-band
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,44 @@
|
||||
sequenceDiagram
|
||||
participant CI as Controller UI
|
||||
participant CC as Controller Core
|
||||
participant HC as Host Core
|
||||
participant HI as Host UI
|
||||
|
||||
note over CI, HI: 1. Session invitation
|
||||
CI->>CC: "Link a mobile"
|
||||
CC-->>CI: Session invitation URI
|
||||
note over CC: Listen for TCP connection
|
||||
activate CC
|
||||
HI->>HC: Session invitation URI
|
||||
|
||||
note over CI, HI: 2. Establishing TLS connection
|
||||
HC-->>CC: TCP connect
|
||||
note over CC, HC: TLS handshake
|
||||
par
|
||||
note over CC: validate client X509 credentials
|
||||
CC->>CI: session code from tlsUnique
|
||||
CI-->>CC: user confirmation
|
||||
and
|
||||
note over HC: validate server X509 credentials
|
||||
HC->>HI: session code from tlsUnique
|
||||
HI-->>HC: user confirmation
|
||||
end
|
||||
|
||||
note over CI, HI: 3. Session verification and protocol negotiation
|
||||
HC->>CC: host HELLO
|
||||
note over CC: validate version, CA fingerprint
|
||||
alt
|
||||
CC-->>HC: controller ERROR
|
||||
else
|
||||
CC-->>HC: controller HELLO
|
||||
note over CC, HC: update stored keys
|
||||
end
|
||||
deactivate CC
|
||||
|
||||
note over CI, HI: 4. Session operation
|
||||
loop
|
||||
CI->>CC: command
|
||||
CC->>HC: XRCP command
|
||||
HC-->>CC: XRCP response
|
||||
CC-->>CI: response
|
||||
end
|
||||
|
After Width: | Height: | Size: 31 KiB |
@@ -1,4 +1,4 @@
|
||||
Revision 1, 2022-01-01
|
||||
Revision 4, 2026-03-09
|
||||
|
||||
Evgeny Poberezkin
|
||||
|
||||
@@ -8,40 +8,47 @@ Evgeny Poberezkin
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [What is SimpleX](#what-is-simplex)
|
||||
- [Network model](#network-model)
|
||||
- [Applications](#applications)
|
||||
- [SimpleX objectives](#simplex-objectives)
|
||||
- [In Comparison](#in-comparison)
|
||||
- [Technical Details](#technical-details)
|
||||
- [Trust in Servers](#trust-in-servers)
|
||||
- [Client -> Server Communication](#client---server-communication)
|
||||
- [Trust in Routers](#trust-in-routers)
|
||||
- [Client -> Router Communication](#client---router-communication)
|
||||
- [2-hop Onion Message Routing](#2-hop-onion-message-routing)
|
||||
- [SimpleX Messaging Protocol](#simplex-messaging-protocol)
|
||||
- [SimpleX Agents](#simplex-agents)
|
||||
- [Encryption Primitives Used](#encryption-primitives-used)
|
||||
- [Threat model](#threat-model)
|
||||
- [Security](#security)
|
||||
- [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.
|
||||
- [SimpleX Messaging Protocol](./simplex-messaging.md) (SMP) is a protocol to send messages in one direction to a recipient, relying on a router in-between. The messages are delivered via uni-directional queues created by recipients.
|
||||
|
||||
- SMP protocol allows to send message via a SMP router playing proxy role using 2-hop onion routing (referred to as "private routing" in messaging clients) to protect transport information of the sender (IP address and session) from the router chosen (and possibly controlled) by the recipient.
|
||||
|
||||
- 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.
|
||||
- A SimpleX router is one of those routers.
|
||||
|
||||
- The SimpleX Network is the term used for the collective of SimpleX Servers that facilitate SMP.
|
||||
- The SimpleX Network is the term used for the collective of SimpleX routers 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 Client libraries speak SMP to SimpleX routers 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.
|
||||
- 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 can also be abstracted into local services.
|
||||
|
||||
- SimpleX Agents communicate with other agents inside e2e encrypted envelopes provided by SMP protocol - the syntax and semantics of the messages exchanged by the agent are defined by [SMP agent protocol](./agent-protocol.md)
|
||||
|
||||
|
||||
*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.*
|
||||
*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 router.*
|
||||
|
||||
```
|
||||
User's Computer Internet Third-Party Server
|
||||
User's Computer Internet Third-Party Router
|
||||
------------------ | ---------------------- | -------------------------
|
||||
| |
|
||||
SimpleX Chat | |
|
||||
@@ -51,11 +58,43 @@ SimpleX as a whole is a platform upon which applications can be built. [SimpleX
|
||||
+----------------+ | |
|
||||
| SimpleX Agent | | |
|
||||
+----------------+ -------------- TLS ---------------- +----------------+
|
||||
| SimpleX Client | ------ SimpleX Messaging Protocol ------> | SimpleX Server |
|
||||
| SimpleX Client | ------ SimpleX Messaging Protocol ------> | SimpleX router |
|
||||
+----------------+ ----------------------------------- +----------------+
|
||||
| |
|
||||
```
|
||||
|
||||
#### Network model
|
||||
|
||||
SimpleX is a general-purpose packet routing network built on top of the Internet. Network endpoints — end-user devices, automated services, AI-enabled applications, IoT devices — exchange data packets through SimpleX network nodes (SMP routers), which accept, buffer, and deliver packets. Each router operates independently and can be operated by any party on standard computing hardware.
|
||||
|
||||
SimpleX routers use resource-based addressing: each address identifies a resource on a router, similar to how the World Wide Web addresses resources via URLs. Internet routers, by comparison, use endpoint-based addressing, where IP addresses identify destination devices. Because of this design, SimpleX network participants do not need globally unique addresses to communicate.
|
||||
|
||||
SimpleX network has two resource-based addressing schemes:
|
||||
|
||||
- *Messaging queues* ([SMP](./simplex-messaging.md)). A queue is a unidirectional, ordered sequence of fixed-size data packets (16,384 bytes each). Each queue has a resource address on a specific router, gated by cryptographic credentials that separately authorize sending and receiving.
|
||||
|
||||
- *Data packets* ([XFTP](./xftp.md)). A data packet is an individually addressed block in one of the standard sizes. Each packet has a unique resource address on a specific router, gated by cryptographic credentials. Data packet addressing is more efficient for delivery of larger payloads than queues.
|
||||
|
||||
Packet delivery follows a two-router path. The sending endpoint submits a packet to a first router, which forwards it to a second router, where the receiving endpoint retrieves it. The sending endpoint's IP address is known only to the first router; the receiving endpoint's IP address is known only to the second router. See [2-hop Onion Message Routing](#2-hop-onion-message-routing) for details.
|
||||
|
||||
Routers buffer packets between submission and retrieval — from seconds to days, enabling asynchronous delivery when endpoints are online at different times. Packets are removed after delivery or after a configured expiration period.
|
||||
|
||||
|
||||
#### Applications
|
||||
|
||||
Applications currently using SimpleX network:
|
||||
|
||||
- **SimpleX Chat** — a peer-to-peer messenger using SimpleX network as a transport layer, in the same way that communication applications use WebRTC, Tor, i2p, or Nym. All communication logic — contacts, conversations, groups, message formats, end-to-end encryption — runs on endpoint devices.
|
||||
|
||||
- **IoT devices** — using the SimpleX queue protocol directly for sensor data collection and device control.
|
||||
|
||||
- **AI-based services** — automated services built on the SimpleX Chat application core.
|
||||
|
||||
- **Secure monitoring and control systems** — applications for equipment monitoring and control, including robotics, using the network for command delivery and telemetry collection.
|
||||
|
||||
[SimpleGo](https://simplego.dev), developed by an independent organization, is a microcontroller-based device running a SimpleX Chat-compatible messenger directly on a microcontroller without a general-purpose operating system. Running over 20 days on a single battery charge, it demonstrates the energy efficiency of resource-based addressing: the device receives packets without continuous polling. A microcontroller-based router implementation that functions simultaneously as a WiFi router is also in development.
|
||||
|
||||
|
||||
#### SimpleX objectives
|
||||
|
||||
1. Provide messaging infrastructure for distributed applications. This infrastructure needs to have the following qualities:
|
||||
@@ -64,7 +103,7 @@ SimpleX as a whole is a platform upon which applications can be built. [SimpleX
|
||||
|
||||
- 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.
|
||||
- Reliability: the messages should be delivered even if some participating network routers 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.
|
||||
|
||||
@@ -72,82 +111,104 @@ SimpleX as a whole is a platform upon which applications can be built. [SimpleX
|
||||
|
||||
- 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.
|
||||
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 routers.
|
||||
|
||||
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.
|
||||
SimpleX network has a design similar to P2P networks, but unlike most P2P networks it consists of clients and routers 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.
|
||||
- simple message queues provided by network routers are used by the clients to create more complex communication scenarios, such as duplex one-to-one communication, transmitting files, group communication without central routers, 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.
|
||||
- routers 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 routers with minimal disruption - even after an in-use router disappears, simply by changing the configuration on which routers the new queues are created.
|
||||
|
||||
- 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
|
||||
#### Trust in Routers
|
||||
|
||||
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.
|
||||
Clients communicate directly with routers (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.
|
||||
Users use multiple routers, and choose where to receive their messages. Accordingly, they send messages to their communication partners' chosen routers either directly, if this is a known/trusted router, or via another SMP router providing proxy functionality to protect IP address and session of the sender.
|
||||
|
||||
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.
|
||||
Although end-to-end encryption is always present, users place a degree of trust in routers they connect to. This trust decision is very similar to a user's choice of email provider; however the trust placed in a SimpleX router is significantly less. Notably, there is no re-used identifier or credential between queues on the same (or different) routers. While a user *may* re-use a transport connection to fetch messages from multiple queues, or connect to a router 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:
|
||||
Users may trust a router 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 deploy and control the routers themselves from the available open-source code. This has the trade-offs of strong trust in the router 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.
|
||||
|
||||
- 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.
|
||||
- They use routers 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, routers do not retain access logs, and permanently delete messages and queues when requested. Messages persist in memory or in a database until they cross a threshold of time, typically on the order of days.[0] There is still a risk that a router 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.
|
||||
|
||||
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 routers. These include rotating the queues in use between users, noise traffic, supporting overlay networks such as Tor, and isolating traffic to different queues to different transport connections (and Tor circuits, if Tor is used).
|
||||
|
||||
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.
|
||||
[0] While configurable by routers, a minimum value is enforced by the default software. SimpleX Agents can provide redundant routing over queues to mitigate against message loss.
|
||||
|
||||
|
||||
#### Client -> Server Communication
|
||||
#### Client -> Router 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]
|
||||
Routers 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, as part of the router address. 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.
|
||||
If the transport protocol's confidentiality is broken, incoming and outgoing messages to the router cannot be correlated by message contents. Additionally, because of encryption at the SMP layer, impersonating the router 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 router 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.
|
||||
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 router 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.
|
||||
After the connection is established, the client sends blocks of a fixed size 16KB, and the router 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 routers can be deployed as onion services and SimpleX clients can communicate with routers 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.
|
||||
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 router simultaneously, the router acts as a 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.
|
||||
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 by observing their local traffic. 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.
|
||||
|
||||
|
||||
#### 2-hop Onion Message Routing
|
||||
|
||||
As SimpleX Messaging Protocol routers providing messaging queues are chosen by the recipients, in case senders connect to these routers directly the router owners (who potentially can be the recipients themselves) can learn senders' IP addresses (if Tor is not used) and which other queues on the same router are accessed by the user in the same transport connection (even if Tor is used).
|
||||
|
||||
While the clients support isolating the messages sent to different queues into different transport connections (and Tor circuits), this is not practical, as it consumes additional traffic and system resources.
|
||||
|
||||
To mitigate this problem SimpleX Messaging Protocol routers support 2-hop onion message routing when the SMP router chosen by the sender forwards the messages to the routers chosen by the recipients, thus protecting both the senders IP addresses and sessions, even if connection isolation and Tor are not used.
|
||||
|
||||
The design of 2-hop onion message routing prevents these potential attacks:
|
||||
|
||||
- MITM by proxy (SMP router that forwards the messages).
|
||||
|
||||
- Identification by the proxy which and how many queues the sender sends messages to (as messages are additionally e2e encrypted between the sender and the destination SMP router).
|
||||
|
||||
- Correlation of messages sent to different queues via the same user session (as random correlation IDs and keys are used for each message).
|
||||
|
||||
See more details about 2-hop onion message routing design in [SimpleX Messaging Protocol](./simplex-messaging.md#proxying-sender-commands)
|
||||
|
||||
Also see [Security](./security.md)
|
||||
|
||||
|
||||
#### 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]
|
||||
SMP is initialized with an in-person or out-of-band introduction message, where Alice provides Bob with details of a router (including IP address or host name, port, and hash of the long-lived offline certificate), a queue ID, and Alice's public keys to agree e2e encryption. 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.
|
||||
When setting up a queue, the router 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 router 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.
|
||||
|
||||
[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:
|
||||
SimpleX agents provide higher-level operations compared to SimpleX Clients, who are primarily concerned with creating queues and communicating with routers using SMP. Agent operations include:
|
||||
|
||||
- Managing sets of bi-directional, redundant queues for communication partners
|
||||
|
||||
@@ -157,153 +218,22 @@ SimpleX agents provide higher-level operations compared to SimpleX Clients, who
|
||||
|
||||
- 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.
|
||||
## Security
|
||||
|
||||
## Threat Model
|
||||
For encryption primitives, threat model, and detailed security analysis, see [Security](./security.md).
|
||||
|
||||
#### Global Assumptions
|
||||
SimpleX provides these security properties:
|
||||
|
||||
- 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.
|
||||
- **End-to-end encryption** using Double Ratchet algorithm with forward secrecy and post-quantum cryptography.
|
||||
|
||||
#### A passive adversary able to monitor the traffic of one user
|
||||
- **No shared identifiers** across connections — contacts cannot prove they communicate with the same user.
|
||||
|
||||
*can:*
|
||||
- **Sender deniability** — neither routers nor recipients can cryptographically prove message origin.
|
||||
|
||||
- identify that and when a user is using SimpleX
|
||||
- **Transport metadata protection** — fixed-size blocks, 2-hop onion routing, and optional connection isolation frustrate traffic correlation.
|
||||
|
||||
- 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
|
||||
- **Out-of-band key exchange** — connection requests passed outside the network protect against MITM attacks.
|
||||
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
Version 1, 2024-06-22
|
||||
|
||||
# Post-quantum resistant augmented double ratchet algorithm (PQDR)
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Comparison with the other approaches](#comparison-with-the-other-approaches)
|
||||
- [PQXDH for post-quantum key agreement](#pqxdh-for-post-quantum-key-agreement) (Signal)
|
||||
- [Hybrid Signal protocol for post-quantum encryption](#hybrid-signal-protocol-for-post-quantum-encryption) (Tutanota)
|
||||
- [Augmented double ratchet algorithm](#augmented-double-ratchet-algorithm)
|
||||
- [Double ratchet with encrypted headers augmented with double PQ KEM](#double-ratchet-with-encrypted-headers-augmented-with-double-pq-kem)
|
||||
- [Initialization](#initialization)
|
||||
- [Encrypting messages](#encrypting-messages)
|
||||
- [Decrypting messages](#decrypting-messages)
|
||||
- [Ratchet message wire format](#ratchet-message-wire-format)
|
||||
- [Encrypted ratchet message](#encrypted-ratchet-message)
|
||||
- [Encrypted message header](#encrypted-message-header)
|
||||
- [Plaintext message header](#plaintext-message-header)
|
||||
- [KEM state machine](#kem-state-machine)
|
||||
- [Implementation considerations](#implementation-considerations)
|
||||
- [Chosen KEM algorithm](#chosen-kem-algorithm)
|
||||
- [Summary](#summary)
|
||||
|
||||
## Overview
|
||||
|
||||
It is a reasonable assumption that "record-now-decrypt-later" attacks are ongoing, so the users want to use cryptographic schemes for end-to-end encryption that are augmented with some post-quantum algorithm that is believed to be resistant to quantum computers.
|
||||
|
||||
SimpleX Chat uses [double-ratchet with header encryption](https://signal.org/docs/specifications/doubleratchet/#double-ratchet-with-header-encryption) to provide end-to-end encryption to messages and files. This document describes augmented algorithm with post-quantum key encapsulation mechanism (KEM) making it resistant to quantum computers.
|
||||
|
||||
Double-ratchet algorithm is a state of the art solution for end to end encryption offering a set of qualities that is not present in any other algorithm:
|
||||
|
||||
- perfect forward secrecy, i.e. compromise of session or long term keys does not lead to the ability to decrypt any of the past messages.
|
||||
- deniability (also known as repudiation), i.e. the fact that the recipient of the message while having the proof of message authenticity, cannot prove to a third party that the sender actually sent this message.
|
||||
- break-in recovery (also know as post-compromise security or future secrecy), i.e. the ability of the end-to-end encryption security to recover from the compromise of the long term keys. This is achieved by generating a new random key pair whenever a new DH key is received (DH ratchet step).
|
||||
|
||||
It is desirable to preserve all these qualities when augmenting the algorithm with a post-quantum algorithm, and having these qualities resistant to both conventional and quantum computers.
|
||||
|
||||
## Comparison with the other approaches
|
||||
|
||||
### PQXDH for post-quantum key agreement
|
||||
|
||||
[The solution](https://signal.org/docs/specifications/pqxdh/) recently [introduced by Signal](https://signal.org/blog/pqxdh/) augments the initial key agreement ([X3DH](https://signal.org/docs/specifications/x3dh/)) that is made prior to double ratchet algorithm. This is believed to provide protection from "record-now-decrypt-later" attack, but if the attacker at any point obtains long term keys from any of the devices, the break-in recovery will not be post-quantum resistant, and the attacker with quantum computer will be able to decrypt all the subsequent messages.
|
||||
|
||||
### Hybrid Signal protocol for post-quantum encryption
|
||||
|
||||
[The solution](https://eprint.iacr.org/2021/875.pdf) [proposed by Tutanota](https://tutanota.com/blog/posts/pqmail-update/) aims to preserve the break-in recovery property of double ratchet, but in doing so it:
|
||||
- replaces rather than augments DH key agreement with post-quantum KEM mechanism, making it potentially vulnerable to conventional computers.
|
||||
- adds signature to the DH ratchet step, to compensate for not keeping DH key agreement, but losing the deniability property for some of the messages.
|
||||
|
||||
## Augmented double ratchet algorithm
|
||||
|
||||
The double ratchet algorithm is augmented with post-quantum KEM mechanism, preserving all properties of the double ratchet algorithm.
|
||||
|
||||
It is possible, because although double ratchet uses DH (which is a non-interactive key exchanges), it uses it "interactively", when the new DH keys are generated by both parties in turns. Parties of double-ratchet encrypted communication can run two post-quantum key encapsulation mechanisms in parallel with both DH and KEM key agreements in each DH ratchet step, making break-in recovery of double ratchet algorithm post-quantum resistant, without losing deniability or resistance to conventional computers.
|
||||
|
||||
Specifically, [double ratchet with encrypted headers](https://signal.org/docs/specifications/doubleratchet/#double-ratchet-with-header-encryption) is augmented with some post-quantum key encapsulation mechanism (KEM) as described below. A possible algorithm for PQ KEM is [NTRU-prime](https://ntruprime.cr.yp.to), that is currently adopted in SSH and has available implementations. It is important though that the proposed scheme can be used with any PQ KEM algorithm.
|
||||
|
||||
The downside of the scheme is its substantial size overhead, as the encapsulation key and encapsulated shared secret are added to the header of each message. For the algorithm described below NTRU-prime adds ~2-4kb to each message (depending on the key size and the chosen variant). See [this table](https://ntruprime.cr.yp.to/security.html) for key and ciphertext sizes and the assessment of the security level for various key sizes.
|
||||
|
||||
It is possible to reduce size overhead by using only one KEM agreement and making only one of two ratchet steps providing post-quantum resistant break-in recovery.
|
||||
|
||||
## Double ratchet with encrypted headers augmented with double PQ KEM
|
||||
|
||||
Algorithm below assumes that in addition to shared secret from the initial key agreement, there will be an encapsulation key available from the party that published its keys (Bob).
|
||||
|
||||
### Initialization
|
||||
|
||||
The double ratchet initialization is defined in pseudo-code. This pseudo-code is identical to Signal algorithm specification except for that parts that add post-quantum key agreement.
|
||||
|
||||
```
|
||||
// Alice obtained Bob's keys and initializes ratchet first
|
||||
def RatchetInitAlicePQ2HE(state, SK, bob_dh_public_key, shared_hka, shared_nhkb, bob_pq_kem_encapsulation_key):
|
||||
state.DHRs = GENERATE_DH()
|
||||
state.DHRr = bob_dh_public_key
|
||||
// below added for post-quantum KEM
|
||||
state.PQRs = GENERATE_PQKEM()
|
||||
state.PQRr = bob_pq_kem_encapsulation_key
|
||||
state.PQRct, state.PQRss = PQKEM-ENC(state.PQRr) // encapsulate: generates shared secret and ciphertext
|
||||
// above added for KEM
|
||||
// the next line augments DH key agreement with PQ shared secret
|
||||
state.RK, state.CKs, state.NHKs = KDF_RK_HE(SK, DH(state.DHRs, state.DHRr) || state.PQRss)
|
||||
state.CKr = None
|
||||
state.Ns = 0
|
||||
state.Nr = 0
|
||||
state.PN = 0
|
||||
state.MKSKIPPED = {}
|
||||
state.HKs = shared_hka
|
||||
state.HKr = None
|
||||
state.NHKr = shared_nhkb
|
||||
|
||||
// Bob initializes ratchet second, having received Alice's connection request
|
||||
def RatchetInitBobPQ2HE(state, SK, bob_dh_key_pair, shared_hka, shared_nhkb, bob_pq_kem_key_pair):
|
||||
state.DHRs = bob_dh_key_pair
|
||||
state.DHRr = None
|
||||
// below added for KEM
|
||||
state.PQRs = bob_pq_kem_key_pair
|
||||
state.PQRr = None
|
||||
state.PQRss = None
|
||||
state.PQRct = None
|
||||
// above added for KEM
|
||||
state.RK = SK
|
||||
state.CKs = None
|
||||
state.CKr = None
|
||||
state.Ns = 0
|
||||
state.Nr = 0
|
||||
state.PN = 0
|
||||
state.MKSKIPPED = {}
|
||||
state.HKs = None
|
||||
state.NHKs = shared_nhkb
|
||||
state.HKr = None
|
||||
state.NHKr = shared_hka
|
||||
```
|
||||
|
||||
`GENERATE_PQKEM` generates decapsulation/encapsulation key pair.
|
||||
|
||||
`PQKEM-ENC` is key encapsulation algorithm.
|
||||
|
||||
Other than commented lines, the above adds parameters `bob_pq_kem_encapsulation_key` and `bob_pq_kem_key_pair` to the ratchet initialization. Otherwise it is identical to the original double ratchet initialization.
|
||||
|
||||
### Encrypting messages
|
||||
|
||||
```
|
||||
def RatchetEncryptPQ2HE(state, plaintext, AD):
|
||||
state.CKs, mk = KDF_CK(state.CKs)
|
||||
// encapsulation key from PQRs and encapsulated shared secret is added to header
|
||||
header = HEADER_PQ2(
|
||||
dh = state.DHRs.public,
|
||||
kem = state.PQRs.public, // added for KEM #2
|
||||
ct = state.PQRct // added for KEM #1
|
||||
pn = state.PN,
|
||||
n = state.Ns,
|
||||
)
|
||||
enc_header = HENCRYPT(state.HKs, header)
|
||||
state.Ns += 1
|
||||
return enc_header, ENCRYPT(mk, plaintext, CONCAT(AD, enc_header))
|
||||
```
|
||||
|
||||
Other than adding encapsulation key and encapsulated shared secret into the header, the above is identical to the original double ratchet message encryption step.
|
||||
|
||||
### Decrypting messages
|
||||
|
||||
```
|
||||
def RatchetDecryptPQ2HE(state, enc_header, ciphertext, AD):
|
||||
plaintext = TrySkippedMessageKeysHE(state, enc_header, ciphertext, AD)
|
||||
if plaintext != None:
|
||||
return plaintext
|
||||
header, dh_ratchet = DecryptHeader(state, enc_header) // DecryptHeader is the same as in double ratchet specification
|
||||
if dh_ratchet:
|
||||
SkipMessageKeysHE(state, header.pn) // SkipMessageKeysHE is the same as in double ratchet specification
|
||||
DHRatchetPQ2HE(state, header)
|
||||
SkipMessageKeysHE(state, header.n)
|
||||
state.CKr, mk = KDF_CK(state.CKr)
|
||||
state.Nr += 1
|
||||
return DECRYPT(mk, ciphertext, CONCAT(AD, enc_header))
|
||||
|
||||
// DecryptHeader is the same as in double ratchet specification
|
||||
def DecryptHeader(state, enc_header):
|
||||
header = HDECRYPT(state.HKr, enc_header)
|
||||
if header != None:
|
||||
return header, False
|
||||
header = HDECRYPT(state.NHKr, enc_header)
|
||||
if header != None:
|
||||
return header, True
|
||||
raise Error()
|
||||
|
||||
def DHRatchetPQ2HE(state, header):
|
||||
state.PN = state.Ns
|
||||
state.Ns = 0
|
||||
state.Nr = 0
|
||||
state.HKs = state.NHKs
|
||||
state.HKr = state.NHKr
|
||||
state.DHRr = header.dh
|
||||
// save new encapsulation key from header
|
||||
state.PQRr = header.kem
|
||||
// decapsulate shared secret from header - KEM #2
|
||||
ss = PQKEM-DEC(state.PQRs.private, header.ct)
|
||||
// use decapsulated shared secret with receiving ratchet
|
||||
state.RK, state.CKr, state.NHKr = KDF_RK_HE(state.RK, DH(state.DHRs, state.DHRr) || ss)
|
||||
state.DHRs = GENERATE_DH()
|
||||
// below is added for KEM
|
||||
state.PQRs = GENERATE_PQKEM() // generate new PQ key pair
|
||||
state.PQRct, state.PQRss = PQKEM-ENC(state.PQRr) // encapsulate: generates shared secret and ciphertext KEM #1
|
||||
// above is added for KEM
|
||||
// use new shared secret with sending ratchet
|
||||
state.RK, state.CKs, state.NHKs = KDF_RK_HE(state.RK, DH(state.DHRs, state.DHRr) || state.PQRss)
|
||||
```
|
||||
|
||||
`PQKEM-DEC` is key decapsulation algorithm.
|
||||
|
||||
`DHRatchetPQ2HE` augments both DH agreements with decapsulated shared secret from the received header and with the new shared secret, respectively. The new shared secret together with the new encapsulation key are saved in the state and will be added to the header in the next sent message.
|
||||
|
||||
Other than augmenting DH key agreements with the shared secrets from KEM, the above is identical to the original double ratchet DH ratchet step.
|
||||
|
||||
It is worth noting that while DH agreements work as ping-pong, when the new received DH key is used for both DH agreements (and only the sent DH key is updated for the second DH key agreement), PQ KEM agreements in the proposed scheme work as a "parallel ping-pong", with two balls in play all the time (two KEM agreements run in parallel).
|
||||
|
||||
## Ratchet message wire format
|
||||
|
||||
The pseudocode above describes the algorithm. This section specifies the actual binary encoding used in SimpleX implementation with Curve448 DH keys, sntrup761 KEM and AES-256-GCM AEAD.
|
||||
|
||||
The ratchet-encrypted message has three encoding layers, from outermost to innermost:
|
||||
|
||||
1. **Encrypted ratchet message** — the complete ratchet message envelope, referenced as an opaque encrypted body in [agent protocol](./agent-protocol.md).
|
||||
2. **Encrypted message header** — the encrypted header within the ratchet message, used as associated data for message body encryption.
|
||||
3. **Plaintext message header** — the DH and KEM ratchet keys and counters.
|
||||
|
||||
### Encrypted ratchet message
|
||||
|
||||
The outer envelope contains the encrypted header (used as associated data for body authentication), the body authentication tag, and the encrypted message body.
|
||||
|
||||
The message body is encrypted with AES-256-GCM using the message key derived from the sending chain key (`KDF_CK`). The associated data for body encryption is the concatenation of the ratchet associated data and the encoded encrypted header.
|
||||
|
||||
```abnf
|
||||
encRatchetMessage = versionedLength encMessageHeader msgAuthTag encMsgBody
|
||||
; encMessageHeader is used as associated data for body decryption: AD = rcAD || encMessageHeader
|
||||
msgAuthTag = 16*16 OCTET ; AES-256-GCM authentication tag for the message body
|
||||
encMsgBody = *OCTET ; AES-256-GCM encrypted padded message body (remaining bytes)
|
||||
```
|
||||
|
||||
### Encrypted message header
|
||||
|
||||
The encrypted header wraps the current ratchet e2e encryption version, an initialization vector, an authentication tag, and the encrypted padded header body.
|
||||
|
||||
The header body is encrypted with AES-256-GCM using the header key (`HKs`). The associated data for header encryption is the ratchet associated data. The header is padded before encryption to a fixed size to prevent leaking information about the KEM state.
|
||||
|
||||
```abnf
|
||||
encMessageHeader = currentVersion headerIV headerAuthTag versionedLength encHeaderBody
|
||||
currentVersion = 2*2 OCTET ; Word16, current ratchet e2e encryption version
|
||||
headerIV = 16*16 OCTET ; AES-256 initialization vector for header encryption
|
||||
headerAuthTag = 16*16 OCTET ; AES-256-GCM authentication tag for the header
|
||||
encHeaderBody = *OCTET ; AES-256-GCM encrypted padded header (see plaintext format below)
|
||||
```
|
||||
|
||||
`versionedLength` uses a 2-byte length prefix (Word16) when the current e2e version supports PQ encryption, or a 1-byte length prefix otherwise. The parser distinguishes the two encodings by peeking at the first byte: values below 32 indicate a 2-byte prefix (as the header is always at least 69 bytes).
|
||||
|
||||
```abnf
|
||||
versionedLength = largeLength / length ; 2-byte for PQ versions, 1-byte for pre-PQ versions
|
||||
```
|
||||
|
||||
The padded header sizes before encryption are: 2310 bytes when PQ is supported, 88 bytes when PQ is not supported. Padding uses a 2-byte big-endian length prefix followed by the plaintext header and `#` fill bytes.
|
||||
|
||||
### Plaintext message header
|
||||
|
||||
```abnf
|
||||
msgHeader = maxVersion dhPublicKey [kemParams] prevMsgCount msgCount
|
||||
maxVersion = 2*2 OCTET ; Word16, max supported e2e encryption version
|
||||
dhPublicKey = length x509encoded ; Curve448 public DH ratchet key
|
||||
kemParams = noKEM / proposedKEM / acceptedKEM
|
||||
; present only when current ratchet version >= pqRatchetE2EEncryptVersion
|
||||
noKEM = %x30 ; "0" - no KEM parameters
|
||||
proposedKEM = %x31 %s"P" kemEncapsulationKey ; KEM proposed, not yet accepted
|
||||
acceptedKEM = %x31 %s"A" kemCiphertext kemEncapsulationKey ; KEM accepted
|
||||
kemEncapsulationKey = largeLength 1158*1158 OCTET ; sntrup761 encapsulation key
|
||||
kemCiphertext = largeLength 1039*1039 OCTET ; sntrup761 ciphertext
|
||||
prevMsgCount = 4*4 OCTET ; Word32, number of messages in previous sending chain
|
||||
msgCount = 4*4 OCTET ; Word32, message number in current sending chain
|
||||
length = 1*1 OCTET
|
||||
largeLength = 2*2 OCTET ; Word16
|
||||
```
|
||||
|
||||
### KEM state machine
|
||||
|
||||
PQ encryption can be enabled or disabled during a connection's lifetime. The KEM parameters in the header reflect three states:
|
||||
|
||||
- **No KEM** (`noKEM`): PQ encryption is not active. The header contains only the DH key, as in the original double ratchet.
|
||||
- **Proposed** (`proposedKEM`): One party generated a KEM key pair and includes the encapsulation key in the header, proposing PQ encryption. No ciphertext is included because the other party has not yet sent its encapsulation key.
|
||||
- **Accepted** (`acceptedKEM`): The party received the other's encapsulation key, performed encapsulation (KEM #1), and includes both the ciphertext and its own new encapsulation key (for KEM #2). This is the steady state for active PQ encryption.
|
||||
|
||||
The transition from Proposed to Accepted happens when a party receives a message containing KEM parameters (either Proposed or Accepted) and responds with its own Accepted parameters. Once both parties are in Accepted state, the double PQ KEM augmentation described in the algorithm above operates in each DH ratchet step.
|
||||
|
||||
## Implementation considerations for SimpleX Messaging Protocol
|
||||
|
||||
As SimpleX Messaging Protocol pads messages to a fixed size, using 16kb transport blocks, the size increase introduced by this scheme can be compensated for by using ZSTD encryption of JSON bodies and image previews encoded as base64. While there may be some rare cases of random texts that would fail to compress, in all real scenarios it would not cause the message size reduction.
|
||||
|
||||
Sharing the initial keys in case of SimpleX Chat it is equivalent to sharing the invitation link. As encapsulation key is large, it may be inconvenient to share it in the link in some contexts, e.g. when QR codes are used.
|
||||
|
||||
It is possible to postpone sharing the encapsulation key until the first message from Alice (confirmation message in SMP protocol), the party sending connection request. The upside here is that the invitation link size would not increase. The downside is that the user profile shared in this confirmation will not be encrypted with PQ-resistant algorithm.
|
||||
|
||||
Another consideration is pairwise ratchets in groups. Key generation in sntrup761 is quite slow - on slow devices it can be as slow as 10-20 keys per second, so using this primitive in groups larger than 10-20 members would result in slow performance.
|
||||
|
||||
For backward compatibility the implementation must support adding PQ-resistant key agreement to the existing connections.
|
||||
|
||||
It is also beneficial to support removing PQ-resistant key agreement from the connections that have them, e.g. as the group size grows.
|
||||
|
||||
### Chosen KEM algorithm
|
||||
|
||||
The implementation uses Streamlined NTRU-Prime 761 (sntrup761) that was also used for OpenSSH for a long time.
|
||||
|
||||
It was chosen over ML-KEM (Kyber) standardized by NIST for several reasons:
|
||||
|
||||
- sntrup761 was used in OpenSSH for a long period of time.
|
||||
- ML-KEM standardization process raised [concerns](https://groups.google.com/a/list.nist.gov/g/pqc-forum/c/WFRDl8DqYQ4) [amongst](https://blog.cr.yp.to/20231003-countcorrectly.html) the experts.
|
||||
- ML-KEM (if modified) is likely to have conflicts with the existing patents, unlike sntrup761.
|
||||
|
||||
It was chosen over non-interactive CTIDH due to its slower implementation, and lack of optimized code for aarch64 CPUs used in mobile devices.
|
||||
|
||||
## Summary
|
||||
|
||||
If chosen PQ KEM proves secure against quantum computer attacks, then the proposed augmented double ratchet will also be secure against quantum computer attack, including break-in recovery property, while keeping deniability and forward secrecy, because the [same proof](https://eprint.iacr.org/2016/1013.pdf) as for double ratchet algorithm would hold here, provided chosen KEM is secure.
|
||||
@@ -0,0 +1,420 @@
|
||||
Version 3, 2025-01-24
|
||||
|
||||
# Overview of push notifications for SimpleX Messaging Routers
|
||||
|
||||
This document describes Notification Router protocol version 3. Version history:
|
||||
- v1: initial version
|
||||
- v2: authenticated commands, command batching
|
||||
- v3: detailed invalid token reason
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [Participating routers](#participating-routers)
|
||||
- [Register device token to receive push notifications](#register-device-token-to-receive-push-notifications)
|
||||
- [Subscribe to connection notifications](#subscribe-to-connection-notifications)
|
||||
- [SimpleX Notification Router protocol](#simplex-notification-router-protocol)
|
||||
- [Register new notification token](#register-new-notification-token)
|
||||
- [Verify notification token](#verify-notification-token)
|
||||
- [Check notification token status](#check-notification-token-status)
|
||||
- [Replace notification token](#replace-notification-token)
|
||||
- [Delete notification token](#delete-notification-token)
|
||||
- [Subscribe to periodic notifications](#subscribe-to-periodic-notifications)
|
||||
- [Create SMP message notification subscription](#create-smp-message-notification-subscription)
|
||||
- [Check notification subscription status](#check-notification-subscription-status)
|
||||
- [Delete notification subscription](#delete-notification-subscription)
|
||||
- [Error responses](#error-responses)
|
||||
- [Threat model](#threat-model)
|
||||
|
||||
## Introduction
|
||||
|
||||
SimpleX Messaging routers already operate as push routers and deliver the messages to subscribed clients as soon as they are sent to the routers.
|
||||
|
||||
The reason for push notifications is to support instant message notifications on iOS that does not allow background services.
|
||||
|
||||
## Participating routers
|
||||
|
||||
The diagram below shows which routers participate in message notification delivery.
|
||||
|
||||
While push provider (e.g., APN) can learn how many notifications are delivered to the user, it cannot access message content, even encrypted, or any message metadata - the notifications are e2e encrypted between SimpleX Notification Router and the user's device.
|
||||
|
||||
```
|
||||
User's iOS device Internet Routers
|
||||
--------------------- . ------------------------ . -----------------------------
|
||||
. .
|
||||
. . can be self-hosted now
|
||||
+--------------+ . . +----------------+
|
||||
| SimpleX Chat | -------------- TLS --------------- | SimpleX |
|
||||
| client |------> SimpleX Messaging Protocol (SMP) ------> | Messaging |
|
||||
+--------------+ ---------------------------------- | Router |
|
||||
^ | . . +----------------+
|
||||
| | . . . . . | . . .
|
||||
| | . . | V |
|
||||
| | . . |SMP| TLS
|
||||
| | . . | | | SimpleX
|
||||
| | . . . . . V . . . NTF Router
|
||||
| | . . +----------------------------------+
|
||||
| | . . | +---------------+ |
|
||||
| | -------------- TLS --------------- | | SimpleX | can be |
|
||||
| |-----------> Notification Router Protocol -----> | | Notifications | self-hosted |
|
||||
| ---------------------------------- | | Subscriber | in the future |
|
||||
| . . | +---------------+ |
|
||||
| . . | | |
|
||||
| . . | V |
|
||||
| . . | +---------------+ |
|
||||
| . . | | SimpleX | |
|
||||
| . . | | Push | |
|
||||
| . . | | Router | |
|
||||
| . . | +---------------+ |
|
||||
| . . +----------------------------------+
|
||||
| . . . . . | . . .
|
||||
| . . | V |
|
||||
| . . |SMP| TLS
|
||||
| . . | | |
|
||||
| . . . . . V . . .
|
||||
| -------------- TLS --------------- +-----------------+
|
||||
|----------------- Notification delivery <-------| Apple PN server |
|
||||
---------------------------------- +-----------------+
|
||||
. .
|
||||
```
|
||||
|
||||
## Register device token to receive push notifications
|
||||
|
||||
This diagram shows the process of registering a device to receive PUSH notifications via Apple Push Notification (APN) servers.
|
||||
|
||||

|
||||
|
||||
## Subscribe to connection notifications
|
||||
|
||||
This diagram shows the process of subscription to notifications, notification delivery and device token update.
|
||||
|
||||

|
||||
|
||||
## SimpleX Notification Router protocol
|
||||
|
||||
To manage notification subscriptions to SMP routers, SimpleX Notification Router provides an RPC protocol with a similar design to SimpleX Messaging Protocol router.
|
||||
|
||||
This protocol sends requests and responses in a fixed size blocks of 512 bytes over TLS, uses the same [syntax of protocol transmissions](./simplex-messaging.md#smp-transmission-and-transport-block-structure) as SMP protocol, and has the same transport [handshake syntax](./simplex-messaging.md#transport-handshake) (except the router certificate is not included in the handshake).
|
||||
|
||||
The client and router use ALPN extension with `ntf/1` protocol name to agree handshake version.
|
||||
|
||||
Protocol commands have this syntax:
|
||||
|
||||
```abnf
|
||||
ntfRouterTransmission = authorization corrId entityId ntfRouterCmd
|
||||
; same transmission structure as SMP, see simplex-messaging.md
|
||||
ntfRouterCmd = newTokenCmd / verifyTokenCmd / checkTokenCmd /
|
||||
replaceTokenCmd / deleteTokenCmd / cronCmd /
|
||||
newSubCmd / checkSubCmd / deleteSubCmd / pingCmd
|
||||
```
|
||||
### Register new notification token
|
||||
|
||||
This command should be used after the client app obtains a token from push notifications provider to register the token with the router.
|
||||
|
||||
Having received this command the router will deliver a test notification via the push provider to validate that the client has this token.
|
||||
|
||||
The command syntax:
|
||||
|
||||
```abnf
|
||||
newTokenCmd = %s"TNEW" SP newToken
|
||||
newToken = %s"T" deviceToken authPubKey clientDhPubKey
|
||||
deviceToken = pushProvider tokenString
|
||||
pushProvider = apnsDev / apnsProd / apnsTest / apnsNull
|
||||
apnsDev = "AD" ; APNS token for development environment
|
||||
apnsProd = "AP" ; APNS token for production environment
|
||||
apnsTest = "AT" ; APNS token for test environment (mock server)
|
||||
apnsNull = "AN" ; token that does not trigger any notification delivery - used for router testing
|
||||
tokenString = shortString
|
||||
authPubKey = length x509encoded ; Ed25519 key used to verify clients commands
|
||||
clientDhPubKey = length x509encoded ; X25519 key to agree e2e encryption between the router and client
|
||||
shortString = length *OCTET
|
||||
length = 1*1 OCTET
|
||||
```
|
||||
|
||||
The router response syntax:
|
||||
|
||||
```abnf
|
||||
tokenIdResp = %s"IDTKN" SP entityId routerDhPubKey
|
||||
entityId = shortString
|
||||
routerDhPubKey = length x509encoded ; X25519 key to agree e2e encryption between the router and client
|
||||
```
|
||||
|
||||
### Verify notification token
|
||||
|
||||
This command is used to verify the token after the device receives the test notification from the push provider.
|
||||
|
||||
The command syntax:
|
||||
|
||||
```abnf
|
||||
verifyTokenCmd = %s"TVFY" SP regCode
|
||||
regCode = shortString
|
||||
```
|
||||
|
||||
The response to this command is `okResp` or `errorResp`
|
||||
|
||||
```abnf
|
||||
okResp = %s"OK"
|
||||
```
|
||||
|
||||
### Check notification token status
|
||||
|
||||
This command is used to check the token status:
|
||||
|
||||
```abnf
|
||||
checkTokenCmd = %s"TCHK"
|
||||
```
|
||||
|
||||
The response to this command:
|
||||
|
||||
```abnf
|
||||
tokenStatusResp = %s"TKN" SP tokenStatus
|
||||
tokenStatus = %s"NEW" / %s"REGISTERED" / tokenInvalid / %s"CONFIRMED" / %s"ACTIVE" / %s"EXPIRED"
|
||||
tokenInvalid = %s"INVALID" ["," invalidReason] ; optional reason added in v3
|
||||
invalidReason = %s"BAD" / %s"TOPIC" / %s"EXPIRED" / %s"UNREGISTERED"
|
||||
```
|
||||
|
||||
### Replace notification token
|
||||
|
||||
This command should be used when push provider issues a new notification token.
|
||||
|
||||
It happens when:
|
||||
- the app data is migrated to another device.
|
||||
- the app is re-installed on the same device.
|
||||
- can happen periodically, at push provider discretion.
|
||||
|
||||
This command allows to replace the token without re-registering and re-subscribing all notification subscriptions.
|
||||
|
||||
Using this command triggers the same verification flow as registering a new token.
|
||||
|
||||
The command syntax:
|
||||
|
||||
```abnf
|
||||
replaceTokenCmd = %s"TRPL" SP deviceToken
|
||||
```
|
||||
|
||||
The response to this command is `okResp` or `errorResp`.
|
||||
|
||||
### Delete notification token
|
||||
|
||||
The command syntax:
|
||||
|
||||
```abnf
|
||||
deleteTokenCmd = %s"TDEL"
|
||||
```
|
||||
|
||||
The response to this command is `okResp` or `errorResp`.
|
||||
|
||||
After this command all message notification subscriptions will be removed and no more notifications will be sent.
|
||||
|
||||
### Subscribe to periodic notifications
|
||||
|
||||
This command enables or disables periodic notifications sent to the client device irrespective of message notifications.
|
||||
|
||||
This is useful for two reasons:
|
||||
- it provides better privacy from notification router, as while the router learns the device token, it doesn't learn anything else about user communications.
|
||||
- it allows to receive messages when notifications were dropped by push provider, e.g. while the device was offline, or lost by notification router, e.g. while it was restarting.
|
||||
|
||||
The command syntax:
|
||||
|
||||
```abnf
|
||||
cronCmd = %s"TCRN" SP interval
|
||||
interval = 2*2 OCTET ; Word16, minutes
|
||||
```
|
||||
|
||||
The interval for periodic notifications is set in minutes, with the minimum of 20 minutes. The client should pass `0` to disable periodic notifications.
|
||||
|
||||
### Create SMP message notification subscription
|
||||
|
||||
This command makes notification router subscribe to message notifications from SMP router and to deliver them to push provider:
|
||||
|
||||
```abnf
|
||||
newSubCmd = %s"SNEW" SP newSub
|
||||
newSub = %s"S" tokenId smpRouter notifierId notifierKey
|
||||
tokenId = shortString ; returned in response to `TNEW` command
|
||||
smpRouter = hosts port fingerprint
|
||||
hosts = length 1*host
|
||||
host = shortString
|
||||
port = shortString
|
||||
fingerprint = shortString
|
||||
notifierId = shortString ; returned by SMP router in response to `NKEY` SMP command
|
||||
notifierKey = length x509encoded ; private key used to authorize requests to subscribe to message notifications
|
||||
```
|
||||
|
||||
The response syntax:
|
||||
|
||||
```abnf
|
||||
subIdResp = %s"IDSUB" SP entityId
|
||||
```
|
||||
|
||||
### Check notification subscription status
|
||||
|
||||
This command syntax:
|
||||
|
||||
```abnf
|
||||
checkSubCmd = %s"SCHK"
|
||||
```
|
||||
|
||||
The response:
|
||||
|
||||
```abnf
|
||||
subStatusResp = %s"SUB" SP subStatus
|
||||
subStatus = %s"NEW" / %s"PENDING" / ; e.g., after SMP router disconnect/timeout while ntf router is retrying to connect
|
||||
%s"ACTIVE" / %s"INACTIVE" / %s"END" / ; if another router subscribed to notifications
|
||||
%s"AUTH" / %s"DELETED" / %s"SERVICE" / subErrStatus
|
||||
subErrStatus = %s"ERR" SP *OCTET
|
||||
```
|
||||
|
||||
### Delete notification subscription
|
||||
|
||||
The command syntax:
|
||||
|
||||
```abnf
|
||||
deleteSubCmd = %s"SDEL"
|
||||
```
|
||||
|
||||
The response to this command is `okResp` or `errorResp`.
|
||||
|
||||
After this command no more message notifications will be sent from this queue.
|
||||
|
||||
### Keep-alive command
|
||||
|
||||
To keep the transport connection alive the clients should use `PING` command:
|
||||
|
||||
```abnf
|
||||
pingCmd = %s"PING"
|
||||
pongResp = %s"PONG"
|
||||
```
|
||||
|
||||
This command is sent unsigned and without entity ID.
|
||||
|
||||
### Error responses
|
||||
|
||||
All commands can return error response:
|
||||
|
||||
```abnf
|
||||
errorResp = %s"ERR" SP errorType
|
||||
```
|
||||
|
||||
Where `errorType` has the same syntax as in [SimpleX Messaging Protocol](./simplex-messaging.md#error-responses)
|
||||
|
||||
## Threat Model
|
||||
|
||||
This threat model compliments SimpleX Messaging Protocol [threat model](./security.md#threat-model)
|
||||
|
||||
#### A passive adversary able to monitor the traffic of one user
|
||||
|
||||
*can:*
|
||||
|
||||
- identify that and a user is using SimpleX push notifications.
|
||||
|
||||
*cannot:*
|
||||
|
||||
- determine which routers a user subscribed to the notifications from.
|
||||
|
||||
#### A passive adversary able to monitor a set of senders and recipients
|
||||
|
||||
*can:*
|
||||
|
||||
- perform more efficient traffic correlation attacks against senders and recipients and correlate senders and recipients within the monitored set, frustrated by the number of users on the routers.
|
||||
|
||||
#### SimpleX Messaging Protocol router
|
||||
|
||||
*can:*
|
||||
|
||||
- learn which messages trigger push notifications.
|
||||
|
||||
- learn IP address of SimpleX notification routers used by the user.
|
||||
|
||||
- drop message notifications.
|
||||
|
||||
- spam a user with invalid notifications.
|
||||
|
||||
*cannot:*
|
||||
|
||||
- learn user device token for push notifications.
|
||||
|
||||
- learn which queues belong to the same users with any additional efficiency compared with not using push notifications.
|
||||
|
||||
#### SimpleX Notification Router subscribed to message notifications
|
||||
|
||||
*can:*
|
||||
|
||||
- learn a user device token.
|
||||
|
||||
- learn how many messaging queues and routers a user receives messages from.
|
||||
|
||||
- learn how many message notifications are delivered to the user from each queue.
|
||||
|
||||
- undetectably drop notifications.
|
||||
|
||||
- spam a user with background notifications.
|
||||
|
||||
*cannot:*
|
||||
|
||||
- learn queue addresses for receiving or sending messages.
|
||||
|
||||
- learn the contents or type of messages (not even encrypted).
|
||||
|
||||
- learn anything about messages sent without notification flag.
|
||||
|
||||
- spam a user with visible notifications (provided the client app can filter push notifications).
|
||||
|
||||
- add, duplicate, or corrupt individual messages that will be shown to the user.
|
||||
|
||||
#### SimpleX Notification Router subscribed ONLY to periodic notifications
|
||||
|
||||
*can:*
|
||||
|
||||
- learn a user device token.
|
||||
|
||||
- drop periodic notifications.
|
||||
|
||||
- spam a user with background notifications.
|
||||
|
||||
*cannot:*
|
||||
|
||||
- learn how many messaging queues and routers a user receives messages from.
|
||||
|
||||
- learn how many message notifications are delivered to the user from each queue.
|
||||
|
||||
- learn queue addresses for receiving or sending messages.
|
||||
|
||||
- learn the contents or type of messages (not even encrypted).
|
||||
|
||||
- learn anything about messages sent without notification flag.
|
||||
|
||||
- spam a user with visible notifications (provided the client app can filter push notifications).
|
||||
|
||||
- add, duplicate, or corrupt individual messages that will be shown to the user.
|
||||
|
||||
#### A user’s contact
|
||||
|
||||
*cannot:*
|
||||
|
||||
- determine if a user uses push notifications or not.
|
||||
|
||||
#### Push notification provider (e.g., APN)
|
||||
|
||||
*can:*
|
||||
|
||||
- learn that a user uses SimpleX app.
|
||||
|
||||
- learn how many notifications are delivered to user's device.
|
||||
|
||||
- drop notifications (in fact, APN coalesces notifications delivered while user's device is offline, delivering only the last one).
|
||||
|
||||
*cannot:*
|
||||
|
||||
- learn which SimpleX Messaging Protocol routers are used by a user (notifications are e2e encrypted).
|
||||
|
||||
- learn which or how many messaging queues a user receives notifications from.
|
||||
|
||||
- learn the contents or type of messages (not even encrypted, notifications only contain encrypted metadata).
|
||||
|
||||
#### An attacker with Internet access
|
||||
|
||||
*cannot:*
|
||||
|
||||
- register notification token not present on attacker's device.
|
||||
|
||||
- enumerate tokens or subscriptions on a SimpleX Notification Router.
|
||||
@@ -0,0 +1,215 @@
|
||||
Revision 1, 2026-03-09
|
||||
|
||||
# SimpleX Network: Security
|
||||
|
||||
This document describes the cryptographic primitives and threat model for the SimpleX network. For a general introduction, see [SimpleX: messaging and application platform](./overview-tjr.md).
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Encryption primitives](#encryption-primitives)
|
||||
- [Threat model](#threat-model)
|
||||
- [Global Assumptions](#global-assumptions)
|
||||
- [A passive adversary able to monitor the traffic of one user](#a-passive-adversary-able-to-monitor-the-traffic-of-one-user)
|
||||
- [A passive adversary able to monitor a set of senders and recipients](#a-passive-adversary-able-to-monitor-a-set-of-senders-and-recipients)
|
||||
- [SimpleX Messaging Protocol router](#simplex-messaging-protocol-router)
|
||||
- [SimpleX Messaging Protocol router that proxies the messages to another SMP router](#simplex-messaging-protocol-router-that-proxies-the-messages-to-another-smp-router)
|
||||
- [An attacker who obtained Alice's (decrypted) chat database](#an-attacker-who-obtained-alices-decrypted-chat-database)
|
||||
- [A user's contact](#a-users-contact)
|
||||
- [An attacker who observes Alice showing an introduction message to Bob](#an-attacker-who-observes-alice-showing-an-introduction-message-to-bob)
|
||||
- [An attacker with Internet access](#an-attacker-with-internet-access)
|
||||
|
||||
|
||||
## Encryption primitives
|
||||
|
||||
- **Router command authorization**: X25519 DH-based authenticated encryption (SMP v7+), providing sender deniability. Ed25519 signatures used for recipient commands and notifier commands.
|
||||
|
||||
- **Per-queue key agreement**: Curve25519 DH exchange to agree:
|
||||
- the shared secret between router and recipient (to encrypt message bodies — avoids shared ciphertext in sender and recipient traffic),
|
||||
- the shared secret between sender and recipient (to encrypt messages end-to-end in each queue — avoids shared ciphertext in redundant queues).
|
||||
|
||||
- **SMP-layer encryption**: [NaCl crypto_box](https://nacl.cr.yp.to/box.html) (curve25519xsalsa20poly1305) for message body encryption between router and recipient, and for e2e per-queue encryption.
|
||||
|
||||
- **Certificate validation**: SHA256 to validate router offline certificates.
|
||||
|
||||
- **End-to-end encryption**: [Double ratchet](https://signal.org/docs/specifications/doubleratchet/) protocol:
|
||||
- Curve448 keys for shared secret agreement via [X3DH](https://signal.org/docs/specifications/x3dh/) with 2 ephemeral keys per side,
|
||||
- optional [SNTRUP761](https://ntruprime.cr.yp.to/) post-quantum KEM running in parallel with the DH ratchet (see [PQDR](./pqdr.md)), providing post-quantum forward secrecy,
|
||||
- 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 routers is not directly tied to their identity or otherwise represents distinguishing information about the user.
|
||||
- The user's client uses 2-hop onion message routing.
|
||||
|
||||
### A passive adversary able to monitor the traffic of one user
|
||||
|
||||
*can:*
|
||||
|
||||
- identify that and when a user is using SimpleX.
|
||||
|
||||
- determine which routers the user receives messages from.
|
||||
|
||||
- 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 messages to.
|
||||
|
||||
- determine the routers used by users' contacts.
|
||||
|
||||
### 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 routers 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 routers.
|
||||
|
||||
- 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 router
|
||||
|
||||
*can:*
|
||||
|
||||
- learn when a queue recipient is online.
|
||||
|
||||
- know how many messages are sent via the queue (although some may be noise or not content messages).
|
||||
|
||||
- learn which messages would trigger notifications even if a user does not use [push notifications](./push-notifications.md).
|
||||
|
||||
- perform the correlation of the queue used to receive messages (matching multiple queues to a single user) via either a re-used transport connection, user's IP Address, or connection timing regularities.
|
||||
|
||||
- learn a recipient'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 or type of messages.
|
||||
|
||||
- distinguish noise messages from regular messages except via timing regularities.
|
||||
|
||||
- compromise the users' end-to-end encryption with an active attack.
|
||||
|
||||
- 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 (provided messages are sent via proxy SMP router).
|
||||
|
||||
- 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 the proxy SMP router (provided messages are sent via proxy SMP router).
|
||||
|
||||
### SimpleX Messaging Protocol router that proxies the messages to another SMP router
|
||||
|
||||
*can:*
|
||||
|
||||
- learn a sender's IP address, as long as Tor is not used.
|
||||
|
||||
- learn when a sender with a given IP address is online.
|
||||
|
||||
- know how many messages are sent from a given IP address and to a given destination SMP router.
|
||||
|
||||
- drop all messages from a given IP address or to a given destination router.
|
||||
|
||||
- unless destination SMP router detects repeated public DH keys of senders, replay messages to a destination router 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 router, 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 the destination SMP router.
|
||||
|
||||
- undetectably add, duplicate, or corrupt individual messages.
|
||||
|
||||
- undetectably drop individual messages, so long as a subsequent message is delivered.
|
||||
|
||||
- learn the contents or type of messages.
|
||||
|
||||
- learn which messages would trigger notifications.
|
||||
|
||||
- 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 the destination SMP routers via 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 router (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 first.
|
||||
|
||||
### 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 routers.
|
||||
|
||||
- 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 router.
|
||||