Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54dc8d42e7 | ||
|
|
0e1562deae | ||
|
|
94540a2c71 | ||
|
|
16367fcb3b | ||
|
|
8be2505fa0 | ||
|
|
a000419bd7 | ||
|
|
c8a8e2c297 | ||
|
|
f7d038ef20 | ||
|
|
4a927d1ae2 | ||
|
|
3a74558e84 | ||
|
|
e3fff98b1b | ||
|
|
acea477aab | ||
|
|
c31668ce28 | ||
|
|
2efe1496d2 | ||
|
|
011e1109e9 | ||
|
|
fbf1afc9f5 | ||
|
|
76e3863cbb | ||
|
|
1c2fbdb2c5 | ||
|
|
1822296481 | ||
|
|
b747080db3 | ||
|
|
20f7b538e5 | ||
|
|
89caf55729 | ||
|
|
c08cfb460b | ||
|
|
03eca19d38 | ||
|
|
a5a3a2cbad | ||
|
|
4a3c5abf32 | ||
|
|
6c6f22051d | ||
|
|
d693868bc0 | ||
|
|
32a64b994e | ||
|
|
a83f85dc58 | ||
|
|
0f7ede5eed | ||
|
|
ce64c91d5a | ||
|
|
66177fd550 | ||
|
|
9d83a9c017 | ||
|
|
019db0ab91 | ||
|
|
8954f39425 | ||
|
|
4d96e3700c | ||
|
|
eaa8221b95 | ||
|
|
af3f70829d | ||
|
|
ff8197b87b | ||
|
|
fd96ee2840 | ||
|
|
605970f6b6 | ||
|
|
dc40c3461a | ||
|
|
4da637a565 | ||
|
|
23cecc7b68 | ||
|
|
9692e636ac | ||
|
|
2b93e0b17d | ||
|
|
9f0b9a83d6 | ||
|
|
27cfc4c62b | ||
|
|
5e39c47975 | ||
|
|
ed0f8c79a2 | ||
|
|
29eb9e4e27 | ||
|
|
e346a81e46 | ||
|
|
e6e17ec637 | ||
|
|
d35bd8a954 | ||
|
|
d1774e5b56 | ||
|
|
9c460966b2 | ||
|
|
d0cd34989d | ||
|
|
d838be773b | ||
|
|
2edc7529f9 | ||
|
|
6d9af2ec17 | ||
|
|
3533929905 | ||
|
|
d8e60ecfdb | ||
|
|
215d2414b7 | ||
|
|
b1489138ea | ||
|
|
9f8db13553 | ||
|
|
511a97c5d0 | ||
|
|
44f0dd39f3 | ||
|
|
bdff274f56 | ||
|
|
83364052b1 | ||
|
|
c6033e51a0 | ||
|
|
0f23b4ab5c | ||
|
|
e27c4f7b81 | ||
|
|
b5869cf169 | ||
|
|
6b03876ef1 | ||
|
|
6a665a0833 | ||
|
|
c5eb65fec8 | ||
|
|
5743069021 | ||
|
|
0df7733125 | ||
|
|
d41c2bec2a | ||
|
|
7e265e5c59 | ||
|
|
2bec353eae | ||
|
|
40da7e76dd | ||
|
|
2e02ba86fd | ||
|
|
02eb1e9822 | ||
|
|
cd48c33c0d | ||
|
|
5258fe9b7e | ||
|
|
e33d5b5184 | ||
|
|
2b1a02d7d4 | ||
|
|
9255d48b69 | ||
|
|
bab689099f | ||
|
|
a0eb53b891 | ||
|
|
db120b6d2e | ||
|
|
64f5d63d2e | ||
|
|
ddc2da8fe4 | ||
|
|
3fe1f3c822 | ||
|
|
448b674fb1 | ||
|
|
cf1cd75a15 | ||
|
|
40164ff21f | ||
|
|
c1266d55a4 | ||
|
|
cf8f78f518 | ||
|
|
accfee5a52 | ||
|
|
cc2066473c | ||
|
|
8fde8e1344 | ||
|
|
deec963de8 | ||
|
|
552759018e | ||
|
|
6d52150672 | ||
|
|
e9f949f117 | ||
|
|
a0c86158d9 | ||
|
|
cc38deb65d | ||
|
|
cf147397a4 | ||
|
|
2f7c115207 | ||
|
|
6ed4dd1515 | ||
|
|
914240108d | ||
|
|
e764a51fc0 | ||
|
|
94de01a06f | ||
|
|
faec32dc5e | ||
|
|
6227215c3f | ||
|
|
3d3eb335e8 | ||
|
|
453e18c4ad | ||
|
|
249bcc7bb3 | ||
|
|
4afb72070a | ||
|
|
524c1de4de | ||
|
|
875a4bac5e | ||
|
|
f02cb2da4c | ||
|
|
2f15ce2662 | ||
|
|
781f8e0000 | ||
|
|
c3872f0a55 | ||
|
|
3e9e144145 | ||
|
|
50c9c95287 | ||
|
|
1b4b184517 | ||
|
|
e4aad7583f | ||
|
|
f20a996797 | ||
|
|
8c8a984c1c | ||
|
|
202922bceb | ||
|
|
3ab5e9d110 | ||
|
|
4ce4fa3423 | ||
|
|
fbf0b8b175 | ||
|
|
1f394b76a6 | ||
|
|
97b8b37325 | ||
|
|
bbf9e28696 | ||
|
|
50664930cc | ||
|
|
cd1adc8307 | ||
|
|
733c9374bf | ||
|
|
a8121fc8ad | ||
|
|
c970674452 | ||
|
|
4a9b5412db | ||
|
|
1ecef81b4d | ||
|
|
2bf8cba4fd | ||
|
|
628169adb4 | ||
|
|
fcef7deec0 | ||
|
|
44535628a5 | ||
|
|
6a48b735d1 | ||
|
|
bd0cc25083 | ||
|
|
15637e545b | ||
|
|
c0dcf283eb | ||
|
|
2ddfb044fc | ||
|
|
acdf4f41c5 | ||
|
|
a3d9ee213d | ||
|
|
cf7346d48b | ||
|
|
4e8657097c | ||
|
|
d8f81b5cdb | ||
|
|
2ae3100bed | ||
|
|
88c6b0d663 | ||
|
|
8659d4de05 | ||
|
|
bccef0ba47 | ||
|
|
d7a008985b | ||
|
|
9775861b6f | ||
|
|
4d333a90fa | ||
|
|
eb1723acce | ||
|
|
eff501ddcc | ||
|
|
b342b1dc59 | ||
|
|
1523c00bcc | ||
|
|
3637d0c610 | ||
|
|
360e55a48f | ||
|
|
ee2a764f93 | ||
|
|
cfe995325a | ||
|
|
3be0008c5a | ||
|
|
5d8febc353 | ||
|
|
c77e7f7e43 | ||
|
|
6858f74897 | ||
|
|
d1b4fa8911 | ||
|
|
7fb48930f6 | ||
|
|
ff038b492c | ||
|
|
b59669a65e | ||
|
|
2ccef1690b | ||
|
|
d4fc638478 | ||
|
|
f66e8239f4 | ||
|
|
56eea29ec3 | ||
|
|
a854b5f2b2 | ||
|
|
63b7956083 | ||
|
|
4f4b4f7eee | ||
|
|
f9c613ffb8 | ||
|
|
14cb88e725 | ||
|
|
6ccbe5e66e | ||
|
|
f921dc3adb | ||
|
|
c4f377a85b | ||
|
|
ca5c4b75eb | ||
|
|
0af6533510 | ||
|
|
42df6a421d | ||
|
|
bc8acf2abf | ||
|
|
19feee881b | ||
|
|
324e01300d | ||
|
|
b549910c17 | ||
|
|
a1eda12475 | ||
|
|
abee1fa70b | ||
|
|
701d06ba01 | ||
|
|
cac3432b03 | ||
|
|
56cc2bc71f | ||
|
|
9c9ba8c25c | ||
|
|
8e024590bc | ||
|
|
f47e7bf3c5 | ||
|
|
5e31c60c35 | ||
|
|
a0a092747a | ||
|
|
36bf92e54f | ||
|
|
1f12697279 | ||
|
|
92a379e75c | ||
|
|
0dddd59b36 | ||
|
|
f4ad3a983e | ||
|
|
b328492dc9 | ||
|
|
698b5c4146 | ||
|
|
acfa65200a | ||
|
|
61e0c346df | ||
|
|
11c235a8a3 | ||
|
|
058e3ac55e | ||
|
|
470b512a88 | ||
|
|
8dd728e834 | ||
|
|
7adcbf839f | ||
|
|
69a5896dac | ||
|
|
4b29855430 | ||
|
|
1d78e35688 | ||
|
|
af903bd405 | ||
|
|
51b0833f98 | ||
|
|
874f9f5cd6 | ||
|
|
66d3465c19 | ||
|
|
d5bf99c32c | ||
|
|
1d3a604988 | ||
|
|
267236a4f8 | ||
|
|
aa17cc55c1 | ||
|
|
fb21d9836e | ||
|
|
e4842f4f47 | ||
|
|
dc920d90d9 | ||
|
|
f53c1f5559 | ||
|
|
0942bf9f1e | ||
|
|
57fc190a91 | ||
|
|
43fb513ef7 | ||
|
|
fbccca9947 | ||
|
|
40000047af | ||
|
|
c2342cba05 | ||
|
|
f3b6ed4db0 | ||
|
|
95db734b2d | ||
|
|
e281efdcb8 | ||
|
|
d2b88a1baa | ||
|
|
8f9e6c2112 | ||
|
|
f917c15b5e | ||
|
|
374526a11b | ||
|
|
29b99d6716 | ||
|
|
d33cf644f5 | ||
|
|
6fc3b26970 | ||
|
|
029fc6e781 | ||
|
|
d9a0e78b04 | ||
|
|
4beaafa35a | ||
|
|
561649a498 | ||
|
|
8d9816809f | ||
|
|
eb5c1c78cb | ||
|
|
19aef52135 | ||
|
|
d06bb2b9c5 | ||
|
|
f43e5d6833 | ||
|
|
7f81396b09 | ||
|
|
f97c1a7712 | ||
|
|
10e0e58ec3 | ||
|
|
0fa40fbfb0 | ||
|
|
f4baa5d672 | ||
|
|
b686bc0c17 | ||
|
|
29332a5e9f | ||
|
|
9cd5f96fe4 | ||
|
|
a2eea4f18f | ||
|
|
413aad5139 | ||
|
|
0a9b6e4ab4 | ||
|
|
f8505d4add | ||
|
|
41d3c14157 | ||
|
|
d4962daf11 | ||
|
|
085b785ee8 | ||
|
|
84ce7a90d0 | ||
|
|
abb3f20ed5 | ||
|
|
a3f58fdc6b | ||
|
|
e3e05d474d | ||
|
|
13a9eee0cf | ||
|
|
959ae34f16 | ||
|
|
8c1a9c529d | ||
|
|
f08d81252d | ||
|
|
afecefc3ad | ||
|
|
e328ae5d06 | ||
|
|
605e6c64c6 | ||
|
|
4bf9f2b535 | ||
|
|
bab6a1577b | ||
|
|
ca6164167e | ||
|
|
42a96d6d00 | ||
|
|
6adbc56021 | ||
|
|
882d2b123b | ||
|
|
da5058a0c4 | ||
|
|
50c210c5c0 | ||
|
|
7d53504bb3 | ||
|
|
e4b47825b5 | ||
|
|
b215bd954d | ||
|
|
26d149d17c | ||
|
|
c66a7e371f | ||
|
|
f2c1455a27 | ||
|
|
f872c25f09 | ||
|
|
e4b77ed9e6 | ||
|
|
5894f91927 | ||
|
|
1459a0613a | ||
|
|
a7b39b710c | ||
|
|
212a9bc4ab | ||
|
|
3fc9afbb35 | ||
|
|
b2d4bfd0a8 | ||
|
|
cfab5a18d3 | ||
|
|
180b4b9dcb | ||
|
|
0437796232 | ||
|
|
02bba01c16 | ||
|
|
68138c08d2 | ||
|
|
6bfaa4985e | ||
|
|
7d99c4b35c | ||
|
|
e9db0a1162 | ||
|
|
b8c23ea8d5 | ||
|
|
b76ef03dbe | ||
|
|
fcaddb7848 | ||
|
|
d788c3ca95 | ||
|
|
e07121266a | ||
|
|
2f39f055c1 | ||
|
|
5d06dde757 | ||
|
|
eb1f9370c1 | ||
|
|
d810db4eed | ||
|
|
d8f07e8dde | ||
|
|
0ab90cb204 | ||
|
|
1d40bb97c2 | ||
|
|
1ce63bee44 | ||
|
|
aafe2d43f5 | ||
|
|
0b259af9cb | ||
|
|
6bbe1dfc66 | ||
|
|
a6f401041a | ||
|
|
1670c9c05e | ||
|
|
cde8a11693 | ||
|
|
3a4f8cb6eb | ||
|
|
e75846aa38 | ||
|
|
23496f1a34 | ||
|
|
9f5e6bfd19 | ||
|
|
cb5ad1619c | ||
|
|
7a238812b7 | ||
|
|
991548b64d | ||
|
|
cc798145d2 | ||
|
|
4339218c57 | ||
|
|
abd694bd5a | ||
|
|
9c1b43791c | ||
|
|
e3d2d6fc91 | ||
|
|
36b4179b8b | ||
|
|
bc26dc1d68 | ||
|
|
4c0164c49e | ||
|
|
40ae809033 | ||
|
|
2c52c514c0 | ||
|
|
57d83ae42d | ||
|
|
f9cd7e5416 | ||
|
|
77f1d45021 | ||
|
|
5ff80f0688 | ||
|
|
e622e12a7f | ||
|
|
6b6ea78eac | ||
|
|
238a2e7fe9 | ||
|
|
82eff6c757 | ||
|
|
0392f849aa | ||
|
|
16f5fdeeb0 | ||
|
|
c3012173e3 | ||
|
|
f6a321e198 | ||
|
|
c82fae72f2 | ||
|
|
9695786aa5 | ||
|
|
56ef95e8ae | ||
|
|
79d67694ed | ||
|
|
8dc051179a | ||
|
|
0b25b57e3c | ||
|
|
07bf2bca3b | ||
|
|
6c14372666 | ||
|
|
2c7cad5378 | ||
|
|
c9501a23a0 | ||
|
|
926e515bc0 | ||
|
|
09b51dc44f | ||
|
|
85d507d5d3 | ||
|
|
b0ac0744e2 | ||
|
|
ba40d75886 | ||
|
|
0c7c0d066d | ||
|
|
3d6ad64d62 | ||
|
|
51d0b48ce1 | ||
|
|
a8260290e7 | ||
|
|
2809963a2e | ||
|
|
d2321b750d | ||
|
|
c2f1dabcd0 | ||
|
|
9e0e6b1afc | ||
|
|
58622c6aaf | ||
|
|
b82452fe03 | ||
|
|
70e7c71762 | ||
|
|
a92827c986 | ||
|
|
f10e3f697c | ||
|
|
ef4d4c9e16 | ||
|
|
ffb4b4763c | ||
|
|
0d9d549cea | ||
|
|
a7c3133c35 | ||
|
|
5b61c6bccd | ||
|
|
d78fb8761e | ||
|
|
cd9cb19765 | ||
|
|
30b36cd756 | ||
|
|
08a4b41691 | ||
|
|
756bd1eca7 | ||
|
|
7a72488275 | ||
|
|
7fa4862a83 | ||
|
|
0ab44b1836 | ||
|
|
d1db7d6f79 | ||
|
|
ec70429106 | ||
|
|
31c78e516d | ||
|
|
b58523d4a2 | ||
|
|
9f981042b9 | ||
|
|
8c2227ccce | ||
|
|
bca44ae54a | ||
|
|
50ddcd3a13 | ||
|
|
3336f987c3 | ||
|
|
1d56ef3a60 | ||
|
|
0c45e0bf76 | ||
|
|
5ccebaeb21 | ||
|
|
5f94240ed4 | ||
|
|
331918bac7 | ||
|
|
f7bd8a3982 | ||
|
|
68c2682e70 | ||
|
|
62485b9367 | ||
|
|
91802fe94f | ||
|
|
0a71822dd0 | ||
|
|
9c12b4edc0 | ||
|
|
c1348aa54f | ||
|
|
4220c3bdaf | ||
|
|
7736ef8576 | ||
|
|
80309a0089 | ||
|
|
bfb556c860 | ||
|
|
3f985e8fd7 | ||
|
|
a3d1f5540d | ||
|
|
3f69636f1a | ||
|
|
60294521f4 | ||
|
|
4b3d04bd27 | ||
|
|
0dc34cd287 | ||
|
|
628930df1f | ||
|
|
46ba047077 | ||
|
|
facb6f4561 | ||
|
|
6a27b82759 | ||
|
|
b5ccce0f6a | ||
|
|
b60408837a | ||
|
|
c2ec691a46 | ||
|
|
ef7c66762e | ||
|
|
9f6316fa6d | ||
|
|
95a2f7560d | ||
|
|
b67c3e5935 | ||
|
|
ce3ca08199 | ||
|
|
fd410280b5 | ||
|
|
c74f4d729b | ||
|
|
49070fffe0 | ||
|
|
8dda99d77f | ||
|
|
4e4eea34f9 | ||
|
|
964daf5442 | ||
|
|
6a1cea93b5 | ||
|
|
ace94d7c69 | ||
|
|
1064e9c315 | ||
|
|
2733a09a40 | ||
|
|
7249cb0f0e | ||
|
|
f8ec838912 | ||
|
|
c6dde772b4 | ||
|
|
633cd675b5 | ||
|
|
83c18272cf | ||
|
|
7652ece045 | ||
|
|
7774fc3271 | ||
|
|
43d24d07dd | ||
|
|
a2204c9b84 | ||
|
|
f718a84335 | ||
|
|
e6fbaf5e50 | ||
|
|
7541ef4c13 | ||
|
|
5eb2822d25 | ||
|
|
0a2cad2745 | ||
|
|
4dc7d9bc77 | ||
|
|
12caa328a8 | ||
|
|
b5c5a8ad2a | ||
|
|
60d0f19121 | ||
|
|
68f52c47dd | ||
|
|
6f3e9dc603 | ||
|
|
17888f89a9 | ||
|
|
f84439f79d | ||
|
|
9d8a9c4fe4 | ||
|
|
45ddecc4b8 | ||
|
|
327f7112d0 | ||
|
|
d38303d5f1 | ||
|
|
65c77c78f3 | ||
|
|
6382367ec0 | ||
|
|
a89e019bb0 | ||
|
|
a638486bb3 | ||
|
|
a211e67829 | ||
|
|
390e99ca95 | ||
|
|
f577fcdacf | ||
|
|
ac7ffc413e | ||
|
|
fb26916eea | ||
|
|
2eff717248 | ||
|
|
d31958855f | ||
|
|
3ba1926b1e | ||
|
|
c62730fe4c | ||
|
|
bb99fdaaa2 | ||
|
|
a6ec93c38e | ||
|
|
33f822d72c | ||
|
|
b6e87e4a3e | ||
|
|
62acbc4ad4 | ||
|
|
4e1184d9eb | ||
|
|
cd22e06b3a | ||
|
|
6ef6bedc03 | ||
|
|
c380431b94 | ||
|
|
f466fa76e5 | ||
|
|
f2409e7280 | ||
|
|
337d5bdfe7 | ||
|
|
800581b2bf | ||
|
|
f060914ab8 | ||
|
|
14d76a1582 | ||
|
|
a37b24a8c2 | ||
|
|
5c6ec96d64 | ||
|
|
7a19ab224b | ||
|
|
7a611bed5a | ||
|
|
a406159ed4 | ||
|
|
14780a47d6 | ||
|
|
4a66f68c55 | ||
|
|
d1e6147adf | ||
|
|
7d1fdadef0 | ||
|
|
09e2e75c42 | ||
|
|
dff5cad1be | ||
|
|
c1f5f9d846 | ||
|
|
9c4778b129 | ||
|
|
137afb68fe | ||
|
|
229e2607d7 | ||
|
|
c380c79560 | ||
|
|
c9994c3a2c | ||
|
|
137ff7043d | ||
|
|
6fe3bfa980 | ||
|
|
2b857876b4 | ||
|
|
b777a4fd93 | ||
|
|
e15a25d92e | ||
|
|
1cd68f4159 | ||
|
|
495439adf5 | ||
|
|
98fac579c0 | ||
|
|
670b3b7974 | ||
|
|
305ae94cce | ||
|
|
a9a6917056 | ||
|
|
502ee39eb3 | ||
|
|
ac899a67c4 | ||
|
|
6e7089284e | ||
|
|
40efdf97de | ||
|
|
7e0bcc7aa0 | ||
|
|
56fea79097 | ||
|
|
26a01dfc40 | ||
|
|
9c3962bbe3 | ||
|
|
1882e37bf0 | ||
|
|
45e264c398 | ||
|
|
41047b5db8 | ||
|
|
15f01af6cf | ||
|
|
bfa4911217 | ||
|
|
d1eba1ef2f | ||
|
|
5f20f0124a | ||
|
|
38f4cbe74f | ||
|
|
c071adb041 | ||
|
|
657c9b1b37 | ||
|
|
4c0414d2d6 | ||
|
|
2f342d3914 | ||
|
|
70823b899f | ||
|
|
b6aa0c886a | ||
|
|
3909c7b10e | ||
|
|
5306489d72 | ||
|
|
f62cc8bda5 | ||
|
|
d8a61de290 | ||
|
|
083d39be22 | ||
|
|
d48da40f7e | ||
|
|
1cf68ae448 | ||
|
|
c81e667e01 | ||
|
|
c95854ee3f | ||
|
|
b5cb5618c1 | ||
|
|
14afa24a0a | ||
|
|
0bfac998a5 | ||
|
|
0775bceb5e | ||
|
|
db5ae65f3c | ||
|
|
92b3137b79 | ||
|
|
77b1d05da2 | ||
|
|
2a89394174 | ||
|
|
4d68042884 | ||
|
|
5e57c5dfd7 | ||
|
|
d87ff42cf5 | ||
|
|
c4f7020aae | ||
|
|
624360bf39 | ||
|
|
ee5f144ea5 | ||
|
|
7ca0ead6a2 | ||
|
|
b415537ba5 | ||
|
|
a5a00f3d02 | ||
|
|
dd4ccce1ba | ||
|
|
584f230c4d | ||
|
|
10c62e7fa2 | ||
|
|
02c023f939 | ||
|
|
6f1d9db8ec | ||
|
|
3c923a3dc0 | ||
|
|
ec0881f50f | ||
|
|
488398df9f | ||
|
|
e452c6ebff | ||
|
|
19edf09ae2 | ||
|
|
4727e568d5 | ||
|
|
4a73a7ecd4 | ||
|
|
83d085cadc | ||
|
|
f3523bbba9 | ||
|
|
33bb38299b | ||
|
|
f314ff1bb6 | ||
|
|
1737686c1e | ||
|
|
5e29e3698e | ||
|
|
5e3f66a4cb | ||
|
|
c8178e7f1f | ||
|
|
c66f9efa55 | ||
|
|
73cad5a6c4 | ||
|
|
51a9750891 | ||
|
|
7bf97fac18 | ||
|
|
14954df6b9 | ||
|
|
388e4d2bcc | ||
|
|
0c866105d2 | ||
|
|
96cf4d288c | ||
|
|
e2cd370513 | ||
|
|
f9f1b8f355 | ||
|
|
f154f7ee3a | ||
|
|
1873e82804 | ||
|
|
400e057dab | ||
|
|
129246c9e6 | ||
|
|
8ca2d64bc9 | ||
|
|
e0acb42a28 | ||
|
|
bd2e251cae | ||
|
|
6b5cc3d2d5 | ||
|
|
5f7fe8b0dc | ||
|
|
1df146c702 | ||
|
|
323fb1f03c | ||
|
|
de01692ffd | ||
|
|
bcf5e25cab | ||
|
|
5aa0e97cd9 | ||
|
|
cf3d0dfdc3 | ||
|
|
83d352cfbe | ||
|
|
7dba734ab8 | ||
|
|
a3101edbb9 | ||
|
|
95fbd70346 | ||
|
|
acf5c15a05 | ||
|
|
560d9ceb71 | ||
|
|
bfa05c9432 | ||
|
|
c45454d9e5 | ||
|
|
f6991539a2 | ||
|
|
5f7366b7bd | ||
|
|
f640e71f82 | ||
|
|
52eef2d7c7 | ||
|
|
f15067cf68 | ||
|
|
ff2b975cd8 | ||
|
|
fe2d6607de | ||
|
|
6bdf4f3ff3 | ||
|
|
218a7756f8 | ||
|
|
d5310c1c6f | ||
|
|
82ae5c623f | ||
|
|
57c9ccfc08 | ||
|
|
58758e0572 | ||
|
|
3e226fc3f2 | ||
|
|
b116408ec3 | ||
|
|
04c65d98da | ||
|
|
316dc7b320 | ||
|
|
6126e72d4f | ||
|
|
606bbf5062 | ||
|
|
bebd1e5fb9 | ||
|
|
db509d0311 | ||
|
|
e1002d5ac0 | ||
|
|
99b3749890 | ||
|
|
ab875198ed | ||
|
|
f3d79ebcda | ||
|
|
01e8c232f0 | ||
|
|
eb7fcae31b | ||
|
|
eb941f7435 | ||
|
|
227d83d0e7 | ||
|
|
057d5eeb24 | ||
|
|
bfcbe4bcd8 | ||
|
|
6b5427b826 | ||
|
|
35e6593581 | ||
|
|
33de5f6fec | ||
|
|
d1d7d51fe0 | ||
|
|
0f9226ba2a | ||
|
|
862f25500d | ||
|
|
da071a0abf | ||
|
|
51491d9cd5 | ||
|
|
d973c834a5 | ||
|
|
e07bedac0e | ||
|
|
2ac903a2dd | ||
|
|
d23254f44a | ||
|
|
9a3afda290 | ||
|
|
68f24f90d1 | ||
|
|
75259d70cf | ||
|
|
96c6339306 | ||
|
|
7b5ebf7bd2 | ||
|
|
cb950ae2e4 | ||
|
|
26b0edabfc | ||
|
|
cda02a3b30 | ||
|
|
dd5137c336 | ||
|
|
e045774caa | ||
|
|
dd6a53b0d2 | ||
|
|
d9084522af | ||
|
|
3829479a21 | ||
|
|
8a4bced569 | ||
|
|
8ac4b77777 | ||
|
|
30c36b488a | ||
|
|
10fcb9771a | ||
|
|
65782d7ef6 | ||
|
|
5e380e1d47 | ||
|
|
9f945492dd | ||
|
|
8ba3e3e45a | ||
|
|
3d9ceff691 | ||
|
|
daad3315eb | ||
|
|
e4d9b481ec | ||
|
|
7af7272635 | ||
|
|
09c6adeabc | ||
|
|
d5f324cb5c | ||
|
|
bf5561c89c | ||
|
|
ab89963f45 | ||
|
|
5ebb546d58 | ||
|
|
46c3589604 | ||
|
|
dffa7a6100 | ||
|
|
bc780343df | ||
|
|
84ce001598 | ||
|
|
92bd8ef335 | ||
|
|
85535de7d8 | ||
|
|
7bbc4e4109 | ||
|
|
248aa234d1 | ||
|
|
26a0665d81 | ||
|
|
dea1845908 | ||
|
|
8ed3eb4581 | ||
|
|
5962c1bb3e | ||
|
|
5b39f51203 | ||
|
|
63532c8b45 | ||
|
|
44bec887fe | ||
|
|
8bd42035aa | ||
|
|
f7f419224f | ||
|
|
8ac47b9bbf | ||
|
|
305bc5cb0a | ||
|
|
fa5a8cb5cd | ||
|
|
5d59e4b2bd | ||
|
|
ad87442811 | ||
|
|
7aacee405e | ||
|
|
4b9ebbbab2 | ||
|
|
f09c77cd31 | ||
|
|
2b0950e78e | ||
|
|
aff2d73e45 | ||
|
|
377b166d8e | ||
|
|
1c7d7e5083 | ||
|
|
772b38d154 | ||
|
|
b517d793d8 | ||
|
|
76e81e1cf0 | ||
|
|
4ec548f8b8 | ||
|
|
885dda21ec | ||
|
|
8aabb6f0e6 | ||
|
|
d4378461ab | ||
|
|
28ddef2d9c | ||
|
|
7df39e9db3 | ||
|
|
aac23c6481 | ||
|
|
af408d50bf | ||
|
|
93956b65c2 | ||
|
|
b7902ee4c8 | ||
|
|
633b3a4bda | ||
|
|
829c198e5f | ||
|
|
6be4839703 | ||
|
|
6ceeb2c9db | ||
|
|
bf6e652e41 | ||
|
|
9a8570c59c | ||
|
|
729cf10ad8 | ||
|
|
afc09a6ec4 | ||
|
|
816703527a | ||
|
|
5fec6c1755 | ||
|
|
cddff78719 | ||
|
|
65ad2eb6a8 | ||
|
|
3187bc8140 | ||
|
|
40ad6db51a | ||
|
|
5e3bc7ee6c | ||
|
|
9104ff0c4a | ||
|
|
65db5cfa18 | ||
|
|
5c1e9682c8 | ||
|
|
36dad0ba86 | ||
|
|
417066c462 | ||
|
|
326050651a | ||
|
|
0028939155 | ||
|
|
2eb5ce24ec | ||
|
|
c4475499ba | ||
|
|
19520787a6 | ||
|
|
b364f168c8 | ||
|
|
a113067647 | ||
|
|
f6888cc9a2 | ||
|
|
07410990a0 | ||
|
|
dbe1c4a781 | ||
|
|
45514c0a65 | ||
|
|
6c7e013007 | ||
|
|
a594972df0 | ||
|
|
e78b7a66d7 | ||
|
|
a2016241df | ||
|
|
5c44ad0a62 | ||
|
|
ad73298936 | ||
|
|
0fbf406800 | ||
|
|
28a3db5edc | ||
|
|
efe799f460 | ||
|
|
f8aa11f419 | ||
|
|
adc8af16b3 | ||
|
|
fc2e51053a | ||
|
|
dc223f564d | ||
|
|
ea77a4653f | ||
|
|
b80fd37a0f | ||
|
|
9d9afb1150 | ||
|
|
12e9fbf75a | ||
|
|
43c2c9d02d | ||
|
|
bea5f0525a | ||
|
|
50074eb33b | ||
|
|
dfa80d6756 | ||
|
|
1b81010e4a | ||
|
|
91470bc65d | ||
|
|
b9f4c50771 | ||
|
|
d1022dc7fc | ||
|
|
b469a90116 | ||
|
|
90d6857128 |
@@ -0,0 +1,4 @@
|
||||
* @epoberezkin @spaced4ndy
|
||||
/Dockerfile @shumvgolove
|
||||
/scripts/docker/ @shumvgolove
|
||||
/scripts/main/ @shumvgolove
|
||||
@@ -0,0 +1,2 @@
|
||||
github: simplex-chat
|
||||
open_collective: simplex-chat
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"template": "${{UNCATEGORIZED}}",
|
||||
"pr_template": "- ${{TITLE}}\n"
|
||||
"pr_template": "- ${{TITLE}}"
|
||||
}
|
||||
|
||||
@@ -4,19 +4,61 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- stable
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
build:
|
||||
name: build-${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-20.04
|
||||
platform_name: 20_04-x86-64
|
||||
- os: ubuntu-22.04
|
||||
platform_name: 22_04-x86-64
|
||||
steps:
|
||||
- name: Clone project
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Haskell
|
||||
uses: haskell/actions/setup@v1
|
||||
with:
|
||||
ghc-version: "8.10.7"
|
||||
cabal-version: "latest"
|
||||
|
||||
- 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: 30
|
||||
shell: bash
|
||||
run: cabal test --test-show-details=direct
|
||||
|
||||
- name: Prepare binaries
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
shell: bash
|
||||
run: |
|
||||
mv $(cabal list-bin smp-server) smp-server-ubuntu-${{ matrix.platform_name}}
|
||||
mv $(cabal list-bin ntf-server) ntf-server-ubuntu-${{ matrix.platform_name}}
|
||||
mv $(cabal list-bin xftp-server) xftp-server-ubuntu-${{ matrix.platform_name}}
|
||||
mv $(cabal list-bin xftp) xftp-ubuntu-${{ matrix.platform_name}}
|
||||
|
||||
- name: Build changelog
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
|
||||
id: build_changelog
|
||||
uses: mikepenz/release-changelog-builder-action@v1
|
||||
with:
|
||||
@@ -28,73 +70,21 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create release
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body: ${{ steps.build_changelog.outputs.changelog }}
|
||||
body: |
|
||||
See full changelog [here](https://github.com/simplex-chat/simplexmq/blob/master/CHANGELOG.md).
|
||||
|
||||
Commits:
|
||||
${{ steps.build_changelog.outputs.changelog }}
|
||||
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 }}
|
||||
|
||||
build:
|
||||
name: build-${{ matrix.os }}
|
||||
if: always()
|
||||
needs: prepare-release
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-20.04
|
||||
cache_path: ~/.stack
|
||||
stack_args: "--test"
|
||||
artifact_rel_path: /bin/dog-food
|
||||
asset_name: dog-food-ubuntu-20_04-x86-64
|
||||
- os: ubuntu-18.04
|
||||
cache_path: ~/.stack
|
||||
stack_args: "--test"
|
||||
artifact_rel_path: /bin/dog-food
|
||||
asset_name: dog-food-ubuntu-18_04-x86-64
|
||||
# TODO re-enable tests for mac and windows when they consistently pass (remove stack_args altogether)
|
||||
- os: macos-latest
|
||||
cache_path: ~/.stack
|
||||
stack_args: ""
|
||||
artifact_rel_path: /bin/dog-food
|
||||
asset_name: dog-food-macos-x86-64
|
||||
- os: windows-latest
|
||||
cache_path: C:/sr
|
||||
stack_args: ""
|
||||
artifact_rel_path: /bin/dog-food.exe
|
||||
asset_name: dog-food-windows-x86-64
|
||||
steps:
|
||||
- name: Clone project
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Stack
|
||||
uses: haskell/actions/setup@v1
|
||||
with:
|
||||
ghc-version: '8.8.4'
|
||||
enable-stack: true
|
||||
stack-version: 'latest'
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ matrix.cache_path }}
|
||||
key: ${{ matrix.os }}-${{ hashFiles('stack.yaml') }}
|
||||
|
||||
- name: Build & test
|
||||
id: build_test
|
||||
run: |
|
||||
stack build ${{ matrix.stack_args }}
|
||||
echo "::set-output name=LOCAL_INSTALL_ROOT::$(stack path --local-install-root)"
|
||||
|
||||
- name: Upload binaries to release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ${{ steps.build_test.outputs.LOCAL_INSTALL_ROOT }}${{ matrix.artifact_rel_path }}
|
||||
asset_name: ${{ matrix.asset_name }}
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Build and push Docker image to Docker Hub
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and push Docker image
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- app: smp-server
|
||||
app_port: 5223
|
||||
- app: xftp-server
|
||||
app_port: 443
|
||||
steps:
|
||||
- name: Clone project
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Extract metadata for Docker image
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.app }}
|
||||
flavor: |
|
||||
latest=auto
|
||||
tags: |
|
||||
type=semver,pattern=v{{version}}
|
||||
type=semver,pattern=v{{major}}.{{minor}}
|
||||
type=semver,pattern=v{{major}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
push: true
|
||||
build-args: |
|
||||
APP=${{ matrix.app }}
|
||||
APP_PORT=${{ matrix.app_port }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -1,4 +1,13 @@
|
||||
*.lock
|
||||
*.cabal
|
||||
*.db
|
||||
*.db.bak
|
||||
*.session.sql
|
||||
tests/tmp
|
||||
dist-newstyle/
|
||||
|
||||
cabal.project.local
|
||||
cabal.project.local~
|
||||
|
||||
.hpc/
|
||||
*.tix
|
||||
.coverage
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
# 5.2.0 (NTF server 1.5.0)
|
||||
|
||||
Agent:
|
||||
- treat agent INACTIVE error as temporary - fixes failed message delivery in some race conditions.
|
||||
- restore connection confirmations after client restart - fixes failed connections.
|
||||
- ratchet resynchronization protocol and API.
|
||||
- increase connection version to mutually supported by both peers on each received message.
|
||||
|
||||
Client:
|
||||
- make timeout for batched functions dependent on the number of batches - fixes expiry on large batches.
|
||||
|
||||
Servers:
|
||||
- add timeout in case of sending TCP traffic and in case of partial delivery of requested blocks to avoid resource leaks.
|
||||
|
||||
# 5.1.2, 5.1.3 (NTF server 1.4.1, 1.4.2)
|
||||
|
||||
Agent:
|
||||
- ACK message on decryption error (fixes stuck message delivery bug)
|
||||
- more robust connection switching logic, API to abort switching the address
|
||||
|
||||
Notification server:
|
||||
- batch subscriptions to SMP servers
|
||||
|
||||
# 5.1.1 (NTF server 1.4.0)
|
||||
|
||||
Agent:
|
||||
- store and check hashes of previous encrypted messages to differentiate between duplicates and decryption errors
|
||||
|
||||
Server:
|
||||
- larger processing queues
|
||||
- expire messages when restoring them
|
||||
|
||||
# 5.1.0
|
||||
|
||||
XFTP client:
|
||||
- check encrypted file exists when uploading
|
||||
- remove user ID from deletion API
|
||||
|
||||
Agent:
|
||||
- vacuum database on migrations
|
||||
|
||||
SMP server:
|
||||
- configure message expiration time in INI file
|
||||
|
||||
# 5.0.0
|
||||
|
||||
SimpleX File Transfer Protocol (XFTP):
|
||||
- XFTP server
|
||||
- XFTP CLI
|
||||
- support XFTP in the agent
|
||||
|
||||
Other changes:
|
||||
- preserve retry interval for sending messages on agent restarts
|
||||
- support IPv6 in the servers and in the agent
|
||||
- support for 32-bit platforms
|
||||
|
||||
Read more about XFTP in this post: https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html
|
||||
|
||||
# 4.4.1
|
||||
|
||||
SMP agent:
|
||||
|
||||
- fix handling of server addresses.
|
||||
- prevent message delivery getting stuck on IO error in SMP client.
|
||||
|
||||
# 4.4.0
|
||||
|
||||
SMP agent:
|
||||
|
||||
- support multiple user profiles with transport session isolation.
|
||||
- support optional transport isolation per connection.
|
||||
- batch connection deletion
|
||||
- improve asynchronous connection deletion – it may now be completed after the client is restarted as well.
|
||||
- improve subscription logic to retry if initial attempt fails.
|
||||
- end SMP client connection after a number of failed PINGs (defalt is 3).
|
||||
|
||||
# 4.3.0
|
||||
|
||||
SMP server:
|
||||
|
||||
- additional server usage statistics.
|
||||
|
||||
SMP agent:
|
||||
|
||||
- increase retry interval when sending messages after ERR QUOTA.
|
||||
|
||||
# 4.2.0
|
||||
|
||||
SMP agent and server:
|
||||
|
||||
- reduce sender traffic in cases when queue quota is exceeded:
|
||||
- server sends quota exceeded message to the recipient when sender receives ERR QUOTA.
|
||||
- recipient sends QCONT message to the send once the queue is drained (via reply queue).
|
||||
- sender retry delays are increased, reducing traffic, but sender instantly resumes delivery where QCONT is received.
|
||||
|
||||
SMP server:
|
||||
|
||||
- increase internal queue sizes.
|
||||
|
||||
SMP agent:
|
||||
|
||||
- deduplicate connection IDs in connect/disconnect responses.
|
||||
- unit tests for Crypto.hs.
|
||||
- fix connection switch to another queue: correctly set primary send queue.
|
||||
|
||||
Notification server (v1.3.0):
|
||||
|
||||
- check token status when sending verification notificaiton.
|
||||
|
||||
# 4.1.0
|
||||
|
||||
SMP agent and server:
|
||||
|
||||
- option to toggle TLS handshake error logs (disabled by default).
|
||||
|
||||
SMP agent:
|
||||
|
||||
- include server address in BROKER error.
|
||||
- api to get hash of double ratchet associated data (for connection verification).
|
||||
- api to get agent statistics.
|
||||
|
||||
# 4.0.0
|
||||
|
||||
SMP server:
|
||||
|
||||
- Basic authentication. The server address can now include an optional password that is required to create messaging queues, so the contacts who message you will not be able to receive messages via your server, unless you share with them the address with the password. It is recommended to enable basic authentication on all private servers by adding `create_password` parameter into AUTH section of server INI file (the previously deployed servers do not have this section, you need to add it).
|
||||
- Disable creating new queues completely with `new_queues: off` parameter in AUTH section of INI file - it can be used to simplify migrating the exising connections to another server.
|
||||
- Updated server CLI with changed defaults:
|
||||
- interactive server initialization, use -y flag to initalize the server non-interactively.
|
||||
- store log is now enabled by default, so messaging queues and messages are restored when the server is restarted (to restore undelivered messages the server needs to be stopped with SIGINT signal).
|
||||
- a random password is now generated by default during the server initialization.
|
||||
|
||||
SMP agent:
|
||||
|
||||
- API to test SMP servers. It connects to the server, creates and deletes a messaging queue. The new SimpleX Chat client uses this API to allow you to test that you have the correct server address, with the valid certificate fingerprint and pasword, before enabling the new server.
|
||||
|
||||
# 3.4.0
|
||||
|
||||
SMP agent:
|
||||
|
||||
- increase concurrency with connection-level locks
|
||||
- fix issues identified in security assessment (see the announcement: https://github.com/simplex-chat/simplex-chat/blob/stable/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md)
|
||||
- manual connection queue rotation
|
||||
- optional client data in connection requests links
|
||||
|
||||
SMP server:
|
||||
|
||||
- specialize monad stack to improve performance
|
||||
- log slow commands
|
||||
|
||||
# 3.3.0
|
||||
|
||||
SMP server:
|
||||
|
||||
- allow repeated KEY command with the same key (to avoid failures on retries)
|
||||
|
||||
SMP agent:
|
||||
|
||||
- enable/disable connection notifications
|
||||
- asynchronous commands that retry on network error
|
||||
- use SQLCipher
|
||||
|
||||
# 3.2.0
|
||||
|
||||
SMP agent:
|
||||
|
||||
- Support multiple server hostnames (including onion hostnames) in server addresses.
|
||||
- Network configuration options.
|
||||
- Options to define rules to choose server hostname.
|
||||
|
||||
# 3.1.0
|
||||
|
||||
SMP server and agent:
|
||||
|
||||
- SMP protocol v4: batching multiple server commands/responses in a transport block.
|
||||
|
||||
# 3.0.0
|
||||
|
||||
SMP server:
|
||||
|
||||
- restore undeliverd messages when the server is restarted.
|
||||
- SMP protocol v3 to support push notification:
|
||||
- updated SEND and MSG to add message flags (for notification flag that contros whether the notification is sent and for any future extensions) and to move message meta-data sent to the recipient into the encrypted envelope.
|
||||
- update NKEY and NID to add e2e encryption keys (for the notification meta-data encryption between SMP server and the client), and update NMSG to include this meta-data.
|
||||
- update ACK command to include message ID (to avoid acknowledging unprocessed message).
|
||||
- add NDEL commands to remove notification subscription credentials from SMP queue.
|
||||
- add GET command to receive messages without subscription - to be used in iOS notification service extension to receive messages without terminating app subscriptions.
|
||||
|
||||
SMP agent:
|
||||
|
||||
- new protocol for duplex connection handshake reducing traffic and connection time.
|
||||
- support for SMP notifications server and managing device token.
|
||||
- remove redundant FQDN validation from TLS handshake to prepare for access via Tor.
|
||||
- support for fully stopping agent and for termporary suspending agent operations.
|
||||
- improve management of duplicate message delivery.
|
||||
|
||||
SMP notifications server v1.0:
|
||||
|
||||
- SMP notifications protocol with version negotiation during handshake.
|
||||
- device token registration and verification (via background notification).
|
||||
- SMP notification subscriptions and push notifications via APNS.
|
||||
- restoring notification subscriptions when the server is restarted.
|
||||
|
||||
# 2.3.0
|
||||
|
||||
SMP server:
|
||||
|
||||
- Save and restore undelivered messages, to avoid losing them. To save messages the server has to be stopped with SIGINT signal, if it is stopped with SIGTERM undelivered messages would not be saved.
|
||||
|
||||
# 2.2.0
|
||||
|
||||
SMP server:
|
||||
|
||||
- Fix sockets/threads/memory leak
|
||||
|
||||
SMP agent:
|
||||
|
||||
- Support stopping and resuming agent with `disconnectAgentClient` / `resumeAgentClient`
|
||||
|
||||
# 2.1.1
|
||||
|
||||
SMP server:
|
||||
|
||||
- gracefully close sockets on client disconnection
|
||||
- CLI warning when deleting server configuration
|
||||
|
||||
# 2.1.0
|
||||
|
||||
SMP server:
|
||||
|
||||
- configuration to expire inactive clients in ini file, increased TTL and check interval for client expiration
|
||||
|
||||
# 2.0.0
|
||||
|
||||
Push notifications server (beta):
|
||||
|
||||
- supports APNS
|
||||
- manage device tokens verification via notification delivery
|
||||
- sending periodic background notification to check messages (not more frequent than every 20 min)
|
||||
|
||||
SMP server:
|
||||
|
||||
- disconnect inactive clients after some period
|
||||
- remove undelivered messages after 30 days
|
||||
- log aggregate usage daily stats: only the number of queues created/secured/deleted/used and messages sent/delivered is logged, as one line per day, so we can plan server capacity and diagnose any problems.
|
||||
|
||||
SMP agent:
|
||||
|
||||
- manage device tokens and notification server connection
|
||||
- DOWN/UP events to the agent user about server disconnections/reconnections are now sent once per server
|
||||
|
||||
# 1.1.0
|
||||
|
||||
SMP server:
|
||||
|
||||
- message TTL and periodic deletion of old messages
|
||||
- configuration to prevent creation of the new queues
|
||||
|
||||
SMP agent:
|
||||
|
||||
- asynchronous connection handshake
|
||||
- configurable SMP servers at run-time
|
||||
- use TCP keep-alive for connection stability
|
||||
- improve stability of connection subscriptions
|
||||
- auto-vacuum DB to remove deleted records
|
||||
|
||||
# 1.0.3
|
||||
|
||||
SMP server:
|
||||
|
||||
- Reduce server message queue quota to 128 messages.
|
||||
|
||||
SMP agent:
|
||||
|
||||
- Add "yes to migrations" option.
|
||||
- Make new SMP client attempt to reconnect on network error.
|
||||
- Reduce connection handshake expiration to 2 days.
|
||||
|
||||
JSON encoding of types used in simplex-chat, some other minor adjustments.
|
||||
|
||||
# 1.0.2
|
||||
|
||||
General:
|
||||
|
||||
- Enable TLS 1.3 parameters for TLS handshake (server and client).
|
||||
- Switch from hs-tls fork to original repo now that it supports getFinished and getPeerFinished APIs for both TLS 1.2 and TLS 1.3.
|
||||
|
||||
SMP server:
|
||||
|
||||
- Perform TLS handshake in a separate thread per-connection.
|
||||
|
||||
SMP agent:
|
||||
|
||||
- Cease attempts to send HELLO after one week timeout.
|
||||
- Coalesce requests to connect to SMP servers, to have 1 connection per server.
|
||||
|
||||
# 1.0.1
|
||||
|
||||
SMP server:
|
||||
|
||||
- Explicitly set line buffering in stdout/stderr to log each line when output is redirected to files.
|
||||
|
||||
# 1.0.0
|
||||
|
||||
Security and privacy improvements:
|
||||
|
||||
- Faster and more secure 2-layer E2E encryption with additional encryption layer between servers and recipients:
|
||||
- application messages in each duplex connection (managed by SMP agents - see [overview](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md)) are encrypted using [double-ratchet algorithm](https://www.signal.org/docs/specifications/doubleratchet/), providing forward secrecy and break-in recovery. This layer uses two Curve448 keys per client for [X3DH key agreement](https://www.signal.org/docs/specifications/x3dh/), SHA512 based HKDFs and AES-GCM AEAD encryption.
|
||||
- SMP client messages are additionally E2E encrypted in each SMP queue to avoid cipher-text correlation of messages sent via multiple redundant queues (that will be supported soon). This and the next layer use [NaCl crypto_box algorithm](https://nacl.cr.yp.to/index.html) with XSalsa20Poly1305 cipher and Curve25519 keys for DH key agreement.
|
||||
- Messages delivered from the servers to the recipients are additionally encrypted to avoid cipher-text correlation between sent and received messages.
|
||||
- To prevent any traffic correlation by content size, SimpleX uses fixed transport block size of 16kb (16384 bytes) with padding on all encryption layers:
|
||||
- application messages are padded to 15788 bytes before E2E double-ratchet encryption.
|
||||
- messages between SMP clients are padded to 16032 bytes before E2E encryption in each SMP queue.
|
||||
- messages from the server to the recipient are padded to padded to 16088 bytes before the additional encryption layer (see above).
|
||||
- TLS 1.2+ with tls-unique channel binding in each command to prevent replay attacks.
|
||||
- Server identity verification via server offline certificate fingerprints included in SMP server addresses.
|
||||
|
||||
New functionality:
|
||||
|
||||
- Support for notification servers with new SMP commands: `NKEY`/`NID`, `NSUB`/`NMSG`.
|
||||
|
||||
Efficiency improvements:
|
||||
|
||||
- Binary protocol encodings to reduce overhead from circa 15% to approximately 3.7% of transmitted application message size, with only 2.2% overhead for SMP protocol messages.
|
||||
- More performant cryptographic algorithms.
|
||||
|
||||
For more information about SimpleX:
|
||||
|
||||
- [SimpleX overview](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md).
|
||||
- [SimpleX chat v1 announcement](https://github.com/simplex-chat/simplex-chat/blob/master/blog/20220112-simplex-chat-v1-released.md).
|
||||
|
||||
# 0.5.2
|
||||
|
||||
- Fix message delivery logic that blocked delivery of all server messages when server per-queue quota exceeded, making it concurrent per SMP queue, not per server.
|
||||
|
||||
# 0.5.1
|
||||
|
||||
- Fix server subscription logic bug that was leading to memory leak / resource exhaustion in some edge cases.
|
||||
|
||||
# 0.5.0
|
||||
|
||||
- No changes in SMP server implementation - it is backwards compatible with v0.4.1
|
||||
- SMP agent changes:
|
||||
- URI syntax for SMP queues and connection requests.
|
||||
- long-term connections links ("contacts") in SMP agent protocol.
|
||||
- agent command changes:
|
||||
- `REQ` notification and `ACPT` command are used only with long-term connection links.
|
||||
- `CONF` notification and `LET` commands are used for normal duplex connections.
|
||||
|
||||
# 0.4.1
|
||||
|
||||
- Include migrations in the package
|
||||
|
||||
# 0.4.0
|
||||
|
||||
- SMP server implementation and [SMP protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md) changes:
|
||||
- support 3072 bit RSA key size
|
||||
- add SMP queue quotas
|
||||
- set default transport block size to 4096 bits
|
||||
- allow SMP client to change transport block size during transport connection handshake
|
||||
- SMP agent implementation and protocol changes:
|
||||
- additional connection confirmation step for initiating party
|
||||
- automatically resume subscribed duplex connections once transport connection is resumed
|
||||
- passing an arbitrary binary information between parties during the duplex connection handshake - can be used to identify parties
|
||||
- asynchronous duplex connection handshake - the parties do not have to be online at the same time
|
||||
- asynchronous message delivery - the agent does not need transport connection to accept client messages for delivery
|
||||
- additional confirmation of message reception from the client to prevent message loss in case of process termination
|
||||
- set transport block size to 8192 bits (in the future the agent protocol can allow to have different block sizes for different duplex connections)
|
||||
- added client commands and notifications (see [agent protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md)):
|
||||
- `REQ` - the notification about joining party establishing connection
|
||||
- `ACPT` - the command to accept connection with the joining party
|
||||
- `INFO` - the notification with the information from the initiating party
|
||||
- `DOWN`/`UP` - the notifications about losing/resuming the connection
|
||||
- `ACK` - the command to confirm that the message reception/processing is complete
|
||||
- `MID` - the response to `SEND` confirming that the message is accepted by the agent
|
||||
- `MERR` - the notification about permanent message delivery error (e.g., `ERR AUTH` indicating that the queue was removed)
|
||||
|
||||
# 0.3.2
|
||||
|
||||
- Support websockets
|
||||
- SMP server CLI commands
|
||||
|
||||
# 0.3.1
|
||||
|
||||
- Released to hackage.org
|
||||
- SMP agent protocol changes:
|
||||
- move SMP server from agent commands NEW/JOIN to agent config
|
||||
- send CON to user only when the 1st party responds HELLO
|
||||
- Fix REPLY vulnerability
|
||||
- Fix transaction busy error
|
||||
|
||||
# 0.3.0
|
||||
|
||||
- SMP encrypted transport over TCP
|
||||
- Standard X509/PKCS8 encoding for RSA keys
|
||||
- Sign and verify agent messages
|
||||
- Verify message integrity based on previous message hash and ID
|
||||
- Prevent timing attack allowing to determine if queue exists
|
||||
- Only allow correct RSA keys and signature sizes
|
||||
|
||||
# 0.2.0
|
||||
|
||||
- SMP client library
|
||||
- SMP agent with E2E encryption
|
||||
|
||||
# 0.1.0
|
||||
|
||||
- SMP protocol server implementation without encryption
|
||||
@@ -1,10 +1,62 @@
|
||||
FROM haskell:8.8.4 AS build-stage
|
||||
# if you encounter "version `GLIBC_2.28' not found" error when running
|
||||
# chat client executable, build with the following base image instead:
|
||||
# FROM haskell:8.8.4-stretch AS build-stage
|
||||
ARG TAG=22.04
|
||||
|
||||
FROM ubuntu:${TAG} AS build
|
||||
|
||||
### Build stage
|
||||
|
||||
# Install curl and git and simplexmq dependencies
|
||||
RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev llvm-12 llvm-12-dev libnuma-dev
|
||||
|
||||
# Specify bootstrap Haskell versions
|
||||
ENV BOOTSTRAP_HASKELL_GHC_VERSION=8.10.7
|
||||
ENV BOOTSTRAP_HASKELL_CABAL_VERSION=3.6.2.0
|
||||
|
||||
# Install ghcup
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 sh
|
||||
|
||||
# Adjust PATH
|
||||
ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH"
|
||||
|
||||
# Set both as default
|
||||
RUN ghcup set ghc "${BOOTSTRAP_HASKELL_GHC_VERSION}" && \
|
||||
ghcup set cabal
|
||||
|
||||
COPY . /project
|
||||
WORKDIR /project
|
||||
RUN stack install
|
||||
|
||||
FROM scratch AS export-stage
|
||||
COPY --from=build-stage /root/.local/bin/dog-food /
|
||||
ARG APP
|
||||
ARG APP_PORT
|
||||
RUN if [ -z "$APP" ] || [ -z "$APP_PORT" ]; then printf "Please spcify \$APP and \$APP_PORT build-arg.\n"; exit 1; fi
|
||||
|
||||
# Compile app
|
||||
RUN cabal update
|
||||
RUN cabal build exe:$APP
|
||||
|
||||
# Create new path containing all files needed
|
||||
RUN mkdir /final
|
||||
WORKDIR /final
|
||||
|
||||
# Strip the binary from debug symbols to reduce size
|
||||
RUN bin=$(find /project/dist-newstyle -name "$APP" -type f -executable) && \
|
||||
mv "$bin" ./ && \
|
||||
strip ./"$APP" &&\
|
||||
mv /project/scripts/docker/entrypoint-"$APP" ./entrypoint
|
||||
|
||||
### Final stage
|
||||
FROM ubuntu:${TAG}
|
||||
|
||||
# Install OpenSSL dependency
|
||||
RUN apt-get update && apt-get install -y openssl libnuma-dev
|
||||
|
||||
# Copy compiled app from build stage
|
||||
COPY --from=build /final /usr/local/bin/
|
||||
|
||||
# Open app listening port
|
||||
ARG APP_PORT
|
||||
EXPOSE $APP_PORT
|
||||
|
||||
# simplexmq requires using SIGINT to correctly preserve undelivered messages and restore them on restart
|
||||
STOPSIGNAL SIGINT
|
||||
|
||||
# Finally, execute helper script
|
||||
ENTRYPOINT [ "/usr/local/bin/entrypoint" ]
|
||||
|
||||
@@ -1,223 +1,301 @@
|
||||
# simplex-messaging
|
||||
# SimpleXMQ
|
||||
|
||||
[](https://github.com/simplex-chat/simplex-messaging/actions?query=workflow%3Abuild)
|
||||
[](https://github.com/simplex-chat/simplex-messaging/releases)
|
||||
[](https://github.com/simplex-chat/simplexmq/actions?query=workflow%3Abuild)
|
||||
[](https://github.com/simplex-chat/simplexmq/releases)
|
||||
|
||||
## Federated chat - private, secure, decentralised
|
||||
📢 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).
|
||||
|
||||
See [simplex.chat](https://simplex.chat) website for chat demo and the explanations of the system and how SMP protocol works.
|
||||
**Please note**: v1 is not backwards compatible, but it has the version negotiation built into all protocol layers for forwards compatibility of this version and backwards compatibility of the future versions, that will be backwards compatible for at least two versions back.
|
||||
|
||||
SMP protocol is semi-formally defined [here](https://github.com/simplex-chat/protocol).
|
||||
If you have a server deployed please deploy a new server to a new host and retire the previous version once it is no longer used.
|
||||
|
||||
Currently only these features are available:
|
||||
- simple 1-to-1 chat with multiple people in the same terminal window.
|
||||
- auto-populated recipient name - just type your messages.
|
||||
- default server is available to play with - `smp.simplex.im:5223` - and you can deploy your own (`smp-server` executable in this repo).
|
||||
- no global identity or names visible to the server(s) - for the privacy of contacts and conversations.
|
||||
- E2E encryption, with public key that has to be passed out-of-band (see below)
|
||||
- authentication of each command/message with automatically generated RSA key pairs, separate for each conversation, the keys are not used as identity (2048 bit keys are used, it can be changed in [code via rsaKeySize setting](https://github.com/simplex-chat/simplex-messaging/blob/master/apps/dog-food/Main.hs))
|
||||
## Message broker for unidirectional (simplex) queues
|
||||
|
||||
Limitations/disclaimers:
|
||||
- no support for chat groups. It is coming in the next major version (i.e., not very soon:)
|
||||
- no delivery notifications - coming soon
|
||||
- no TCP transport encryption - coming soon (messages are encrypted e2e though, only random connection IDs and server commands are visible, but not the contents of the message)
|
||||
- system and protocol security was not audited yet, so you probably should NOT use it yet for high security communications - unless you know what you are doing.
|
||||
SimpleXMQ is a message broker for managing message queues and sending messages over public network. It consists of SMP server, SMP client library and SMP agent that implement [SMP protocol](./protocol/simplex-messaging.md) for client-server communication and [SMP agent protocol](./protocol/agent-protocol.md) to manage duplex connections via simplex queues on multiple SMP servers.
|
||||
|
||||
## How to run chat client locally
|
||||
SMP protocol is inspired by [Redis serialization protocol](https://redis.io/topics/protocol), but it is much simpler - it currently has only 10 client commands and 8 server responses.
|
||||
|
||||
Install [Haskell stack](https://docs.haskellstack.org/en/stable/README/):
|
||||
SimpleXMQ is implemented in Haskell - it benefits from robust software transactional memory (STM) and concurrency primitives that Haskell provides.
|
||||
|
||||
```shell
|
||||
curl -sSL https://get.haskellstack.org/ | sh
|
||||
## SimpleXMQ roadmap
|
||||
|
||||
- SimpleX service protocol and application template - to enable users building services and chat bots that work over SimpleX protocol stack. The first such service will be a notification service for a mobile app.
|
||||
- SMP queue redundancy and rotation in SMP agent connections.
|
||||
- SMP agents synchronization to share connections and messages between multiple agents (it would allow using multiple devices for [simplex-chat](https://github.com/simplex-chat/simplex-chat)).
|
||||
|
||||
## Components
|
||||
|
||||
### SMP server
|
||||
|
||||
[SMP server](./apps/smp-server/Main.hs) can be run on any Linux distribution, including low power/low memory devices. OpenSSL library is required for initialization.
|
||||
|
||||
To initialize the server use `smp-server init -n <fqdn>` (or `smp-server init --ip <ip>` for IP based address) command - it will generate keys and certificates for TLS transport. The fingerprint of offline certificate is used as part of the server address to protect client/server connection against man-in-the-middle attacks: `smp://<fingerprint>@<hostname>[:5223]`.
|
||||
|
||||
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.
|
||||
|
||||
Starting from version 2.3.0, when store log is enabled, the server would also enable saving undelivered messages on exit and restoring them on start. This can be disabled via a separate setting `restore_messages` in `smp-server.ini` file. Saving messages would only work if the server is stopped with SIGINT signal (keyboard interrupt), if it is stopped with SIGTERM signal the messages would not be saved.
|
||||
|
||||
> **Please note:** On initialization SMP server creates a chain of two certificates: a self-signed CA certificate ("offline") and a server certificate used for TLS handshake ("online"). **You should store CA certificate private key securely and delete it from the server. If server TLS credential is compromised this key can be used to sign a new one, keeping the same server identity and established connections.** CA private key location by default is `/etc/opt/simplex/ca.key`.
|
||||
|
||||
SMP server implements [SMP protocol](./protocol/simplex-messaging.md).
|
||||
|
||||
#### Running SMP server on MacOS
|
||||
|
||||
SMP server requires OpenSSL library for initialization. On MacOS OpenSSL library may be replaced with LibreSSL, which doesn't support required algorithms. Before initializing SMP server verify you have OpenSSL installed:
|
||||
|
||||
```sh
|
||||
openssl version
|
||||
```
|
||||
|
||||
and build the project:
|
||||
If it says "LibreSSL", please install original OpenSSL:
|
||||
|
||||
```shell
|
||||
$ git clone git@github.com:simplex-chat/simplex-messaging.git
|
||||
$ cd simplex-messaging
|
||||
$ stack install
|
||||
$ dog-food
|
||||
```sh
|
||||
brew update
|
||||
brew install openssl
|
||||
echo 'PATH="/opt/homebrew/opt/openssl@3/bin:$PATH"' >> ~/.zprofile # or follow whatever instructions brew suggests
|
||||
. ~/.zprofile # or restart your terminal to start a new session
|
||||
```
|
||||
|
||||
If you'd prefer to not set up Haskell locally, on Linux you may instead build the chat client executable using [docker build with custom output](https://docs.docker.com/engine/reference/commandline/build/#custom-build-outputs):
|
||||
Now `openssl version` should be saying "OpenSSL". You can now run `smp-server init` to initialize your SMP server.
|
||||
|
||||
```shell
|
||||
$ git clone git@github.com:simplex-chat/simplex-messaging.git
|
||||
$ cd simplex-messaging
|
||||
$ DOCKER_BUILDKIT=1 docker build --output ~/.local/bin .
|
||||
$ dog-food
|
||||
### SMP client library
|
||||
|
||||
[SMP client](./src/Simplex/Messaging/Client.hs) is a Haskell library to connect to SMP servers that allows to:
|
||||
|
||||
- execute commands with a functional API.
|
||||
- receive messages and other notifications via STM queue.
|
||||
- automatically send keep-alive commands.
|
||||
|
||||
### SMP agent
|
||||
|
||||
[SMP agent library](./src/Simplex/Messaging/Agent.hs) can be used to run SMP agent as part of another application and to communicate with the agent via STM queues, without serializing and parsing commands and responses.
|
||||
|
||||
Haskell type [ACommand](./src/Simplex/Messaging/Agent/Protocol.hs) represents SMP agent protocol to communicate via STM queues.
|
||||
|
||||
See [simplex-chat](https://github.com/simplex-chat/simplex-chat) terminal UI for the example of integrating SMP agent into another application.
|
||||
|
||||
[SMP agent executable](./apps/smp-agent/Main.hs) can be used to run a standalone SMP agent process that implements plaintext [SMP agent protocol](./protocol/agent-protocol.md) via TCP port 5224, so it can be used via telnet. It can be deployed in private networks to share access to the connections between multiple applications and services.
|
||||
|
||||
## Using SMP server and SMP agent
|
||||
|
||||
You can either run your own SMP server locally or deploy using [Linode StackScript](https://cloud.linode.com/stackscripts/748014), or try local SMP agent with the deployed servers:
|
||||
|
||||
`smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im`
|
||||
|
||||
`smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im`
|
||||
|
||||
`smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im`
|
||||
|
||||
It's the easiest to try SMP agent via a prototype [simplex-chat](https://github.com/simplex-chat/simplex-chat) terminal UI.
|
||||
|
||||
## Deploy SMP/XFTP servers on Linux
|
||||
|
||||
You can run your SMP/XFTP server as a Linux process, optionally using a service manager for booting and restarts.
|
||||
|
||||
Notice that `smp-server` and `xftp-server` requires `openssl` as run-time dependency (it is used to generate server certificates during initialization). Install it with your packet manager:
|
||||
|
||||
```sh
|
||||
# For Ubuntu
|
||||
apt update && apt install openssl
|
||||
```
|
||||
|
||||
> **NOTE:** When running chat client executable built with the latter approach, if you encounter ``version `GLIBC_2.28' not found`` error, rebuild it with `haskell:8.8.4-stretch` base image instead (you'd have to change it in your local [Dockerfile](Dockerfile)).
|
||||
### Install binaries
|
||||
|
||||
`dog-food` (as in "eating your own dog food" - it is an early prototype) starts chat client with default parameters. By default, app data directory is created in the home directory (`~/.simplex`, or `%APPDATA%/simplex` on Windows), and SQLite database file `smp-chat.db` is initialized in it. The default SMP server is `smp.simplex.im:5223`.
|
||||
#### Using Docker
|
||||
|
||||
To specify a different file path for the chat database use `-d` command line option:
|
||||
On Linux, you can deploy smp and xftp server using Docker. This will download image from [Docker Hub](https://hub.docker.com/r/simplexchat).
|
||||
|
||||
```shell
|
||||
$ dog-food -d my-chat.db
|
||||
1. Create directories for persistent Docker configuration:
|
||||
|
||||
```sh
|
||||
mkdir -p $HOME/simplex/{xftp,smp}/{config,logs} && mkdir -p $HOME/simplex/xftp/files
|
||||
```
|
||||
|
||||
2. Run your Docker container.
|
||||
|
||||
- `smp-server`
|
||||
|
||||
You must change **your_ip_or_domain**. `-e "pass=password"` is optional variable to password-protect your `smp` server:
|
||||
```sh
|
||||
docker run -d \
|
||||
-e "ADDR=your_ip_or_domain" \
|
||||
-e "PASS=password" \
|
||||
-p 5223:5223 \
|
||||
-v $HOME/simplex/smp/config:/etc/opt/simplex:z \
|
||||
-v $HOME/simplex/smp/logs:/var/opt/simplex:z \
|
||||
simplexchat/smp-server:latest
|
||||
```
|
||||
|
||||
- `xftp-server`
|
||||
|
||||
You must change **your_ip_or_domain** and **maximum_storage**.
|
||||
```sh
|
||||
docker run -d \
|
||||
-e "ADDR=your_ip_or_domain" \
|
||||
-e "QUOTA=maximum_storage" \
|
||||
-p 443:443 \
|
||||
-v $HOME/simplex/xftp/config:/etc/opt/simplex-xftp:z \
|
||||
-v $HOME/simplex/xftp/logs:/var/opt/simplex-xftp:z \
|
||||
-v $HOME/simplex/xftp/files:/srv/xftp:z \
|
||||
simplexchat/xftp-server:latest
|
||||
```
|
||||
|
||||
#### Using installation script
|
||||
|
||||
**Please note** that currently, only Ubuntu distribution is supported.
|
||||
|
||||
You can install and setup servers automatically using our script:
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/install.sh -o simplex-server-install.sh \
|
||||
&& if echo 'b8cf2be103f21f9461d9a500bcd3db06ab7d01d68871b07f4bd245195cbead1d simplex-server-install.sh' | sha256sum -c; then chmod +x ./simplex-server-install.sh && ./simplex-server-install.sh; rm ./simplex-server-install.sh; else echo "SHA-256 checksum is incorrect!" && rm ./simplex-server-install.sh; fi
|
||||
```
|
||||
|
||||
If you deployed your own SMP server you can set client to use it via `-s` option:
|
||||
### Build from source
|
||||
|
||||
```shell
|
||||
$ dog-food -s smp.example.com:5223
|
||||
#### Using Docker
|
||||
|
||||
> **Please note:** to build the app use source code from [stable branch](https://github.com/simplex-chat/simplexmq/tree/stable).
|
||||
|
||||
On Linux, you can build smp server using Docker.
|
||||
|
||||
1. Build your images:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/simplex-chat/simplexmq
|
||||
cd simplexmq
|
||||
git checkout stable
|
||||
DOCKER_BUILDKIT=1 docker build -t local/smp-server --build-arg APP="smp-server" --build-arg APP_PORT="5223" . # For xmp-server
|
||||
DOCKER_BUILDKIT=1 docker build -t local/xftp-server --build-arg APP="xftp-server" --build-arg APP_PORT="443" . # For xftp-server
|
||||
```
|
||||
|
||||
2. Create directories for persistent Docker configuration:
|
||||
|
||||
```sh
|
||||
mkdir -p $HOME/simplex/{xftp,smp}/{config,logs} && mkdir -p $HOME/simplex/xftp/files
|
||||
```
|
||||
|
||||
3. Run your Docker container.
|
||||
|
||||
- `smp-server`
|
||||
|
||||
You must change **your_ip_or_domain**. `-e "pass=password"` is optional variable to password-protect your `smp` server:
|
||||
```sh
|
||||
docker run -d \
|
||||
-e "ADDR=your_ip_or_domain" \
|
||||
-e "PASS=password" \
|
||||
-p 5223:5223 \
|
||||
-v $HOME/simplex/smp/config:/etc/opt/simplex:z \
|
||||
-v $HOME/simplex/smp/logs:/var/opt/simplex:z \
|
||||
simplexchat/smp-server:latest
|
||||
```
|
||||
|
||||
- `xftp-server`
|
||||
|
||||
You must change **your_ip_or_domain** and **maximum_storage**.
|
||||
```sh
|
||||
docker run -d \
|
||||
-e "ADDR=your_ip_or_domain" \
|
||||
-e "QUOTA=maximum_storage" \
|
||||
-p 443:443 \
|
||||
-v $HOME/simplex/xftp/config:/etc/opt/simplex-xftp:z \
|
||||
-v $HOME/simplex/xftp/logs:/var/opt/simplex-xftp:z \
|
||||
-v $HOME/simplex/xftp/files:/srv/xftp:z \
|
||||
simplexchat/xftp-server:latest
|
||||
```
|
||||
|
||||
#### Using your distribution
|
||||
|
||||
1. Install [Haskell GHCup](https://www.haskell.org/ghcup/), GHC 8.10.7 and cabal:
|
||||
|
||||
```sh
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
|
||||
ghcup install ghc 8.10.7
|
||||
ghcup install cabal
|
||||
ghcup set ghc 8.10.7
|
||||
ghcup set cabal
|
||||
```
|
||||
|
||||
2. Build the project:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/simplex-chat/simplexmq
|
||||
cd simplexmq
|
||||
git checkout stable
|
||||
# On Ubuntu. Depending on your distribution, use your package manager to determine package names.
|
||||
apt-get update && apt-get install -y build-essential libgmp3-dev zlib1g-dev
|
||||
cabal update
|
||||
cabal install
|
||||
```
|
||||
|
||||
- Initialize SMP server with `smp-server init [-l] -n <fqdn>` or `smp-server init [-l] --ip <ip>` - depending on how you initialize it, either FQDN or IP will be used for server's address.
|
||||
|
||||
- Run `smp-server start` to start SMP server, or you can configure a service manager to run it as a service.
|
||||
|
||||
- Optionally, `smp-server` can be setup for having an onion address in `tor` network. See: [`scripts/tor`](./scripts/tor/). In this case, the server address can have both public and onion hostname pointing to the same server, to allow two people connect when only one of them is using Tor. The server address would be: `smp://<fingerprint>@<public_hostname>,<onion_hostname>`
|
||||
|
||||
See [this section](#smp-server) for more information. Run `smp-server -h` and `smp-server init -h` for explanation of commands and options.
|
||||
|
||||
[<img alt="Linode" src="./img/linode.svg" align="right" width="200">](https://cloud.linode.com/stackscripts/748014)
|
||||
|
||||
## Deploy SMP server on Linode
|
||||
|
||||
\* You can use free credit Linode offers when [creating a new account](https://www.linode.com/) to deploy an SMP server.
|
||||
|
||||
Deployment on Linode is performed via StackScripts, which serve as recipes for Linode instances, also called Linodes. To deploy SMP server on Linode:
|
||||
|
||||
- Create a Linode account or login with an already existing one.
|
||||
- Open [SMP server StackScript](https://cloud.linode.com/stackscripts/748014) and click "Deploy New Linode".
|
||||
- You can optionally configure the following parameters:
|
||||
- SMP Server store log flag for queue persistence on server restart, recommended.
|
||||
- [Linode API token](https://www.linode.com/docs/guides/getting-started-with-the-linode-api#get-an-access-token) to attach server address etc. as tags to Linode and to add A record to your 2nd level domain (e.g. `example.com` [domain should be created](https://cloud.linode.com/domains/create) in your account prior to deployment). The API token access scopes:
|
||||
- read/write for "linodes"
|
||||
- read/write for "domains"
|
||||
- Domain name to use instead of Linode IP address, e.g. `smp1.example.com`.
|
||||
- Choose the region and plan, Shared CPU Nanode with 1Gb is sufficient.
|
||||
- Provide ssh key to be able to connect to your Linode via ssh. If you haven't provided a Linode API token this step is required to login to your Linode and get the server's fingerprint either from the welcome message or from the file `/etc/opt/simplex/fingerprint` after server starts. See [Linode's guide on ssh](https://www.linode.com/docs/guides/use-public-key-authentication-with-ssh/) .
|
||||
- Deploy your Linode. After it starts wait for SMP server to start and for tags to appear (if a Linode API token was provided). It may take up to 5 minutes depending on the connection speed on the Linode. Connecting Linode IP address to provided domain name may take some additional time.
|
||||
- Get `address` and `fingerprint` either from Linode tags (click on a tag and copy it's value from the browser search panel) or via ssh.
|
||||
- Great, your own SMP server is ready! If you provided FQDN use `smp://<fingerprint>@<fqdn>` as SMP server address in the client, otherwise use `smp://<fingerprint>@<ip_address>`.
|
||||
|
||||
Please submit an [issue](https://github.com/simplex-chat/simplexmq/issues) if any problems occur.
|
||||
|
||||
[<img alt="DigitalOcean" src="/img/digitalocean.png" align="right" width="300">](https://marketplace.digitalocean.com/apps/simplex-server)
|
||||
|
||||
## Deploy SMP server on DigitalOcean
|
||||
|
||||
> 🚧 DigitalOcean snapshot is currently not up to date, it will soon be updated 🏗️
|
||||
|
||||
\* When creating a DigitalOcean account you can use [this link](https://try.digitalocean.com/freetrialoffer/) to get free credit. (You would still be required either to provide your credit card details or make a confirmation pre-payment with PayPal)
|
||||
|
||||
To deploy SMP server use [SimpleX Server 1-click app](https://marketplace.digitalocean.com/apps/simplex-server) from DigitalOcean marketplace:
|
||||
|
||||
- Create a DigitalOcean account or login with an already existing one.
|
||||
- Click 'Create SimpleX server Droplet' button.
|
||||
- Choose the region and plan according to your requirements (Basic plan should be sufficient).
|
||||
- Finalize Droplet creation.
|
||||
- Open "Console" on your Droplet management page to get SMP server fingerprint - either from the welcome message or from `/etc/opt/simplex/fingerprint`. Alternatively you can manually SSH to created Droplet, see [DigitalOcean instruction](https://docs.digitalocean.com/products/droplets/how-to/connect-with-ssh/).
|
||||
- Great, your own SMP server is ready! Use `smp://<fingerprint>@<ip_address>` as SMP server address in the client.
|
||||
|
||||
Please submit an [issue](https://github.com/simplex-chat/simplexmq/issues) if any problems occur.
|
||||
|
||||
> **Please note:** SMP server uses server address as a Common Name for server certificate generated during initialization. If you would like your server address to be FQDN instead of IP address, you can log in to your Droplet and run the commands below to re-initialize the server. Alternatively you can use [Linode StackScript](https://cloud.linode.com/stackscripts/748014) which allows this parameterization.
|
||||
|
||||
```sh
|
||||
smp-server delete
|
||||
smp-server init [-l] -n <fqdn>
|
||||
```
|
||||
|
||||
You can still talk to people using default or any other server, it only affects the location of the message queue when you initiate the connection (and the reply queue can be on another server, as set by the other party's client).
|
||||
## SMP server design
|
||||
|
||||
Run `dog-food --help` to see all available options.
|
||||

|
||||
|
||||
### Using chat client
|
||||
## SMP agent design
|
||||
|
||||
Once chat client is started, use `/add <name1>` to create a new connection and generate an invitation to send to your contact via any other communication channel (`<name1>` - is any name you want to use for that contact).
|
||||

|
||||
|
||||
Invitation has format `smp::<server>::<queue_id>::<public_key_for_this_queue_only>` - this needs to be shared with another party, via any other chat. It can only be used once - even if this is intercepted, the attacker would not be able to use it to send you the messages via this queue once your contact confirms that the connection is established.
|
||||
## License
|
||||
|
||||
The party that received the invitation should use `/accept <name2> <invitation>` to accept the connection (`<name2>` is any name that the accepting party wants to use for you).
|
||||
|
||||
For example, if Alice and Bob want to chat, with Alice initiating, Alice would use [in her chat client]:
|
||||
|
||||
```
|
||||
/add bob
|
||||
```
|
||||
|
||||
And then send the generated invitation to Bob out-of-band. Bob then would use [in his chat client]:
|
||||
|
||||
```
|
||||
/accept alice <alice's invitation>
|
||||
```
|
||||
|
||||
They would then use `@<name> <message>` commands to send messages. One may also press Space or just start typing a message to send a message to the contact that was the last.
|
||||
|
||||
If you exit from chat client (or if internet connection is interrupted) you need to use `/chat <name>` to activate conversation with respective contact - it is not resumed automatically (it will improve soon).
|
||||
|
||||
Since SMP doesn't use global identity (all account information is managed by clients), you should configure your name to use in invitations for your contacts:
|
||||
|
||||
```
|
||||
/name alice
|
||||
```
|
||||
|
||||
Now Alice's invitations would be generated with her name in it for others' convenience.
|
||||
|
||||
Use `/help` in chat to see the list of available commands and their explanation.
|
||||
|
||||
### Accessing chat history
|
||||
|
||||
You can access your chat history by opening a connection to your SQLite database file and querying `messages` table, for example:
|
||||
|
||||
```sql
|
||||
select * from messages
|
||||
where conn_alias = cast('alice' as blob)
|
||||
order by internal_id desc;
|
||||
|
||||
select * from messages
|
||||
where conn_alias = cast('alice' as blob)
|
||||
and body like '%cats%';
|
||||
```
|
||||
|
||||
> **NOTE:** Beware that SQLite foreign key constraints are disabled by default, and must be **[enabled separately for each database connection](https://sqlite.org/foreignkeys.html#fk_enable)**. The latter can be achieved by running `PRAGMA foreign_keys = ON;` command on an open database connection. By running data altering queries without enabling foreign keys prior to that, you may risk putting your database in an inconsistent state.
|
||||
|
||||
## 🚧 [further README not up to date] SMP server demo 🏗
|
||||
|
||||
This is a demo implementation of SMP ([simplex messaging protocol](https://github.com/simplex-chat/protocol/blob/master/simplex-messaging.md)) server.
|
||||
|
||||
It has a very limited utility (if any) for real applications, as it lacks the following protocol features:
|
||||
|
||||
- cryptographic signature verification, instead it simply compares provided "signature" with stored "public key", effectively treating them as plain text passwords.
|
||||
- there is no transport encryption
|
||||
|
||||
Because of these limitations, it is easy to experiment with the protocol logic via telnet.
|
||||
|
||||
You can either run it locally or try with the deployed demo server:
|
||||
|
||||
```bash
|
||||
telnet smp.simplex.im 5223
|
||||
```
|
||||
|
||||
## Run locally
|
||||
|
||||
[Install stack](https://docs.haskellstack.org/en/stable/install_and_upgrade/) and `stack run`.
|
||||
|
||||
## Usage example
|
||||
|
||||
Lines you should send are prefixed with `>` character, you should not type them.
|
||||
|
||||
Comments are prefixed with `--`, they are not part of transmissions.
|
||||
|
||||
`>` on its own means you need to press `return` - telnet should be configured to send it as CRLF.
|
||||
|
||||
1. Create simplex message queue:
|
||||
|
||||
```telnet
|
||||
>
|
||||
> abcd -- correlation ID, any string
|
||||
>
|
||||
> NEW 1234 -- 1234 is recipient's key
|
||||
|
||||
abcd
|
||||
|
||||
IDS QuCLU4YxgS7wcPFA YB4CCATREHkaQcEh -- recipient and sender IDs for the queue
|
||||
```
|
||||
|
||||
2. Sender can send their "key" to the queue:
|
||||
|
||||
```telnet
|
||||
> -- no signature (just press enter)
|
||||
> bcda -- correlation ID, any string
|
||||
> YB4CCATREHkaQcEh -- sender ID for the queue
|
||||
> SEND :key abcd
|
||||
|
||||
bcda
|
||||
YB4CCATREHkaQcEh
|
||||
OK
|
||||
```
|
||||
|
||||
3. Secure queue with sender's "key"
|
||||
|
||||
```telnet
|
||||
> 1234 -- recipient's "signature" - same as "key" in the demo
|
||||
> cdab
|
||||
> QuCLU4YxgS7wcPFA -- recipient ID
|
||||
> KEY abcd -- "key" provided by sender
|
||||
|
||||
cdab
|
||||
QuCLU4YxgS7wcPFA
|
||||
OK
|
||||
```
|
||||
|
||||
4. Sender can now send messages to the queue
|
||||
|
||||
```telnet
|
||||
> abcd -- sender's "signature" - same as "key" in the demo
|
||||
> dabc -- correlation ID
|
||||
> YB4CCATREHkaQcEh -- sender ID
|
||||
> SEND :hello
|
||||
|
||||
dabc
|
||||
YB4CCATREHkaQcEh
|
||||
OK
|
||||
```
|
||||
|
||||
5. Recipient recieves the message and acknowledges it to receive further messages
|
||||
|
||||
```telnet
|
||||
|
||||
-- no correlation ID for messages delivered without client command
|
||||
QuCLU4YxgS7wcPFA
|
||||
MSG ECA3w3ID 2020-10-18T20:19:36.874Z 5
|
||||
hello
|
||||
> 1234
|
||||
> abcd
|
||||
> QuCLU4YxgS7wcPFA
|
||||
> ACK
|
||||
|
||||
abcd
|
||||
QuCLU4YxgS7wcPFA
|
||||
OK
|
||||
```
|
||||
|
||||
## Design
|
||||
|
||||

|
||||
[AGPL v3](./LICENSE)
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
|
||||
module ChatOptions (getChatOpts, ChatOpts (..)) where
|
||||
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Options.Applicative
|
||||
import Simplex.Messaging.Agent.Transmission (SMPServer (..), smpServerP)
|
||||
import System.FilePath (combine)
|
||||
import System.Info (os)
|
||||
import Types
|
||||
|
||||
data ChatOpts = ChatOpts
|
||||
{ name :: Maybe ByteString,
|
||||
dbFileName :: String,
|
||||
smpServer :: SMPServer,
|
||||
termMode :: TermMode
|
||||
}
|
||||
|
||||
chatOpts :: FilePath -> Parser ChatOpts
|
||||
chatOpts appDir =
|
||||
ChatOpts
|
||||
<$> option
|
||||
(Just <$> str)
|
||||
( long "name"
|
||||
<> short 'n'
|
||||
<> metavar "NAME"
|
||||
<> help "optional name to use for invitations"
|
||||
<> value Nothing
|
||||
)
|
||||
<*> strOption
|
||||
( long "database"
|
||||
<> short 'd'
|
||||
<> metavar "DB_FILE"
|
||||
<> help ("sqlite database file path (" <> defaultDbFilePath <> ")")
|
||||
<> value defaultDbFilePath
|
||||
)
|
||||
<*> option
|
||||
parseSMPServer
|
||||
( long "server"
|
||||
<> short 's'
|
||||
<> metavar "SERVER"
|
||||
<> help "SMP server to use (smp.simplex.im:5223)"
|
||||
<> value (SMPServer "smp.simplex.im" (Just "5223") Nothing)
|
||||
)
|
||||
<*> option
|
||||
parseTermMode
|
||||
( long "term"
|
||||
<> short 't'
|
||||
<> metavar "TERM"
|
||||
<> help ("terminal mode: editor or basic (" <> termModeName deafultTermMode <> ")")
|
||||
<> value deafultTermMode
|
||||
)
|
||||
where
|
||||
defaultDbFilePath = combine appDir "smp-chat.db"
|
||||
deafultTermMode
|
||||
| os == "mingw32" = TermModeBasic
|
||||
| otherwise = TermModeEditor
|
||||
|
||||
parseSMPServer :: ReadM SMPServer
|
||||
parseSMPServer = eitherReader $ A.parseOnly (smpServerP <* A.endOfInput) . B.pack
|
||||
|
||||
parseTermMode :: ReadM TermMode
|
||||
parseTermMode = maybeReader $ \case
|
||||
"basic" -> Just TermModeBasic
|
||||
"editor" -> Just TermModeEditor
|
||||
_ -> Nothing
|
||||
|
||||
getChatOpts :: FilePath -> IO ChatOpts
|
||||
getChatOpts appDir = execParser opts
|
||||
where
|
||||
opts =
|
||||
info
|
||||
(chatOpts appDir <**> helper)
|
||||
( fullDesc
|
||||
<> header "Chat prototype using Simplex Messaging Protocol (SMP)"
|
||||
<> progDesc "Start chat with DB_FILE file and use SERVER as SMP server"
|
||||
)
|
||||
@@ -1,105 +0,0 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module ChatTerminal
|
||||
( ChatTerminal (..),
|
||||
newChatTerminal,
|
||||
chatTerminal,
|
||||
updateUsername,
|
||||
ttyContact,
|
||||
ttyFromContact,
|
||||
)
|
||||
where
|
||||
|
||||
import ChatTerminal.Basic (getLn, putLn)
|
||||
import ChatTerminal.Core
|
||||
import ChatTerminal.POSIX
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Concurrent.Async (race_)
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Numeric.Natural
|
||||
import Styled
|
||||
import qualified System.Console.ANSI as C
|
||||
import Types
|
||||
|
||||
newChatTerminal :: Natural -> Maybe Contact -> TermMode -> IO ChatTerminal
|
||||
newChatTerminal qSize user termMode = do
|
||||
inputQ <- newTBQueueIO qSize
|
||||
outputQ <- newTBQueueIO qSize
|
||||
activeContact <- newTVarIO Nothing
|
||||
username <- newTVarIO user
|
||||
termSize <- fromMaybe (0, 0) <$> C.getTerminalSize
|
||||
let lastRow = fst termSize - 1
|
||||
termState <- newTVarIO $ newTermState user
|
||||
termLock <- newTMVarIO ()
|
||||
nextMessageRow <- newTVarIO lastRow
|
||||
threadDelay 500000 -- this delay is the same as timeout in getTerminalSize
|
||||
return ChatTerminal {inputQ, outputQ, activeContact, username, termMode, termState, termSize, nextMessageRow, termLock}
|
||||
|
||||
newTermState :: Maybe Contact -> TerminalState
|
||||
newTermState user =
|
||||
TerminalState
|
||||
{ inputString = "",
|
||||
inputPosition = 0,
|
||||
inputPrompt = promptString user
|
||||
}
|
||||
|
||||
chatTerminal :: ChatTerminal -> IO ()
|
||||
chatTerminal ct
|
||||
| termSize ct == (0, 0) || termMode ct == TermModeBasic =
|
||||
run basicReceiveFromTTY basicSendToTTY
|
||||
| otherwise = do
|
||||
initTTY
|
||||
updateInput ct
|
||||
run receiveFromTTY sendToTTY
|
||||
where
|
||||
run receive send = race_ (receive ct) (send ct)
|
||||
|
||||
basicReceiveFromTTY :: ChatTerminal -> IO ()
|
||||
basicReceiveFromTTY ct =
|
||||
forever $ getLn >>= atomically . writeTBQueue (inputQ ct)
|
||||
|
||||
basicSendToTTY :: ChatTerminal -> IO ()
|
||||
basicSendToTTY ct = forever $ readOutputQ ct >>= putLn
|
||||
|
||||
withTermLock :: ChatTerminal -> IO () -> IO ()
|
||||
withTermLock ChatTerminal {termLock} action = do
|
||||
_ <- atomically $ takeTMVar termLock
|
||||
action
|
||||
atomically $ putTMVar termLock ()
|
||||
|
||||
receiveFromTTY :: ChatTerminal -> IO ()
|
||||
receiveFromTTY ct@ChatTerminal {inputQ, activeContact, termSize, termState} =
|
||||
forever $
|
||||
getKey >>= processKey >> withTermLock ct (updateInput ct)
|
||||
where
|
||||
processKey :: Key -> IO ()
|
||||
processKey = \case
|
||||
KeyEnter -> submitInput
|
||||
key -> atomically $ do
|
||||
ac <- readTVar activeContact
|
||||
modifyTVar termState $ updateTermState ac (snd termSize) key
|
||||
|
||||
submitInput :: IO ()
|
||||
submitInput = do
|
||||
msg <- atomically $ do
|
||||
ts <- readTVar termState
|
||||
writeTVar termState $ ts {inputString = "", inputPosition = 0}
|
||||
let s = inputString ts
|
||||
writeTBQueue inputQ s
|
||||
return s
|
||||
withTermLock ct . printMessage ct $ styleMessage msg
|
||||
|
||||
sendToTTY :: ChatTerminal -> IO ()
|
||||
sendToTTY ct = forever $ do
|
||||
msg <- readOutputQ ct
|
||||
withTermLock ct $ do
|
||||
printMessage ct msg
|
||||
updateInput ct
|
||||
|
||||
readOutputQ :: ChatTerminal -> IO StyledString
|
||||
readOutputQ = atomically . readTBQueue . outputQ
|
||||
@@ -1,81 +0,0 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
|
||||
module ChatTerminal.Basic where
|
||||
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
import Styled
|
||||
import System.Console.ANSI.Types
|
||||
import System.Exit (exitSuccess)
|
||||
import System.Terminal as C
|
||||
|
||||
getLn :: IO String
|
||||
getLn = withTerminal $ runTerminalT getTermLine
|
||||
|
||||
putLn :: StyledString -> IO ()
|
||||
putLn s =
|
||||
withTerminal . runTerminalT $
|
||||
putStyled s >> C.putLn >> flush
|
||||
|
||||
putStyled :: MonadTerminal m => StyledString -> m ()
|
||||
putStyled (s1 :<>: s2) = putStyled s1 >> putStyled s2
|
||||
putStyled (Styled [] s) = putString s
|
||||
putStyled (Styled sgr s) = setSGR sgr >> putString s >> resetAttributes
|
||||
|
||||
setSGR :: MonadTerminal m => [SGR] -> m ()
|
||||
setSGR = mapM_ $ \case
|
||||
Reset -> resetAttributes
|
||||
SetConsoleIntensity BoldIntensity -> setAttribute bold
|
||||
SetConsoleIntensity _ -> resetAttribute bold
|
||||
SetItalicized True -> setAttribute italic
|
||||
SetItalicized _ -> resetAttribute italic
|
||||
SetUnderlining NoUnderline -> resetAttribute underlined
|
||||
SetUnderlining _ -> setAttribute underlined
|
||||
SetSwapForegroundBackground True -> setAttribute inverted
|
||||
SetSwapForegroundBackground _ -> resetAttribute inverted
|
||||
SetColor l i c -> setAttribute . layer l . intensity i $ color c
|
||||
SetBlinkSpeed _ -> pure ()
|
||||
SetVisible _ -> pure ()
|
||||
SetRGBColor _ _ -> pure ()
|
||||
SetPaletteColor _ _ -> pure ()
|
||||
SetDefaultColor _ -> pure ()
|
||||
where
|
||||
layer = \case
|
||||
Foreground -> foreground
|
||||
Background -> background
|
||||
intensity = \case
|
||||
Dull -> id
|
||||
Vivid -> bright
|
||||
color = \case
|
||||
Black -> black
|
||||
Red -> red
|
||||
Green -> green
|
||||
Yellow -> yellow
|
||||
Blue -> blue
|
||||
Magenta -> magenta
|
||||
Cyan -> cyan
|
||||
White -> white
|
||||
|
||||
getTermLine :: MonadTerminal m => m String
|
||||
getTermLine = getChars ""
|
||||
where
|
||||
getChars s = awaitEvent >>= processKey s
|
||||
processKey s = \case
|
||||
Right (KeyEvent key ms) -> case key of
|
||||
CharKey c
|
||||
| ms == mempty || ms == shiftKey -> do
|
||||
C.putChar c
|
||||
flush
|
||||
getChars (c : s)
|
||||
| otherwise -> getChars s
|
||||
EnterKey -> do
|
||||
C.putLn
|
||||
flush
|
||||
pure $ reverse s
|
||||
BackspaceKey -> do
|
||||
moveCursorBackward 1
|
||||
eraseChars 1
|
||||
flush
|
||||
getChars $ if null s then s else tail s
|
||||
_ -> getChars s
|
||||
Left Interrupt -> liftIO exitSuccess
|
||||
_ -> getChars s
|
||||
@@ -1,130 +0,0 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module ChatTerminal.Core where
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.List (dropWhileEnd)
|
||||
import qualified Data.Text as T
|
||||
import SimplexMarkdown
|
||||
import Styled
|
||||
import System.Console.ANSI.Types
|
||||
import Types
|
||||
|
||||
data ChatTerminal = ChatTerminal
|
||||
{ inputQ :: TBQueue String,
|
||||
outputQ :: TBQueue StyledString,
|
||||
activeContact :: TVar (Maybe Contact),
|
||||
username :: TVar (Maybe Contact),
|
||||
termMode :: TermMode,
|
||||
termState :: TVar TerminalState,
|
||||
termSize :: (Int, Int),
|
||||
nextMessageRow :: TVar Int,
|
||||
termLock :: TMVar ()
|
||||
}
|
||||
|
||||
data TerminalState = TerminalState
|
||||
{ inputPrompt :: String,
|
||||
inputString :: String,
|
||||
inputPosition :: Int
|
||||
}
|
||||
|
||||
data Key
|
||||
= KeyLeft
|
||||
| KeyRight
|
||||
| KeyUp
|
||||
| KeyDown
|
||||
| KeyAltLeft
|
||||
| KeyAltRight
|
||||
| KeyCtrlLeft
|
||||
| KeyCtrlRight
|
||||
| KeyShiftLeft
|
||||
| KeyShiftRight
|
||||
| KeyEnter
|
||||
| KeyBack
|
||||
| KeyTab
|
||||
| KeyEsc
|
||||
| KeyChars String
|
||||
| KeyUnsupported
|
||||
deriving (Eq)
|
||||
|
||||
inputHeight :: TerminalState -> ChatTerminal -> Int
|
||||
inputHeight ts ct = length (inputPrompt ts <> inputString ts) `div` snd (termSize ct) + 1
|
||||
|
||||
updateTermState :: Maybe Contact -> Int -> Key -> TerminalState -> TerminalState
|
||||
updateTermState ac tw key ts@TerminalState {inputString = s, inputPosition = p} = case key of
|
||||
KeyChars cs -> insertCharsWithContact cs
|
||||
KeyTab -> insertChars " "
|
||||
KeyBack -> backDeleteChar
|
||||
KeyLeft -> setPosition $ max 0 (p - 1)
|
||||
KeyRight -> setPosition $ min (length s) (p + 1)
|
||||
KeyUp -> setPosition $ let p' = p - tw in if p' > 0 then p' else p
|
||||
KeyDown -> setPosition $ let p' = p + tw in if p' <= length s then p' else p
|
||||
KeyAltLeft -> setPosition prevWordPos
|
||||
KeyAltRight -> setPosition nextWordPos
|
||||
KeyCtrlLeft -> setPosition prevWordPos
|
||||
KeyCtrlRight -> setPosition nextWordPos
|
||||
KeyShiftLeft -> setPosition 0
|
||||
KeyShiftRight -> setPosition $ length s
|
||||
_ -> ts
|
||||
where
|
||||
insertCharsWithContact cs
|
||||
| null s && cs /= "@" && cs /= "/" =
|
||||
insertChars $ contactPrefix <> cs
|
||||
| otherwise = insertChars cs
|
||||
insertChars = ts' . if p >= length s then append else insert
|
||||
append cs = let s' = s <> cs in (s', length s')
|
||||
insert cs = let (b, a) = splitAt p s in (b <> cs <> a, p + length cs)
|
||||
contactPrefix = case ac of
|
||||
Just (Contact c) -> "@" <> B.unpack c <> " "
|
||||
Nothing -> ""
|
||||
backDeleteChar
|
||||
| p == 0 || null s = ts
|
||||
| p >= length s = ts' backDeleteLast
|
||||
| otherwise = ts' backDelete
|
||||
backDeleteLast = if null s then (s, 0) else let s' = init s in (s', length s')
|
||||
backDelete = let (b, a) = splitAt p s in (init b <> a, p - 1)
|
||||
setPosition p' = ts' (s, p')
|
||||
prevWordPos
|
||||
| p == 0 || null s = p
|
||||
| otherwise =
|
||||
let before = take p s
|
||||
beforeWord = dropWhileEnd (/= ' ') $ dropWhileEnd (== ' ') before
|
||||
in max 0 $ p - length before + length beforeWord
|
||||
nextWordPos
|
||||
| p >= length s || null s = p
|
||||
| otherwise =
|
||||
let after = drop p s
|
||||
afterWord = dropWhile (/= ' ') $ dropWhile (== ' ') after
|
||||
in min (length s) $ p + length after - length afterWord
|
||||
ts' (s', p') = ts {inputString = s', inputPosition = p'}
|
||||
|
||||
styleMessage :: String -> StyledString
|
||||
styleMessage = \case
|
||||
"" -> ""
|
||||
s@('@' : _) -> let (c, rest) = span (/= ' ') s in Styled selfSGR c <> markdown rest
|
||||
s -> markdown s
|
||||
where
|
||||
markdown :: String -> StyledString
|
||||
markdown = styleMarkdown . parseMarkdown . T.pack
|
||||
|
||||
updateUsername :: ChatTerminal -> Maybe Contact -> STM ()
|
||||
updateUsername ct a = do
|
||||
writeTVar (username ct) a
|
||||
modifyTVar (termState ct) $ \ts -> ts {inputPrompt = promptString a}
|
||||
|
||||
promptString :: Maybe Contact -> String
|
||||
promptString a = maybe "" (B.unpack . toBs) a <> "> "
|
||||
|
||||
ttyContact :: Contact -> StyledString
|
||||
ttyContact (Contact a) = Styled contactSGR $ B.unpack a
|
||||
|
||||
ttyFromContact :: Contact -> StyledString
|
||||
ttyFromContact (Contact a) = Styled contactSGR $ B.unpack a <> ">"
|
||||
|
||||
contactSGR :: [SGR]
|
||||
contactSGR = [SetColor Foreground Vivid Yellow]
|
||||
|
||||
selfSGR :: [SGR]
|
||||
selfSGR = [SetColor Foreground Vivid Cyan]
|
||||
@@ -1,102 +0,0 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
|
||||
module ChatTerminal.POSIX where
|
||||
|
||||
import ChatTerminal.Core
|
||||
import Control.Concurrent.STM
|
||||
import Styled
|
||||
import qualified System.Console.ANSI as C
|
||||
import System.IO
|
||||
|
||||
initTTY :: IO ()
|
||||
initTTY = do
|
||||
hSetEcho stdin False
|
||||
hSetBuffering stdin NoBuffering
|
||||
hSetBuffering stdout NoBuffering
|
||||
|
||||
updateInput :: ChatTerminal -> IO ()
|
||||
updateInput ct@ChatTerminal {termSize, termState, nextMessageRow} = do
|
||||
C.hideCursor
|
||||
ts <- readTVarIO termState
|
||||
nmr <- readTVarIO nextMessageRow
|
||||
let (th, tw) = termSize
|
||||
ih = inputHeight ts ct
|
||||
iStart = th - ih
|
||||
prompt = inputPrompt ts
|
||||
(cRow, cCol) = relativeCursorPosition tw $ length prompt + inputPosition ts
|
||||
if nmr >= iStart
|
||||
then atomically $ writeTVar nextMessageRow iStart
|
||||
else clearLines nmr iStart
|
||||
C.setCursorPosition (max nmr iStart) 0
|
||||
putStr $ prompt <> inputString ts <> " "
|
||||
C.clearFromCursorToLineEnd
|
||||
C.setCursorPosition (iStart + cRow) cCol
|
||||
C.showCursor
|
||||
where
|
||||
clearLines :: Int -> Int -> IO ()
|
||||
clearLines from till
|
||||
| from >= till = return ()
|
||||
| otherwise = do
|
||||
C.setCursorPosition from 0
|
||||
C.clearFromCursorToLineEnd
|
||||
clearLines (from + 1) till
|
||||
|
||||
relativeCursorPosition :: Int -> Int -> (Int, Int)
|
||||
relativeCursorPosition width pos =
|
||||
let row = pos `div` width
|
||||
col = pos - row * width
|
||||
in (row, col)
|
||||
|
||||
printMessage :: ChatTerminal -> StyledString -> IO ()
|
||||
printMessage ChatTerminal {termSize, nextMessageRow} msg = do
|
||||
nmr <- readTVarIO nextMessageRow
|
||||
C.setCursorPosition nmr 0
|
||||
let (th, tw) = termSize
|
||||
lc <- printLines tw msg
|
||||
atomically . writeTVar nextMessageRow $ min (th - 1) (nmr + lc)
|
||||
where
|
||||
printLines :: Int -> StyledString -> IO Int
|
||||
printLines tw ss = do
|
||||
let s = styledToANSITerm ss
|
||||
ls
|
||||
| null s = [""]
|
||||
| otherwise = lines s <> ["" | last s == '\n']
|
||||
print_ ls
|
||||
return $ foldl (\lc l -> lc + (length l `div` tw) + 1) 0 ls
|
||||
|
||||
print_ :: [String] -> IO ()
|
||||
print_ [] = return ()
|
||||
print_ (l : ls) = do
|
||||
putStr l
|
||||
C.clearFromCursorToLineEnd
|
||||
putStr "\n"
|
||||
print_ ls
|
||||
|
||||
getKey :: IO Key
|
||||
getKey = charsToKey . reverse <$> keyChars ""
|
||||
where
|
||||
charsToKey = \case
|
||||
"\ESC" -> KeyEsc
|
||||
"\ESC[A" -> KeyUp
|
||||
"\ESC[B" -> KeyDown
|
||||
"\ESC[D" -> KeyLeft
|
||||
"\ESC[C" -> KeyRight
|
||||
"\ESCb" -> KeyAltLeft
|
||||
"\ESCf" -> KeyAltRight
|
||||
"\ESC[1;5D" -> KeyCtrlLeft
|
||||
"\ESC[1;5C" -> KeyCtrlRight
|
||||
"\ESC[1;2D" -> KeyShiftLeft
|
||||
"\ESC[1;2C" -> KeyShiftRight
|
||||
"\n" -> KeyEnter
|
||||
"\DEL" -> KeyBack
|
||||
"\t" -> KeyTab
|
||||
'\ESC' : _ -> KeyUnsupported
|
||||
cs -> KeyChars cs
|
||||
|
||||
keyChars cs = do
|
||||
c <- getChar
|
||||
more <- hReady stdin
|
||||
-- for debugging - uncomment this, comment line after:
|
||||
-- (if more then keyChars else \c' -> print (reverse c') >> return c') (c : cs)
|
||||
(if more then keyChars else return) (c : cs)
|
||||
@@ -1,250 +0,0 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Main where
|
||||
|
||||
import ChatOptions
|
||||
import ChatTerminal
|
||||
import Control.Applicative ((<|>))
|
||||
import Control.Concurrent.STM
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad.Reader
|
||||
import Data.Attoparsec.ByteString.Char8 (Parser)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Functor (($>))
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding
|
||||
import Numeric.Natural
|
||||
import Simplex.Messaging.Agent (getSMPAgentClient, runSMPAgentClient)
|
||||
import Simplex.Messaging.Agent.Client (AgentClient (..))
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Agent.Transmission
|
||||
import Simplex.Messaging.Client (smpDefaultConfig)
|
||||
import Simplex.Messaging.Util (raceAny_)
|
||||
import SimplexMarkdown
|
||||
import Styled
|
||||
import System.Directory (getAppUserDataDirectory)
|
||||
import System.Exit (exitFailure)
|
||||
import System.Info (os)
|
||||
import Types
|
||||
|
||||
cfg :: AgentConfig
|
||||
cfg =
|
||||
AgentConfig
|
||||
{ tcpPort = undefined, -- TODO maybe take it out of config
|
||||
rsaKeySize = 2048 `div` 8,
|
||||
connIdBytes = 12,
|
||||
tbqSize = 16,
|
||||
dbFile = "smp-chat.db",
|
||||
smpCfg = smpDefaultConfig
|
||||
}
|
||||
|
||||
logCfg :: LogConfig
|
||||
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
data ChatClient = ChatClient
|
||||
{ inQ :: TBQueue ChatCommand,
|
||||
outQ :: TBQueue ChatResponse,
|
||||
smpServer :: SMPServer,
|
||||
username :: TVar (Maybe Contact)
|
||||
}
|
||||
|
||||
-- | GroupMessage ChatGroup ByteString
|
||||
-- | AddToGroup Contact
|
||||
data ChatCommand
|
||||
= ChatHelp
|
||||
| AddContact Contact
|
||||
| AcceptContact Contact SMPQueueInfo
|
||||
| ChatWith Contact
|
||||
| SetName Contact
|
||||
| SendMessage Contact ByteString
|
||||
|
||||
chatCommandP :: Parser ChatCommand
|
||||
chatCommandP =
|
||||
"/help" $> ChatHelp
|
||||
<|> "/add " *> (AddContact <$> contact)
|
||||
<|> "/accept " *> acceptContact
|
||||
<|> "/chat " *> chatWith
|
||||
<|> "/name " *> setName
|
||||
<|> "@" *> sendMessage
|
||||
where
|
||||
acceptContact = AcceptContact <$> contact <* A.space <*> smpQueueInfoP
|
||||
chatWith = ChatWith <$> contact
|
||||
setName = SetName <$> contact
|
||||
sendMessage = SendMessage <$> contact <* A.space <*> A.takeByteString
|
||||
contact = Contact <$> A.takeTill (== ' ')
|
||||
|
||||
data ChatResponse
|
||||
= ChatHelpInfo
|
||||
| Invitation SMPQueueInfo
|
||||
| Connected Contact
|
||||
| ReceivedMessage Contact ByteString
|
||||
| Disconnected Contact
|
||||
| YesYes
|
||||
| ErrorInput ByteString
|
||||
| ChatError AgentErrorType
|
||||
| NoChatResponse
|
||||
|
||||
serializeChatResponse :: Maybe Contact -> ChatResponse -> StyledString
|
||||
serializeChatResponse name = \case
|
||||
ChatHelpInfo -> chatHelpInfo
|
||||
Invitation qInfo -> "ask your contact to enter: /accept " <> showName name <> " " <> (bPlain . serializeSmpQueueInfo) qInfo
|
||||
Connected c -> ttyContact c <> " connected"
|
||||
ReceivedMessage c t -> ttyFromContact c <> " " <> msgPlain t
|
||||
Disconnected c -> "disconnected from " <> ttyContact c <> " - try \"/chat " <> bPlain (toBs c) <> "\""
|
||||
YesYes -> "you got it!"
|
||||
ErrorInput t -> "invalid input: " <> bPlain t
|
||||
ChatError e -> "chat error: " <> plain (show e)
|
||||
NoChatResponse -> ""
|
||||
where
|
||||
showName Nothing = "<your name>"
|
||||
showName (Just (Contact a)) = bPlain a
|
||||
msgPlain = styleMarkdown . parseMarkdown . decodeUtf8With onError
|
||||
onError _ _ = Just '?'
|
||||
|
||||
chatHelpInfo :: StyledString
|
||||
chatHelpInfo =
|
||||
"Using chat:\n\
|
||||
\/add <name> - create invitation to send out-of-band\n\
|
||||
\ to your contact <name>\n\
|
||||
\ (any unique string without spaces)\n\
|
||||
\/accept <name> <invitation> - accept <invitation>\n\
|
||||
\ (a string that starts from \"smp::\")\n\
|
||||
\ from your contact <name>\n\
|
||||
\/name <name> - set <name> to use in invitations\n\
|
||||
\@<name> <message> - send <message> (any string) to contact <name>\n\
|
||||
\ @<name> can be omitted to send to previous"
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
ChatOpts {dbFileName, smpServer, name, termMode} <- welcomeGetOpts
|
||||
let user = Contact <$> name
|
||||
t <- getChatClient smpServer user
|
||||
ct <- newChatTerminal (tbqSize cfg) user termMode
|
||||
-- setLogLevel LogInfo -- LogError
|
||||
-- withGlobalLogging logCfg $
|
||||
env <- newSMPAgentEnv cfg {dbFile = dbFileName}
|
||||
dogFoodChat t ct env
|
||||
|
||||
welcomeGetOpts :: IO ChatOpts
|
||||
welcomeGetOpts = do
|
||||
appDir <- getAppUserDataDirectory "simplex"
|
||||
opts@ChatOpts {dbFileName, termMode} <- getChatOpts appDir
|
||||
putStrLn "simpleX chat prototype"
|
||||
putStrLn $ "db: " <> dbFileName
|
||||
when (os == "mingw32") $ windowsWarning termMode
|
||||
putStrLn "type \"/help\" for usage information"
|
||||
pure opts
|
||||
|
||||
windowsWarning :: TermMode -> IO ()
|
||||
windowsWarning = \case
|
||||
m@TermModeBasic -> do
|
||||
putStrLn $ "running in Windows (terminal mode is " <> termModeName m <> ", no utf8 support)"
|
||||
putStrLn "it is recommended to use Windows Subsystem for Linux (WSL)"
|
||||
m -> do
|
||||
putStrLn $ "running in Windows, terminal mode " <> termModeName m <> " is not supported"
|
||||
exitFailure
|
||||
|
||||
dogFoodChat :: ChatClient -> ChatTerminal -> Env -> IO ()
|
||||
dogFoodChat t ct env = do
|
||||
c <- runReaderT getSMPAgentClient env
|
||||
raceAny_
|
||||
[ runReaderT (runSMPAgentClient c) env,
|
||||
sendToAgent t ct c,
|
||||
sendToChatTerm t ct,
|
||||
receiveFromAgent t ct c,
|
||||
receiveFromChatTerm t ct,
|
||||
chatTerminal ct
|
||||
]
|
||||
|
||||
getChatClient :: SMPServer -> Maybe Contact -> IO ChatClient
|
||||
getChatClient srv name = atomically $ newChatClient (tbqSize cfg) srv name
|
||||
|
||||
newChatClient :: Natural -> SMPServer -> Maybe Contact -> STM ChatClient
|
||||
newChatClient qSize smpServer name = do
|
||||
inQ <- newTBQueue qSize
|
||||
outQ <- newTBQueue qSize
|
||||
username <- newTVar name
|
||||
return ChatClient {inQ, outQ, smpServer, username}
|
||||
|
||||
receiveFromChatTerm :: ChatClient -> ChatTerminal -> IO ()
|
||||
receiveFromChatTerm t ct = forever $ do
|
||||
atomically (readTBQueue $ inputQ ct)
|
||||
>>= processOrError . A.parseOnly (chatCommandP <* A.endOfInput) . encodeUtf8 . T.pack
|
||||
where
|
||||
processOrError = \case
|
||||
Left err -> atomically . writeTBQueue (outQ t) . ErrorInput $ B.pack err
|
||||
Right ChatHelp -> atomically . writeTBQueue (outQ t) $ ChatHelpInfo
|
||||
Right (SetName a) -> atomically $ do
|
||||
let user = Just a
|
||||
writeTVar (username (t :: ChatClient)) user
|
||||
updateUsername ct user
|
||||
writeTBQueue (outQ t) YesYes
|
||||
Right cmd -> atomically $ writeTBQueue (inQ t) cmd
|
||||
|
||||
sendToChatTerm :: ChatClient -> ChatTerminal -> IO ()
|
||||
sendToChatTerm ChatClient {outQ, username} ChatTerminal {outputQ} = forever $ do
|
||||
atomically (readTBQueue outQ) >>= \case
|
||||
NoChatResponse -> return ()
|
||||
resp -> do
|
||||
name <- readTVarIO username
|
||||
atomically . writeTBQueue outputQ $ serializeChatResponse name resp
|
||||
|
||||
sendToAgent :: ChatClient -> ChatTerminal -> AgentClient -> IO ()
|
||||
sendToAgent ChatClient {inQ, smpServer} ct AgentClient {rcvQ} = do
|
||||
atomically $ writeTBQueue rcvQ ("1", "", SUBALL) -- hack for subscribing to all
|
||||
forever . atomically $ do
|
||||
cmd <- readTBQueue inQ
|
||||
writeTBQueue rcvQ `mapM_` agentTransmission cmd
|
||||
setActiveContact cmd
|
||||
where
|
||||
setActiveContact :: ChatCommand -> STM ()
|
||||
setActiveContact cmd =
|
||||
writeTVar (activeContact ct) $ case cmd of
|
||||
ChatWith a -> Just a
|
||||
SendMessage a _ -> Just a
|
||||
_ -> Nothing
|
||||
agentTransmission :: ChatCommand -> Maybe (ATransmission 'Client)
|
||||
agentTransmission = \case
|
||||
AddContact a -> transmission a $ NEW smpServer
|
||||
AcceptContact a qInfo -> transmission a $ JOIN qInfo $ ReplyVia smpServer
|
||||
ChatWith a -> transmission a SUB
|
||||
SendMessage a msg -> transmission a $ SEND msg
|
||||
ChatHelp -> Nothing
|
||||
SetName _ -> Nothing
|
||||
transmission :: Contact -> ACommand 'Client -> Maybe (ATransmission 'Client)
|
||||
transmission (Contact a) cmd = Just ("1", a, cmd)
|
||||
|
||||
receiveFromAgent :: ChatClient -> ChatTerminal -> AgentClient -> IO ()
|
||||
receiveFromAgent t ct c = forever . atomically $ do
|
||||
resp <- chatResponse <$> readTBQueue (sndQ c)
|
||||
writeTBQueue (outQ t) resp
|
||||
setActiveContact resp
|
||||
where
|
||||
chatResponse :: ATransmission 'Agent -> ChatResponse
|
||||
chatResponse (_, a, resp) = case resp of
|
||||
INV qInfo -> Invitation qInfo
|
||||
CON -> Connected contact
|
||||
END -> Disconnected contact
|
||||
MSG {m_body} -> ReceivedMessage contact m_body
|
||||
SENT _ -> NoChatResponse
|
||||
OK -> Connected contact -- hack for subscribing to all
|
||||
ERR e -> ChatError e
|
||||
where
|
||||
contact = Contact a
|
||||
setActiveContact :: ChatResponse -> STM ()
|
||||
setActiveContact = \case
|
||||
Connected a -> set $ Just a
|
||||
ReceivedMessage a _ -> set $ Just a
|
||||
Disconnected _ -> set Nothing
|
||||
_ -> return ()
|
||||
where
|
||||
set a = writeTVar (activeContact ct) a
|
||||
@@ -1,122 +0,0 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module SimplexMarkdown where
|
||||
|
||||
import Control.Applicative ((<|>))
|
||||
import Data.Attoparsec.Text (Parser)
|
||||
import qualified Data.Attoparsec.Text as A
|
||||
import Data.Either (fromRight)
|
||||
import Data.Functor (($>))
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.String
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Styled
|
||||
import System.Console.ANSI.Types
|
||||
|
||||
data Markdown = Markdown Format Text | Markdown :|: Markdown
|
||||
deriving (Show)
|
||||
|
||||
data Format
|
||||
= Bold
|
||||
| Italic
|
||||
| Underline
|
||||
| StrikeThrough
|
||||
| Colored Color
|
||||
| NoFormat
|
||||
deriving (Show)
|
||||
|
||||
instance Semigroup Markdown where (<>) = (:|:)
|
||||
|
||||
instance Monoid Markdown where mempty = unmarked ""
|
||||
|
||||
instance IsString Markdown where fromString = unmarked . T.pack
|
||||
|
||||
unmarked :: Text -> Markdown
|
||||
unmarked = Markdown NoFormat
|
||||
|
||||
styleMarkdown :: Markdown -> StyledString
|
||||
styleMarkdown (s1 :|: s2) = styleMarkdown s1 <> styleMarkdown s2
|
||||
styleMarkdown (Markdown f s) = Styled sgr $ T.unpack s
|
||||
where
|
||||
sgr = case f of
|
||||
Bold -> [SetConsoleIntensity BoldIntensity]
|
||||
Italic -> [SetUnderlining SingleUnderline, SetItalicized True]
|
||||
Underline -> [SetUnderlining SingleUnderline]
|
||||
StrikeThrough -> [SetSwapForegroundBackground True]
|
||||
Colored c -> [SetColor Foreground Vivid c]
|
||||
NoFormat -> []
|
||||
|
||||
formats :: Map Char Format
|
||||
formats =
|
||||
M.fromList
|
||||
[ ('*', Bold),
|
||||
('_', Italic),
|
||||
('+', Underline),
|
||||
('~', StrikeThrough),
|
||||
('^', Colored White)
|
||||
]
|
||||
|
||||
colors :: Map Text Color
|
||||
colors =
|
||||
M.fromList
|
||||
[ ("red", Red),
|
||||
("green", Green),
|
||||
("blue", Blue),
|
||||
("yellow", Yellow),
|
||||
("cyan", Cyan),
|
||||
("magenta", Magenta),
|
||||
("r", Red),
|
||||
("g", Green),
|
||||
("b", Blue),
|
||||
("y", Yellow),
|
||||
("c", Cyan),
|
||||
("m", Magenta)
|
||||
]
|
||||
|
||||
parseMarkdown :: Text -> Markdown
|
||||
parseMarkdown s = fromRight (unmarked s) $ A.parseOnly (markdownP <* A.endOfInput) s
|
||||
|
||||
markdownP :: Parser Markdown
|
||||
markdownP = merge <$> A.many' fragmentP
|
||||
where
|
||||
merge :: [Markdown] -> Markdown
|
||||
merge [] = ""
|
||||
merge [f] = f
|
||||
merge (f : fs) = foldl (:|:) f fs
|
||||
fragmentP :: Parser Markdown
|
||||
fragmentP =
|
||||
A.anyChar >>= \case
|
||||
' ' -> unmarked . (" " <>) <$> A.takeWhile (== ' ')
|
||||
c -> case M.lookup c formats of
|
||||
Just (Colored White) -> coloredP
|
||||
Just f -> formattedP c "" f
|
||||
Nothing -> unformattedP c
|
||||
formattedP :: Char -> Text -> Format -> Parser Markdown
|
||||
formattedP c p f = do
|
||||
s <- A.takeTill (== c)
|
||||
(A.char c $> Markdown f s) <|> noFormat (T.singleton c <> p <> s)
|
||||
coloredP :: Parser Markdown
|
||||
coloredP = do
|
||||
color <- A.takeWhile (\c -> c /= ' ' && c /= '^')
|
||||
case M.lookup color colors of
|
||||
Just c ->
|
||||
let f = Colored c
|
||||
in (A.char ' ' *> formattedP '^' (color <> " ") f)
|
||||
<|> (A.char '^' $> Markdown f color)
|
||||
<|> noFormat ("^" <> color)
|
||||
_ -> noFormat ("^" <> color)
|
||||
unformattedP :: Char -> Parser Markdown
|
||||
unformattedP c = unmarked . (T.singleton c <>) <$> wordsP
|
||||
wordsP :: Parser Text
|
||||
wordsP = do
|
||||
s <- (<>) <$> A.takeTill (== ' ') <*> A.takeWhile (== ' ')
|
||||
A.peekChar >>= \case
|
||||
Nothing -> pure s
|
||||
Just c -> case M.lookup c formats of
|
||||
Just _ -> pure s
|
||||
Nothing -> (s <>) <$> wordsP
|
||||
noFormat :: Text -> Parser Markdown
|
||||
noFormat = pure . unmarked
|
||||
@@ -1,29 +0,0 @@
|
||||
module Styled (StyledString (..), plain, bPlain, styledToANSITerm, styledToPlain) where
|
||||
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.String
|
||||
import System.Console.ANSI (SGR (..), setSGRCode)
|
||||
|
||||
data StyledString = Styled [SGR] String | StyledString :<>: StyledString
|
||||
|
||||
instance Semigroup StyledString where (<>) = (:<>:)
|
||||
|
||||
instance Monoid StyledString where mempty = plain ""
|
||||
|
||||
instance IsString StyledString where fromString = plain
|
||||
|
||||
plain :: String -> StyledString
|
||||
plain = Styled []
|
||||
|
||||
bPlain :: ByteString -> StyledString
|
||||
bPlain = Styled [] . B.unpack
|
||||
|
||||
styledToANSITerm :: StyledString -> String
|
||||
styledToANSITerm (Styled [] s) = s
|
||||
styledToANSITerm (Styled sgr s) = setSGRCode sgr <> s <> setSGRCode [Reset]
|
||||
styledToANSITerm (s1 :<>: s2) = styledToANSITerm s1 <> styledToANSITerm s2
|
||||
|
||||
styledToPlain :: StyledString -> String
|
||||
styledToPlain (Styled _ s) = s
|
||||
styledToPlain (s1 :<>: s2) = styledToPlain s1 <> styledToPlain s2
|
||||
@@ -1,14 +0,0 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
|
||||
module Types where
|
||||
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
|
||||
newtype Contact = Contact {toBs :: ByteString}
|
||||
|
||||
data TermMode = TermModeBasic | TermModeEditor deriving (Eq)
|
||||
|
||||
termModeName :: TermMode -> String
|
||||
termModeName = \case
|
||||
TermModeBasic -> "basic"
|
||||
TermModeEditor -> "editor"
|
||||
@@ -0,0 +1,18 @@
|
||||
module Main where
|
||||
|
||||
import Control.Logger.Simple
|
||||
import Simplex.Messaging.Notifications.Server.Main
|
||||
|
||||
cfgPath :: FilePath
|
||||
cfgPath = "/etc/opt/simplex-notifications"
|
||||
|
||||
logPath :: FilePath
|
||||
logPath = "/var/opt/simplex-notifications"
|
||||
|
||||
logCfg :: LogConfig
|
||||
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
setLogLevel LogDebug -- change to LogError in production
|
||||
withGlobalLogging logCfg $ ntfServerCLI cfgPath logPath
|
||||
@@ -1,21 +1,34 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Main where
|
||||
|
||||
import Control.Logger.Simple
|
||||
import Simplex.Messaging.Agent (runSMPAgent)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import qualified Data.Map.Strict as M
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Client (smpDefaultConfig)
|
||||
import Simplex.Messaging.Agent.Server (runSMPAgent)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..))
|
||||
import Simplex.Messaging.Client (defaultNetworkConfig)
|
||||
import Simplex.Messaging.Transport (TLS, Transport (..))
|
||||
|
||||
cfg :: AgentConfig
|
||||
cfg =
|
||||
AgentConfig
|
||||
{ tcpPort = "5224",
|
||||
rsaKeySize = 2048 `div` 8,
|
||||
connIdBytes = 12,
|
||||
tbqSize = 16,
|
||||
dbFile = "smp-agent.db",
|
||||
smpCfg = smpDefaultConfig
|
||||
cfg = defaultAgentConfig
|
||||
|
||||
agentDbFile :: String
|
||||
agentDbFile = "smp-agent.db"
|
||||
|
||||
agentDbKey :: String
|
||||
agentDbKey = ""
|
||||
|
||||
servers :: InitialAgentServers
|
||||
servers =
|
||||
InitialAgentServers
|
||||
{ smp = M.fromList [(1, L.fromList ["smp://bU0K-bRg24xWW__lS0umO1Zdw_SXqpJNtm1_RrPLViE=@localhost:5223"])],
|
||||
ntf = [],
|
||||
xftp = M.empty,
|
||||
netCfg = defaultNetworkConfig
|
||||
}
|
||||
|
||||
logCfg :: LogConfig
|
||||
@@ -25,5 +38,5 @@ main :: IO ()
|
||||
main = do
|
||||
putStrLn $ "SMP agent listening on port " ++ tcpPort (cfg :: AgentConfig)
|
||||
setLogLevel LogInfo -- LogError
|
||||
withGlobalLogging logCfg $
|
||||
runSMPAgent cfg
|
||||
Right st <- createAgentStore agentDbFile agentDbKey MCConsole
|
||||
withGlobalLogging logCfg $ runSMPAgent (transport @TLS) cfg servers st
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
module Main where
|
||||
|
||||
import Simplex.Messaging.Server (runSMPServer)
|
||||
import Simplex.Messaging.Server.Env.STM
|
||||
import Control.Logger.Simple
|
||||
import Simplex.Messaging.Server.Main
|
||||
|
||||
cfg :: ServerConfig
|
||||
cfg =
|
||||
ServerConfig
|
||||
{ tcpPort = "5223",
|
||||
tbqSize = 16,
|
||||
queueIdBytes = 12,
|
||||
msgIdBytes = 6
|
||||
}
|
||||
cfgPath :: FilePath
|
||||
cfgPath = "/etc/opt/simplex"
|
||||
|
||||
logPath :: FilePath
|
||||
logPath = "/var/opt/simplex"
|
||||
|
||||
logCfg :: LogConfig
|
||||
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
putStrLn $ "Listening on port " ++ tcpPort cfg
|
||||
runSMPServer cfg
|
||||
setLogLevel LogDebug
|
||||
withGlobalLogging logCfg $ smpServerCLI cfgPath logPath
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
module Main where
|
||||
|
||||
import Control.Logger.Simple
|
||||
import Simplex.FileTransfer.Server.Main
|
||||
|
||||
cfgPath :: FilePath
|
||||
cfgPath = "/etc/opt/simplex-xftp"
|
||||
|
||||
logPath :: FilePath
|
||||
logPath = "/var/opt/simplex-xftp"
|
||||
|
||||
logCfg :: LogConfig
|
||||
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
setLogLevel LogDebug -- change to LogError in production
|
||||
withGlobalLogging logCfg $ xftpServerCLI cfgPath logPath
|
||||
@@ -0,0 +1,6 @@
|
||||
module Main where
|
||||
|
||||
import Simplex.FileTransfer.Client.Main
|
||||
|
||||
main :: IO ()
|
||||
main = xftpClientCLI
|
||||
@@ -0,0 +1,29 @@
|
||||
packages: .
|
||||
-- packages: . ../direct-sqlcipher ../sqlcipher-simple
|
||||
-- packages: . ../hs-socks
|
||||
-- packages: . ../http2
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/aeson.git
|
||||
tag: 3eb66f9a68f103b5f1489382aad89f5712a64db7
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/hs-socks.git
|
||||
tag: a30cc7a79a08d8108316094f8f2f82a0c5e1ac51
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/kazu-yamamoto/http2.git
|
||||
tag: b5a1b7200cf5bc7044af34ba325284271f6dff25
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/direct-sqlcipher.git
|
||||
tag: 34309410eb2069b029b8fc1872deb1e0db123294
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/sqlcipher-simple.git
|
||||
tag: 5e154a2aeccc33ead6c243ec07195ab673137221
|
||||
@@ -8,7 +8,7 @@ digraph SMPAgent {
|
||||
|
||||
subgraph clusterPersistence {
|
||||
graph [fontsize=11 color=gray]
|
||||
label="persistence (sqlite)\nQ: can multiple threads use it"
|
||||
label="persistence (sqlite)"
|
||||
connectionsStore [shape=cylinder label="duplex connections,\nSMP queues,\nrecent messages"]
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
|
||||
-->
|
||||
<!-- Title: SMPAgent Pages: 1 -->
|
||||
<svg width="1073pt" height="1142pt"
|
||||
viewBox="0.00 0.00 1073.00 1142.34" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1138.3374)">
|
||||
<title>SMPAgent</title>
|
||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1138.3374 1069,-1138.3374 1069,4 -4,4"/>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>clusterPersistence</title>
|
||||
<polygon fill="none" stroke="#c0c0c0" points="927,-512.7374 927,-615.3874 1057,-615.3874 1057,-512.7374 927,-512.7374"/>
|
||||
<text text-anchor="middle" x="992" y="-601.4874" font-family="arial" font-size="11.00" fill="#000000">persistence (sqlite)</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>clusterAgent</title>
|
||||
<polygon fill="none" stroke="#c0c0c0" points="175,-963.3624 175,-1040.1624 389,-1040.1624 389,-963.3624 175,-963.3624"/>
|
||||
<text text-anchor="middle" x="282" y="-1023.5624" font-family="arial" font-size="14.00" fill="#000000">agent threads</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster">
|
||||
<title>clusterUserTCP</title>
|
||||
<polygon fill="none" stroke="#c0c0c0" points="8,-509.375 8,-933.3624 230,-933.3624 230,-509.375 8,-509.375"/>
|
||||
<text text-anchor="middle" x="119" y="-916.7624" font-family="arial" font-size="14.00" fill="#000000">1 group per user TCP connection</text>
|
||||
</g>
|
||||
<g id="clust5" class="cluster">
|
||||
<title>clusterUserTCPThreads</title>
|
||||
<polygon fill="none" stroke="#c0c0c0" points="38,-517.375 38,-610.7499 222,-610.7499 222,-517.375 38,-517.375"/>
|
||||
<text text-anchor="middle" x="130" y="-596.8499" font-family="arial" font-size="11.00" fill="#000000">user TCP threads</text>
|
||||
</g>
|
||||
<g id="clust6" class="cluster">
|
||||
<title>clusterUser</title>
|
||||
<polygon fill="none" stroke="#c0c0c0" points="250,-8 250,-826.5624 919,-826.5624 919,-8 250,-8"/>
|
||||
<text text-anchor="middle" x="584.5" y="-809.9624" font-family="arial" font-size="14.00" fill="#000000">1 group per user TCP connection</text>
|
||||
</g>
|
||||
<g id="clust7" class="cluster">
|
||||
<title>clusterUserInterface</title>
|
||||
<polygon fill="none" stroke="#c0c0c0" points="258,-400.375 258,-485.175 419,-485.175 419,-400.375 258,-400.375"/>
|
||||
<text text-anchor="middle" x="338.5" y="-471.275" font-family="arial" font-size="11.00" fill="#000000">user queues</text>
|
||||
</g>
|
||||
<g id="clust8" class="cluster">
|
||||
<title>clusterUserThreads</title>
|
||||
<polygon fill="none" stroke="#c0c0c0" points="622,-634.3874 622,-793.7624 865,-793.7624 865,-634.3874 622,-634.3874"/>
|
||||
<text text-anchor="middle" x="743.5" y="-779.8624" font-family="arial" font-size="11.00" fill="#000000">user threads</text>
|
||||
<text text-anchor="middle" x="743.5" y="-766.6624" font-family="arial" font-size="11.00" fill="#000000">Note: `user agent` sends</text>
|
||||
<text text-anchor="middle" x="743.5" y="-753.4624" font-family="arial" font-size="11.00" fill="#000000">all commands to `commands TBQueue`s</text>
|
||||
<text text-anchor="middle" x="743.5" y="-740.2624" font-family="arial" font-size="11.00" fill="#000000">(invalid commands with attached responses),</text>
|
||||
<text text-anchor="middle" x="743.5" y="-727.0624" font-family="arial" font-size="11.00" fill="#000000">and only valid commands to `server TBQueue`.</text>
|
||||
<text text-anchor="middle" x="743.5" y="-713.8624" font-family="arial" font-size="11.00" fill="#000000">It is used to respond in correct order.</text>
|
||||
</g>
|
||||
<g id="clust10" class="cluster">
|
||||
<title>clusterClient</title>
|
||||
<polygon fill="none" stroke="#c0c0c0" points="427,-128.6 427,-482.975 812,-482.975 812,-128.6 427,-128.6"/>
|
||||
<text text-anchor="middle" x="619.5" y="-466.375" font-family="arial" font-size="14.00" fill="#000000">1 group per SMP client/server connection</text>
|
||||
</g>
|
||||
<g id="clust11" class="cluster">
|
||||
<title>clusterServerThreads</title>
|
||||
<polygon fill="none" stroke="#c0c0c0" points="564,-136.6 564,-229.975 758,-229.975 758,-136.6 564,-136.6"/>
|
||||
<text text-anchor="middle" x="661" y="-216.075" font-family="arial" font-size="11.00" fill="#000000">SMP client threads</text>
|
||||
</g>
|
||||
<!-- main -->
|
||||
<g id="node1" class="node">
|
||||
<title>main</title>
|
||||
<polygon fill="none" stroke="#ffa500" points="382.542,-1106.2499 363.271,-1134.4251 324.729,-1134.4251 305.458,-1106.2499 324.729,-1078.0747 363.271,-1078.0747 382.542,-1106.2499"/>
|
||||
<text text-anchor="middle" x="344" y="-1109.5499" font-family="arial" font-size="11.00" fill="#000000">main</text>
|
||||
<text text-anchor="middle" x="344" y="-1096.3499" font-family="arial" font-size="11.00" fill="#000000">thread</text>
|
||||
</g>
|
||||
<!-- connectClnt -->
|
||||
<g id="node4" class="node">
|
||||
<title>connectClnt</title>
|
||||
<polygon fill="none" stroke="#ffa500" points="288.8246,-989.3624 262.4123,-1007.3624 209.5877,-1007.3624 183.1754,-989.3624 209.5877,-971.3624 262.4123,-971.3624 288.8246,-989.3624"/>
|
||||
<text text-anchor="middle" x="236" y="-986.0624" font-family="arial" font-size="11.00" fill="#000000">connectClnt</text>
|
||||
</g>
|
||||
<!-- main->connectClnt -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>main->connectClnt</title>
|
||||
<path fill="none" stroke="#ffa500" stroke-dasharray="5,2" d="M321.6223,-1082.0307C303.5136,-1062.4318 278.0793,-1034.9044 259.6406,-1014.9484"/>
|
||||
<polygon fill="#ffa500" stroke="#ffa500" points="252.7011,-1007.4378 262.7926,-1011.7288 256.0942,-1011.1102 259.4874,-1014.7826 259.4874,-1014.7826 259.4874,-1014.7826 256.0942,-1011.1102 256.1823,-1017.8365 252.7011,-1007.4378 252.7011,-1007.4378"/>
|
||||
<text text-anchor="middle" x="308.7235" y="-1051.1624" font-family="arial" font-size="10.00" fill="#ffa500">race</text>
|
||||
</g>
|
||||
<!-- runClnt -->
|
||||
<g id="node5" class="node">
|
||||
<title>runClnt</title>
|
||||
<polygon fill="none" stroke="#ffa500" points="380.5019,-989.3624 362.251,-1007.3624 325.749,-1007.3624 307.4981,-989.3624 325.749,-971.3624 362.251,-971.3624 380.5019,-989.3624"/>
|
||||
<text text-anchor="middle" x="344" y="-986.0624" font-family="arial" font-size="11.00" fill="#000000">runClnt</text>
|
||||
</g>
|
||||
<!-- main->runClnt -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>main->runClnt</title>
|
||||
<path fill="none" stroke="#ffa500" stroke-dasharray="5,2" d="M344,-1077.9547C344,-1059.6552 344,-1035.9253 344,-1017.631"/>
|
||||
<polygon fill="#ffa500" stroke="#ffa500" points="344,-1007.5699 348.5001,-1017.5699 344,-1012.5699 344.0001,-1017.5699 344.0001,-1017.5699 344.0001,-1017.5699 344,-1012.5699 339.5001,-1017.57 344,-1007.5699 344,-1007.5699"/>
|
||||
<text text-anchor="middle" x="353.7235" y="-1051.1624" font-family="arial" font-size="10.00" fill="#ffa500">race</text>
|
||||
</g>
|
||||
<!-- aSock -->
|
||||
<g id="node2" class="node">
|
||||
<title>aSock</title>
|
||||
<polygon fill="none" stroke="#006400" points="287.8624,-1124.2499 160.1376,-1124.2499 160.1376,-1088.2499 287.8624,-1088.2499 287.8624,-1124.2499"/>
|
||||
<text text-anchor="middle" x="224" y="-1102.9499" font-family="arial" font-size="11.00" fill="#000000">user agent TCP socket</text>
|
||||
</g>
|
||||
<!-- aSock->connectClnt -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>aSock->connectClnt</title>
|
||||
<path fill="none" stroke="#006400" d="M225.8487,-1088.2424C227.781,-1069.421 230.8399,-1039.6253 233.0952,-1017.6565"/>
|
||||
<polygon fill="#006400" stroke="#006400" points="234.1294,-1007.583 237.5845,-1017.9903 233.6188,-1012.5568 233.1081,-1017.5307 233.1081,-1017.5307 233.1081,-1017.5307 233.6188,-1012.5568 228.6316,-1017.0711 234.1294,-1007.583 234.1294,-1007.583"/>
|
||||
</g>
|
||||
<!-- connectionsStore -->
|
||||
<g id="node3" class="node">
|
||||
<title>connectionsStore</title>
|
||||
<path fill="none" stroke="#000000" d="M1048.7017,-580.4228C1048.7017,-583.7287 1023.2871,-586.414 992,-586.414 960.7129,-586.414 935.2983,-583.7287 935.2983,-580.4228 935.2983,-580.4228 935.2983,-526.5021 935.2983,-526.5021 935.2983,-523.1962 960.7129,-520.5109 992,-520.5109 1023.2871,-520.5109 1048.7017,-523.1962 1048.7017,-526.5021 1048.7017,-526.5021 1048.7017,-580.4228 1048.7017,-580.4228"/>
|
||||
<path fill="none" stroke="#000000" d="M1048.7017,-580.4228C1048.7017,-577.117 1023.2871,-574.4316 992,-574.4316 960.7129,-574.4316 935.2983,-577.117 935.2983,-580.4228"/>
|
||||
<text text-anchor="middle" x="992" y="-563.3624" font-family="arial" font-size="11.00" fill="#000000">duplex connections,</text>
|
||||
<text text-anchor="middle" x="992" y="-550.1624" font-family="arial" font-size="11.00" fill="#000000">SMP queues,</text>
|
||||
<text text-anchor="middle" x="992" y="-536.9624" font-family="arial" font-size="11.00" fill="#000000">recent messages</text>
|
||||
</g>
|
||||
<!-- uSock -->
|
||||
<g id="node6" class="node">
|
||||
<title>uSock</title>
|
||||
<polygon fill="none" stroke="#006400" points="169.5337,-900.5624 16.4663,-900.5624 16.4663,-864.5624 169.5337,-864.5624 169.5337,-900.5624"/>
|
||||
<text text-anchor="middle" x="93" y="-879.2624" font-family="arial" font-size="11.00" fill="#000000">user connection TCP socket</text>
|
||||
</g>
|
||||
<!-- connectClnt->uSock -->
|
||||
<g id="edge23" class="edge">
|
||||
<title>connectClnt->uSock</title>
|
||||
<path fill="none" stroke="#006400" stroke-dasharray="5,2" d="M198.6778,-978.637C182.4066,-972.8176 163.7253,-964.4984 148.985,-953.3624 133.1431,-941.3941 119.2685,-923.9297 109.2945,-909.3413"/>
|
||||
<polygon fill="#006400" stroke="#006400" points="103.6056,-900.7046 112.8644,-906.5804 106.356,-904.8801 109.1064,-909.0557 109.1064,-909.0557 109.1064,-909.0557 106.356,-904.8801 105.3483,-911.5311 103.6056,-900.7046 103.6056,-900.7046"/>
|
||||
<text text-anchor="middle" x="165.5075" y="-944.3624" font-family="arial" font-size="10.00" fill="#006400">connect</text>
|
||||
</g>
|
||||
<!-- uRcv -->
|
||||
<g id="node7" class="node">
|
||||
<title>uRcv</title>
|
||||
<polygon fill="none" stroke="#ffa500" points="214.0134,-553.4624 193.0067,-581.6377 150.9933,-581.6377 129.9866,-553.4624 150.9933,-525.2872 193.0067,-525.2872 214.0134,-553.4624"/>
|
||||
<text text-anchor="middle" x="172" y="-556.7624" font-family="arial" font-size="11.00" fill="#000000">user</text>
|
||||
<text text-anchor="middle" x="172" y="-543.5624" font-family="arial" font-size="11.00" fill="#000000">receive</text>
|
||||
</g>
|
||||
<!-- connectClnt->uRcv -->
|
||||
<g id="edge24" class="edge">
|
||||
<title>connectClnt->uRcv</title>
|
||||
<path fill="none" stroke="#ffa500" stroke-dasharray="5,2" d="M237.1238,-971.0203C239.9206,-916.7089 244.541,-752.4513 208,-623.3874 204.8674,-612.323 199.8997,-600.9707 194.6434,-590.7174"/>
|
||||
<polygon fill="#ffa500" stroke="#ffa500" points="189.7794,-581.6499 198.472,-588.3349 192.143,-586.056 194.5065,-590.4621 194.5065,-590.4621 194.5065,-590.4621 192.143,-586.056 190.541,-592.5893 189.7794,-581.6499 189.7794,-581.6499"/>
|
||||
<text text-anchor="middle" x="247.7235" y="-837.5624" font-family="arial" font-size="10.00" fill="#ffa500">race</text>
|
||||
</g>
|
||||
<!-- uSnd -->
|
||||
<g id="node8" class="node">
|
||||
<title>uSnd</title>
|
||||
<polygon fill="none" stroke="#ffa500" points="111.5662,-553.4624 95.2831,-581.6377 62.7169,-581.6377 46.4338,-553.4624 62.7169,-525.2872 95.2831,-525.2872 111.5662,-553.4624"/>
|
||||
<text text-anchor="middle" x="79" y="-556.7624" font-family="arial" font-size="11.00" fill="#000000">user</text>
|
||||
<text text-anchor="middle" x="79" y="-543.5624" font-family="arial" font-size="11.00" fill="#000000">send</text>
|
||||
</g>
|
||||
<!-- connectClnt->uSnd -->
|
||||
<g id="edge25" class="edge">
|
||||
<title>connectClnt->uSnd</title>
|
||||
<path fill="none" stroke="#ffa500" stroke-dasharray="5,2" d="M232.6954,-971.3264C227.2453,-941.3316 216.1632,-879.2768 208,-826.5624 201.0253,-781.5225 212.2627,-655.5799 180,-623.3874 161.268,-604.6963 143.493,-629.327 121,-615.3874 111.1934,-609.31 103.1361,-599.9964 96.7947,-590.4234"/>
|
||||
<polygon fill="#ffa500" stroke="#ffa500" points="91.4524,-581.6443 100.4951,-587.8476 94.0516,-585.9157 96.6509,-590.187 96.6509,-590.187 96.6509,-590.187 94.0516,-585.9157 92.8067,-592.5263 91.4524,-581.6443 91.4524,-581.6443"/>
|
||||
<text text-anchor="middle" x="219.7235" y="-837.5624" font-family="arial" font-size="10.00" fill="#ffa500">race</text>
|
||||
</g>
|
||||
<!-- uAgent -->
|
||||
<g id="node11" class="node">
|
||||
<title>uAgent</title>
|
||||
<polygon fill="none" stroke="#ffa500" points="715.5622,-670.4749 697.7811,-698.6502 662.2189,-698.6502 644.4378,-670.4749 662.2189,-642.2997 697.7811,-642.2997 715.5622,-670.4749"/>
|
||||
<text text-anchor="middle" x="680" y="-673.7749" font-family="arial" font-size="11.00" fill="#000000">user</text>
|
||||
<text text-anchor="middle" x="680" y="-660.5749" font-family="arial" font-size="11.00" fill="#000000">agent</text>
|
||||
</g>
|
||||
<!-- runClnt->uAgent -->
|
||||
<g id="edge26" class="edge">
|
||||
<title>runClnt->uAgent</title>
|
||||
<path fill="none" stroke="#ffa500" stroke-dasharray="5,2" d="M363.0275,-971.304C418.9032,-918.2741 582.8091,-762.7159 650.3686,-698.5972"/>
|
||||
<polygon fill="#ffa500" stroke="#ffa500" points="657.7387,-691.6025 653.5831,-701.7505 654.112,-695.0444 650.4853,-698.4864 650.4853,-698.4864 650.4853,-698.4864 654.112,-695.0444 647.3875,-695.2224 657.7387,-691.6025 657.7387,-691.6025"/>
|
||||
<text text-anchor="middle" x="482.7235" y="-879.5624" font-family="arial" font-size="10.00" fill="#ffa500">race</text>
|
||||
</g>
|
||||
<!-- uProcess -->
|
||||
<g id="node12" class="node">
|
||||
<title>uProcess</title>
|
||||
<polygon fill="none" stroke="#ffa500" points="849.4801,-670.4749 822.24,-698.6502 767.76,-698.6502 740.5199,-670.4749 767.76,-642.2997 822.24,-642.2997 849.4801,-670.4749"/>
|
||||
<text text-anchor="middle" x="795" y="-673.7749" font-family="arial" font-size="11.00" fill="#000000">process</text>
|
||||
<text text-anchor="middle" x="795" y="-660.5749" font-family="arial" font-size="11.00" fill="#000000">responses</text>
|
||||
</g>
|
||||
<!-- runClnt->uProcess -->
|
||||
<g id="edge27" class="edge">
|
||||
<title>runClnt->uProcess</title>
|
||||
<path fill="none" stroke="#ffa500" stroke-dasharray="5,2" d="M375.4171,-984.2599C446.9624,-971.0733 623.6511,-929.4235 725,-826.5624 757.4412,-793.6372 776.3095,-743.0546 786.1177,-708.4539"/>
|
||||
<polygon fill="#ffa500" stroke="#ffa500" points="788.7618,-698.7069 790.4867,-709.5363 787.4527,-703.5325 786.1436,-708.3581 786.1436,-708.3581 786.1436,-708.3581 787.4527,-703.5325 781.8006,-707.1799 788.7618,-698.7069 788.7618,-698.7069"/>
|
||||
<text text-anchor="middle" x="686.7235" y="-879.5624" font-family="arial" font-size="10.00" fill="#ffa500">race</text>
|
||||
</g>
|
||||
<!-- uSock->uRcv -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>uSock->uRcv</title>
|
||||
<path fill="none" stroke="#006400" d="M97.3599,-864.3998C109.8154,-812.5125 145.7537,-662.8 162.808,-591.7545"/>
|
||||
<polygon fill="#006400" stroke="#006400" points="165.1822,-581.8642 167.2236,-592.6383 164.0151,-586.7261 162.848,-591.5879 162.848,-591.5879 162.848,-591.5879 164.0151,-586.7261 158.4723,-590.5375 165.1822,-581.8642 165.1822,-581.8642"/>
|
||||
</g>
|
||||
<!-- uInq -->
|
||||
<g id="node9" class="node">
|
||||
<title>uInq</title>
|
||||
<polygon fill="none" stroke="#000000" points="392.5723,-449.7766 347.4277,-449.7766 347.4277,-414.5733 392.5723,-414.5733 392.5723,-408.5733 410.5723,-432.175 392.5723,-455.7766 392.5723,-449.7766"/>
|
||||
<text text-anchor="middle" x="379" y="-442.075" font-family="arial" font-size="11.00" fill="#000000">user</text>
|
||||
<text text-anchor="middle" x="379" y="-428.875" font-family="arial" font-size="11.00" fill="#000000">receive</text>
|
||||
<text text-anchor="middle" x="379" y="-415.675" font-family="arial" font-size="11.00" fill="#000000">TBQueue</text>
|
||||
</g>
|
||||
<!-- uRcv->uInq -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>uRcv->uInq</title>
|
||||
<path fill="none" stroke="#006400" d="M207.752,-544.8492C243.6388,-534.9021 299.1678,-515.8485 339,-485.175 346.5794,-479.3383 353.4481,-471.7165 359.2704,-464.0802"/>
|
||||
<polygon fill="#006400" stroke="#006400" points="365.1617,-455.8721 362.9865,-466.6201 362.2462,-459.9341 359.3307,-463.9961 359.3307,-463.9961 359.3307,-463.9961 362.2462,-459.9341 355.6749,-461.3722 365.1617,-455.8721 365.1617,-455.8721"/>
|
||||
</g>
|
||||
<!-- uOutq -->
|
||||
<g id="node10" class="node">
|
||||
<title>uOutq</title>
|
||||
<polygon fill="none" stroke="#000000" points="329.5723,-449.7766 284.4277,-449.7766 284.4277,-455.7766 266.4277,-432.175 284.4277,-408.5733 284.4277,-414.5733 329.5723,-414.5733 329.5723,-449.7766"/>
|
||||
<text text-anchor="middle" x="298" y="-442.075" font-family="arial" font-size="11.00" fill="#000000">user</text>
|
||||
<text text-anchor="middle" x="298" y="-428.875" font-family="arial" font-size="11.00" fill="#000000">send</text>
|
||||
<text text-anchor="middle" x="298" y="-415.675" font-family="arial" font-size="11.00" fill="#000000">TBQueue</text>
|
||||
</g>
|
||||
<!-- uRcv->uOutq -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>uRcv->uOutq</title>
|
||||
<path fill="none" stroke="#00ff00" d="M196.5932,-529.7891C216.4531,-510.6719 244.5472,-483.6285 266.1304,-462.8527"/>
|
||||
<polygon fill="#00ff00" stroke="#00ff00" points="273.4688,-455.7886 269.3851,-465.9658 269.8666,-459.2562 266.2643,-462.7237 266.2643,-462.7237 266.2643,-462.7237 269.8666,-459.2562 263.1435,-459.4817 273.4688,-455.7886 273.4688,-455.7886"/>
|
||||
</g>
|
||||
<!-- uSnd->uSock -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>uSnd->uSock</title>
|
||||
<path fill="none" stroke="#006400" d="M80.2082,-581.8642C82.8558,-644.1008 89.1434,-791.9036 91.7913,-854.15"/>
|
||||
<polygon fill="#006400" stroke="#006400" points="92.2274,-864.3998 87.3063,-854.6001 92.0148,-859.4043 91.8022,-854.4088 91.8022,-854.4088 91.8022,-854.4088 92.0148,-859.4043 96.2982,-854.2175 92.2274,-864.3998 92.2274,-864.3998"/>
|
||||
</g>
|
||||
<!-- uInq->uAgent -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>uInq->uAgent</title>
|
||||
<path fill="none" stroke="#006400" d="M396.6501,-455.9955C404.2553,-465.5484 413.5612,-476.3588 423,-485.175 495.6201,-553.0045 593.7284,-617.3782 645.1887,-649.3958"/>
|
||||
<polygon fill="#006400" stroke="#006400" points="653.9378,-654.8067 643.0659,-653.374 649.6853,-652.1767 645.4329,-649.5468 645.4329,-649.5468 645.4329,-649.5468 649.6853,-652.1767 647.7999,-645.7196 653.9378,-654.8067 653.9378,-654.8067"/>
|
||||
</g>
|
||||
<!-- uOutq->uSnd -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>uOutq->uSnd</title>
|
||||
<path fill="none" stroke="#006400" d="M266.3235,-441.7172C229.3231,-453.7931 167.2864,-476.9926 121,-509.375 115.6566,-513.1132 110.4497,-517.6098 105.6237,-522.2822"/>
|
||||
<polygon fill="#006400" stroke="#006400" points="98.3887,-529.6856 102.1596,-519.3885 101.8833,-526.1096 105.378,-522.5337 105.378,-522.5337 105.378,-522.5337 101.8833,-526.1096 108.5964,-525.6789 98.3887,-529.6856 98.3887,-529.6856"/>
|
||||
</g>
|
||||
<!-- uAgent->connectionsStore -->
|
||||
<g id="edge21" class="edge">
|
||||
<title>uAgent->connectionsStore</title>
|
||||
<path fill="none" stroke="#880000" d="M711.4852,-644.5965C717.9336,-640.4916 724.9231,-636.8075 732,-634.3874 772.2162,-620.6351 884.2913,-641.9405 923,-624.3874 937.8878,-617.6364 951.2411,-606.1048 962.1439,-594.2794"/>
|
||||
<polygon fill="#880000" stroke="#880000" points="703.07,-650.3759 708.7656,-641.0051 707.1916,-647.5452 711.3132,-644.7146 711.3132,-644.7146 711.3132,-644.7146 707.1916,-647.5452 713.8608,-648.424 703.07,-650.3759 703.07,-650.3759"/>
|
||||
<polygon fill="#880000" stroke="#880000" points="968.9461,-586.5133 965.7424,-597.0007 965.6517,-590.2745 962.3573,-594.0358 962.3573,-594.0358 962.3573,-594.0358 965.6517,-590.2745 958.9722,-591.0708 968.9461,-586.5133 968.9461,-586.5133"/>
|
||||
</g>
|
||||
<!-- uAgent->uOutq -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>uAgent->uOutq</title>
|
||||
<path fill="none" stroke="#00ff00" d="M647.3434,-665.3697C591.4951,-656.2129 480.4257,-636.0193 447,-615.3874 386.1848,-577.8496 339.1057,-506.4848 315.2537,-464.7649"/>
|
||||
<polygon fill="#00ff00" stroke="#00ff00" points="310.2445,-455.8352 319.0616,-462.3552 312.6907,-460.196 315.1369,-464.5567 315.1369,-464.5567 315.1369,-464.5567 312.6907,-460.196 311.2123,-466.7583 310.2445,-455.8352 310.2445,-455.8352"/>
|
||||
</g>
|
||||
<!-- runClient -->
|
||||
<g id="node14" class="node">
|
||||
<title>runClient</title>
|
||||
<polygon fill="none" stroke="#ffa500" points="722.7244,-432.175 701.3622,-450.175 658.6378,-450.175 637.2756,-432.175 658.6378,-414.175 701.3622,-414.175 722.7244,-432.175"/>
|
||||
<text text-anchor="middle" x="680" y="-428.875" font-family="arial" font-size="11.00" fill="#000000">runClient</text>
|
||||
</g>
|
||||
<!-- uAgent->runClient -->
|
||||
<g id="edge30" class="edge">
|
||||
<title>uAgent->runClient</title>
|
||||
<path fill="none" stroke="#ffa500" stroke-dasharray="5,2" d="M680,-642.203C680,-596.3198 680,-506.1699 680,-460.3495"/>
|
||||
<polygon fill="#ffa500" stroke="#ffa500" points="680,-450.2297 684.5001,-460.2297 680,-455.2297 680.0001,-460.2297 680.0001,-460.2297 680.0001,-460.2297 680,-455.2297 675.5001,-460.2298 680,-450.2297 680,-450.2297"/>
|
||||
<text text-anchor="middle" x="688.3335" y="-550.4624" font-family="arial" font-size="10.00" fill="#ffa500">fork</text>
|
||||
</g>
|
||||
<!-- sOutq -->
|
||||
<g id="node15" class="node">
|
||||
<title>sOutq</title>
|
||||
<polygon fill="none" stroke="#000000" points="785.5723,-444.175 740.4277,-444.175 740.4277,-420.175 785.5723,-420.175 785.5723,-414.175 803.5723,-432.175 785.5723,-450.175 785.5723,-444.175"/>
|
||||
<text text-anchor="middle" x="772" y="-435.475" font-family="arial" font-size="11.00" fill="#000000">srv send</text>
|
||||
<text text-anchor="middle" x="772" y="-422.275" font-family="arial" font-size="11.00" fill="#000000">TBQueue</text>
|
||||
</g>
|
||||
<!-- uAgent->sOutq -->
|
||||
<g id="edge14" class="edge">
|
||||
<title>uAgent->sOutq</title>
|
||||
<path fill="none" stroke="#0000ff" d="M691.5879,-642.1215C694.8569,-633.6035 698.2645,-624.1836 701,-615.3874 716.6229,-565.151 708.4801,-548.8178 729,-500.375 735.1504,-485.8551 744.2149,-470.9073 752.4376,-458.7199"/>
|
||||
<polygon fill="#0000ff" stroke="#0000ff" points="758.2226,-450.3792 756.2211,-461.1608 755.373,-454.4877 752.5234,-458.5962 752.5234,-458.5962 752.5234,-458.5962 755.373,-454.4877 748.8257,-456.0316 758.2226,-450.3792 758.2226,-450.3792"/>
|
||||
</g>
|
||||
<!-- userState -->
|
||||
<g id="node19" class="node">
|
||||
<title>userState</title>
|
||||
<polygon fill="none" stroke="#000000" points="890.7837,-482.5758 887.7837,-486.5758 866.7837,-486.5758 863.7837,-482.5758 821.2163,-482.5758 821.2163,-381.7742 890.7837,-381.7742 890.7837,-482.5758"/>
|
||||
<text text-anchor="middle" x="856" y="-468.475" font-family="arial" font-size="11.00" fill="#000000">connected</text>
|
||||
<text text-anchor="middle" x="856" y="-455.275" font-family="arial" font-size="11.00" fill="#000000">servers,</text>
|
||||
<text text-anchor="middle" x="856" y="-442.075" font-family="arial" font-size="11.00" fill="#000000">subscribed</text>
|
||||
<text text-anchor="middle" x="856" y="-428.875" font-family="arial" font-size="11.00" fill="#000000">queues,</text>
|
||||
<text text-anchor="middle" x="856" y="-415.675" font-family="arial" font-size="11.00" fill="#000000">sent</text>
|
||||
<text text-anchor="middle" x="856" y="-402.475" font-family="arial" font-size="11.00" fill="#000000">commands</text>
|
||||
<text text-anchor="middle" x="856" y="-389.275" font-family="arial" font-size="11.00" fill="#000000">(STM)</text>
|
||||
</g>
|
||||
<!-- uAgent->userState -->
|
||||
<g id="edge19" class="edge">
|
||||
<title>uAgent->userState</title>
|
||||
<path fill="none" stroke="#ff8888" d="M705.4839,-635.9704C734.1546,-597.1508 781.1323,-533.5442 814.9848,-487.7086"/>
|
||||
<polygon fill="#ff8888" stroke="#ff8888" points="699.5075,-644.0622 701.8288,-633.3448 702.478,-640.0402 705.4485,-636.0182 705.4485,-636.0182 705.4485,-636.0182 702.478,-640.0402 709.0683,-638.6917 699.5075,-644.0622 699.5075,-644.0622"/>
|
||||
<polygon fill="#ff8888" stroke="#ff8888" points="820.9337,-479.654 818.6124,-490.3714 817.9632,-483.6759 814.9927,-487.6979 814.9927,-487.6979 814.9927,-487.6979 817.9632,-483.6759 811.3729,-485.0245 820.9337,-479.654 820.9337,-479.654"/>
|
||||
</g>
|
||||
<!-- uProcess->connectionsStore -->
|
||||
<g id="edge22" class="edge">
|
||||
<title>uProcess->connectionsStore</title>
|
||||
<path fill="none" stroke="#880000" d="M851.5097,-660.0083C892.5279,-651.4468 943.5859,-638.4864 960,-624.3874 968.7086,-616.9071 975.1618,-606.5771 979.8973,-596.1137"/>
|
||||
<polygon fill="#880000" stroke="#880000" points="841.5933,-662.0345 850.49,-655.6236 846.4921,-661.0335 851.3909,-660.0325 851.3909,-660.0325 851.3909,-660.0325 846.4921,-661.0335 852.2918,-664.4414 841.5933,-662.0345 841.5933,-662.0345"/>
|
||||
<polygon fill="#880000" stroke="#880000" points="983.7787,-586.5449 984.1898,-597.5031 981.8992,-591.1782 980.0198,-595.8116 980.0198,-595.8116 980.0198,-595.8116 981.8992,-591.1782 975.8498,-594.12 983.7787,-586.5449 983.7787,-586.5449"/>
|
||||
</g>
|
||||
<!-- uProcess->uOutq -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>uProcess->uOutq</title>
|
||||
<path fill="none" stroke="#006400" d="M782.4156,-642.109C763.1961,-600.1829 726.1195,-524.9349 701,-509.375 632.2716,-466.8022 409.1254,-523.6095 338,-485.175 329.0427,-480.3347 321.505,-472.5258 315.4734,-464.3728"/>
|
||||
<polygon fill="#006400" stroke="#006400" points="309.7367,-455.862 319.0575,-461.639 312.5314,-460.0081 315.326,-464.1542 315.326,-464.1542 315.326,-464.1542 312.5314,-460.0081 311.5946,-466.6694 309.7367,-455.862 309.7367,-455.862"/>
|
||||
</g>
|
||||
<!-- uProcess->sOutq -->
|
||||
<g id="edge15" class="edge">
|
||||
<title>uProcess->sOutq</title>
|
||||
<path fill="none" stroke="#0000ff" d="M792.2713,-642.203C787.8428,-596.3198 779.1418,-506.1699 774.7193,-460.3495"/>
|
||||
<polygon fill="#0000ff" stroke="#0000ff" points="773.7426,-450.2297 779.1826,-459.7511 774.223,-455.2066 774.7034,-460.1835 774.7034,-460.1835 774.7034,-460.1835 774.223,-455.2066 770.2242,-460.6158 773.7426,-450.2297 773.7426,-450.2297"/>
|
||||
</g>
|
||||
<!-- uProcess->userState -->
|
||||
<g id="edge20" class="edge">
|
||||
<title>uProcess->userState</title>
|
||||
<path fill="none" stroke="#ff8888" d="M804.7422,-632.4164C814.4107,-594.6461 829.3748,-536.188 840.6433,-492.1666"/>
|
||||
<polygon fill="#ff8888" stroke="#ff8888" points="802.237,-642.203 800.3575,-631.3994 803.477,-637.3592 804.717,-632.5154 804.717,-632.5154 804.717,-632.5154 803.477,-637.3592 809.0764,-633.6313 802.237,-642.203 802.237,-642.203"/>
|
||||
<polygon fill="#ff8888" stroke="#ff8888" points="843.1328,-482.4415 845.0123,-493.2451 841.8928,-487.2854 840.6529,-492.1292 840.6529,-492.1292 840.6529,-492.1292 841.8928,-487.2854 836.2934,-491.0132 843.1328,-482.4415 843.1328,-482.4415"/>
|
||||
</g>
|
||||
<!-- uRespq -->
|
||||
<g id="node13" class="node">
|
||||
<title>uRespq</title>
|
||||
<polygon fill="none" stroke="#000000" points="744.5723,-57.4017 699.4277,-57.4017 699.4277,-22.1983 744.5723,-22.1983 744.5723,-16.1983 762.5723,-39.8 744.5723,-63.4017 744.5723,-57.4017"/>
|
||||
<text text-anchor="middle" x="731" y="-49.7" font-family="arial" font-size="11.00" fill="#000000">user</text>
|
||||
<text text-anchor="middle" x="731" y="-36.5" font-family="arial" font-size="11.00" fill="#000000">SMP</text>
|
||||
<text text-anchor="middle" x="731" y="-23.3" font-family="arial" font-size="11.00" fill="#000000">TBQueue</text>
|
||||
</g>
|
||||
<!-- uRespq->uProcess -->
|
||||
<g id="edge29" class="edge">
|
||||
<title>uRespq->uProcess</title>
|
||||
<path fill="none" stroke="#000000" d="M762.5896,-44.0035C813.9769,-51.7359 910,-70.3048 910,-101.1 910,-553.4624 910,-553.4624 910,-553.4624 910,-594.8589 872.935,-626.9659 840.89,-646.9888"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="832.0197,-652.3135 838.2775,-643.3085 836.3067,-649.7401 840.5936,-647.1667 840.5936,-647.1667 840.5936,-647.1667 836.3067,-649.7401 842.9097,-651.0249 832.0197,-652.3135 832.0197,-652.3135"/>
|
||||
</g>
|
||||
<!-- sAgent -->
|
||||
<g id="node17" class="node">
|
||||
<title>sAgent</title>
|
||||
<polygon fill="none" stroke="#ffa500" points="656.0134,-172.6875 635.0067,-200.8627 592.9933,-200.8627 571.9866,-172.6875 592.9933,-144.5122 635.0067,-144.5122 656.0134,-172.6875"/>
|
||||
<text text-anchor="middle" x="614" y="-175.9875" font-family="arial" font-size="11.00" fill="#000000">server</text>
|
||||
<text text-anchor="middle" x="614" y="-162.7875" font-family="arial" font-size="11.00" fill="#000000">receive</text>
|
||||
</g>
|
||||
<!-- runClient->sAgent -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>runClient->sAgent</title>
|
||||
<path fill="none" stroke="#ffa500" stroke-dasharray="5,2" d="M663.5617,-414.1714C650.0082,-397.4105 633,-371.0774 633,-344.475 633,-344.475 633,-344.475 633,-257.475 633,-242.0587 629.9721,-225.439 626.3339,-211.0334"/>
|
||||
<polygon fill="#ffa500" stroke="#ffa500" points="623.6113,-200.9841 630.5697,-209.4594 624.9188,-205.8101 626.2263,-210.6361 626.2263,-210.6361 626.2263,-210.6361 624.9188,-205.8101 621.8829,-211.8129 623.6113,-200.9841 623.6113,-200.9841"/>
|
||||
<text text-anchor="middle" x="642.7235" y="-297.975" font-family="arial" font-size="10.00" fill="#ffa500">race</text>
|
||||
</g>
|
||||
<!-- sSnd -->
|
||||
<g id="node18" class="node">
|
||||
<title>sSnd</title>
|
||||
<polygon fill="none" stroke="#ffa500" points="750.0217,-172.6875 731.0109,-200.8627 692.9891,-200.8627 673.9783,-172.6875 692.9891,-144.5122 731.0109,-144.5122 750.0217,-172.6875"/>
|
||||
<text text-anchor="middle" x="712" y="-175.9875" font-family="arial" font-size="11.00" fill="#000000">server</text>
|
||||
<text text-anchor="middle" x="712" y="-162.7875" font-family="arial" font-size="11.00" fill="#000000">send</text>
|
||||
</g>
|
||||
<!-- runClient->sSnd -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>runClient->sSnd</title>
|
||||
<path fill="none" stroke="#ffa500" stroke-dasharray="5,2" d="M684.2567,-414.1504C688.048,-396.5013 693,-368.7944 693,-344.475 693,-344.475 693,-344.475 693,-257.475 693,-242.0587 696.0279,-225.439 699.6661,-211.0334"/>
|
||||
<polygon fill="#ffa500" stroke="#ffa500" points="702.3887,-200.9841 704.1171,-211.8129 701.0812,-205.8101 699.7737,-210.6361 699.7737,-210.6361 699.7737,-210.6361 701.0812,-205.8101 695.4303,-209.4594 702.3887,-200.9841 702.3887,-200.9841"/>
|
||||
<text text-anchor="middle" x="702.7235" y="-297.975" font-family="arial" font-size="10.00" fill="#ffa500">race</text>
|
||||
</g>
|
||||
<!-- sOutq->sSnd -->
|
||||
<g id="edge16" class="edge">
|
||||
<title>sOutq->sSnd</title>
|
||||
<path fill="none" stroke="#0000ff" d="M767.4159,-414.1688C763.3329,-396.5332 758,-368.8362 758,-344.475 758,-344.475 758,-344.475 758,-257.475 758,-238.8558 749.3238,-220.4171 739.5075,-205.5139"/>
|
||||
<polygon fill="#0000ff" stroke="#0000ff" points="733.653,-197.1731 743.0814,-202.7728 736.5256,-201.2656 739.3981,-205.3581 739.3981,-205.3581 739.3981,-205.3581 736.5256,-201.2656 735.7149,-207.9434 733.653,-197.1731 733.653,-197.1731"/>
|
||||
</g>
|
||||
<!-- sSock -->
|
||||
<g id="node16" class="node">
|
||||
<title>sSock</title>
|
||||
<polygon fill="none" stroke="#0000ff" points="619.2006,-450.175 434.7994,-450.175 434.7994,-414.175 619.2006,-414.175 619.2006,-450.175"/>
|
||||
<text text-anchor="middle" x="527" y="-428.875" font-family="arial" font-size="11.00" fill="#000000">SMP client connection TCP socket</text>
|
||||
</g>
|
||||
<!-- sSock->sAgent -->
|
||||
<g id="edge18" class="edge">
|
||||
<title>sSock->sAgent</title>
|
||||
<path fill="none" stroke="#0000ff" d="M533.3267,-413.9457C538.8538,-396.3776 546,-368.9339 546,-344.475 546,-344.475 546,-344.475 546,-257.475 546,-234.2863 561.6699,-213.8223 577.9653,-198.8106"/>
|
||||
<polygon fill="#0000ff" stroke="#0000ff" points="585.9492,-191.8912 581.3395,-201.8412 582.1708,-195.1659 578.3923,-198.4406 578.3923,-198.4406 578.3923,-198.4406 582.1708,-195.1659 575.4451,-195.04 585.9492,-191.8912 585.9492,-191.8912"/>
|
||||
</g>
|
||||
<!-- sAgent->uRespq -->
|
||||
<g id="edge28" class="edge">
|
||||
<title>sAgent->uRespq</title>
|
||||
<path fill="none" stroke="#000000" d="M636.8365,-146.75C655.8728,-125.1288 683.0563,-94.254 703.3269,-71.2308"/>
|
||||
<polygon fill="#000000" stroke="#000000" points="710.1839,-63.4427 706.9532,-73.9219 706.8798,-67.1955 703.5757,-70.9482 703.5757,-70.9482 703.5757,-70.9482 706.8798,-67.1955 700.1982,-67.9746 710.1839,-63.4427 710.1839,-63.4427"/>
|
||||
</g>
|
||||
<!-- sSnd->sSock -->
|
||||
<g id="edge17" class="edge">
|
||||
<title>sSnd->sSock</title>
|
||||
<path fill="none" stroke="#0000ff" d="M693.7955,-200.8992C685.9738,-211.2174 676.0767,-222.2439 665,-229.975 634.5137,-251.2531 586,-220.2974 586,-257.475 586,-344.475 586,-344.475 586,-344.475 586,-368.2876 570.8403,-390.4932 555.8897,-406.6024"/>
|
||||
<polygon fill="#0000ff" stroke="#0000ff" points="548.6344,-414.0012 552.4229,-403.7105 552.1352,-410.4312 555.6359,-406.8612 555.6359,-406.8612 555.6359,-406.8612 552.1352,-410.4312 558.8489,-410.0118 548.6344,-414.0012 548.6344,-414.0012"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 506.94 131.5"><path d="m109.65 58.21-18.21-10.07-15.37 9.38-.19 9.59-7.47-4.92-10.16 6.19-.44-10.48-10.48-7 10-5.17c-.08 0 0 1-1.48-34.34l-23.65-11.39-32.2 10 7.19 34.63 10.81 8.37-8.22 3.9 5.37 26.1 7.55 7.09-5.4 3.29 4.19 20.18 16.94 17.94c.08-.1 1.78-1.41 21.79-17.32l-.58-13.71 8.58 7.28c.12-.12 1.84-1.39 17.56-13.89l.61-10.14 6.48 4.5c.13-.12 1.58-1.22 14.26-11.33z" fill="#231f20"/><path d="m94.54 69 15.11-10.79-18.21-10.07-15.37 9.38z"/><path d="m92.87 88.2 1.67-19.2-18.47-11.48-.38 18.75z" fill="#004b16"/><path d="m68.22 107.73v-19.89l-18.66-14.15 1.47 19.54z" fill="#004b16"/><path d="m68.22 87.84 18.74-13.43-18.55-12.22-18.85 11.5z"/><path d="m38.43 131.48-2.98-20.32-18.15-17.8 4.19 20.18z" fill="#004b16"/><path d="m35.45 111.16 23.91-17.06-18.53-15.09-23.53 14.35z"/><path d="m33.9 100.6-3.94-26.88-20.22-16.81 5.41 26.07z" fill="#004b16"/><path d="m29.96 73.72 27.85-15.82-20.8-13.94-27.27 12.95z"/><path d="m28.07 60.88-5.4-36.81-22.67-14.06 7.19 34.62z" fill="#004b16"/><path d="m22.67 24.07 33.14-12.7-23.61-11.37-32.2 10.01z"/><g fill="#1cb35c"><path d="m107.13 76.87c-14.02 11.13-14.33 11.33-14.26 11.33 1.74-20.1 1.59-19.2 1.67-19.2 16-11.45 15-10.79 15.11-10.79z"/><path d="m85.78 93.84c-17.35 13.8-17.63 13.89-17.56 13.89-.17-20.82-.07-19.89 0-19.89 20-14.3 18.67-13.43 18.74-13.43z"/><path d="m60.22 114.16c-21.66 17.22-21.86 17.32-21.79 17.32-3.07-20.94-3-20.32-3-20.32 25.47-18.16 23.86-17.06 23.93-17.06z"/><path d="m55.81 11.37c1.52 35.37 1.4 34.34 1.48 34.34-28.66 14.89-29.29 15.17-29.22 15.17-5.52-37.63-5.47-36.81-5.4-36.81z"/><path d="m57.81 57.9c1.15 26.81 1 25.88 1.11 25.88-24.81 16.67-25.09 16.82-25 16.82-4-27.58-4-26.88-3.94-26.88z"/></g><path xmlns="http://www.w3.org/2000/svg" d="m151.61 14.24 16.58-4v79.88q0 13.13 7.83 15.65-3.84 7.31-13.13 7.3-11.28 0-11.28-15.66z"/><path d="m186.66 111.72v-57.42h-9.08v-13.6h25.86v71zm8.56-98.54a9.62 9.62 0 1 1 -9.62 9.62 9.63 9.63 0 0 1 9.62-9.62z"/><path d="m262.94 111.74v-41.06c0-6.05-1.16-10.48-3.48-13.26s-6.11-4.18-11.37-4.18a17.74 17.74 0 0 0 -7.8 2.06 18 18 0 0 0 -6.46 5.1v51.34h-16.59v-71h11.94l3 6.64q6.76-8 20-8 12.66 0 20 7.59t7.33 21.19v43.58z"/><path d="m288.42 76.06q0-16.26 9.38-26.47t24.77-10.21q16.19 0 25.14 9.82t9 26.86q0 17-9.12 27t-25 10q-16.18 0-25.17-10.12t-9-26.88zm17.24 0q0 23.47 16.91 23.48a14.54 14.54 0 0 0 12.31-6.11q4.55-6.09 4.54-17.37 0-23.14-16.85-23.15a14.61 14.61 0 0 0 -12.33 6.09q-4.58 6.11-4.58 17.06z"/><path d="m412.37 111.74v-4.31c-1.37 1.5-3.71 2.82-7 3.95a31.33 31.33 0 0 1 -10.15 1.69q-14.87 0-23.38-9.42t-8.52-26.27q0-16.84 9.78-27.42a32 32 0 0 1 24.51-10.58 32.37 32.37 0 0 1 14.72 3.32v-28.46l16.58-4v101.5zm0-54.06a17.6 17.6 0 0 0 -11.07-4.24q-10 0-15.33 6.07t-5.37 17.41q0 22.15 21.36 22.15a16.11 16.11 0 0 0 5.87-1.42c2.32-1 3.83-1.92 4.54-2.89z"/><path d="m505.55 81.3h-50.74q.46 8.49 5.83 13.2t14.46 4.7q11.34 0 17.25-5.9l6.43 12.7q-8.75 7.09-26.13 7.1-16.25 0-25.7-9.52t-9.45-26.58q0-16.78 10.38-27.2a33.91 33.91 0 0 1 24.9-10.41q15.45 0 24.81 9.22t9.35 23.48a46.28 46.28 0 0 1 -1.39 9.21zm-50.15-12.47h34.89q-1.73-15.58-17.24-15.59-14.2 0-17.65 15.59z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# Links to scripts/configs
|
||||
bin="https://github.com/simplex-chat/simplexmq/releases/latest/download"
|
||||
bin_smp="$bin/smp-server-ubuntu-20_04-x86-64"
|
||||
bin_xftp="$bin/xftp-server-ubuntu-20_04-x86-64"
|
||||
|
||||
scripts="https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/scripts/main"
|
||||
scripts_systemd_smp="$scripts/smp-server.service"
|
||||
scripts_systemd_xftp="$scripts/xftp-server.service"
|
||||
scripts_update="$scripts/simplex-servers-update"
|
||||
scripts_uninstall="$scripts/simplex-servers-uninstall"
|
||||
scripts_stopscript="$scripts/simplex-servers-stopscript"
|
||||
|
||||
# Default installation paths
|
||||
path_bin="/usr/local/bin"
|
||||
path_bin_smp="$path_bin/smp-server"
|
||||
path_bin_xftp="$path_bin/xftp-server"
|
||||
path_bin_update="$path_bin/simplex-servers-update"
|
||||
path_bin_uninstall="$path_bin/simplex-servers-uninstall"
|
||||
path_bin_stopscript="$path_bin/simplex-servers-stopscript"
|
||||
|
||||
path_conf_etc="/etc/opt"
|
||||
path_conf_var="/var/opt"
|
||||
path_conf_smp="$path_conf_etc/simplex $path_conf_var/simplex"
|
||||
path_conf_xftp="$path_conf_etc/simplex-xftp $path_conf_var/simplex-xftp /srv/xftp"
|
||||
|
||||
path_systemd="/etc/systemd/system"
|
||||
path_systemd_smp="$path_systemd/smp-server.service"
|
||||
path_systemd_xftp="$path_systemd/xftp-server.service"
|
||||
|
||||
# Defaut users
|
||||
user_smp="smp"
|
||||
user_xftp="xftp"
|
||||
|
||||
GRN='\033[0;32m'
|
||||
BLU='\033[1;34m'
|
||||
YLW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
logo='
|
||||
____ _ _ __ __
|
||||
/ ___|(_)_ __ ___ _ __ | | ___\ \/ /
|
||||
\___ \| | '"'"'_ ` _ \| '"'"'_ \| |/ _ \\ /
|
||||
___) | | | | | | | |_) | | __// \
|
||||
|____/|_|_| |_| |_| .__/|_|\___/_/\_\
|
||||
|_|
|
||||
'
|
||||
|
||||
welcome="Welcome to SMP/XFTP installation script! Here's what we're going to do:
|
||||
${GRN}1.${NC} Install latest binaries from GitHub releases:
|
||||
- smp: ${YLW}${path_bin_smp}${NC}
|
||||
- xftp: ${YLW}${path_bin_xftp}${NC}
|
||||
${GRN}2.${NC} Create server directories:
|
||||
- smp: ${YLW}${path_conf_smp}${NC}
|
||||
- xftp: ${YLW}${path_conf_xftp}${NC}
|
||||
${GRN}3.${NC} Setup user for each server:
|
||||
- xmp: ${YLW}${user_smp}${NC}
|
||||
- xftp: ${YLW}${user_xftp}${NC}
|
||||
${GRN}4.${NC} Create systemd services:
|
||||
- smp: ${YLW}${path_systemd_smp}${NC}
|
||||
- xftp: ${YLW}${path_systemd_xftp}${NC}
|
||||
${GRN}5.${NC} Install stopscript (systemd), update and uninstallation script:
|
||||
- all: ${YLW}${path_bin_update}${NC}, ${YLW}${path_bin_uninstall}${NC}, ${YLW}${path_bin_stopscript}${NC}
|
||||
|
||||
Press ${GRN}ENTER${NC} to continue or ${RED}Ctrl+C${NC} to cancel installation"
|
||||
|
||||
end="Installtion is complete!
|
||||
|
||||
Please checkout our server guides:
|
||||
- smp: ${GRN}https://simplex.chat/docs/server.html${NC}
|
||||
- xftp: ${GRN}https://simplex.chat/docs/xftp-server.html${NC}
|
||||
|
||||
To uninstall with full clean-up, simply run: ${YLW}sudo /usr/local/bin/simplex-servers-uninstall${NC}
|
||||
"
|
||||
|
||||
setup_bins() {
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_smp" -o "$path_bin_smp" && chmod +x "$path_bin_smp"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_xftp" -o "$path_bin_xftp" && chmod +x "$path_bin_xftp"
|
||||
}
|
||||
|
||||
setup_users() {
|
||||
useradd -M "$user_smp" 2> /dev/null || true
|
||||
useradd -M "$user_xftp" 2> /dev/null || true
|
||||
}
|
||||
|
||||
setup_dirs() {
|
||||
# Unquoted varibles, so field splitting can occur
|
||||
mkdir -p $path_conf_smp
|
||||
chown "$user_smp":"$user_smp" $path_conf_smp
|
||||
mkdir -p $path_conf_xftp
|
||||
chown "$user_xftp":"$user_xftp" $path_conf_xftp
|
||||
}
|
||||
|
||||
setup_systemd() {
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd_smp" -o "$path_systemd_smp"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd_xftp" -o "$path_systemd_xftp"
|
||||
}
|
||||
|
||||
setup_scripts() {
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_update" -o "$path_bin_update" && chmod +x "$path_bin_update"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_uninstall" -o "$path_bin_uninstall" && chmod +x "$path_bin_uninstall"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_stopscript" -o "$path_bin_stopscript" && chmod +x "$path_bin_stopscript"
|
||||
}
|
||||
|
||||
checks() {
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
printf "This script is intended to be run with root privileges. Please re-run script using sudo."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
checks
|
||||
|
||||
printf "%b\n%b\n" "${BLU}$logo${NC}" "$welcome"
|
||||
read ans
|
||||
|
||||
printf "Installing binaries..."
|
||||
setup_bins
|
||||
printf "${GRN} Done!${NC}\n"
|
||||
|
||||
printf "Creating users..."
|
||||
setup_users
|
||||
printf "${GRN} Done!${NC}\n"
|
||||
|
||||
printf "Creating directories..."
|
||||
setup_dirs
|
||||
printf "${GRN} Done!${NC}\n"
|
||||
|
||||
printf "Creating systemd services..."
|
||||
setup_systemd
|
||||
printf "${GRN} Done!${NC}\n"
|
||||
|
||||
printf "Installing stopscript, update and uninstallation script..."
|
||||
setup_scripts
|
||||
printf "${GRN} Done!${NC}\n"
|
||||
|
||||
printf "%b" "$end"
|
||||
}
|
||||
|
||||
main
|
||||
@@ -1,40 +1,90 @@
|
||||
name: simplex-messaging
|
||||
version: 0.1.0.0
|
||||
#synopsis:
|
||||
#description:
|
||||
homepage: https://github.com/simplex-chat/simplex-messaging#readme
|
||||
name: simplexmq
|
||||
version: 5.2.0
|
||||
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: Evgeny Poberezkin
|
||||
maintainer: evgeny@poberezkin.com
|
||||
copyright: 2020 Evgeny Poberezkin
|
||||
category: Web, System, Services, Cryptography
|
||||
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
|
||||
|
||||
dependencies:
|
||||
- aeson == 2.0.*
|
||||
- ansi-terminal >= 0.10 && < 0.12
|
||||
- asn1-encoding == 0.9.*
|
||||
- asn1-types == 0.3.*
|
||||
- async == 2.2.*
|
||||
- attoparsec == 0.13.*
|
||||
- base >= 4.7 && < 5
|
||||
- attoparsec == 0.14.*
|
||||
- base >= 4.14 && < 5
|
||||
- base64-bytestring >= 1.0 && < 1.3
|
||||
- bytestring == 0.10.*
|
||||
- containers
|
||||
- cryptonite == 0.26.*
|
||||
- case-insensitive == 1.2.*
|
||||
- composition == 1.0.*
|
||||
- constraints >= 0.12 && < 0.14
|
||||
- containers == 0.6.*
|
||||
- cryptonite >= 0.27 && < 0.30
|
||||
- cryptostore == 0.2.*
|
||||
- data-default == 0.7.*
|
||||
- direct-sqlcipher == 2.3.*
|
||||
- directory == 1.3.*
|
||||
- filepath == 1.4.*
|
||||
- http-types == 0.12.*
|
||||
- http2 == 4.1.*
|
||||
- generic-random >= 1.3 && < 1.5
|
||||
- ini == 0.4.1
|
||||
- iproute == 1.7.*
|
||||
- iso8601-time == 0.1.*
|
||||
- memory == 0.15.*
|
||||
- mtl
|
||||
- network == 3.1.*
|
||||
- network-transport == 0.5.*
|
||||
- mtl == 2.2.*
|
||||
- network >= 3.1.2.7 && < 3.2
|
||||
- network-transport == 0.5.4
|
||||
- optparse-applicative >= 0.15 && < 0.17
|
||||
- QuickCheck == 2.14.*
|
||||
- process == 1.6.*
|
||||
- random >= 1.1 && < 1.3
|
||||
- simple-logger == 0.1.*
|
||||
- sqlite-simple == 0.4.*
|
||||
- stm
|
||||
- template-haskell == 2.15.*
|
||||
- socks == 0.6.*
|
||||
- sqlcipher-simple == 0.4.*
|
||||
- stm == 2.5.*
|
||||
- template-haskell == 2.16.*
|
||||
- temporary == 1.3.*
|
||||
- text == 1.2.*
|
||||
- time == 1.9.*
|
||||
- time-compat == 1.9.*
|
||||
- time-manager == 0.0.*
|
||||
- tls >= 1.6.0 && < 1.7
|
||||
- transformers == 0.5.*
|
||||
- unliftio == 0.2.*
|
||||
- unliftio-core == 0.1.*
|
||||
- unliftio-core == 0.2.*
|
||||
- websockets == 0.12.*
|
||||
- x509 == 1.7.*
|
||||
- x509-store == 1.6.*
|
||||
- x509-validation == 1.6.*
|
||||
- yaml == 0.11.*
|
||||
|
||||
flags:
|
||||
swift:
|
||||
description: Enable swift JSON format
|
||||
manual: True
|
||||
default: False
|
||||
|
||||
when:
|
||||
- condition: flag(swift)
|
||||
cpp-options:
|
||||
- -DswiftJSON
|
||||
|
||||
library:
|
||||
source-dirs: src
|
||||
@@ -44,7 +94,23 @@ executables:
|
||||
source-dirs: apps/smp-server
|
||||
main: Main.hs
|
||||
dependencies:
|
||||
- simplex-messaging
|
||||
- simplexmq
|
||||
ghc-options:
|
||||
- -threaded
|
||||
|
||||
ntf-server:
|
||||
source-dirs: apps/ntf-server
|
||||
main: Main.hs
|
||||
dependencies:
|
||||
- simplexmq
|
||||
ghc-options:
|
||||
- -threaded
|
||||
|
||||
xftp-server:
|
||||
source-dirs: apps/xftp-server
|
||||
main: Main.hs
|
||||
dependencies:
|
||||
- simplexmq
|
||||
ghc-options:
|
||||
- -threaded
|
||||
|
||||
@@ -52,31 +118,32 @@ executables:
|
||||
source-dirs: apps/smp-agent
|
||||
main: Main.hs
|
||||
dependencies:
|
||||
- simplex-messaging
|
||||
- simplexmq
|
||||
ghc-options:
|
||||
- -threaded
|
||||
|
||||
dog-food:
|
||||
source-dirs: apps/dog-food
|
||||
xftp:
|
||||
source-dirs: apps/xftp
|
||||
main: Main.hs
|
||||
dependencies:
|
||||
- ansi-terminal == 0.10.*
|
||||
- optparse-applicative == 0.15.*
|
||||
- simplex-messaging
|
||||
- terminal == 0.2.*
|
||||
- simplexmq
|
||||
ghc-options:
|
||||
- -threaded
|
||||
|
||||
tests:
|
||||
smp-server-test:
|
||||
simplexmq-test:
|
||||
source-dirs: tests
|
||||
main: Test.hs
|
||||
dependencies:
|
||||
- simplex-messaging
|
||||
- simplexmq
|
||||
- deepseq == 1.4.*
|
||||
- hspec == 2.7.*
|
||||
- hspec-core == 2.7.*
|
||||
- HUnit == 1.6.*
|
||||
- random == 1.1.*
|
||||
- QuickCheck == 2.14.*
|
||||
- silently == 1.2.*
|
||||
- main-tester == 0.2.*
|
||||
- timeit == 2.0.*
|
||||
|
||||
ghc-options:
|
||||
# - -haddock
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
# SMP agent protocol - duplex communication over SMP protocol
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Abstract](#abstract)
|
||||
- [SMP agent](#smp-agent)
|
||||
- [SMP servers management](#smp-servers-management)
|
||||
- [SMP agent protocol components](#smp-agent-protocol-components)
|
||||
- [Duplex connection procedure](#duplex-connection-procedure)
|
||||
- [Communication between SMP agents](#communication-between-smp-agents)
|
||||
- [Message syntax](#messages-between-smp-agents)
|
||||
- [HELLO message](#hello-message)
|
||||
- [REPLY message](#reply-message)
|
||||
- [MSG message](#msg-message)
|
||||
- [INV message](#inv-message)
|
||||
- [ACK message](#ack-message)
|
||||
- [NEW message](#new-message)
|
||||
- [DEL message](#del-message)
|
||||
- [SMP agent commands](#smp-agent-commands)
|
||||
- [Client commands and server responses](#client-commands-and-server-responses)
|
||||
- [NEW command and INV response](#new-command-and-inv-response)
|
||||
- [JOIN command](#join-command)
|
||||
- [CONF notification and LET command](#conf-notification-and-let-command)
|
||||
- [REQ notification and ACPT command](#req-notification-and-acpt-command)
|
||||
- [INFO and CON notifications](#info-and-con-notifications)
|
||||
- [SUB command](#sub-command)
|
||||
- [SEND command and MID, SENT and MERR responses](#send-command-and-mid-sent-and-merr-responses)
|
||||
- [MSG notification](#msg-notification)
|
||||
- [END notification](#end-notification)
|
||||
- [OFF command](#off-command)
|
||||
- [DEL command](#del-command)
|
||||
- [Connection request](#connection-request)
|
||||
|
||||
## Abstract
|
||||
|
||||
The purpose of SMP agent protocol is to define the syntax and the semantics of communications between the client and the agent that connects to [SMP](./simplex-messaging.md) servers.
|
||||
|
||||
It provides:
|
||||
- protocol to create and manage bi-directional (duplex) connections between the users of SMP agents consisting of two (or more) separate unidirectional (simplex) SMP queues, abstracting away multiple steps required to establish bi-directional connections and any information about the servers location from the users of the agent protocol.
|
||||
- management of E2E encryption between SMP agents, generating ephemeral asymmetric keys for each connection.
|
||||
- SMP command authentication on SMP servers, generating ephemeral keys for each SMP queue.
|
||||
- TCP/TLS transport handshake with SMP servers.
|
||||
- validation of message integrity.
|
||||
|
||||
SMP agent protocol provides no encryption or security on the client side - it is assumed that the agent is executed in the trusted and secure environment, in one of three ways:
|
||||
- via TCP network using secure connection.
|
||||
- via local port (when the agent runs on the same device as a separate process).
|
||||
- via agent library, when the agent logic is included directly into the client application - [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) uses this approach.
|
||||
|
||||
## SMP agent
|
||||
|
||||
SMP agents communicate with each other via SMP servers using [simplex messaging protocol (SMP)](./simplex-messaging.md) according to the commands received from its users. This protocol is a middle layer in SimpleX protocols (above SMP protocol but below any application level protocol) - it is intended to be used by client-side applications that need secure asynchronous bi-directional communication channels ("connections").
|
||||
|
||||
The agent must have a persistent storage to manage the states of known connections and of the client-side information of SMP queues that each connection consists of, and also the buffer of the most recent sent and received messages. The number of the messages that should be stored is implementation specific, depending on the error management approach that the agent implements; at the very least the agent must store the hashes and IDs of the last received and sent messages.
|
||||
|
||||
## SMP servers management
|
||||
|
||||
SMP agent protocol commands do not contain the addresses of the SMP servers that the agent will use to create and use the connections (excluding the server address in queue URIs used in JOIN command). The list of the servers is a part of the agent configuration and can be dynamically changed by the agent implementation:
|
||||
- by the client applications via any API that is outside of scope of this protocol.
|
||||
- by the agents themselves based on availability and latency of the configured servers.
|
||||
|
||||
## SMP agent protocol components
|
||||
|
||||
SMP agent protocol has 3 main parts:
|
||||
|
||||
- the syntax and semantics of the messages that SMP agents exchange with each other in order to:
|
||||
- negotiate establishing unidirectional (simplex) encrypted queues on SMP servers.
|
||||
- exchange client messages and delivery notifications, providing sequential message IDs and message integrity (by including the hash of the previous message).
|
||||
- the syntax and semantics of the commands that are sent by the agent clients to the agents. This protocol allows to create and manage multiple connections, each consisting of two or more SMP queues.
|
||||
- the syntax and semantics of the message that the clients of SMP agents should send out-of-band (as pre-shared "invitation" including queue URIs) to protect [E2E encryption][1] from active attacks ([MITM attacks][2]).
|
||||
|
||||
## Duplex connection procedure
|
||||
|
||||

|
||||
|
||||
The procedure of establishing a duplex connection is explained on the example of Alice and Bob creating a bi-directional connection consisting of two unidirectional (simplex) queues, using SMP agents (A and B) to facilitate it, and two different SMP servers (which could be the same server). It is shown on the diagram above and has these steps:
|
||||
|
||||
1. Alice requests the new connection from the SMP agent A using SMP NEW command.
|
||||
2. Agent A creates an SMP connection on the server (using [SMP protocol](./simplex-messaging.md)) and responds to Alice with the invitation that contains queue information and the encryption key Bob's agent B should use. The invitation format is described in [Connection request](#connection-request).
|
||||
3. Alice sends the [connection request](#connection-request) to Bob via any secure channel (out-of-band message).
|
||||
4. Bob sends `JOIN` command with the connection request as a parameter to agent B to accept the connection.
|
||||
5. Establishing Alice's SMP queue (with SMP protocol commands):
|
||||
- Agent B sends an "SMP confirmation" with SMP SEND command to the SMP queue specified in the connection request - SMP confirmation is an unauthenticated message with an ephemeral key that will be used to authenticate Bob's commands to the queue, as described in SMP protocol, and Bob's info (profile, public key for E2E encryption, etc.). This message is encrypted using key passed in the connection request (or with the derived key, in which case public key for key derivation should be sent in clear text).
|
||||
- Agent A receives the SMP confirmation containing Bob's key and info as SMP MSG.
|
||||
- Agent A notifies Alice sending REQ notification with Bob's info.
|
||||
- Alice accepts connection request with ACPT command.
|
||||
- Agent A secures the queue with SMP KEY command.
|
||||
- Agent B tries sending authenticated SMP SEND command with agent `HELLO` message until it succeeds. Once it succeeds, Bob's agent "knows" the queue is secured.
|
||||
6. Agent B creates a new SMP queue on the server.
|
||||
7. Establish Bob's SMP queue:
|
||||
- Agent B sends `REPLY` message (SMP SEND command) with the connection request to this 2nd queue to Alice's agent (via the 1st queue) - this connection request SHOULD use "simplex" URI scheme.
|
||||
- Agent A, having received `REPLY` message, sends unauthenticated message (SMP SEND) to SMP queue with Alice agent's ephemeral key that will be used to authenticate Alice's commands to the queue, as described in SMP protocol, and Alice's info.
|
||||
- Bob's agent receives the key and Alice's information and secures the queue (SMP KEY).
|
||||
- Bob's agent sends the notification `INFO` with Alice's information to Bob.
|
||||
- Alice's agent keeps sending `HELLO` message until it succeeds.
|
||||
8. Agents A and B notify Alice and Bob that connection is established.
|
||||
- Once sending `HELLO` succeeds, Alice's agent sends to Alice `CON` notification that confirms that now both parties can communicate.
|
||||
- Once Bob's agent receives `HELLO` from Alice's agent, it sends to Bob `CON` notification as well.
|
||||
|
||||
At this point the duplex connection between Alice and Bob is established, they can use `SEND` command to send messages. The diagram also shows how the connection status changes for both parties, where the first part is the status of the SMP queue to receive messages, and the second part - the status of the queue to send messages.
|
||||
|
||||
The most communication happens between the agents and servers, from the point of view of Alice and Bob there are 4 steps (not including notifications):
|
||||
|
||||
1. Alice requests a new connection with `NEW` command and receives the invitation.
|
||||
2. Alice passes connection request out-of-band to Bob.
|
||||
3. Bob accepts the connection with `JOIN` command with the connection request to his agent.
|
||||
4. Alice accepts the connection with `ACPT` command.
|
||||
5. Both parties receive `CON` notification once duplex connection is established.
|
||||
|
||||
Clients SHOULD support establishing duplex connection asynchronously (when parties are intermittently offline) by persisting intermediate states and resuming SMP queue subscriptions.
|
||||
|
||||
## Communication between SMP agents
|
||||
|
||||
To establish duplex connections and to send messages on behalf of their clients, SMP agents communicate via SMP servers.
|
||||
|
||||
Agents use SMP message client body (the part of the SMP message after header - see [SMP protocol](./simplex-messaging.md)) to transmit agent client messages and exchange messages between each other.
|
||||
|
||||
Each SMP message client body, once decrypted, contains 3 parts (one of them may include binary message body), as defined by `decryptedSmpMessageBody` syntax:
|
||||
|
||||
- `agentMsgHeader` - agent message header that contains sequential agent message ID for a particular SMP queue, agent timestamp (ISO8601) and the hash of the previous message.
|
||||
- `agentMessage` - a command/message to the other SMP agent:
|
||||
- to establish the connection with two SMP queues (`helloMsg`, `replyQueueMsg`)
|
||||
- to send and to acknowledge user messages (`clientMsg`, `acknowledgeMsg`)
|
||||
- to manage SMP queue rotation (`newQueueMessage`, `deleteQueueMsg`)
|
||||
- to manage encryption key rotation (TODO)
|
||||
- `msgPadding` - an optional message padding to make all SMP messages have constant size, to prevent servers from observing the actual message size. The only case the message padding can be absent is when the message has exactly the maximum size, in all other cases the message MUST be padded to a fixed size.
|
||||
|
||||
### Messages between SMP agents
|
||||
|
||||
Message syntax below uses [ABNF][3] with [case-sensitive strings extension][4].
|
||||
|
||||
```abnf
|
||||
decryptedSmpMessageBody = agentMsgHeader CRLF agentMessage CRLF msgPadding
|
||||
agentMsgHeader = agentMsgId SP previousMsgHash ; here `agentMsgId` is sequential ID set by the sending agent
|
||||
agentMsgId = 1*DIGIT
|
||||
previousMsgHash = encoded
|
||||
encoded = <base64 encoded>
|
||||
|
||||
agentMessage = helloMsg / replyQueueMsg /
|
||||
clientMsg / invitationMsg /
|
||||
newQueueMessage / deleteQueueMsg
|
||||
|
||||
msgPadding = *OCTET ; optional random bytes to get messages to the same size (as defined in SMP message size)
|
||||
|
||||
helloMsg = %s"H"
|
||||
|
||||
replyQueueMsg = %s"R" connectionRequest ; `connectionRequest` is defined below
|
||||
; this message can only be sent by the second connection party
|
||||
|
||||
clientMsg = %s"M" clientMsgBody
|
||||
clientMsgBody = *OCTET
|
||||
|
||||
; TODO remove and move to "public" header
|
||||
invitationMsg = %s"INV" SP connReqInvitation SP connInfo
|
||||
; `connReqInvitation` and `connInfo` are defined below
|
||||
|
||||
newQueueMsg = %s"N" queueURI
|
||||
; this message can be sent by any party to add SMP queue to the connection.
|
||||
; NOT SUPPORTED in the current implementation
|
||||
|
||||
deleteQueueMsg = %s"D" queueURI
|
||||
; notification that the queue with passed URI will be deleted
|
||||
; no need to notify the other party about suspending queue separately, as suspended and deleted queues are indistinguishable to the sender
|
||||
; NOT SUPPORTED in the current implementation
|
||||
```
|
||||
|
||||
#### HELLO message
|
||||
|
||||
This is the first message that both agents send after the respective SMP queue is secured by the receiving agent (see diagram). It MAY contain the public key that the recipient would use to verify messages signed by the sender.
|
||||
|
||||
Sending agent might need to retry sending HELLO message, as it would not have any other confirmation that the queue is secured other than the success of sending this message with the signed SMP SEND command.
|
||||
|
||||
#### REPLY message
|
||||
|
||||
This is the message that is sent by the agent that received an out-of-band connection request to pass the connection request for the reply SMP queues to the agent that originated the connection (see diagram).
|
||||
|
||||
#### MSG message
|
||||
|
||||
This is the agent envelope used to send client messages once the connection is established. Do not confuse it with the MSG response from SMP server to the agent and MSG response from SMP agent to the client that are sent in different contexts.
|
||||
|
||||
#### INV message
|
||||
|
||||
This message is sent to the SMP queue(s) in `connReqContact`, to establish a new connection via existing unsecured queue, that acts as a permanent connection link of a user.
|
||||
|
||||
#### ACK message
|
||||
|
||||
This message is sent to confirm the client message reception. It includes received message number, message hash and the reception status.
|
||||
|
||||
#### NEW message
|
||||
|
||||
This message is sent to add an additional SMP queue to the connection. Unlike REPLY message it can be sent at any time.
|
||||
|
||||
#### DEL message
|
||||
|
||||
This message is sent to notify that the queue with passed URI will be deleted - having received this message, the receiving agent should no longer send messages to this queue. In case it was the last remaining send queue in the duplex connection, the agent MAY also delete the reply queue(s) in the connection.
|
||||
|
||||
## SMP agent commands
|
||||
|
||||
This part describes the transmissions between users and client-side SMP agents: commands that the users send to create and operate duplex connections and SMP agent responses and messages they deliver.
|
||||
|
||||
Commands syntax below is provided using [ABNF][3] with [case-sensitive strings extension][4].
|
||||
|
||||
Each transmission between the user and SMP agent must have this format/syntax:
|
||||
|
||||
```abnf
|
||||
agentTransmission = [corrId] CRLF [connId] CRLF agentCommand
|
||||
|
||||
corrId = 1*(%x21-7F) ; any characters other than control/whitespace
|
||||
|
||||
connId = encoded
|
||||
|
||||
agentCommand = (userCmd / agentMsg) CRLF
|
||||
userCmd = newCmd / joinCmd / letCmd / acceptCmd / subscribeCmd / sendCmd / acknowledgeCmd / suspendCmd / deleteCmd
|
||||
agentMsg = invitation / confMsg / connReqMsg / connInfo / connected / unsubscribed / connDown / connUp / messageId / sent / messageError / message / received / ok / error
|
||||
|
||||
newCmd = %s"NEW" SP connectionMode [SP %s"NO_ACK"] ; response is `invitation` or `error`
|
||||
; NO_ACK parameter currently not supported
|
||||
|
||||
connectionMode = %s"INV" / %s"CON"
|
||||
|
||||
invitation = %s"INV" SP connectionRequest ; `connectionRequest` is defined below
|
||||
|
||||
confMsg = %s"CONF" SP confirmationId SP msgBody
|
||||
; msgBody here is any binary information identifying connection request
|
||||
|
||||
letCmd = %s"LET" SP confirmationId SP msgBody
|
||||
; msgBody here is any binary information identifying connecting party
|
||||
|
||||
confirmationId = 1*DIGIT
|
||||
|
||||
connReqMsg = %s"REQ" SP invitationId SP msgBody
|
||||
; msgBody here is any binary information identifying connection request
|
||||
|
||||
acceptCmd = %s"ACPT" SP invitationId SP msgBody
|
||||
; msgBody here is any binary information identifying connecting party
|
||||
|
||||
invitationId = 1*DIGIT
|
||||
|
||||
connInfo = %s"INFO" SP msgBody
|
||||
; msgBody here is any binary information identifying connecting party
|
||||
|
||||
connected = %s"CON"
|
||||
|
||||
subscribeCmd = %s"SUB" ; response is `ok` or `error`
|
||||
|
||||
unsubscribed = %s"END"
|
||||
; when another agent (or another client of the same agent)
|
||||
; subscribes to the same SMP queue on the server
|
||||
|
||||
connDown = %s"DOWN"
|
||||
; lost connection (e.g. because of Internet connectivity or server is down)
|
||||
|
||||
connUp = %s"UP"
|
||||
; restored connection
|
||||
|
||||
joinCmd = %s"JOIN" SP connectionRequest SP connInfo [SP %s"NO_REPLY"] [SP %s"NO_ACK"]
|
||||
; `connectionRequest` and `connInfo` are defined below
|
||||
; response is `connected` or `error`
|
||||
; parameters NO_REPLY and NO_ACK are currently not supported
|
||||
|
||||
suspendCmd = %s"OFF" ; can be sent by either party, response `ok` or `error`
|
||||
|
||||
deleteCmd = %s"DEL" ; can be sent by either party, response `ok` or `error`
|
||||
|
||||
sendCmd = %s"SEND" SP msgBody
|
||||
; send syntax is similar to that of SMP protocol, but it is wrapped in SMP message
|
||||
msgBody = stringMsg | binaryMsg
|
||||
stringMsg = ":" string ; until CRLF in the transmission
|
||||
string = *(%x01-09 / %x0B-0C / %x0E-FF %) ; any characters other than NUL, CR and LF
|
||||
binaryMsg = size CRLF msgBody CRLF ; the last CRLF is in addition to CRLF in the transmission
|
||||
size = 1*DIGIT ; size in bytes
|
||||
msgBody = *OCTET ; any content of specified size - safe for binary
|
||||
|
||||
messageId = %s"MID" SP agentMsgId
|
||||
|
||||
sent = %s"SENT" SP agentMsgId
|
||||
|
||||
messageError = %s"MERR" SP agentMsgId SP <errorType>
|
||||
|
||||
message = %s"MSG" SP msgIntegrity SP recipientMeta SP brokerMeta SP senderMeta SP binaryMsg
|
||||
recipientMeta = %s"R=" agentMsgId "," agentTimestamp ; receiving agent message metadata
|
||||
brokerMeta = %s"B=" brokerMsgId "," brokerTimestamp ; broker (server) message metadata
|
||||
senderMeta = %s"S=" agentMsgId ; sending agent message ID
|
||||
brokerMsgId = encoded
|
||||
brokerTimestamp = <date-time>
|
||||
msgIntegrity = ok / msgIntegrityError
|
||||
|
||||
msgIntegrityError = %s"ERR" SP msgIntegrityErrorType
|
||||
msgIntegrityErrorType = skippedMsgErr / badMsgIdErr / badHashErr
|
||||
|
||||
skippedMsgErr = %s"NO_ID" SP missingFromMsgId SP missingToMsgId
|
||||
badMsgIdErr = %s"ID" SP previousMsgId ; ID is lower than the previous
|
||||
badHashErr = %s"HASH"
|
||||
|
||||
missingFromMsgId = agentMsgId
|
||||
missingToMsgId = agentMsgId
|
||||
previousMsgId = agentMsgId
|
||||
|
||||
acknowledgeCmd = %s"ACK" SP agentMsgId ; ID assigned by receiving agent (in MSG "R")
|
||||
|
||||
received = %s"RCVD" SP agentMsgId SP msgIntegrity
|
||||
; ID assigned by sending agent (in SENT response)
|
||||
; currently not implemented
|
||||
|
||||
msgStatus = ok | error
|
||||
|
||||
ok = %s"OK"
|
||||
|
||||
error = %s"ERR" SP <errorType>
|
||||
```
|
||||
|
||||
### Client commands and server responses
|
||||
|
||||
#### NEW command and INV response
|
||||
|
||||
`NEW` command is used to create a connection and a connection request to be sent out-of-band to another protocol user (the joining party). It should be used by the client of the agent that initiates creating a duplex connection (the initiating party).
|
||||
|
||||
`INV` response is sent by the agent to the client of the initiating party.
|
||||
|
||||
`NEW` command has `connectionMode` parameter to define the connection mode - to be used to communicate with a single contact (invitation mode, `connectionMode` is `INV`) or to accept connection requests from anybody (contact mode, `connectionMode` is `CON`). The type of connection request is determined by `connectionMode` parameter.
|
||||
|
||||
#### JOIN command
|
||||
|
||||
It is used to create a connection and accept the connection request received out-of-band. It should be used by the client of the agent that accepts the connection (the joining party).
|
||||
|
||||
#### CONF notification and LET command
|
||||
|
||||
When the joining party uses `JOIN` command to accept connection invitation created with `NEW INV` command, the initiating party will receive `CONF` notification with some numeric identifier and an additional binary information, that can be used to identify the joining party or for any other purpose.
|
||||
|
||||
To continue with the connection the initiating party should use `LET` command.
|
||||
|
||||
#### REQ notification and ACPT command
|
||||
|
||||
When the joining party uses `JOIN` command to connect to the contact created with `NEW CON` command, the initiating party will receive `REQ` notification with some numeric identifier and an additional binary information, that can be used to identify the joining party or for any other purpose.
|
||||
|
||||
To continue with the connection the party that created the contact should use `ACPT` command.
|
||||
|
||||
#### INFO and CON notifications
|
||||
|
||||
After the initiating party proceeds with the connection using `ACPT` command, the joining party will receive `INFO` notification that can be used to identify the initiating party or for any other purpose.
|
||||
|
||||
Once the connection is established and ready to accept client messages, both agents will send `CON` notification to their clients.
|
||||
|
||||
#### SUB command
|
||||
|
||||
This command can be used by the client to resume receiving messages from the connection that was created in another TCP/client session. Agent response to this command can be `OK` or `ERR` in case connection does not exist (or can only be used to send connections - e.g. when the reply queue was not created).
|
||||
|
||||
#### SEND command and MID, SENT, RCVD and MERR responses
|
||||
|
||||
`SEND` command is used by the client to send messages.
|
||||
|
||||
`MID` response with the message ID (the sequential message number that includes both sent and received messages in the connection) is sent to the client to confirm that the message is accepted by the agent, before it is sent to the SMP server.
|
||||
|
||||
`SENT` notification is sent by the agent to confirm that the message was delivered to at least one of SMP servers. This notification contains the same message ID as `MID` notification. `SENT` notification, depending on network availability, can be sent at any time later, potentially in the next client session.
|
||||
|
||||
`RCVD` notification is sent by the agent when it receives `ACK` message from the receiving agent. This notification contains reception status, only one successful notification will be sent, and multiple error notifications will be sent in case `ACK` had error status.
|
||||
|
||||
In case of the failure to send the message for any other reason than network connection or message queue quota - e.g. authentication error (`ERR AUTH`) or syntax error (`ERR CMD error`), the agent will send to the client `MERR` notification with the message ID, and this message delivery will no longer be attempted to this SMP queue.
|
||||
|
||||
#### MSG notification
|
||||
|
||||
It is sent by the agent to the client when agent receives the message from the SMP server. It has message ID and timestamp from both the receiving and sending agents and from SMP server:
|
||||
- recipient agent ID is intended to be used to refer to the message in the future.
|
||||
- sender agent ID is intended to be used to identify any missed / skipped message(s)
|
||||
- broker ID should be used to detect duplicate deliveries (it would happen if TCP connection is lost before the message is acknowledged by the agent - see [SMP protocol](./simplex-messaging.md))
|
||||
|
||||
#### END notification
|
||||
|
||||
It is sent by the agent to the client when agent receives SMP protocol `END` notification from SMP server. It indicates that another agent has subscribed to the same SMP queue on the server and the server terminated the subscription of the current agent.
|
||||
|
||||
#### DOWN and UP notifications
|
||||
|
||||
These notifications are sent when server or network connection is, respectively, `DOWN` or back `UP`.
|
||||
|
||||
All the subscriptions made in the current client session will be automatically resumed when `UP` notification is received.
|
||||
|
||||
#### OFF command
|
||||
|
||||
It is used to suspend the receiving SMP queue - sender will no longer be able to send the messages to the connection, but the recipient can retrieve the remaining messages. Agent response to this command can be `OK` or `ERR`. This command is irreversible.
|
||||
|
||||
#### DEL command
|
||||
|
||||
It is used to delete the connection and all messages in it, as well as the receiving SMP queue and all messages in it that were remaining on the server. Agent response to this command can be `OK` or `ERR`. This command is irreversible.
|
||||
|
||||
## Connection request
|
||||
|
||||
Connection request `connectionRequest` is generated by SMP agent in response to `newCmd` command (`"NEW"`), used by another party user with `joinCmd` command (`"JOIN"`), and then another connection request is sent by the agent in `replyQueueMsg` and used by the first party agent to connect to the reply queue (the second part of the process is invisible to the users).
|
||||
|
||||
Connection request syntax:
|
||||
|
||||
```
|
||||
connectionRequest = connectionScheme "/" connReqType "#/?smp=" smpQueues "&e2e=" e2eEncryption
|
||||
connReqType = %s"invitation" / %s"contact"
|
||||
; this parameter has the same meaning as connectionMode in agent commands
|
||||
; `NEW INV` creates `invitation` connection request, `NEW CON` - `contact`
|
||||
connectionScheme = (%s"https://" clientAppServer) | %s"simplex:"
|
||||
clientAppServer = hostname [ ":" port ]
|
||||
; client app server, e.g. simplex.chat
|
||||
e2eEncryption = encryptionScheme ":" publicKey
|
||||
encryptionScheme = %s"rsa" ; end-to-end encryption and key exchange protocols,
|
||||
; the current hybrid encryption scheme (RSA-OAEP/AES-256-GCM-SHA256)
|
||||
; will be replaced with double ratchet protocol and DH key exchange.
|
||||
publicKey = <base64url X509 SPKI key encoding>
|
||||
smpQueues = smpQueue [ "," 1*smpQueue ] ; SMP queues for the connection
|
||||
smpQueue = <URL-encoded queueURI defined in SMP protocol>
|
||||
```
|
||||
|
||||
All parameters are passed via URI hash to avoid sending them to the server (in case "https" scheme is used) - they can be used by the client-side code and processed by the client application. Parameters `smp` and `e2e` can be present in any order, any unknown additional parameters SHOULD be ignored.
|
||||
|
||||
`clientAppServer` is not an SMP server - it is a server that shows the instruction on how to download the client app that will connect using this connection request. This server can also host a mobile or desktop app manifest so that this link is opened directly in the app if it is installed on the device.
|
||||
|
||||
"simplex" URI scheme in `connectionProtocol` can be used instead of client app server, to connect without creating any web traffic. Client apps MUST support this URI scheme.
|
||||
|
||||
See SMP protocol [out-of-band messages](./simplex-messaging.md#out-of-band-messages) for syntax of `queueURI`.
|
||||
|
||||
[1]: https://en.wikipedia.org/wiki/End-to-end_encryption
|
||||
[2]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack
|
||||
[3]: https://tools.ietf.org/html/rfc5234
|
||||
[4]: https://tools.ietf.org/html/rfc7405
|
||||
@@ -0,0 +1,71 @@
|
||||
sequenceDiagram
|
||||
participant A as Alice
|
||||
participant AA as Alice's<br>agent
|
||||
participant AS as Alice's<br>server
|
||||
participant BS as Bob's<br>server
|
||||
participant BA as Bob's<br>agent
|
||||
participant B as Bob
|
||||
|
||||
note over AA, BA: status (receive/send): NONE/NONE
|
||||
|
||||
note over A, AA: 1. request connection<br>from agent
|
||||
A ->> AA: NEW: create<br>duplex connection
|
||||
|
||||
note over AA, AS: 2. create Alice's SMP queue
|
||||
AA ->> AS: NEW: create SMP queue
|
||||
AS ->> AA: IDS: SMP queue IDs
|
||||
note over AA: status: NEW/NONE
|
||||
|
||||
AA ->> A: INV: invitation<br>to connect
|
||||
|
||||
note over A, B: 3. out-of-band invitation
|
||||
A ->> B: OOB: invitation to connect
|
||||
|
||||
note over BA, B: 4. accept connection
|
||||
B ->> BA: JOIN:<br>via invitation info
|
||||
note over BA: status: NONE/NEW
|
||||
|
||||
note over BA, BS: 5. create Bob's SMP queue
|
||||
BA ->> BS: NEW: create SMP queue
|
||||
BS ->> BA: IDS: SMP queue IDs
|
||||
note over BA: status: NEW/NEW
|
||||
|
||||
note over BA, AA: 6. establish Alice's SMP queue
|
||||
BA ->> AS: SEND: Bob's info and sender server key (SMP confirmation with reply queues)
|
||||
note over BA: status: NEW/CONFIRMED
|
||||
|
||||
AS ->> AA: MSG: Bob's info and<br>sender server key
|
||||
note over AA: status: CONFIRMED/NONE
|
||||
AA ->> AS: ACK: confirm message
|
||||
AA ->> A: CONF: connection request ID<br>and Bob's info
|
||||
A ->> AA: LET: accept connection request,<br>send Alice's info
|
||||
AA ->> AS: KEY: secure queue
|
||||
note over AA: status: SECURED/NONE
|
||||
|
||||
AA ->> BS: SEND: Alice's info and sender's server key (SMP confirmation without reply queues)
|
||||
note over AA: status: SECURED/CONFIRMED
|
||||
|
||||
BS ->> BA: MSG: Alice's info and<br>sender's server key
|
||||
note over BA: status: CONFIRMED/CONFIRMED
|
||||
BA ->> B: INFO: Alice's info
|
||||
BA ->> BS: ACK: confirm message
|
||||
BA ->> BS: KEY: secure queue
|
||||
note over BA: status: SECURED/CONFIRMED
|
||||
|
||||
BA ->> AS: SEND: HELLO: only needs to be sent once in v2
|
||||
|
||||
note over BA: status: SECURED/ACTIVE
|
||||
note over BA, B: 7a. notify Bob<br>about connection success
|
||||
BA ->> B: CON: connected
|
||||
|
||||
AS ->> AA: MSG: HELLO: Alice's agent<br>knows Bob can send
|
||||
note over AA: status: SECURED/ACTIVE
|
||||
AA ->> AS: ACK: confirm message
|
||||
note over A, AA: 7a. notify Alice<br>about connection success
|
||||
AA ->> A: CON: connected
|
||||
|
||||
AA ->> BS: SEND: HELLO: only needs to be sent once in v2
|
||||
note over AA: status: ACTIVE/ACTIVE
|
||||
BS ->> BA: MSG: HELLO: Bob's agent<br>knows Alice can send
|
||||
note over BA: status: ACTIVE/ACTIVE
|
||||
BA ->> BS: ACK: confirm message
|
||||
@@ -0,0 +1,79 @@
|
||||
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 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 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
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
BS ->> BA: MSG: Alice's info and<br>sender's server key
|
||||
note over BA: status: CONFIRMED/ACTIVE
|
||||
BA ->> B: INFO: Alice's info
|
||||
BA ->> BS: ACK: confirm message
|
||||
BA ->> BS: KEY: secure queue
|
||||
note over BA: status: SECURED/ACTIVE
|
||||
|
||||
AA ->> BS: SEND: HELLO: try sending until successful
|
||||
deactivate AA
|
||||
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
|
||||
BA ->> B: CON: connected
|
||||
|
After Width: | Height: | Size: 35 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
|
||||
@@ -0,0 +1,40 @@
|
||||
sequenceDiagram
|
||||
participant M as mobile app
|
||||
participant C as chat core
|
||||
participant A as agent
|
||||
participant S as SMP server
|
||||
participant N as NTF server
|
||||
participant APN as APN
|
||||
|
||||
note over M, APN: register subscription
|
||||
|
||||
alt register existing
|
||||
M -->> A: on /_ntf register, for subscribed queues
|
||||
else create new connection
|
||||
A -->> S: NEW / JOIN
|
||||
note over A, S: ...<br>Connection handshake<br>...
|
||||
S -->> A: CON
|
||||
end
|
||||
A ->> S: NKEY nKey
|
||||
S ->> A: NID nId
|
||||
A ->> N: SNEW tknId dhKey (smpServer, nId, nKey)
|
||||
N ->> A: ID subId dhKey
|
||||
N ->> S: NSUB nId
|
||||
S ->> N: OK [/ NMSG]
|
||||
|
||||
note over M, APN: notify about message
|
||||
|
||||
S ->> N: NMSG
|
||||
N ->> APN: APNSMutableContent<br>ntfQueue, nonce
|
||||
APN ->> M: UNMutableNotificationContent
|
||||
note over M, S: ...<br>Client awaken, message is received<br>...
|
||||
S ->> M: message
|
||||
note over M: mutate notification
|
||||
|
||||
note over M, APN: change APN token
|
||||
|
||||
APN ->> M: new device token
|
||||
M -->> C: /_ntf_sub update tkn
|
||||
C -->> A: updateNtfToken()
|
||||
A -->> N: TUPD tknId newDeviceToken
|
||||
note over M, N: ...<br>Verify token<br>...
|
||||
@@ -0,0 +1,20 @@
|
||||
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: confirm queue<br>("public" key SK for<br>sending messages<br>and any optional<br>info encrypted with<br>"public" key EK)
|
||||
B ->> S: 3. confirm queue ("SEND" command not signed)
|
||||
|
||||
S ->> A: 4. deliver Bob's message
|
||||
note over A: decrypt message<br>("private" key EK)
|
||||
A ->> S: 5. secure queue ("KEY", RK-signed)
|
||||
|
||||
note over S: 6. simplex<br>queue RID<br>is ready to use!
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,26 @@
|
||||
sequenceDiagram
|
||||
participant S as sender (client)
|
||||
participant A as transport
|
||||
participant Q as pubsub
|
||||
participant P as transport
|
||||
participant R as receiver (client)
|
||||
note over R: sign subscription (1)
|
||||
R ->> P: subscribe to messages
|
||||
note over P: verify subscriber (1)
|
||||
alt subscriber verified?
|
||||
P -->> Q: subscribe
|
||||
else
|
||||
P ->> R: reject subscription
|
||||
end
|
||||
note over S: sign message (2)
|
||||
S ->> A: send message
|
||||
note over A: verify sender (2)
|
||||
alt sender verified?
|
||||
A -->> Q: queue message
|
||||
activate Q
|
||||
else
|
||||
A ->> S: reject message
|
||||
end
|
||||
Q -->> P: take message
|
||||
deactivate Q
|
||||
P ->> R: deliver message
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,10 @@
|
||||
sequenceDiagram
|
||||
participant B as Bob (sender)
|
||||
participant S as server (queue RID)
|
||||
participant A as Alice (recipient)
|
||||
|
||||
note over B: encrypt message<br>("public" key EK)
|
||||
B ->> S: 1. send message to SID (SK-signed command)
|
||||
|
||||
S ->> A: 2. receive messages from RID (RK-signed subscription)
|
||||
note over A: decrypt message<br>("private" key EK)
|
||||
|
After Width: | Height: | Size: 7.7 KiB |
@@ -0,0 +1,11 @@
|
||||
graph LR
|
||||
VS{{"verify sender (SK)"}}
|
||||
VR{{"verify recipient (RK)"}}
|
||||
|
||||
S(sender) -->|msg| VS
|
||||
subgraph "server (queue RID, SID)"
|
||||
VS --> DB[("storage")]
|
||||
DB --> VR
|
||||
end
|
||||
R(recipient) -->|"1) sub"| VR
|
||||
VR -->|"2) msg"| R
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,313 @@
|
||||
Revision 1, 2022-01-01
|
||||
|
||||
Evgeny Poberezkin
|
||||
|
||||
# SimpleX: messaging and application platform
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [What is SimpleX](#what-is-simplex)
|
||||
- [SimpleX objectives](#simplex-objectives)
|
||||
- [In Comparison](#in-comparison)
|
||||
- [Technical Details](#technical-details)
|
||||
- [Trust in Servers](#trust-in-servers)
|
||||
- [Client -> Server Communication](#client---server-communication)
|
||||
- [SimpleX Messaging Protocol](#simplex-messaging-protocol)
|
||||
- [SimpleX Agents](#simplex-agents)
|
||||
- [Encryption Primitives Used](#encryption-primitives-used)
|
||||
- [Threat model](#threat-model)
|
||||
- [Acknowledgements](#acknowledgements)
|
||||
|
||||
## Introduction
|
||||
|
||||
#### What is SimpleX
|
||||
|
||||
SimpleX as a whole is a platform upon which applications can be built. [SimpleX Chat](https://github.com/simplex-chat/simplex-chat) is one such application that also serves as an example and reference application.
|
||||
|
||||
- [SimpleX Messaging Protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md) (SMP) is a protocol to send messages in one direction to a recipient, relying on a server in-between. The messages are delivered via uni-directional queues created by recipients.
|
||||
|
||||
- SMP runs over a transport protocol (shown below as TLS) that provides integrity, server authentication, confidentiality, and transport channel binding.
|
||||
|
||||
- A SimpleX Server is one of those servers.
|
||||
|
||||
- The SimpleX Network is the term used for the collective of SimpleX Servers that facilitate SMP.
|
||||
|
||||
- SimpleX Client libraries speak SMP to SimpleX Servers and provide a low-level API not generally intended to be used by applications.
|
||||
|
||||
- SimpleX Agents interface with SimpleX Clients to provide a more high-level API intended to be used by applications. Typically they are embedded as libraries, but are designed so they can also be abstracted into local services.
|
||||
|
||||
|
||||
*Diagram showing the SimpleX Chat app, with logical layers of the chat application interfacing with a SimpleX Agent library, which in turn interfaces with a SimpleX Client library. The Client library in turn speaks the Messaging Protocol to a SimpleX Server.*
|
||||
|
||||
```
|
||||
User's Computer Internet Third-Party Server
|
||||
------------------ | ---------------------- | -------------------------
|
||||
| |
|
||||
SimpleX Chat | |
|
||||
| |
|
||||
+----------------+ | |
|
||||
| Chat App | | |
|
||||
+----------------+ | |
|
||||
| SimpleX Agent | | |
|
||||
+----------------+ -------------- TLS ---------------- +----------------+
|
||||
| SimpleX Client | ------ SimpleX Messaging Protocol ------> | SimpleX Server |
|
||||
+----------------+ ----------------------------------- +----------------+
|
||||
| |
|
||||
```
|
||||
|
||||
#### SimpleX objectives
|
||||
|
||||
1. Provide messaging infrastructure for distributed applications. This infrastructure needs to have the following qualities:
|
||||
|
||||
- Security against passive and active (man-in-the-middle) attacks: the parties should have reliable end-to-end encryption and be able to detect the presence of an active attacker who modified, deleted or added messages.
|
||||
|
||||
- Privacy: protect against traffic correlation attacks to determine the contacts that the users communicate with.
|
||||
|
||||
- Reliability: the messages should be delivered even if some participating network servers or receiving clients fail, with “at least once” delivery guarantee.
|
||||
|
||||
- Integrity: the messages sent in one direction are ordered in a way that sender and recipient agree on; the recipient can detect when a message was removed or changed.
|
||||
|
||||
- Asynchronous delivery: it should not be required that both communicating parties (client devices, services or applications) are online for reliable message delivery.
|
||||
|
||||
- Low latency: the delay introduced by the network should not be higher than 100ms-1s in addition to the underlying TCP network latency.
|
||||
|
||||
2. Provide better communication security and privacy than the alternative instant messaging solutions. In particular SimpleX provides better privacy of metadata (who talks to whom and when) and better security against active network attackers and malicious servers.
|
||||
|
||||
3. Balance user experience with privacy requirements, prioritizing experience of mobile device users.
|
||||
|
||||
#### In Comparison
|
||||
|
||||
SimpleX network has a design similar to P2P networks, but unlike most P2P networks it consists of clients and servers without depending on any centralized component.
|
||||
In comparison to more traditional messaging applications (e.g. WhatsApp, Signal, Telegram) the key differences of SimpleX network are:
|
||||
|
||||
- participants do not need to have globally unique addresses to communicate, instead they use redundant unidirectional (simplex) messaging queues, with a separate set of queues for each contact.
|
||||
|
||||
- connection requests are passed out-of-band, non-optionally protecting key exchange against man-in-the-middle attack.
|
||||
|
||||
- simple message queues provided by network servers are used by the clients to create more complex communication scenarios, such as duplex one-to-one communication, transmitting files, group communication without central servers, and content/communication channels.
|
||||
|
||||
- servers do not store any user information (no user profiles or contacts, or messages once they are delivered), and primarily use in-memory persistence.
|
||||
|
||||
- users can change servers with minimal disruption - even after an in-use server disappears, simply by changing the configuration on which servers the new queues are created.
|
||||
|
||||
## Technical Details
|
||||
|
||||
#### Trust in Servers
|
||||
|
||||
Clients communicate directly with servers (but not with other clients) using SimpleX Messaging Protocol (SMP) running over some transport protocol that provides integrity, server authentication, confidentiality, and transport channel binding. By default, we assume this transport protocol is TLS.
|
||||
|
||||
Users use multiple servers, and choose where to receive their messages. Accordingly, they send messages to their communication partners' chosen servers.
|
||||
|
||||
Although end-to-end encryption is always present, users place a degree of trust in servers. This trust decision is very similar to a user's choice of email provider; however the trust placed in a SimpleX server is significantly less. Notably, there is no re-used identifier or credential between queues on the same (or different) servers. While a user *may* re-use a connection to fetch from multiple queues, or connect to a server from the same IP address, both are choices a user may opt into to break the promise of un-correlatable queues.
|
||||
|
||||
Users may trust a server because:
|
||||
|
||||
- They deploy and control the servers themselves from the available open-source code. This has the trade-offs of strong trust in the server but limited metadata obfuscation to a passive network observer. Techniques such as noise traffic, traffic mixing (incurring latency), and using an onion routing transport protocol can mitigate that latter.
|
||||
|
||||
- They use servers from a trusted commercial provider. The more clients the provider has, the less metadata about the communication times is leaked to the network observers.
|
||||
|
||||
- Users trust their contacts and the servers they chose.
|
||||
|
||||
By default, servers do not retain access logs, and permanently delete messages and queues when requested. Messages persist only in memory until they cross a threshold of time, typically on the order of days.[0] There is still a risk that a server maliciously records all queues and messages (even though encrypted) sent via the same transport connection to gain a partial knowledge of the user’s communications graph and other meta-data.
|
||||
|
||||
SimpleX supports measures (managed transparently to the user at the agent level) to mitigate the trust placed in servers. These include rotating the queues in use between users, noise traffic, and supporting overlay networks such as Tor.
|
||||
|
||||
[0] While configurable by servers, a minimum value is enforced by the default software. SimpleX Agents provide redundant routing over queues to mitigate against message loss.
|
||||
|
||||
|
||||
#### Client -> Server Communication
|
||||
|
||||
Utilizing TLS grants the SimpleX Messaging Protocol (SMP) server authentication and metadata protection to a passive network observer. But SMP does not rely on the transport protocol for message confidentiality or client authentication. The SMP protocol itself provides end-to-end confidentiality, authentication, and integrity of messages between communicating parties.
|
||||
|
||||
Servers have long-lived, self-signed, offline certificates whose hash is pre-shared with clients over secure channels - either provided with the client library or provided in the secure introduction between clients. The offline certificate signs an online certificate used in the transport protocol handshake. [0]
|
||||
|
||||
If the transport protocol's confidentiality is broken, incoming and outgoing messages to the server cannot be correlated by message contents. Additionally, because of encryption at the SMP layer, impersonating the server is not sufficient to pass (and therefore correlate) a message from a sender to recipient - the only attack possible is to drop the messages. Only by additionally *compromising* the server can one pass and correlate messages.
|
||||
|
||||
It's important to note that the SMP protocol does not do server authentication. Instead we rely upon the fact that an attacker who tricks the transport protocol into authenticating the server incorrectly cannot do anything with the SMP messages except drop them.
|
||||
|
||||
After the connection is established, the client sends blocks of a fixed size 16Kb, and the server replies with the blocks of the same size to reduce metadata observable to a network adversary. The protocol has been designed to make traffic correlation attacks difficult, adapting ideas from Tor, remailers, and more general onion and mix networks. It does not try to replace Tor though - SimpleX servers can be deployed as onion services and SimpleX clients can communicate with servers over Tor to further improve participants privacy.
|
||||
|
||||
By using fixed-size blocks, oversized for the expected content, the vast majority of traffic is uniform in nature. When enough traffic is transiting a server simultaneously, the server acts as a (very) low-latency mix node. We can't rely on this behavior to make a security claim, but we have engineered to take advantage of it when we can. As mentioned, this holds true even if the transport connection is compromised.
|
||||
|
||||
The protocol does not protect against attacks targeted at particular users with known identities - e.g., if the attacker wants to prove that two known users are communicating, they can achieve it. At the same time, it substantially complicates large-scale traffic correlation, making determining the real user identities much less effective.
|
||||
|
||||
[0] Future versions of SMP may add support for revocation lists of certificates, presently this risk is mitigated by the SMP protocol itself.
|
||||
|
||||
|
||||
#### SimpleX Messaging Protocol
|
||||
|
||||
SMP is initialized with an in-person or out-of-band introduction message, where Alice provides Bob with details of a server (including IP, port, and hash of the long-lived offline certificate), a queue ID, and Alice's public key for her receiving queue. These introductions are similar to the PANDA key-exchange, in that if observed, the adversary can race to establish the communication channel instead of the intended participant. [0]
|
||||
|
||||
Because queues are uni-directional, Bob provides an identically-formatted introduction message to Alice over Alice's now-established receiving queue.
|
||||
|
||||
When setting up a queue, the server will create separate sender and recipient queue IDs (provided to Alice during set-up and Bob during initial connection). Additionally, during set-up Alice will perform a DH exchange with the server to agree upon a shared secret. This secret will be used to re-encrypt Bob's incoming message before Alice receives it, creating the anti-correlation property earlier-described should the transport encryption be compromised.
|
||||
|
||||
[0] Users can additionally create public 'contact queues' that are only used to receive connection requests.
|
||||
|
||||
#### SimpleX Agents
|
||||
|
||||
SimpleX agents provide higher-level operations compared to SimpleX Clients, who are primarily concerned with creating queues and communicating with servers using SMP. Agent operations include:
|
||||
|
||||
- Managing sets of bi-directional, redundant queues for communication partners
|
||||
|
||||
- Providing end-to-end encryption of messages
|
||||
|
||||
- Rotating queues periodically with communication partners
|
||||
|
||||
- Noise traffic
|
||||
|
||||
#### Encryption Primitives Used
|
||||
|
||||
- Ed448 to sign/verify commands to SMP servers (Ed25519 is also supported via client/server configuration).
|
||||
- Curve25519 for DH exchange to agree:
|
||||
- the shared secret between server and recipient (to encrypt message bodies - it avoids shared cipher-text in sender and recipient traffic)
|
||||
- the shared secret between sender and recipient (to encrypt messages end-to-end in each queue - it avoids shared cipher-text in redundant queues).
|
||||
- [NaCl crypto_box](https://nacl.cr.yp.to/box.html) encryption scheme (curve25519xsalsa20poly1305) for message body encryption between server and recipient and for E2E per-queue encryption.
|
||||
- SHA256 to validate server offline certificates.
|
||||
- [double ratchet](https://signal.org/docs/specifications/doubleratchet/) protocol for end-to-end message encryption between the agents:
|
||||
- Curve448 keys to agree shared secrets required for double ratchet initialization (using [X3DH](https://signal.org/docs/specifications/x3dh/) key agreement with 2 ephemeral keys for each side),
|
||||
- AES-GCM AEAD cipher,
|
||||
- SHA512-based HKDF for key derivation.
|
||||
|
||||
## Threat Model
|
||||
|
||||
#### Global Assumptions
|
||||
|
||||
- A user protects their local database and key material
|
||||
- The user's application is authentic, and no local malware is running
|
||||
- The cryptographic primitives in use are not broken
|
||||
- A user's choice of servers is not directly tied to their identity or otherwise represents distinguishing information about the user.
|
||||
|
||||
#### A passive adversary able to monitor the traffic of one user
|
||||
|
||||
*can:*
|
||||
|
||||
- identify that and when a user is using SimpleX
|
||||
|
||||
- block SimpleX traffic
|
||||
|
||||
- determine which servers the user communicates with
|
||||
|
||||
- observe how much traffic is being sent, and make guesses as to its purpose.
|
||||
|
||||
*cannot:*
|
||||
|
||||
- see who sends messages to the user and who the user sends the messages to
|
||||
|
||||
#### A passive adversary able to monitor a set of senders and recipients
|
||||
|
||||
*can:*
|
||||
|
||||
- identify who and when is using SimpleX
|
||||
|
||||
- learn which SimpleX Messaging Protocol servers are used as receive queues for which users
|
||||
|
||||
- learn when messages are sent and received
|
||||
|
||||
- perform traffic correlation attacks against senders and recipients and correlate senders and recipients within the monitored set, frustrated by the number of users on the servers
|
||||
|
||||
- observe how much traffic is being sent, and make guesses as to its purpose
|
||||
|
||||
*cannot, even in case of a compromised transport protocol:*
|
||||
|
||||
- perform traffic correlation attacks with any increase in efficiency over a non-compromised transport protocol
|
||||
|
||||
#### SimpleX Messaging Protocol server
|
||||
|
||||
*can:*
|
||||
|
||||
- learn when a queue recipient or sender is online
|
||||
|
||||
- know how many messages are sent via the queue (although some may be noise)
|
||||
|
||||
- perform queue correlation (matching multiple queues to a single user) via either a re-used transport connection, user's IP Address, or connection timing regularities
|
||||
|
||||
- learn a user's IP address, track them through other IP addresses they use to access the same queue, and infer information (e.g. employer) based on the IP addresses, as long as Tor is not used.
|
||||
|
||||
- drop all future messages inserted into a queue, detectable only over other, redundant queues
|
||||
|
||||
- lie about the state of a queue to the recipient and/or to the sender (e.g. suspended or deleted when it is not).
|
||||
|
||||
- spam a user with invalid messages
|
||||
|
||||
*cannot:*
|
||||
|
||||
- undetectably add, duplicate, or corrupt individual messages
|
||||
|
||||
- undetectably drop individual messages, so long as a subsequent message is delivered
|
||||
|
||||
- learn the contents of messages
|
||||
|
||||
- distinguish noise messages from regular messages except via timing regularities
|
||||
|
||||
- compromise the user's end-to-end encryption with an active attack
|
||||
|
||||
#### An attacker who obtained Alice's (decrypted) chat database
|
||||
|
||||
*can:*
|
||||
|
||||
- see the history of all messages exchanged by Alice with her communication partners
|
||||
|
||||
- see shared profiles of contacts and groups
|
||||
|
||||
- surreptitiously receive new messages sent to Alice via existing queues; until communication queues are rotated or the Double-Ratchet advances forward
|
||||
|
||||
- prevent Alice from receiving all new messages sent to her - either surreptitiously by emptying the queues regularly or overtly by deleting them
|
||||
|
||||
- send messages from the user to their contacts; recipients will detect it as soon as the user sends the next message, because the previous message hash won’t match (and potentially won’t be able to decrypt them in case they don’t keep the previous ratchet keys).
|
||||
|
||||
*cannot:*
|
||||
|
||||
- impersonate a sender and send messages to the user whose database was stolen. Doing so requires also compromising the server (to place the message in the queue, that is possible until the Double-Ratchet advances forward) or the user's device at a subsequent time (to place the message in the database).
|
||||
|
||||
- undetectably communicate at the same time as Alice with her contacts. Doing so would result in the contact getting different messages with repeated IDs.
|
||||
|
||||
- undetectably monitor message queues in realtime without alerting the user they are doing so, as a second subscription request unsubscribes the first and notifies the second.
|
||||
|
||||
#### A user’s contact
|
||||
|
||||
*can:*
|
||||
|
||||
- spam the user with messages
|
||||
|
||||
- forever retain messages from the user
|
||||
|
||||
*cannot:*
|
||||
|
||||
- cryptographically prove to a third-party that a message came from a user (assuming the user’s device is not seized)
|
||||
|
||||
- prove that two contacts they have is the same user
|
||||
|
||||
- cannot collaborate with another of the user's contacts to confirm they are communicating with the same user
|
||||
|
||||
#### An attacker who observes Alice showing an introduction message to Bob
|
||||
|
||||
*can:*
|
||||
|
||||
- Impersonate Bob to Alice
|
||||
|
||||
*cannot:*
|
||||
|
||||
- Impersonate Alice to Bob
|
||||
|
||||
#### An attacker with Internet access
|
||||
|
||||
*can:*
|
||||
|
||||
- Denial of Service SimpleX messaging servers
|
||||
|
||||
- spam a user's public “contact queue” with connection requests
|
||||
|
||||
*cannot:*
|
||||
|
||||
- send messages to a user who they are not connected with
|
||||
|
||||
- enumerate queues on a SimpleX server
|
||||
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Efim Poberezkin contributed to the design and implementation of [SimpleX Messaging Protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md) and [SimpleX Agent Protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md) since 2019.
|
||||
|
||||
Adam Langley's [Pond](https://github.com/agl/pond) inspired some of the recent improvements and the structure of this document.
|
||||
@@ -0,0 +1,908 @@
|
||||
# Simplex Messaging Protocol (SMP)
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Abstract](#abstract)
|
||||
- [Introduction](#introduction)
|
||||
- [SMP Model](#smp-model)
|
||||
- [Out-of-band messages](#out-of-band-messages)
|
||||
- [Simplex queue](#simplex-queue)
|
||||
- [SMP queue URI](#smp-queue-uri)
|
||||
- [SMP procedure](#smp-procedure)
|
||||
- [SMP qualities and features](#smp-qualities-and-features)
|
||||
- [Cryptographic algorithms](#cryptographic-algorithms)
|
||||
- [Simplex queue IDs](#simplex-queue-ids)
|
||||
- [Server security requirements](#server-security-requirements)
|
||||
- [Message delivery notifications](#message-delivery-notifications)
|
||||
- [SMP Transmission structure](#smp-transmission-structure)
|
||||
- [SMP commands](#smp-commands)
|
||||
- [Correlating responses with commands](#correlating-responses-with-commands)
|
||||
- [Command authentication](#command-authentication)
|
||||
- [Keep-alive command](#keep-alive-command)
|
||||
- [Recipient commands](#recipient-commands)
|
||||
- [Create queue command](#create-queue-command)
|
||||
- [Subscribe to queue](#subscribe-to-queue)
|
||||
- [Secure queue command](#secure-queue-command)
|
||||
- [Enable notifications command](#enable-notifications-command)
|
||||
- [Disable notifications command](#disable-notifications-command)
|
||||
- [Acknowledge message delivery](#acknowledge-message-delivery)
|
||||
- [Suspend queue](#suspend-queue)
|
||||
- [Delete queue](#delete-queue)
|
||||
- [Sender commands](#sender-commands)
|
||||
- [Send message](#send-message)
|
||||
- [Notifier commands](#notifier-commands)
|
||||
- [Subscribe to queue notifications](#subscribe-to-queue-notifications)
|
||||
- [Server messages](#server-messages)
|
||||
- [Queue IDs response](#queue-ids-response)
|
||||
- [Deliver queue message](#deliver-queue-message)
|
||||
- [Notifier queue ID response](#notifier-queue-id-response)
|
||||
- [Deliver message notification](#deliver-message-notification)
|
||||
- [Subscription END notification](#subscription-end-notification)
|
||||
- [Error responses](#error-responses)
|
||||
- [OK response](#ok-response)
|
||||
- [Appendices](#appendices)
|
||||
- [Appendix A. Transport connection with the SMP server](#appendix-a)
|
||||
|
||||
## Abstract
|
||||
|
||||
Simplex Messaging Protocol is a transport agnostic client-server protocol for asynchronous distributed secure unidirectional message transmission via persistent simplex message queues.
|
||||
|
||||
It's designed with the focus on communication security and integrity, under the assumption that any part of the message transmission network can be compromised.
|
||||
|
||||
It is designed as a low level protocol for other application protocols to solve the problem of secure and private message transmission, making [MITM attack][1] very difficult at any part of the message transmission system.
|
||||
|
||||
This document describes SMP protocol versions 3 and 4, the previous versions are discontinued.
|
||||
|
||||
## Introduction
|
||||
|
||||
The objective of Simplex Messaging Protocol (SMP) is to facilitate the secure and private unidirectional transfer of messages from senders to recipients via persistent simplex queues managed by the message broker (server).
|
||||
|
||||
SMP is independent of any particular transmission system and requires only a reliable ordered data stream channel. While this document describes transport over TCP, other transports are also possible.
|
||||
|
||||
The protocol describes the set of commands that recipients and senders can exchange with SMP servers to create and to operate unidirectional "queues" (a data abstraction identifying one of many communication channels managed by the server) and to send messages from the sender to the recipient via the SMP server.
|
||||
|
||||
More complex communication scenarios can be designed using multiple queues - for example, a duplex communication channel can be made of 2 simplex queues.
|
||||
|
||||
The protocol is designed with the focus on privacy and security, to some extent deprioritizing reliability by requiring that SMP servers only store messages until they are acknowledged by the recipients and, in any case, for a limited period of time. For communication scenarios requiring more reliable transmission the users should use several SMP servers to pass each message and implement some additional protocol to ensure that messages are not removed, inserted or changed - this is out of scope of this document.
|
||||
|
||||
SMP does not use any form of participants' identities and provides [E2EE][2] without the possibility of [MITM attack][1] relying on two pre-requisites:
|
||||
|
||||
- the users can establish a secure encrypted transport connection with the SMP server. [Appendix A](#appendix-a) describes SMP transport protocol of such connection over TCP, but any other transport connection protocol can be used.
|
||||
|
||||
- the recipient can pass a single message to the sender via a pre-existing secure and private communication channel (out-of-band message) - the information in this message is used to encrypt messages and to establish connection with SMP server.
|
||||
|
||||
## SMP Model
|
||||
|
||||
The SMP model has three communication participants: the recipient, the message broker (SMP server) that is chosen and, possibly, controlled by the recipient, and the sender.
|
||||
|
||||
SMP server manages multiple "simplex queues" - data records on the server that identify communication channels from the senders to the recipients. The same communicating party that is the sender in one queue, can be the recipient in another - without exposing this fact to the server.
|
||||
|
||||
The queue record consists of 2 unique random IDs generated by the server, one for the recipient and another for the sender, and 2 keys to authenticate the recipient and the sender respectively, provided by the client. The users of SMP protocol must use a unique key for each queue, to avoid the possibility of aggregating and analyzing their queues in case SMP server is compromised.
|
||||
|
||||
Creating and using the queue requires sending commands to the SMP server from the recipient and the sender - they are described in detail in [SMP commands](#smp-commands) section.
|
||||
|
||||
## Out-of-band messages
|
||||
|
||||
The out-of-band message with the queue information is sent via some trusted alternative channel from the recipient to the sender. This message is used to share one or several [queue URIs](#smp-queue-uri) that parties can use to establish the initial connection, the encryption scheme and, it can include the public key(s) for end-to-end encryption.
|
||||
|
||||
The approach to out-of-band message passing and their syntax should be defined in application-level protocols.
|
||||
|
||||
## Simplex queue
|
||||
|
||||
The simplex queue is the main unit of SMP protocol. It is used by:
|
||||
|
||||
- Sender of the queue (who received out-of-band message) to send messages to the server using sender's queue ID, signed by sender's key.
|
||||
|
||||
- Recipient of the queue (who created the queue and sent out-of-band message) will use it to retrieve messages from the server, signing the commands by the recipient key. Recipient decrypts the messages with the key negotiated during the creation of the queue.
|
||||
|
||||
- Participant identities are not shared with the server - new unique keys and queue IDs are used for each queue.
|
||||
|
||||
This simplex queue can serve as a building block for more complex communication network. For example, two (or more, for redundancy) simplex queues can be used to create a duplex communication channel. Higher level primitives that are only known to system participants in their client applications can be created as well - e.g., contacts, conversations, groups and broadcasts. Simplex messaging servers only have the information about the low-level simplex queues. In this way a high level of privacy and security of the communication is provided. Application level primitives are not in scope of this protocol.
|
||||
|
||||
This approach is based on the concept of [unidirectional networks][4] that are used for applications with high level of information security.
|
||||
|
||||
Access to each queue is controlled with unique (not shared with other queues) asymmetric key pairs, separate for the sender and the recipient. The sender and the receiver have private keys, and the server has associated public keys to authenticate participants' commands by verifying cryptographic signatures.
|
||||
|
||||
The messages sent over the queue are end-to-end encrypted using the DH secret agreed via out-of-band message and SMP confirmation.
|
||||
|
||||
**Simplex queue diagram:**
|
||||
|
||||

|
||||
|
||||
Queue is defined by recipient ID `RID` and sender ID `SID`, unique for the server. Sender key (`SK`) is used by the server to verify sender's commands (identified by `SID`) to send messages. Recipient key (`RK`) is used by the server to verify recipient's commands (identified by `RID`) to retrieve messages.
|
||||
|
||||
The protocol uses different IDs for sender and recipient in order to provide an additional privacy by preventing the correlation of senders and recipients commands sent over the network - in case the encrypted transport is compromised, it would still be difficult to correlate senders and recipients without access to the queue records on the server.
|
||||
|
||||
## SMP queue URI
|
||||
|
||||
The SMP queue URIs MUST include server identity, queue hostname, an optional port, sender queue ID and the public key that the clients must use to verify responses. Server identity is used to establish secure connection protected from MITM attack with SMP server (see [Appendix A](#appendix-a) for SMP transport protocol).
|
||||
|
||||
The [ABNF][8] syntax of the queue URI is:
|
||||
|
||||
```abnf
|
||||
queueURI = %s"smp://" smpServer "/" queueId "#" recipientDhPublicKey
|
||||
smpServer = serverIdentity "@" srvHost [":" port]
|
||||
srvHost = <hostname> ; RFC1123, RFC5891
|
||||
port = 1*DIGIT
|
||||
serverIdentity = base64url
|
||||
queueId = base64url
|
||||
base64url = <base64url encoded binary> ; RFC4648, section 5
|
||||
recipientDhPublicKey = x509UrlEncoded
|
||||
; the recipient's Curve25519 key for DH exchange to derive the secret
|
||||
; that the sender will use to encrypt delivered messages
|
||||
; using [NaCl crypto_box][16] encryption scheme (curve25519xsalsa20poly1305).
|
||||
|
||||
x509UrlEncoded = <base64url X509 key encoding>
|
||||
```
|
||||
|
||||
`hostname` can be IP address or domain name, as defined in RFC 1123, section 2.1.
|
||||
|
||||
`port` is optional, the default TCP port for SMP protocol is 5223.
|
||||
|
||||
`serverIdentity` is a required hash of the server certificate SPKI block (without line breaks, header and footer) used by the client to validate server certificate during transport handshake (see [Appendix A](#appendix-a))
|
||||
|
||||
## SMP procedure
|
||||
|
||||
The SMP procedure of creating a simplex queue on SMP server is explained using participants Alice (the recipient) who wants to receive messages from Bob (the sender).
|
||||
|
||||
To create and start using a simplex queue Alice and Bob follow these steps:
|
||||
|
||||
1. Alice creates a simplex queue on the server:
|
||||
|
||||
1. Decides which SMP server to use (can be the same or different server that Alice uses for other queues) and opens secure encrypted transport connection to the chosen SMP server (see [Appendix A](#appendix-a)).
|
||||
|
||||
2. Generates a new random public/private key pair (encryption key - `EK`) that she did not use before for Bob to encrypt the messages.
|
||||
|
||||
3. Generates another new random public/private key pair (recipient key - `RK`) that she did not use before for her to sign commands and to decrypt the transmissions received from the server.
|
||||
|
||||
4. Generates one more random key pair (recipient DH key - `RDHK`) to negotiate symmetric key that will be used by the server to encrypt message bodies delivered to Alice (to avoid shared cipher-text inside transport connection).
|
||||
|
||||
5. Sends `"NEW"` command to the server to create a simplex queue (see `create` in [Create queue command](#create-queue-command)). This command contains previously generated unique "public" keys `RK` and `RDHK`. `RK` will be used to verify the following commands related to the same queue signed by its private counterpart, for example to subscribe to the messages received to this queue or to update the queue, e.g. by setting the key required to send the messages (initially Alice creates the queue that accepts unsigned messages, so anybody could send the message via this queue if they knew the queue sender's ID and server address).
|
||||
|
||||
6. The server sends `"IDS"` response with queue IDs (`queueIds`):
|
||||
|
||||
- Recipient ID `RID` for Alice to manage the queue and to receive the messages.
|
||||
|
||||
- Sender ID `SID` for Bob to send messages to the queue.
|
||||
|
||||
- Server public DH key (`SDHK`) to negotiate a shared secret for message body encryption, that Alice uses to derive a shared secret with the server `SS`.
|
||||
|
||||
2. Alice sends an out-of-band message to Bob via the alternative channel that both Alice and Bob trust (see [protocol abstract](#simplex-messaging-protocol-abstract)). The message must include:
|
||||
|
||||
- Unique "public" key (`EK`) that Bob must use for E2E key agreement.
|
||||
|
||||
- SMP server hostname and information to open secure encrypted transport connection (see [Appendix A](#appendix-a)).
|
||||
|
||||
- Sender queue ID `SID` for Bob to use.
|
||||
|
||||
3. Bob, having received the out-of-band message from Alice, connects to the queue:
|
||||
|
||||
1. Generates a new random public/private key pair (sender key - `SK`) that he did not use before for him to sign messages sent to Alice's server.
|
||||
|
||||
2. Prepares the confirmation message for Alice to secure the queue. This message includes:
|
||||
|
||||
- Previously generated "public" key `SK` that will be used by Alice's server to authenticate Bob's messages, once the queue is secured.
|
||||
|
||||
- Optionally, any additional information (application specific, e.g. Bob's profile name and details).
|
||||
|
||||
3. Encrypts the confirmation body with the "public" key `EK` (that Alice provided via the out-of-band message).
|
||||
|
||||
4. Sends the encrypted message to the server with queue ID `SID` (see `send` in [Send message](#send-message)). This initial message to the queue must not be signed - signed messages will be rejected until Alice secures the queue (below).
|
||||
|
||||
4. Alice receives Bob's message from the server using recipient queue ID `RID` (possibly, via the same transport connection she already has opened - see `message` in [Deliver queue message](#deliver-queue-message)):
|
||||
|
||||
1. She decrypts received message body using the secret `SS`.
|
||||
|
||||
2. She decrypts received message with [key agreed with sender using] "private" key `EK`.
|
||||
|
||||
3. Even though anybody could have sent the message to the queue with ID `SID` before it is secured (e.g. if communication is compromised), Alice would ignore all messages until the decryption succeeds (i.e. the result contains the expected message format). Optionally, in the client application, she also may identify Bob using the information provided, but it is out of scope of SMP protocol.
|
||||
|
||||
5. Alice secures the queue `RID` with `"KEY"` command so only Bob can send messages to it (see [Secure queue command](#secure-queue-command)):
|
||||
|
||||
1. She sends the `KEY` command with `RID` signed with "private" key `RK` to update the queue to only accept requests signed by "private" key `SK` provided by Bob. This command contains unique "public" key `SK` previously generated by Bob.
|
||||
|
||||
2. From this moment the server will accept only signed commands to `SID`, so only Bob will be able to send messages to the queue `SID` (corresponding to `RID` that Alice has).
|
||||
|
||||
3. Once queue is secured, Alice deletes `SID` and `SK` - even if Alice's client is compromised in the future, the attacker would not be able to send messages pretending to be Bob.
|
||||
|
||||
6. The simplex queue `RID` is now ready to be used.
|
||||
|
||||
This flow is shown on the sequence diagram below.
|
||||
|
||||
**Creating simplex queue from Bob to Alice:**
|
||||
|
||||

|
||||
|
||||
Bob now can securely send messages to Alice:
|
||||
|
||||
1. Bob sends the message:
|
||||
|
||||
1. He encrypts the message to Alice with "public" key `EK` (provided by Alice, only known to Bob, used only for one simplex queue).
|
||||
|
||||
2. He signs `"SEND"` command to the server queue `SID` using the "private" key `SK` (that only he knows, used only for this queue).
|
||||
|
||||
3. He sends the command to the server (see `send` in [Send message](#send-message)), that the server will authenticate using the "public" key `SK` (that Alice earlier received from Bob and provided to the server via `"KEY"` command).
|
||||
|
||||
2. Alice receives the message(s):
|
||||
|
||||
1. She signs `"SUB"` command to the server to subscribe to the queue `RID` with the "private" key `RK` (see `subscribe` in [Subscribe to queue](#subscribe-to-queue)).
|
||||
|
||||
2. The server, having authenticated Alice's command with the "public" key `RK` that she provided, delivers Bob's message(s) (see `message` in [Deliver queue message](#deliver-queue-message)).
|
||||
|
||||
3. She decrypts Bob's message(s) with the "private" key `EK` (that only she has).
|
||||
|
||||
4. She acknowledges the message reception to the server with `"ACK"` so that the server can delete the message and deliver the next messages.
|
||||
|
||||
This flow is show on sequence diagram below.
|
||||
|
||||
**Sending messages from Bob to Alice via simplex queue:**
|
||||
|
||||

|
||||
|
||||
**Simplex queue operation:**
|
||||
|
||||

|
||||
|
||||
Sequence diagram does not show E2E encryption - server knows nothing about encryption between the sender and the receiver.
|
||||
|
||||
A higher level protocol application protocol should define the semantics that allow to use two simplex queues (or two sets of queues for redundancy) for the bi-directional or any other communication scenarios.
|
||||
|
||||
The SMP is intentionally unidirectional - it provides no answer to how Bob will know that the transmission succeeded, and whether Alice received any messages. There may be a scenario when Alice wants to securely receive the messages from Bob, but she does not want Bob to have any proof that she received any messages - this low-level protocol can be used in this scenario, as all Bob knows as a fact is that he was able to send one unsigned message to the server that Alice provided, and now he can only send messages signed with the key `SK` that he sent to the server - it does not prove that any message was received by Alice.
|
||||
|
||||
For practical purposes of bi-directional conversation, now that Bob can securely send encrypted messages to Alice, Bob can create the second simplex queue that will allow Alice to send messages to Bob in the same way, sending the second queue details via the first queue. If both Alice and Bob have their respective unique "public" keys (Alice's and Bob's `EK`s of two separate queues), or pass additional keys to sign the messages, the conversation can be both encrypted and signed.
|
||||
|
||||
The established queues can also be used to change the encryption keys providing [forward secrecy][5], or to negotiate using other SMP queue(s).
|
||||
|
||||
This protocol also can be used for off-the-record messaging, as Alice and Bob can use multiple queues between them and only information they pass to each other allows proving their identity, so if they want to share anything off-the-record they can initiate a new queue without linking it to any other information they exchanged. As a result, this protocol provides better anonymity and better protection from [MITM][1] than [OTR][6] protocol.
|
||||
|
||||
## SMP qualities and features
|
||||
|
||||
Simplex Messaging Protocol:
|
||||
|
||||
- Defines only message-passing protocol:
|
||||
|
||||
- Transport agnostic - the protocol does not define how clients connect to the servers. It can be implemented over any ordered data stream channel: TCP connection, HTTP with long polling, websockets, etc.
|
||||
|
||||
- Not semantic - the protocol does not assign any meaning to queues and messages. While on the application level the queues and messages can have different meaning (e.g., for messages: text or image chat message, message acknowledgement, participant profile information, status updates, changing "public" key to encrypt messages, changing servers, etc.), on SMP protocol level all the messages are binary and their meaning can only be interpreted by client applications and not by the servers - this interpretation is out of scope of this protocol.
|
||||
|
||||
- Client-server architecture:
|
||||
|
||||
- Multiple servers, that can be deployed by the system users, can be used to send and retrieve messages.
|
||||
|
||||
- Servers do not communicate with each other and do not "know" about other servers.
|
||||
|
||||
- Clients only communicate with servers (excluding the initial out-of-band message), so the message passing is asynchronous.
|
||||
|
||||
- For each queue, the message recipient defines the server through which the sender should send messages.
|
||||
|
||||
- While multiple servers and multiple queues can be used to pass each message, it is in scope of application level protocol(s), and out of scope of this protocol.
|
||||
|
||||
- Servers store messages only until they are retrieved by the recipients, and in any case, for a limited time.
|
||||
|
||||
- Servers are required to NOT store any message history or delivery log, but even if the server is compromised, it does not allow to decrypt the messages or to determine the list of queues established by any participant - this information is only stored on client devices.
|
||||
|
||||
- The only element provided by SMP servers is simplex queues:
|
||||
|
||||
- Each queue is created and managed by the queue recipient.
|
||||
|
||||
- Asymmetric encryption is used to sign and verify the requests to send and receive the messages.
|
||||
|
||||
- One unique "public" key is used by the servers to authenticate requests to send the messages into the queue, and another unique "public" key - to retrieve the messages from the queue. "Unique" here means that each "public" key is used only for one queue and is not used for any other context - effectively, this key is not public and does not represent any participant identity.
|
||||
|
||||
- Both recipient and sender "public" keys are provided to the server by the queue recipient. "Public" key `RK` is provided when the queue is created, public key `SK` is provided when the queue is secured.
|
||||
|
||||
- The "public" keys known to the server and used to authenticate commands from the participants are unrelated to the keys used to encrypt and decrypt the messages - the latter keys are also unique per each queue but they are only known to participants, not to the servers.
|
||||
|
||||
- Messaging graph can be asymmetric: Bob's ability to send messages to Alice does not automatically lead to the Alice's ability to send messages to Bob.
|
||||
|
||||
## Cryptographic algorithms
|
||||
|
||||
Simplex messaging clients and servers must cryptographically sign commands, responses and messages for the following operations:
|
||||
|
||||
- With the recipient's key `RK` (server to verify):
|
||||
- create the queue (`NEW`)
|
||||
- subscribe to queue (`SUB`)
|
||||
- secure the queue (`KEY`)
|
||||
- enable queue notifications (`NKEY`)
|
||||
- acknowledge received messages (`ACK`)
|
||||
- suspend the queue (`OFF`)
|
||||
- delete the queue (`DEL`)
|
||||
- With the sender's key `SK` (server to verify):
|
||||
- send messages (`SEND`)
|
||||
- With the optional notifier's key:
|
||||
- subscribe to message notifications (`NSUB`)
|
||||
- With the server's key (for recipient and sender to verify)
|
||||
- queue IDs response (`IDS`)
|
||||
- notifier queue ID response (`NID`)
|
||||
- delivered messages (`MSG`)
|
||||
- `OK` and `ERR` responses (excluding error responses not related to a queue)
|
||||
|
||||
To sign/verify transmissions clients and servers MUST use Ed25519 or Ed448 algorithm defined in [RFC8709][15].
|
||||
|
||||
To encrypt/decrypt message bodies delivered to the recipients, servers/clients MUST use x25519 or x448 algorithm defined in [RFC8709][15] to derive the shared secret (TODO encryption scheme).
|
||||
|
||||
Clients MUST encrypt message bodies sent via SMP servers - the protocol for this end-to-end encryption should be chosen by the clients using SMP protocol.
|
||||
|
||||
The reasons to use these algorithms:
|
||||
|
||||
- Faster operation than RSA algorithms.
|
||||
- DH key exchange provides forward secrecy.
|
||||
|
||||
Future versions of the protocol may allow different cryptographic algorithms.
|
||||
|
||||
## Simplex queue IDs
|
||||
|
||||
Simplex messaging servers MUST generate 2 different IDs for each new queue - for the recipient (that created the queue) and for the sender. It is REQUIRED that:
|
||||
|
||||
- These IDs are different and unique within the server.
|
||||
- Based on random bytes generated with cryptographically strong pseudo-random number generator.
|
||||
|
||||
## Server security requirements
|
||||
|
||||
Simplex messaging server implementations MUST NOT create, store or send to any other servers:
|
||||
|
||||
- Logs of the client commands and transport connections in the production environment.
|
||||
|
||||
- History of deleted queues, retrieved or acknowledged messages (deleted queues MAY be stored temporarily as part of the queue persistence implementation).
|
||||
|
||||
- Snapshots of the database they use to store queues and messages (instead simplex messaging clients must manage redundancy by using more than one simplex messaging server). In-memory persistence is recommended.
|
||||
|
||||
- Any other information that may compromise privacy or [forward secrecy][4] of communication between clients using simplex messaging servers.
|
||||
|
||||
## Message delivery notifications
|
||||
|
||||
Supporting message delivery while the client mobile app is not running requires sending push notifications with the device token. All alternative mechanisms for background message delivery are unreliable, particularly on iOS platform. Obviously, supporting push notification delivery by simply subscribing to messages would reduce meta-data privacy as it allows to see all queues that a given device uses.
|
||||
|
||||
To protect the privacy of the recipients, there are several commands in SMP protocol that allow enabling and subscribing to message notifications from SMP queues, using separate set of "notifier keys" and via separate queue IDs - as long as SMP server is not compromised, these notifier queue IDs cannot be correlated with recipient or sender queue IDs.
|
||||
|
||||
The clients can optionally instruct a dedicated push notification server to subscribe to notifications and deliver push notifications to the device, which can then retrieve the messages in the background and send local notifications to the user - this is out of scope of SMP protocol. The commands that SMP protocol provides to allow it:
|
||||
|
||||
- `enableNotifications` (`"NKEY"`) with `notifierId` (`"NID"`) response - see [Enable notifications command](#enable-notifications-command).
|
||||
- `disableNotifications` (`"NDEL"`) - see [Disable notifications command](#disable-notifications-command).
|
||||
- `subscribeNotifications` (`"NSUB"`) - see [Subscribe to queue notifications](#subscribe-to-queue-notifications).
|
||||
- `messageNotification` (`"NMSG"`) - see [Deliver message notification](#deliver-message-notification).
|
||||
|
||||
[`SEND` command](#send-message) includes the notification flag to instruct SMP server whether to send the notification - this flag is forwarded to the recepient inside encrypted envelope, together with the timestamp and the message body, so even if TLS is compromised this flag cannot be used for traffic correlation.
|
||||
|
||||
## SMP Transmission andtransport block structure
|
||||
|
||||
Each transport block (SMP transmission) has a fixed size of 16384 bytes for traffic uniformity.
|
||||
|
||||
From SMP version 4 each block can contain multiple transmissions, version 3 blocks have 1 transmission.
|
||||
Some parts of SMP transmission are padded to a fixed size; this padding is uniformly added as a word16 encoded in network byte order - see `paddedString` syntax.
|
||||
|
||||
In places where some part of the transmission should be padded, the syntax for `paddedNotation` is used:
|
||||
|
||||
```abnf
|
||||
paddedString = originalLength string pad
|
||||
originalLength = 2*2 OCTET
|
||||
pad = N*N"#" ; where N = paddedLength - originalLength - 2
|
||||
|
||||
paddedNotation = <padded(string, paddedLength)>
|
||||
; string - un-padded string
|
||||
; paddedLength - required length after padding, including 2 bytes for originalLength
|
||||
```
|
||||
|
||||
Each transmission/block for SMP v3 between the client and the server must have this format/syntax:
|
||||
|
||||
```abnf
|
||||
paddedTransmission = <padded(transmission), 16384>
|
||||
transmission = [signature] SP signed
|
||||
signed = sessionIdentifier SP [corrId] SP [queueId] SP smpCommand
|
||||
; corrId is required in client commands and server responses,
|
||||
; it is empty in server notifications.
|
||||
corrId = 1*32(%x21-7F) ; any characters other than control/whitespace
|
||||
queueId = encoded ; max 32 bytes when decoded (24 bytes is used),
|
||||
; empty queue ID is used with "create" command and in some server responses
|
||||
signature = encoded
|
||||
; empty signature can be used with "send" before the queue is secured with secure command
|
||||
; signature is always empty with "ping" and "serverMsg"
|
||||
encoded = <base64 encoded binary>
|
||||
```
|
||||
|
||||
`base64` encoding should be used with padding, as defined in section 4 of [RFC 4648][9]
|
||||
|
||||
Transport block for SMP v4 has this syntax:
|
||||
|
||||
```abnf
|
||||
paddedTransportBlock = <padded(transportBlock), 16384>
|
||||
transportBlock = transmissionCount transmissions
|
||||
transmissionCount = 1*1 OCTET ; equal or greater than 1
|
||||
transmissions = transmissionLength transmission [transmissions]
|
||||
transmissionLength = 2*2 OCTET ; word16 encoded in network byte order
|
||||
```
|
||||
|
||||
## SMP commands
|
||||
|
||||
Commands syntax below is provided using [ABNF][8] with [case-sensitive strings extension][8a].
|
||||
|
||||
```abnf
|
||||
smpCommand = ping / recipientCmd / send / subscribeNotifications / serverMsg
|
||||
recipientCmd = create / subscribe / secure / enableNotifications / disableNotifications /
|
||||
acknowledge / suspend / delete
|
||||
serverMsg = queueIds / message / notifierId / messageNotification /
|
||||
unsubscribed / ok / error
|
||||
```
|
||||
|
||||
The syntax of specific commands and responses is defined below.
|
||||
|
||||
### Correlating responses with commands
|
||||
|
||||
The server should send `queueIds`, `error` and `ok` responses in the same order within each queue ID as the commands received in the transport connection, so that they can be correlated by the clients. To simplify correlation of commands and responses, the server must use the same `corrId` in the response as in the command sent by the client.
|
||||
|
||||
If the transport connection is closed before some responses are sent, these responses should be discarded.
|
||||
|
||||
### Command authentication
|
||||
|
||||
SMP servers must authenticate all transmissions (excluding `ping` and initial `send` commands) by verifying the client signatures. Command signature should be generated by applying the algorithm specified for the queue to the `signed` block of the transmission, using the key associated with the queue ID (recipient's, sender's or notifier's, depending on which queue ID is used).
|
||||
|
||||
### Keep-alive command
|
||||
|
||||
To keep the transport connection alive and to generate noise traffic the clients should use `ping` command to which the server responds with `ok` response. This command should be sent unsigned and without queue ID.
|
||||
|
||||
```abnf
|
||||
ping = %s"PING"
|
||||
```
|
||||
|
||||
This command is always send unsigned.
|
||||
|
||||
### Recipient commands
|
||||
|
||||
Sending any of the commands in this section (other than `create`, that is sent without queue ID) is only allowed with recipient's ID (`RID`). If sender's ID is used the server must respond with `"ERR AUTH"` response (see [Error responses](#error-responses)).
|
||||
|
||||
#### Create queue command
|
||||
|
||||
This command is sent by the recipient to the SMP server to create a new queue. The syntax is:
|
||||
|
||||
```abnf
|
||||
create = %s"NEW " recipientSignaturePublicKey recipientDhPublicKey
|
||||
recipientSignaturePublicKey = length x509encoded
|
||||
; the recipient's Ed25519 or Ed448 public key to verify commands for this queue
|
||||
|
||||
recipientDhPublicKey = length x509encoded
|
||||
; the recipient's Curve25519 key for DH exchange to derive the secret
|
||||
; that the server will use to encrypt delivered message bodies
|
||||
; using [NaCl crypto_box][16] encryption scheme (curve25519xsalsa20poly1305).
|
||||
|
||||
x509encoded = <binary X509 key encoding>
|
||||
|
||||
length = 1*1 OCTET
|
||||
```
|
||||
|
||||
If the queue is created successfully, the server must send `queueIds` response with the recipient's and sender's queue IDs and public keys to sign all responses and messages and to encrypt delivered message bodies:
|
||||
|
||||
```abnf
|
||||
queueIds = %s"IDS " recipientId senderId srvDhPublicKey
|
||||
serverDhPublicKey = length x509encoded
|
||||
; the server's Curve25519 key for DH exchange to derive the secret
|
||||
; that the server will use to encrypt delivered message bodies to the recipient
|
||||
recipientId = length *OCTET ; 16-24 bytes
|
||||
senderId = length *OCTET ; 16-24 bytes
|
||||
```
|
||||
|
||||
Once the queue is created, the recipient gets automatically subscribed to receive the messages from that queue, until the transport connection is closed. The `subscribe` command is needed only to start receiving the messages from the existing queue when the new transport connection is opened.
|
||||
|
||||
`NEW` transmission MUST be signed using the private part of the `recipientSignaturePublicKey` – this verifies that the client has the private key that will be used to sign subsequent commands for this queue.
|
||||
|
||||
`IDS` response transmission MUST be sent with empty queue ID (the third part of the transmission).
|
||||
|
||||
#### Subscribe to queue
|
||||
|
||||
When the simplex queue was not created in the current transport connection, the recipient must use this command to start receiving messages from it:
|
||||
|
||||
```abnf
|
||||
subscribe = %s"SUB"
|
||||
```
|
||||
|
||||
If subscription is successful the server must respond with the first available message or with `ok` response if no messages are available. The recipient will continue receiving the messages from this queue until the transport connection is closed or until another transport connection subscribes to the same simplex queue - in this case the first subscription should be cancelled and [subscription END notification](#subscription-end-notification) delivered.
|
||||
|
||||
The first message will be delivered either immediately or as soon as it is available; to receive the following message the recipient must acknowledge the reception of the message (see [Acknowledge message delivery](#acknowledge-message-delivery)).
|
||||
|
||||
This transmission and its response MUST be signed.
|
||||
|
||||
#### Secure queue command
|
||||
|
||||
This command is sent by the recipient to the server to add sender's key to the queue:
|
||||
|
||||
```abnf
|
||||
secure = %s"KEY " senderSignaturePublicKey
|
||||
senderSignaturePublicKey = length x509encoded
|
||||
; the sender's Ed25519 or Ed448 key to verify SEND commands for this queue
|
||||
```
|
||||
|
||||
`senderKey` is received from the sender as part of the first message - see [Send Message](#send-message) command.
|
||||
|
||||
Once the queue is secured only signed messages can be sent to it.
|
||||
|
||||
#### Enable notifications command
|
||||
|
||||
This command is sent by the recipient to the server to add notifier's key to the queue, to allow push notifications server to receive notifications when the message arrives, via a separate queue ID, without receiving message content.
|
||||
|
||||
```abnf
|
||||
enableNotifications = %s"NKEY " notifierKey recipientNotificationDhPublicKey
|
||||
notifierKey = length x509encoded
|
||||
; the notifier's Ed25519 or Ed448 public key public key to verify NSUB command for this queue
|
||||
|
||||
recipientNotificationDhPublicKey = length x509encoded
|
||||
; the recipient's Curve25519 key for DH exchange to derive the secret
|
||||
; that the server will use to encrypt notification metadata (encryptedNMsgMeta in NMSG)
|
||||
; using [NaCl crypto_box][16] encryption scheme (curve25519xsalsa20poly1305).
|
||||
```
|
||||
|
||||
The server will respond with `notifierId` response if notifications were enabled and the notifier's key was successfully added to the queue:
|
||||
|
||||
```abnf
|
||||
notifierId = %s"NID " notifierId srvNotificationDhPublicKey
|
||||
notifierId = length *OCTET ; 16-24 bytes
|
||||
srvNotificationDhPublicKey = length x509encoded
|
||||
; the server's Curve25519 key for DH exchange to derive the secret
|
||||
; that the server will use to encrypt notification metadata to the recipient (encryptedNMsgMeta in NMSG)
|
||||
```
|
||||
|
||||
This response is sent with the recipient's queue ID (the third part of the transmission).
|
||||
|
||||
To receive the message notifications, `subscribeNotifications` command ("NSUB") must be sent signed with the notifier's key.
|
||||
|
||||
#### Disable notifications command
|
||||
|
||||
This command is sent by the recipient to the server to remove notifier's credentials from the queue:
|
||||
|
||||
```abnf
|
||||
disableNotifications = %s"NDEL"
|
||||
```
|
||||
|
||||
The server must respond `ok` to this command if it was successful.
|
||||
|
||||
Once notifier's credentials are removed server will no longer send "NMSG" for this queue to notifier.
|
||||
|
||||
#### Acknowledge message delivery
|
||||
|
||||
The recipient should send the acknowledgement of message delivery once the message was stored in the client, to notify the server that the message should be deleted:
|
||||
|
||||
```abnf
|
||||
acknowledge = %s"ACK"
|
||||
```
|
||||
|
||||
Even if acknowledgement is not sent by the recipient, the server should limit the time of message storage, whether it was delivered to the recipient or not.
|
||||
|
||||
Having received the acknowledgement, SMP server should immediately delete the message and then send the next available message or respond with `ok` if there are no more messages available in this simplex queue.
|
||||
|
||||
#### Suspend queue
|
||||
|
||||
The recipient can suspend a queue prior to deleting it to make sure that no messages are lost:
|
||||
|
||||
```abnf
|
||||
suspend = %s"OFF"
|
||||
```
|
||||
|
||||
The server must respond with `"ERR AUTH"` to any messages sent after the queue was suspended (see [Error responses](#error-responses)).
|
||||
|
||||
The server must respond `ok` to this command if it was successful.
|
||||
|
||||
This command can be sent multiple times (in case transport connection was interrupted and the response was not delivered), the server should still respond `ok` even if the queue is already suspended.
|
||||
|
||||
There is no command to resume the queue. Servers must delete suspended queues that were not deleted after some period of time.
|
||||
|
||||
#### Delete queue
|
||||
|
||||
The recipient can delete the queue, whether it was suspended or not.
|
||||
|
||||
All undelivered messages must be deleted as soon as this command is received, before the response is sent.
|
||||
|
||||
```abnf
|
||||
delete = %s"DEL"
|
||||
```
|
||||
|
||||
### Sender commands
|
||||
|
||||
Currently SMP defines only one command that can be used by senders - `send` message. This command must be used with sender's ID, if recipient's ID is used the server must respond with `"ERR AUTH"` response (see [Error responses](#error-responses)).
|
||||
|
||||
#### Send message
|
||||
|
||||
This command is sent to the server by the sender both to confirm the queue after the sender received out-of-band message from the recipient and to send messages after the queue is secured:
|
||||
|
||||
```abnf
|
||||
send = %s"SEND " msgFlags SP smpEncMessage
|
||||
msgFlags = notificationFlag reserved
|
||||
notificationFlag = %s"T" / %s"F"
|
||||
smpEncMessage = smpPubHeader sentMsgBody ; message up to 16088 bytes
|
||||
smpPubHeader = smpClientVersion ("1" senderPublicDhKey / "0")
|
||||
smpClientVersion = word16
|
||||
senderPublicDhKey = length x509encoded
|
||||
; sender's Curve25519 public key to agree DH secret for E2E encryption in this queue
|
||||
; it is only sent in confirmation message
|
||||
x509encoded = <binary X509 key encoding>
|
||||
sentMsgBody = 16032*16032 OCTET
|
||||
; E2E-encrypted smpClientMessage padded to 16032 bytes before encryption
|
||||
word16 = 2*2 OCTET
|
||||
```
|
||||
|
||||
The first message is sent to confirm the queue - it should contain sender's server key (see decrypted message syntax below) - this first message must be sent without signature.
|
||||
|
||||
Once the queue is secured (see [Secure queue command](#secure-queue-command)), the following send commands must be sent with the signature.
|
||||
|
||||
The server must respond with `"ERR AUTH"` response in the following cases:
|
||||
|
||||
- the queue does not exist or is suspended
|
||||
- the queue is secured but the transmission does NOT have a signature
|
||||
- the queue is NOT secured but the transmission has a signature
|
||||
|
||||
Until the queue is secured, the server should accept any number of unsigned messages - it both enables the legitimate sender to resend the confirmation in case of failure and also allows the simplex messaging client to ignore any confirmation messages that may be sent by the attackers (assuming they could have intercepted the queue ID in the server response, but do not have a correct encryption key passed to sender in out-of-band message).
|
||||
|
||||
The body should be encrypted with the recipient's "public" key (`EK`); once decrypted it must have this format:
|
||||
|
||||
```abnf
|
||||
sentMsgBody = <encrypted padded(smpClientMessage, 16032)>
|
||||
smpClientMessage = smpPrivHeader clientMsgBody
|
||||
smpPrivHeader = emptyHeader / smpConfirmationHeader
|
||||
emptyHeader = " "
|
||||
smpConfirmationHeader = %s"K" senderKey
|
||||
senderKey = length x509encoded
|
||||
; the sender's Ed25519 or Ed448 public key to sign SEND commands for this queue
|
||||
clientMsgBody = *OCTET ; up to 16016 in case of emptyHeader
|
||||
```
|
||||
|
||||
`clientHeader` in the initial unsigned message is used to transmit sender's server key and can be used in the future revisions of SMP protocol for other purposes.
|
||||
|
||||
SMP transmission structure for sent messages:
|
||||
|
||||
```
|
||||
------- transmission (= 16384 bytes)
|
||||
2 | originalLength
|
||||
276- | signature sessionId corrId queueId %s"SEND" SP (1+114 + 1+32? + 1+32 + 1+24 + 4+1 = 210)
|
||||
....... smpEncMessage (= 16088 bytes = 16384 - 296 bytes)
|
||||
8- | smpPubHeader (for messages it is only version and '0' to mean "no DH key" = 3 bytes)
|
||||
24 | nonce for smpClientMessage
|
||||
16 | auth tag for smpClientMessage
|
||||
------- smpClientMessage (E2E encrypted, = 16032 bytes = 16088 - 48)
|
||||
2 | originalLength
|
||||
12- | smpPrivHeader
|
||||
.......
|
||||
| clientMsgBody (<= 16016 bytes = 16032 - 14)
|
||||
.......
|
||||
0+ | smpClientMessage pad
|
||||
------- smpClientMessage end
|
||||
|
|
||||
....... smpEncMessage end
|
||||
18+ | transmission pad
|
||||
------- transmission end
|
||||
```
|
||||
|
||||
SMP transmission structure for received messages:
|
||||
|
||||
```
|
||||
------- transmission (= 16384 bytes)
|
||||
2 | originalLength
|
||||
276- | signature sessionId corrId queueId %s"MSG" SP msgId timestamp (1+114 + 1+32? + 1+32 + 1+24 + 3+1 + 24+1 + 8 = 243)
|
||||
16 | auth tag (msgId is used as nonce)
|
||||
------- serverEncryptedMsg (= 16090 bytes = 16384 - 294 bytes)
|
||||
2 | originalLength
|
||||
....... smpEncMessage (= 16088 bytes = 16090 - 2 bytes)
|
||||
16- | smpPubHeader (empty header for the message)
|
||||
24 | nonce for smpClientMessage
|
||||
16 | auth tag for smpClientMessage
|
||||
------- smpClientMessage (E2E encrypted, = 16032 bytes = 16088 - 56 bytes)
|
||||
2 | originalLength
|
||||
16- | smpPrivHeader (empty header for the message)
|
||||
....... clientMsgBody (<= 16016 bytes = 16032 - 16)
|
||||
-- TODO move internal structure (below) to agent protocol
|
||||
20- | agentPublicHeader (the size is for user messages post handshake, without E2E X3DH keys - it is version and 'M' for the messages - 3 bytes in total)
|
||||
....... E2E double-ratchet encrypted (<= 15996 bytes = 16016 - 20)
|
||||
1 | encoded double ratchet header length (it is 123 now)
|
||||
123 | encoded double ratchet header, including:
|
||||
2 | version
|
||||
16 | double-ratchet header iv
|
||||
16 | double-ratchet header auth tag
|
||||
1+88 | double-ratchet header (actual size is 69 bytes, the rest is reserved)
|
||||
16 | message auth tag (IV generated from chain ratchet)
|
||||
------- encrypted agent message (= 15856 bytes = 15996 - 140)
|
||||
2 | originalLength
|
||||
64- | agentHeader (the actual size is 41 = 8 + 1+32)
|
||||
2 | %s"MM"
|
||||
.......
|
||||
| application message (<= 15788 bytes = 15856 - 68)
|
||||
.......
|
||||
0+ | encrypted agent message pad
|
||||
------- encrypted agent message end
|
||||
|
|
||||
....... E2E double-ratchet encrypted end
|
||||
|
|
||||
-- TODO move internal structure (above) to agent protocol
|
||||
....... clientMsgBody end
|
||||
0+ | smpClientMessage pad
|
||||
------- smpClientMessage end
|
||||
|
|
||||
....... smpEncMessage end
|
||||
0+ | serverEncryptedMsg pad
|
||||
------- serverEncryptedMsg end
|
||||
0+ | transmission pad
|
||||
------- transmission end
|
||||
```
|
||||
|
||||
### Notifier commands
|
||||
|
||||
#### Subscribe to queue notifications
|
||||
|
||||
The push notifications server (notifier) must use this command to start receiving message notifications from the queue:
|
||||
|
||||
```abnf
|
||||
subscribeNotifications = %s"NSUB"
|
||||
```
|
||||
|
||||
If subscription is successful the server must respond with `ok` response if no messages are available. The notifier will be receiving the message notifications from this queue until the transport connection is closed or until another transport connection subscribes to notifications from the same simplex queue - in this case the first subscription should be cancelled and [subscription END notification](#subscription-end-notification) delivered.
|
||||
|
||||
The first message notification will be delivered either immediately or as soon as the message is available.
|
||||
|
||||
### Server messages
|
||||
|
||||
#### Queue IDs response
|
||||
|
||||
Server must respond with this message when the new queue is created.
|
||||
|
||||
See its syntax in [Create queue command](#create-queue-command)
|
||||
|
||||
#### Deliver queue message
|
||||
|
||||
The server must deliver messages to all subscribed simplex queues on the currently open transport connection. The syntax for the message delivery is:
|
||||
|
||||
```abnf
|
||||
message = %s"MSG " msgId encryptedRcvMsgBody
|
||||
encryptedMsgBody = <encrypt paddedSentMsgBody> ; server-encrypted padded sent msgBody
|
||||
paddedSentMsgBody = <padded(sentMsgBody, maxMessageLength + 2)> ; maxMessageLength = 16088
|
||||
encryptedRcvMsgBody = <encrypt rcvMsgBody> ; server-encrypted meta-data and padded sent msgBody
|
||||
rcvMsgBody = timestamp msgFlags SP paddedSentMsgBody
|
||||
msgId = length 24*24OCTET
|
||||
timestamp = 8*8OCTET
|
||||
```
|
||||
|
||||
`msgId` - unique message ID generated by the server based on cryptographically strong random bytes. It should be used by the clients to detect messages that were delivered more than once (in case the transport connection was interrupted and the server did not receive the message delivery acknowledgement). Message ID is used as a nonce for server/recipient encryption of message bodies.
|
||||
|
||||
`timestamp` - system time when the server received the message from the sender as **a number of seconds** since Unix epoch (1970-01-01) encoded as 64-bit integer in network byte order. If a client system/language does not support 64-bit integers, until 2106 it is safe to simply skip the first 4 zero bytes and decode 32-bit unsigned integer (or as signed integer until 2038).
|
||||
|
||||
`paddedSentMsgBody` - see syntax in [Send message](#send-message)
|
||||
|
||||
When server delivers the messages to the recipient, message body should be encrypted with the secret derived from DH exchange using the keys passed during the queue creation and returned with `queueIds` response.
|
||||
|
||||
This is done to prevent the possibility of correlation of incoming and outgoing traffic of SMP server inside transport protocol.
|
||||
|
||||
#### Notifier queue ID response
|
||||
|
||||
Server must respond with this message when queue notifications are enabled.
|
||||
|
||||
See its syntax in [Enable notifications command](#enable-notifications-command)
|
||||
|
||||
#### Deliver message notification
|
||||
|
||||
The server must deliver message notifications to all simplex queues that were subscribed with `subscribeNotifications` command ("NSUB") on the currently open transport connection. The syntax for the message notification delivery is:
|
||||
|
||||
```abnf
|
||||
messageNotification = %s"NMSG " nmsgNonce encryptedNMsgMeta
|
||||
|
||||
encryptedNMsgMeta = <encrypted message metadata passed in notification>
|
||||
; metadata E2E encrypted between server and recipient containing server's message ID and timestamp (allows extension),
|
||||
; to be passed to the recipient by the notifier for them to decrypt
|
||||
; with key negotiated in NKEY and NID commands using nmsgNonce
|
||||
|
||||
nmsgNonce = <nonce used in NaCl crypto_box encryption scheme>
|
||||
; nonce used by the server for encryption of message metadata, to be passed to the recipient by the notifier
|
||||
; for them to use in decryption of E2E encrypted metadata
|
||||
```
|
||||
|
||||
Message notification does not contain any message data or non E2E encrypted metadata.
|
||||
|
||||
#### Subscription END notification
|
||||
|
||||
When another transport connection is subscribed to the same simplex queue, the server should unsubscribe and to send the notification to the previously subscribed transport connection:
|
||||
|
||||
```abnf
|
||||
unsubscribed = %s"END"
|
||||
```
|
||||
|
||||
No further messages should be delivered to unsubscribed transport connection.
|
||||
|
||||
#### Error responses
|
||||
|
||||
- incorrect block format, encoding or signature size (`BLOCK`).
|
||||
- missing or different session ID - tls-unique binding of TLS transport (`SESSION`)
|
||||
- command errors (`CMD`):
|
||||
- error parsing command (`SYNTAX`)
|
||||
- prohibited command (`PROHIBITED`) - any server response sent from client or `ACK` sent without active subscription or without message delivery.
|
||||
- transmission has no required signature or queue ID (`NO_AUTH`)
|
||||
- transmission has unexpected credentials (`HAS_AUTH`)
|
||||
- transmission has no required queue ID (`NO_QUEUE`)
|
||||
- authentication error (`AUTH`) - incorrect signature, unknown (or suspended) queue, sender's ID is used in place of recipient's and vice versa, and some other cases (see [Send message](#send-message) command).
|
||||
- message queue quota exceeded error (`QUOTA`) - too many messages were sent to the message queue. Further messages can only be sent after the recipient retrieves the messages.
|
||||
- sent message is too large (> 16088) to be delivered (`LARGE_MSG`).
|
||||
- internal server error (`INTERNAL`).
|
||||
|
||||
The syntax for error responses:
|
||||
|
||||
```abnf
|
||||
error = %s"ERR " errorType
|
||||
errorType = %s"BLOCK" / %s"SESSION" / %s"CMD " cmdError / %s"AUTH" / %s"LARGE_MSG" /%s"INTERNAL"
|
||||
cmdError = %s"SYNTAX" / %s"PROHIBITED" / %s"NO_AUTH" / %s"HAS_AUTH" / %s"NO_ENTITY"
|
||||
```
|
||||
|
||||
Server implementations must aim to respond within the same time for each command in all cases when `"ERR AUTH"` response is required to prevent timing attacks (e.g., the server should perform signature verification even when the queue does not exist on the server or the signature of different size is sent, using any RSA key with the same size as the signature size).
|
||||
|
||||
### OK response
|
||||
|
||||
When the command is successfully executed by the server, it should respond with OK response:
|
||||
|
||||
```abnf
|
||||
ok = %s"OK"
|
||||
```
|
||||
|
||||
## Appendices
|
||||
|
||||
### Appendix A.
|
||||
|
||||
**SMP transport protocol.**
|
||||
|
||||
Both the recipient and the sender can use TCP or some other, possibly higher level, transport protocol to communicate with the server. The default TCP port for SMP server is 5223.
|
||||
|
||||
For scenarios when meta-data privacy is critical, it is recommended that clients:
|
||||
|
||||
- communicating over Tor network,
|
||||
- establish a separate connection for each SMP queue,
|
||||
- send noise traffic (using PING command).
|
||||
|
||||
In addition to that, the servers can be deployed as Tor onion services.
|
||||
|
||||
The transport protocol should provide the following:
|
||||
|
||||
- server authentication (by matching server certificate hash with `serverIdentity`),
|
||||
- forward secrecy (by encrypting the traffic using ephemeral keys agreed during transport handshake),
|
||||
- integrity (preventing data modification by the attacker without detection),
|
||||
- unique channel binding (`sessionIdentifier`) to include in the signed part of SMP transmissions.
|
||||
|
||||
By default, the client and server communicate using [TLS 1.3 protocol][13] restricted to:
|
||||
|
||||
- TLS_CHACHA20_POLY1305_SHA256 cipher suite (for better performance on mobile devices),
|
||||
- ed25519 and ed448 EdDSA algorithms for signatures,
|
||||
- x25519 and x448 ECDHE groups for key exchange.
|
||||
- servers must send the chain of exactly 2 self-signed certificates in the handshake, with the first (offline) certificate one signing the second (online) certificate. Offline certificate fingerprint is used as a server identity - it is a part of SMP server address.
|
||||
- The clients must abort the connection in case a different number of certificates is sent.
|
||||
- server and client TLS configuration should not allow resuming the sessions.
|
||||
|
||||
During TLS handshake the client must validate that the fingerprint of the online server certificate is equal to the `serverIdentity` the client received as part of SMP server address; if the server identity does not match the client must abort the connection.
|
||||
|
||||
Once TLS handshake is complete, client and server will exchange blocks of fixed size (16384 bytes).
|
||||
|
||||
The first block sent by the server should be `serverHello` and the client should respond with `clientHello` - these blocks are used to agree SMP protocol version:
|
||||
|
||||
```abnf
|
||||
serverHello = minSmpVersion maxSmpVersion sessionIdentifier pad
|
||||
minSmpVersion = smpVersion
|
||||
maxSmpVersion = smpVersion
|
||||
sessionIdentifier = length *OCTET
|
||||
; unique session identifier derived from transport connection handshake
|
||||
; it should be included in all SMP transmissions sent in this transport connection.
|
||||
|
||||
clientHello = smpVersion pad
|
||||
; chosen SMP protocol version - it must be the maximum supported version
|
||||
; within the range offered by the server
|
||||
|
||||
smpVersion = 2*2OCTET ; Word16 version number
|
||||
|
||||
pad = *OCTET
|
||||
```
|
||||
|
||||
For TLS transport client should assert that `sessionIdentifier` is equal to `tls-unique` channel binding defined in [RFC 5929][14] (TLS Finished message struct); we pass it in `serverHello` block to allow communication over some other transport protocol (possibly, with another channel binding).
|
||||
|
||||
[1]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack
|
||||
[2]: https://en.wikipedia.org/wiki/End-to-end_encryption
|
||||
[3]: https://en.wikipedia.org/wiki/QR_code
|
||||
[4]: https://en.wikipedia.org/wiki/Unidirectional_network
|
||||
[5]: https://en.wikipedia.org/wiki/Forward_secrecy
|
||||
[6]: https://en.wikipedia.org/wiki/Off-the-Record_Messaging
|
||||
[8]: https://tools.ietf.org/html/rfc5234
|
||||
[8a]: https://tools.ietf.org/html/rfc7405
|
||||
[9]: https://tools.ietf.org/html/rfc4648#section-4
|
||||
[10]: https://tools.ietf.org/html/rfc3339
|
||||
[11]: https://tools.ietf.org/html/rfc5280
|
||||
[12]: https://tools.ietf.org/html/rfc7714
|
||||
[13]: https://datatracker.ietf.org/doc/html/rfc8446
|
||||
[14]: https://datatracker.ietf.org/doc/html/rfc5929#section-3
|
||||
[15]: https://www.rfc-editor.org/rfc/rfc8709.html
|
||||
[16]: https://nacl.cr.yp.to/box.html
|
||||
@@ -0,0 +1,18 @@
|
||||
Scheme name: simplex
|
||||
|
||||
Status: Provisional
|
||||
|
||||
Applications/protocols that use this scheme name:
|
||||
This scheme is used for connection requests in SimpleX Agent Protocol,
|
||||
a middle layer protocol for managing bi-directional communication via
|
||||
redundant unidirectional SimpleX Messaging Protocol queues.
|
||||
|
||||
Contact: Evgeny Poberezkin <ep@simplex.chat>
|
||||
|
||||
Change controller: Evgeny Poberezkin <ep@simplex.chat>
|
||||
|
||||
References:
|
||||
The syntax for connection requests in the latest version of SimpleX Agent Protocol:
|
||||
https://github.com/simplex-chat/simplexmq/blob/master/protocol/agent-protocol.md#connection-request
|
||||
SimpleX Messaging Protocol:
|
||||
https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md
|
||||
@@ -0,0 +1,16 @@
|
||||
Scheme name: smp
|
||||
|
||||
Status: Provisional
|
||||
|
||||
Applications/protocols that use this scheme name:
|
||||
This scheme is used for URIs of message queues in SimpleX Messaging Protocol,
|
||||
a client-server protocol for asynchronous distributed unidirectional
|
||||
message transmission via persistent message queues.
|
||||
|
||||
Contact: Evgeny Poberezkin <ep@simplex.chat>
|
||||
|
||||
Change controller: Evgeny Poberezkin <ep@simplex.chat>
|
||||
|
||||
References:
|
||||
The syntax for message queue URIs in the latest version of SimpleX Messaging Protocol:
|
||||
https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#smp-queue-uri
|
||||
@@ -0,0 +1,48 @@
|
||||
# Streams
|
||||
|
||||
Managing dedicated SMP queues for fast synchronous communication
|
||||
|
||||
## Problem
|
||||
|
||||
SMP agent protocol implementation provides the most secure way to distribute keys achieving the following qualities:
|
||||
|
||||
- compromising the sender agent/device allows to send messages, but not to read them.
|
||||
- compromising the recipient agent/device allows to receive messages, but not to send them (neither public encryption key is stored - TBC if public key can be restored from the private - nor server authentication key is available).
|
||||
- compromising the server does not expose any information about messages content, as encrypted section has the same size.
|
||||
|
||||
The current hybrid encryption scheme uses a new symmetric key for each message, and because the symmetric key is not persisted at any point, it is difficult to obtain it and send counterfeit messages on behalf of the sender (even in case both the server and recipient are compromised).
|
||||
|
||||
There are 2 downsides of the current scheme:
|
||||
|
||||
1. RSA encryption/decryption is relatively slow even for 2048 key size and gets much slower for larger key sizes. It is not a problem for the chat messages (and any content updates) that happen infrequently, but it makes the transmission of large files and any other streaming communication slow.
|
||||
|
||||
2. If the large file or voice/video calls were to be sent via the same queue as normal messages/content updates, the server and any passive observer would be able to understand when such transmissions happen (even if performance was not a problem).
|
||||
|
||||
## Proposed solution
|
||||
|
||||
For every fast/dense data transmission use a separate short-lived connection with dedicated SMP queues that the agents would provision to receive such transmissions.
|
||||
|
||||
Such connections that can be unidirectional or duplex can be managed by agents via additional set of agent commands using "stream" abstraction.
|
||||
|
||||
The protocol to create and manage such stream could be the following:
|
||||
|
||||
1. The sender client notifies the agent about the stream it has to create for a specific duplex connection (the connection must be duplex for it to be possible): `<conn_id> SNEW <size>` (where `conn_id` is the existing duplex connection, `size` is the size for the data to be transferred; 0 for unbounded streams), the response is `<conn_id> STREAM <stream_id> <size>`.
|
||||
2. The sender agent sends to the recipient agent the "envelope" `STREAM <stream_id> <size>`
|
||||
3. The receiving agent would notify the client about availability of the stream by sending `<conn_id> STREAM <stream_id> <size>` notification.
|
||||
4. Receiving client would send `<conn_id> SJOIN <stream_id>` command to the receiving agent.
|
||||
5. Receiving agent provisions the SMP queue on the random SMP server (we planned to remove SMP server from NEW command anyway). Possibly, there will be a limited list of servers that support streams (that is un-throttled number of messages), and stream support can be communicated via welcome header and change with AUTH SMP command.
|
||||
6. Receiving agent sends the envelop to sending agent `SJOIN <stream_id> <invitation>` (where invitation has the same format as in out-of-band message, but it has symmetric AES key and IV instead of asymmetric RSA key - the encryption scheme should codified in the invitation to make it generic).
|
||||
7. Sending agent and receiving agent confirm and secure SMP queue as usual. This is still not solving the problem that the sending agent should sign each command. We might extend SMP protocol to create a new queue type that allows authenticating the sender only once per TCP session so that messages can be sent without signing - the server can see that these queues are special anyway, as there will be much faster traffic there.
|
||||
8. Sending agent, once the queue is confirmed notifies the sending client that the stream is ready to accept data by sending notification `<conn_id> SREADY <stream_id> <max_chunk_size>`.
|
||||
9. Sending agent client can now send packets into the stream up to total `<size>` limit if it was specified by using command `<conn_id> SPUT <stream_id> <chunk_no> <chunk_size> <binary> ` (the chunk size should not be bigger than `max_chunk_size`, the agent would pad it to this size before sending to the receiving agent)
|
||||
10. Sending agent would send the chunk to the receiving agent in `CHUNK <chunk_no> <ts> <chunk_size> <binary>` envelope, where the first 4 bytes in decrypted binary is chunk size.
|
||||
11. Receiving agent would send the chunk to the client as `<conn_id> SDATA <stream_id> <chunk_no> <ts> <status> <chunk_size> <binary>` (possibly, with three IDs and timestamps as with messages).
|
||||
12. Both the receiving and sending clients can terminate the stream with `<conn_id> SCLOSE <stream_id>` command. The agent would send `SCLOSED <stream_id>` envelope and the opposite client would receive `<conn_id> SCLOSED <stream_id>` notification - no more data will be accepted into this stream.
|
||||
13. If the stream was bounded with some `size` then both the sending and receiving client would receive `SCLOSED` notification after the last chunk of correct size was sent (and the incorrect chunk would be rejected).
|
||||
14. If the stream was interrupted then the recipient client can request resuming this stream using `<conn_id> SRESUME <stream_id> <from_chunk_no>`, the recipient agent would send "envelope" `SRESUME <stream_id> <from_chunk_no> <invitation>`.
|
||||
|
||||
To be clarified:
|
||||
1. Possibly the internal stream ID used by clients should be different from stream ID communicated between agents, and in any case it should be connection scoped (same as with messages).
|
||||
2. There seems to be no need to rotate symmetric keys, disconnections can be handled.
|
||||
3. Same as we plan for redundant SMP queues with one connection, agents could use multiple queues per stream to increase the bandwidth and/or transmission reliability (e.g. for file transfers, where interruptions are not problematic multiple queues can be used in parallel, while for calls agents can duplicate all transmitted bytes so that losing some queues or TCP disconnects do not result in communication interruption).
|
||||
4. Possibly the stream creation could be initiated by the receiving agent, to have it more similar to connection creation procedure.
|
||||
@@ -0,0 +1,21 @@
|
||||
# SMP confirmation timeout recovery
|
||||
|
||||
## Problem
|
||||
|
||||
When sending an SMP confirmation a network timeout can lead to the following race condition:
|
||||
- server receives the confirmation while the joining party fails to receive the server's response;
|
||||
- joining party deletes the connection together with credentials sent in the confirmation for securing the queue;
|
||||
- initiating party will receive the confirmation from the server and secure the queue;
|
||||
- on subsequent attempt to join via the same invitation link initiating party will generate new credentials and fail authorization.
|
||||
|
||||
This renders the joining party permanently unable to join via that invitation link and complete the connection.
|
||||
|
||||
## Solution
|
||||
|
||||
A possible solution is to keep and try to reuse same credentials on subsequent attempts:
|
||||
- joining party has to remember invitation link when saving the connection;
|
||||
- if SMP confirmation fails due to network timeout joining party doesn't delete the connection and keeps the credentials;
|
||||
- when joining, joining party checks whether such invitation link was already used for a connection, if yes:
|
||||
- joining party tries to send SMP confirmation with the same credentials;
|
||||
- if this SMP confirmation fails with authorization error (for example it can happen due to race condition explained above) joining party tries to send HELLO message;
|
||||
- if HELLO message fails with authorization error (it can happen if connection was deleted or secured with different credentials), the recovery is no longer possible and connection can be deleted.
|
||||
@@ -0,0 +1,160 @@
|
||||
# SimpleX File Transfer protocol
|
||||
|
||||
## Problem
|
||||
|
||||
Sending files as currently implemented in SimpleX Chat is inefficient for these reasons:
|
||||
|
||||
1. Slow: message delivery requires acknowledgement for each message before the server sends the next message.
|
||||
2. Not fully asynchronous:
|
||||
a. SMP queue size is limited, so the whole file cannot be sent/stored in server memory.
|
||||
b. Recipient needs to accept the file before it is sent.
|
||||
3. No broadcast: sending the same file to multiple recipients requires sending it multiple times.
|
||||
|
||||
## Possible solutions
|
||||
|
||||
These problems could be solved within SMP protocol:
|
||||
|
||||
- streaming messages without waiting for acknowledgements.
|
||||
- bigger queue sizes stored on hard drive and accepting large files without waiting for the recipient confirmation.
|
||||
- adding broadcast to SMP servers.
|
||||
|
||||
These solution are complex and they do not scale well.
|
||||
|
||||
Another approach would be adopting some open-source file storage, possibly S3-compatible, to host files while "in transit". The downsides are:
|
||||
|
||||
1. Missing features. S3-compatible file servers require a service layer that we would need to develop to manage user accounts and permissions. There are solutions that allow self-registering users and uploading files without service layer, but they are not flexible enough in managing policies for these files.
|
||||
2. Limited meta-data protection, both on the application and on transport layer: same address for sending and receiving a file, file size is known to the server, etc.
|
||||
3. More difficult to self-host for the users when the server has 2 components (a 3rd party solution with our service component).
|
||||
|
||||
We are not considering P2P solutions because of their bad meta-data privacy and requirement of having a single network - the same reason why P2P design is not used for SimpleX messaging network.
|
||||
|
||||
A proposed solution is to develop a file hosting service (SimpleX File Transfer service) that would have better meta-data protection and functionality than the alternatives, using the ideas from SMP protocol design.
|
||||
|
||||
The parameters of the solution would be:
|
||||
|
||||
1. The server does not have knowledge of the users and actual files – only anonymously uploaded fixed-size chunks (considered 8Mb, maybe we should allow 2 sizes, e.g. 1Mb and 8Mb).
|
||||
2. The server should allow broadcast, so that a chunk can be downloaded by multiple recipients.
|
||||
3. There should be no identifiers and ciphertext in common inside TLS between sender and recipient traffic, and between the traffic of different recipients of the same file. This approach can be extended to support public chunks that have persistent identifiers.
|
||||
4. The server should prevent multiple uploads of the same chunk. This can be extended to allow multiple downloads from the same persistent address.
|
||||
5. Clients can send the chunks from the same file via multiple servers, both by splitting chunks between them and for redundancy.
|
||||
|
||||
## Design approach
|
||||
|
||||
File transfer servers will be chosen by file senders. The servers will allow senders to anonymously upload fixed-size file chunks.
|
||||
|
||||
### Transport protocol
|
||||
|
||||
- binary-encoded commands sent as fixed-size padded block in the body of HTTP2 POST request, similar to SMP and notifications server protocol transmission encodings.
|
||||
- HTTP2 POST with a fixed size padded block body for file upload and download.
|
||||
|
||||
Block size - 4096 bytes (it would fit ~120 Ed25519 recipient keys).
|
||||
|
||||
The reasons to use HTTP2:
|
||||
|
||||
- avoid the need to have two hostnames (or two different ports) for commands and file uploads.
|
||||
- compatibility with the existing HTTP2 client libraries.
|
||||
|
||||
The reason not to use JSON bodies:
|
||||
|
||||
- bigger request size, so fewer recipient keys would fit in a single request
|
||||
- signature over command has to be outside of JSON anyway.
|
||||
|
||||
The reason not to use URI segments / HTTP verbs / REST semantics is to have consistent request size.
|
||||
|
||||
### Required server commands:
|
||||
|
||||
- File sender:
|
||||
- create file chunk record.
|
||||
- Parameters:
|
||||
- Ed25519 key for subsequent sender commands and Ed25519 keys for commands of each recipient.
|
||||
- chunk size.
|
||||
- Response:
|
||||
- chunk ID for the sender and different IDs for all recipients.
|
||||
- add recipients to file chunk
|
||||
- Parameters:
|
||||
- sender's chunk ID
|
||||
- Ed25519 keys for commands of each recipient.
|
||||
- Response:
|
||||
- chunk IDs for new recipients.
|
||||
- upload file chunk.
|
||||
- delete file chunk (invalidates all recipient IDs).
|
||||
- File recipient:
|
||||
- download file chunk:
|
||||
- chunk ID
|
||||
- DH key for additional encryption of the chunk.
|
||||
- command should be signed with the key passed by the sender when creating chunk record.
|
||||
- delete file chunk ID (only for one recipient): signed with the same key.
|
||||
|
||||
### Storage model
|
||||
|
||||
Same as for SMP and notifications server - in-memory storage of the records, with adding to append-only log, restored and compacted on server restart.
|
||||
|
||||
### Sending file
|
||||
|
||||
To send the file, the sender will:
|
||||
|
||||
- compute SHA512 digest
|
||||
- pad the file to match the whole number of chunks in size,
|
||||
- encrypt it with a randomly chosen symmetric key and IV (e.g., using NaCL crypto_secretbox),
|
||||
- split into fixed size chunks
|
||||
- upload each chunk to a randomly chosen server.
|
||||
|
||||
The sending client should generate more per-recipient keys than the actual number of recipients, possibly rounding up to a power of 2, to conceal the actual number of intended recipients.
|
||||
|
||||
Then the sending client will combine addresses of all chunks and other information into "file description", different for each file recipient, that will include:
|
||||
|
||||
- an encryption key that was used to encrypt the file (the same for all recipients).
|
||||
- file SHA512 digest
|
||||
- list of chunk descriptions; information for each chunk:
|
||||
- private Ed25519 key to sign commands for file transfer server.
|
||||
- chunk address (server host and chunk ID).
|
||||
- chunk sha512 digest
|
||||
|
||||
To reduce the size, chunk descriptions will be grouped by the server host.
|
||||
|
||||
This "file description" itself will be sent as a small file over an authenticated channel, to prevent file description modification. To estimate its size:
|
||||
|
||||
- each chunk \* redundancy per chunk, assuming chunks are grouped per server:
|
||||
- 1-based chunk number in the file - 8 bytes (including any overhead)
|
||||
- Ed25519 key (different for each recipient / chunk combination) - 32 bytes \* 4/3 (base64, assuming text encoding)
|
||||
- chunk ID (different for each recipient) - 64 bytes \* 4/3
|
||||
- optional (only in the first chunk occurence) chunk sha512 digests - 64 bytes \* 4/3
|
||||
- server addresses - say, 128 bytes per server
|
||||
- sha512 digest - 64 bytes \* 4/3
|
||||
- encryption key - 32 bytes \* 4/3
|
||||
- IV - 32 bytes \* 4/3
|
||||
- encoding overhead - say, 256 bytes
|
||||
|
||||
For 1gb file, sent via 4 different servers, in 8Mb chunks, with redundancy 2, the size of "file description", assuming text encoding, will be ~45kb (`128 * (8 + 32 + 64) * 2 * 4/3 + 128 * 64 * 4/3 + 128 * 4 + (64 + 32 + 32) * 4/3 + 256`).
|
||||
|
||||
File description format (yml):
|
||||
|
||||
```
|
||||
name: file.ext
|
||||
size: 33200000
|
||||
chunk: 8mb
|
||||
hash: abc=
|
||||
key: abc=
|
||||
iv: abc=
|
||||
parts:
|
||||
- server: xftp://abc=@example1.com
|
||||
chunks: [1:abc=:def=:ghi=, 3:abc=:def=:ghi=]
|
||||
- server: xftp://abc=@example2.com
|
||||
chunks: [2:abc=:def=:ghi=, 4:abc=:def=:ghi=:2mb]
|
||||
- server: xftp://abc=@example3.com
|
||||
chunks: [1:abc=:def=, 4:abc=:def=]
|
||||
- server: xftp://abc=@example4.com
|
||||
chunks: [2:abc=:def=, 3:abc=:def=]
|
||||
```
|
||||
|
||||
This file description is sent to all recipients via normal messages, split to 15780 byte chunks if needed.
|
||||
|
||||
### Receiving file
|
||||
|
||||
Having received the description, the recipient will:
|
||||
|
||||
- download all chunks falling back to secondary servers, if needed
|
||||
- combine the chunks into a file
|
||||
- decrypt the file
|
||||
- un-pad it
|
||||
- validate file digest
|
||||
@@ -0,0 +1,20 @@
|
||||
# SMP and SMP agent protocol extensions to manage queue quotas
|
||||
|
||||
## Problem
|
||||
|
||||
SMP servers define a limit on the number of messages that can be stored on the server. When the sender reaches this limit, they continue trying to send the message with exponential back off, until the server allows it. To reduce the traffic we added expiration on unsent messages of 2 days that:
|
||||
|
||||
- adds its own set of problems because of expired control messages - e.g., group fragmentation.
|
||||
- doesn't solve problem of traffic in active groups with many inactive members - there are still too many retries in queues that reached capacity.
|
||||
|
||||
## Solution
|
||||
|
||||
Sending agent permanently stops trying to send messages having received `ERR QUOTA` (or, for the period of clients migration, increases TTL much more than in cases of network errors).
|
||||
|
||||
Add SMP server event that means "quota reached" (e.g. `QTA` or `MSG QUOTA`) - it will be delivered to the recipient once all messages are retrieved at the same point in message stream where the sender received `ERR QUOTA`. Alternatively, it could be a message flag that is set on the last message in the queue at a point the sender received `ERR QUOTA`.
|
||||
|
||||
Add SMP agent message that means "quota available, resume sending" (e.g. `A_QTA` or `A_RESUME`) – the receiving agent will send it via the reply queue to the sending agent that will resume sending messages at this point.
|
||||
|
||||
Possibly, increase TTL for local messages to 30 days. That might require separately addressing the problem of permanently failing servers.
|
||||
|
||||
We discussed a possible solution of merging all messages to one server into one delivery queue, so that having multiple failing queues on this server will not result in multiple connection attempts, and this proposal also answers the question what to do with the messages when quota is exceeded - the whole queue can be marked as suspended, and processed separately from others - TBD.
|
||||
@@ -0,0 +1,366 @@
|
||||
# Re-sync encryption ratchets
|
||||
|
||||
## Problem
|
||||
|
||||
See https://github.com/simplex-chat/simplexmq/pull/743/files for problem and high-level solution.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Diagnosing ratchet de-synchronization
|
||||
|
||||
Message decryption happens in `agentClientMsg`, in `agentRatchetDecrypt`, which can return decryption result or error. Decryption error can be differentiated in `agentClientMsg` result pattern match, in Left cases, where we already differentiate duplicate error (`AGENT A_DUPLICATE`).
|
||||
|
||||
Question: Which decryption errors can be diagnosed as ratchet de-synchronization?
|
||||
|
||||
Possibly any `AGENT A_CRYPTO` error. Definitely on `RATCHET_HEADER`, TBC other. See `cryptoError :: C.CryptoError -> AgentErrorType` for conversion from decryption errors to `A_CRYPTO` or other agent errors. We're only interested in crypto errors, as other are either other client implementation errors, internal errors, or already processed duplicate error.
|
||||
|
||||
Proposed classification of crypto errors, based on `AgentCryptoError`:
|
||||
|
||||
`DECRYPT_AES` -> re-sync allowed (recommended/required?)
|
||||
|
||||
`DECRYPT_CB` -> re-sync allowed (recommended/required?)
|
||||
|
||||
`RATCHET_HEADER` -> **re-sync required**
|
||||
|
||||
`RATCHET_EARLIER` -> re-sync allowed
|
||||
|
||||
`RATCHET_SKIPPED` -> **re-sync required**
|
||||
|
||||
Ratchet re-synchronization could be started automatically on diagnosing de-synchronization, based on these errors. As a potentially dangerous feature (e.g., implementation error could lead to infinite re-sync loop causing large traffic consumption), initially it will be available via agent functional api for client to call. Ratchet de-synchronization will instead produce an event prompting client to re-synchronize.
|
||||
|
||||
Diagnosing possible ratchet de-synchronization also will be recorded as connection state - `ratchet_desync_state` field in `connections` table. Client should be prohibited to start ratchet re-synchronization unless `ratchet_desync_state` is set.
|
||||
|
||||
Event should not be repeated for following received messages that can't be decrypted - based on `ratchet_desync_state`. If a received message can be decrypted, `ratchet_desync_state` should be set to NULL and a new event sent, indicating ratchet has healed.
|
||||
|
||||
New event - `RDESYNC :: RatchetDesyncState -> ConnectionStats -> ACommand Agent AEConn`
|
||||
|
||||
```haskell
|
||||
data RatchetDesyncState
|
||||
= RDResyncAllowed
|
||||
| RDResyncRequired
|
||||
| RDHealed
|
||||
```
|
||||
|
||||
New field should be added to `ConnectionStats` - `ratchetDesyncState :: Maybe RatchetDesyncState`, based on `ratchet_desync_state`.
|
||||
|
||||
> On `RDESYNC` events chat should create chat item, prompting ratchet re-synchronization or notifying it has healed.
|
||||
> If connection has diagnosed ratchet de-sync, chat item should have a button to start ratchet re-sync.
|
||||
> We'd have to get `ConnectionStats` on chat level for this instead of chat info.
|
||||
> This wouldn't work for groups. One option is to add `ConnectionStats` to `GroupMember` type and update on events.
|
||||
> Same could be done for `Contact` then.
|
||||
|
||||
To consider - allow to start ratchet re-synchronization at any time regardless of this field as an experimental feature. In chat it could be behind "Developer tools" + additional "Experimental" toggle. Agent api would have `force :: Bool` as parameter, allowing to bypass `ratchet_desync_state`. Should `ratchet_resync_state` (see below) still be honored in this case?
|
||||
|
||||
### Re-synchronization process
|
||||
|
||||
\*\*\*\*\*
|
||||
|
||||
Basic idea is the following:
|
||||
|
||||
Both agents send new ratchet keys and compute a new shared secret. Agent that starts re-synchronization should record this fact in the connection state. Agent that receives a new key should respond with a key of its own, unless it has recorded that it itself started re-synchronization in the connection state.
|
||||
|
||||
It can happen that both agents start re-synchronizing simultaneously. In this case they both would record it in the connection state and would not respond with a new message - instead they would use each other's already sent keys.
|
||||
|
||||
Agent has both keys if:
|
||||
|
||||
- It initiates with the first key, and then receives the second key;
|
||||
- It receives the first key and then generates its own in response.
|
||||
|
||||
After agent has both keys, it initiates new ratchet depending on keys ordering. The agent that sent the lower key should use `initRcvRatchet` function, the agent that sent the greater key should use `initSndRatchet` (or vice versa - but they should deterministically choose different sides).
|
||||
|
||||
\*\*\*\*\*
|
||||
|
||||
State whether the ratchet re-synchronization is in progress should be tracked in database via `connections` table new `ratchet_resync_state` field.
|
||||
|
||||
New functional api:
|
||||
|
||||
```haskell
|
||||
resyncConnectionRatchet :: AgentErrorMonad m => AgentClient -> ConnId -> m ConnectionStats
|
||||
```
|
||||
|
||||
or if we want to allow re-synchronizing ratchet at any time even if de-synchronization wasn't diagnosed:
|
||||
|
||||
```haskell
|
||||
resyncConnectionRatchet :: AgentErrorMonad m => AgentClient -> ConnId -> Bool -> m ConnectionStats
|
||||
resyncConnectionRatchet c connId force = ...
|
||||
```
|
||||
|
||||
Possibly client command?
|
||||
|
||||
``` haskell
|
||||
data ACommand (p :: AParty) (e :: AEntity) where
|
||||
...
|
||||
RESYNC_RATCHET :: Bool -> ACommand Client AEConn
|
||||
```
|
||||
|
||||
New event - `RRESYNC :: RatchetResyncState -> ConnectionStats -> ACommand Agent AEConn`
|
||||
|
||||
```haskell
|
||||
data RatchetResyncState
|
||||
= RRStarted
|
||||
| RRAgreedSnd
|
||||
| RRAgreedRcv
|
||||
| RRComplete
|
||||
```
|
||||
|
||||
New `ConnectionStats` field - `ratchetResyncState :: Maybe RatchetResyncState`.
|
||||
|
||||
When called, it should:
|
||||
|
||||
- Generate new keys.
|
||||
- Update database connection state.
|
||||
- Set `ratchet_desync_state` to NULL.
|
||||
- Set `ratchet_resync_state` to `RRStarted`.
|
||||
- Delete old ratchet from `ratchets` (is it safe?), create new ratchet.
|
||||
- Send `AgentRatchetKey` message.
|
||||
- Return updated `ConnectionStats` to client.
|
||||
|
||||
> On `RRESYNC` events chat should create chat item, and reset connection verification.
|
||||
> Parameterized `RRESYNC` allows to distinguish: start and end of re-synchronization for initiating party; chat item direction - `RRESYNC RRStarted` is snd, `RRESYNC RRAgreedSnd/Rcv` and `RRESYNC RRComplete` are rcv (`RRESYNC RRAgreedSnd/Rcv` chat item could be omitted).
|
||||
|
||||
AgentRatchetKey is a new message on the level of AgentMsgEnvelope - encrypted with queue level e2e encryption, but not with connection level e2e encryption (since ratchet de-synchronized).
|
||||
|
||||
```haskell
|
||||
data AgentMsgEnvelope
|
||||
= ...
|
||||
| AgentRatchetKey
|
||||
{ agentVersion :: Version,
|
||||
e2eEncryption :: E2ERatchetParams 'C.X448,
|
||||
info :: ByteString -- for extension
|
||||
}
|
||||
```
|
||||
|
||||
On receiving `AgentRatchetKey`, if the receiving client hasn't started the ratchet re-synchronization itself (check `ratchet_resync_state`), it should:
|
||||
|
||||
- Generate new keys and compute new shared secret, initializing ratchet based on keys comparison.
|
||||
- Update database connection state.
|
||||
- Set `ratchet_resync_state` to `RRAgreedSnd/Rcv` (depending on whether ratchet was initialized as sending or receiving).
|
||||
- Delete old ratchet from `ratchets`, create new ratchet.
|
||||
- Reply with its own `AgentRatchetKey`.
|
||||
- Notify client with `RRESYNC RRAgreedSnd/Rcv`.
|
||||
- If ratchet was initialized as sending, send `EREADY` message, notifying other agent ratchet is re-synced.
|
||||
|
||||
New agent message:
|
||||
|
||||
```haskell
|
||||
data AMessage
|
||||
= ...
|
||||
| -- ratchet re-synchronization is complete, with last decrypted sender message id
|
||||
EREADY PrevExternalSndId
|
||||
```
|
||||
|
||||
On receiving `AgentRatchetKey`, if the receiving client started re-sync:
|
||||
|
||||
- Compute new shared secret, initializing ratchet based on keys comparison.
|
||||
- Update database connection state.
|
||||
- Set `ratchet_resync_state` to `RRAgreedSnd/Rcv` (depending on whether ratchet was initialized as sending or receiving).
|
||||
- Update ratchet.
|
||||
- Notify client with `RRESYNC RRAgreedSnd/Rcv`.
|
||||
- If ratchet was initialized as sending, send `EREADY` message.
|
||||
|
||||
After agent receives `EREADY` (or any other message that successfully decrypts):
|
||||
|
||||
- Reset `ratchet_resync_state` to NULL.
|
||||
- Notify client with `RRESYNC RRComplete`.
|
||||
- If ratchet was initialized as receiving, send reply `EREADY` message.
|
||||
|
||||
### State transitions
|
||||
|
||||
For initiating party:
|
||||
|
||||
```
|
||||
+------------+
|
||||
| Ratchet ok |
|
||||
+------------+
|
||||
|
|
||||
| message received, decryption error
|
||||
* ---->|-----------------------------------+
|
||||
| |
|
||||
V new (clarifying) V
|
||||
+-----------------+ error +------------------+
|
||||
| Re-sync allowed |--------------->| Re-sync required |
|
||||
+-----------------+ +------------------+
|
||||
| |
|
||||
|-----------------------------------|
|
||||
| | alternative - message received,
|
||||
| re-sync started by client | successfully decrypted
|
||||
V V
|
||||
+-----------------+ +------------+
|
||||
| Re-sync started | | Ratchet ok |
|
||||
+-----------------+ +------------+
|
||||
|
|
||||
| other party replied with new ratchet key
|
||||
V
|
||||
+----------------+
|
||||
| Re-sync agreed |----> * message received, decryption error
|
||||
| snd / rcv | (should remember agreed state for reply EREADY?)
|
||||
+----------------+
|
||||
|
|
||||
| message received, successfully decrypted
|
||||
| (can be, but not necessarily, EREADY)
|
||||
V
|
||||
+------------+
|
||||
| Ratchet ok |
|
||||
+------------+
|
||||
```
|
||||
|
||||
For replying party:
|
||||
|
||||
```
|
||||
+------------+
|
||||
| Ratchet ok |
|
||||
+------------+
|
||||
|
|
||||
| other party sent new ratchet key
|
||||
V
|
||||
+----------------+
|
||||
| Re-sync agreed |
|
||||
| snd / rcv |
|
||||
+----------------+
|
||||
|
|
||||
| message received, successfully decrypted
|
||||
| (can be, but not necessarily, EREADY)
|
||||
V
|
||||
+------------+
|
||||
| Ratchet ok |
|
||||
+------------+
|
||||
```
|
||||
|
||||
### Ratchet state model
|
||||
|
||||
#### 2 state variables
|
||||
|
||||
Above we considered model with separate de-sync and re-sync state.
|
||||
|
||||
| Desync \ Resync | Nothing | RRStarted | RRAgreedSnd | RRAgreedRcv |
|
||||
| --- | :---: | :---: | :---: | :---: |
|
||||
| **Nothing** | 1 | 3 | 4 | 4 |
|
||||
| **RDResyncAllowed** | 2 | | 5 | 5 |
|
||||
| **RDResyncRequired** | 2 | | 5 | 5 |
|
||||
|
||||
1: Ratchet is ok.
|
||||
|
||||
2: Re-sync diagnosed, not in progress.
|
||||
|
||||
3: Rs-sync started, diagnosing de-sync is prohibited.
|
||||
|
||||
4: Re-sync agreed.
|
||||
|
||||
5: Re-sync agreed, new de-sync is diagnosed.
|
||||
|
||||
Combination 5 is possible in case de-sync was diagnosed before message that could be decrypted is received, for example if `EREADY` failed to deliver and no other decryptable message followed. We shouldn't prohibit diagnosing de-sync in this case, because agent may never exit "Agreed" state (if new decryptable message is never received). We also shouldn't overwrite/forget state of re-sync, even if we diagnose new possible de-sync, because if the decryptable `EREADY` is received and ratchet is in `RRAgreedRcv` state, it should respond with reply `EREADY`.
|
||||
|
||||
Some combinations should be impossible:
|
||||
|
||||
- `RDResyncAllowed` with `RRStarted`.
|
||||
|
||||
- `RDResyncRequired` with `RRStarted`.
|
||||
|
||||
`RDHealed` is equivalent to `Nothing` and only used for `RDESYNC` event, `Maybe RatchetDesyncState` can be replaced with `RatchetDesyncState`, with single new constructor `RDNoDesync` replacing `Nothing` and `RDHealed`.
|
||||
|
||||
`RRComplete` is equivalent to `Nothing` and only used for `RRESYNC` event, `Maybe RatchetResyncState` can be replaced with `RatchetResyncState`, with single new constructor `RDNoResync` replacing `Nothing` and `RRComplete`.
|
||||
|
||||
#### Single state variable
|
||||
|
||||
Another option is two have a single state variable describing ratchet.
|
||||
|
||||
```haskell
|
||||
data RatchetState
|
||||
= RSOk
|
||||
| RSResyncAllowed
|
||||
| RSResyncRequired
|
||||
| RSResyncStarted
|
||||
| RRResyncAgreedSnd
|
||||
| RRResyncAgreedRcv
|
||||
|
||||
-- When `resyncConnectionRatchet` is not prohibited. Can override with `force`.
|
||||
-- Currently we check:`(isJust ratchetDesyncState || force) && ratchetResyncState /= Just RRStarted`.
|
||||
resyncConnectionRatchetAllowed :: RatchetState -> Bool
|
||||
resyncConnectionRatchetAllowed = \case
|
||||
RSOk -> False
|
||||
RSResyncAllowed -> True
|
||||
RSResyncRequired -> True
|
||||
RSResyncStarted -> False -- `force` shouldn't override
|
||||
RRResyncAgreedSnd -> False
|
||||
RRResyncAgreedRcv -> False
|
||||
|
||||
-- When we register and notify about ratchet de-synchronization.
|
||||
-- Currently we check: `(isNothing ratchetDesyncState && ratchetResyncState /= Just RRStarted)`.
|
||||
-- We should also allow to update from Allowed to Required.
|
||||
shouldNotifyRDESYNC :: RatchetState -> Bool
|
||||
shouldNotifyRDESYNC = \case
|
||||
RSOk -> True
|
||||
RSResyncAllowed -> False -- only if new error implies Required
|
||||
RSResyncRequired -> False
|
||||
RSResyncStarted -> False
|
||||
RRResyncAgreedSnd -> True
|
||||
RRResyncAgreedRcv -> True
|
||||
|
||||
-- When we prohibit connection switch, for `checkRatchetDesync`.
|
||||
-- Currently we check: `(ratchetDesyncState == Just RDResyncRequired || ratchetResyncState == Just RRStarted)`
|
||||
-- Also use in `runSmpQueueMsgDelivery` to pause delivery?
|
||||
ratchetDesynced :: RatchetState -> Bool
|
||||
ratchetDesynced = \case
|
||||
RSOk -> False
|
||||
RSResyncAllowed -> False
|
||||
RSResyncRequired -> True
|
||||
RSResyncStarted -> True
|
||||
RRResyncAgreedSnd -> False
|
||||
RRResyncAgreedRcv -> False
|
||||
```
|
||||
|
||||
Having a single state variable limits differentiation described for combination 5 in matrix. It also limits possible differentiations in client between events when ratchet is healed on its own, and when ratchet re-sync is completed after agents negotiation. Overall, since matrix is not very sparse and allows for more fine-grained decision-making, having separate state variables for de-sync and re-sync seems preferred.
|
||||
|
||||
#### Single state variable simplified (final version)
|
||||
|
||||
```haskell
|
||||
data RatchetSyncState
|
||||
= RSOk
|
||||
| RSAllowed
|
||||
| RSRequired
|
||||
| RSStarted
|
||||
| RSAgreed
|
||||
|
||||
-- event
|
||||
RSYNC :: RatchetSyncState -> ConnectionStats -> ACommand Agent AEConn`
|
||||
|
||||
-- ConnectionStats field
|
||||
ratchetSyncState :: RatchetSyncState
|
||||
```
|
||||
|
||||
Updated design decisions:
|
||||
|
||||
1. Single constructor for "Agreed" state. Differentiating `RRResyncAgreedSnd` and `RRResyncAgreedRcv` allowed for easier processing of `EREADY` by helping to determine whether reply `EREADY` has to be sent. However, it duplicated information already present in ratchet's state, and can be instead worked around by remembering and analyzing ratchet state pre decryption.
|
||||
|
||||
2. Prohibit transition from "Agreed" state to "Desync" states. This would make possible edge-cases that leave ratchet in de-synchronized state without ability to progress (e.g. failed delivery of `AgentRatchetKey`), but would simplify state machine by removing dedicated "Desync" variable. Besides, there's still a recovery way with a `force` option.
|
||||
|
||||
3. Treat "Agreed" as unfinished state - prohibit new messages to be enqueued, etc. Reception of any decryptable message transitions ratchet to "Ok" state.
|
||||
|
||||
Possible improvements:
|
||||
|
||||
- Repeatedly triggering re-synchronization while in "Started"/"Agreed" state re-sends same keys and EREADY.
|
||||
- Cooldown period, during which repeat re-synchronization is prohibited.
|
||||
|
||||
### Skipped messages
|
||||
|
||||
Options:
|
||||
|
||||
1. Ignore skipped messages.
|
||||
2. Stop sending new messages while connection re-synchronizes (can use `ratchet_resync_state`).
|
||||
|
||||
- Initiator shouldn't send new messages until receives `AgentRatchetKey` from second party.
|
||||
- Second party knows new shared secret immediately after processing first `AgentRatchetKey`, so it's not necessary to limit?
|
||||
|
||||
3. 2 + Re-send skipped messages first.
|
||||
|
||||
- Add `last_external_snd_msg_id` to `AgentRatchetKey`? + see link above
|
||||
|
||||
4. Re-send only messages after the latest ratchet step. \*
|
||||
|
||||
It may be okay to ignore skipped messages, or at most implement option 2, as ratchet de-synchronization is usually caused by misuse (human error) - the most common cause of ratchet de-sync seems to be sending and receiving messages after running agent with old database backup. In this case user has already seen most skipped messages, and it can be expected to not have them after switching to an old backup. So in this case the only "really skipped" messages are those that were sent during the latest ratchet step and failed to decrypt, triggering ratchet re-sync (\* another option is to only re-send those).
|
||||
|
||||
Besides, depending on time of backup there may be an arbitrary large number of skipped messages, which may consume a lot of traffic and may halt delivery of up-to-date messages for some time.
|
||||
|
||||
It may be better to have request for repeat delivery as a separate feature, that can be requested in necessary contexts - for example for group stability.
|
||||
|
||||
Can servers delivery failure lead to de-sync? If message is lost on server and never delivered, ratchet wouldn't advance, so there's no room for de-sync? If yes, re-evaluate.
|
||||
@@ -31,7 +31,7 @@ Agent disconnected from host:port
|
||||
### Received command from the client
|
||||
|
||||
```
|
||||
n --> A : corrId connAlias parsed_command // raw_command
|
||||
n --> A : corrId connId parsed_command // raw_command
|
||||
```
|
||||
|
||||
`raw_command` is added only in case of parsing failure.
|
||||
@@ -53,13 +53,13 @@ In case the response is a message or notification, corrId should be replaced wit
|
||||
### Interpreted ("unwrapped") SMP message as agent message
|
||||
|
||||
```
|
||||
Agent msg : connAlias parsed_message // raw_message
|
||||
Agent msg : connId parsed_message // raw_message
|
||||
```
|
||||
|
||||
### Sent response / message to the client
|
||||
|
||||
```
|
||||
n <-- A : corrId connAlias parsed_command // raw_command
|
||||
n <-- A : corrId connId parsed_command // raw_command
|
||||
```
|
||||
|
||||
### Database changes
|
||||
@@ -16,49 +16,60 @@ For initial implementation I propose approach to be as simple as possible as lon
|
||||
|
||||
One of the consideration is to use [noise protocol framework](https://noiseprotocol.org/noise.html), this section describes ad hoc protocol though.
|
||||
|
||||
During TCP session both client and server should use symmetric AES 256 bit encryption using the session key that will be established during the handshake.
|
||||
During TCP session both client and server should use symmetric AES 256 bit encryption using two session keys and two base IVs that will be agreed during the handshake. Both client and the server should maintain two 32-bit word counters, one for sent and one for the received messages. The IV for each message should be computed by xor-ing the sequential message counter, starting from 0, with the first 32 bits of agreed base IV. TODO - explain it in a more formal way, also document how 32-bit word is encoded - with the most or least significant byte first (currently encodeWord32 from Network.Transport.Internal is used)
|
||||
|
||||
To establish the session key, the server should have an asymmetric key pair generated during server deployment and unknown to the clients. The users should know the key hash (256 bits) and additional server ID (256 bits) in advance in order to be able to establish connection.
|
||||
To establish the session keys and base IVs, the server should have an asymmetric key pair generated during server deployment and unknown to the clients. The users should know the key hash (256 bits) in advance in order to be able to establish connection.
|
||||
|
||||
The handshake sequence could be the following:
|
||||
The handshake sequence is the following:
|
||||
|
||||
1. Once the connection is established, the server sends its public key to the client
|
||||
2. The client compares the hash of the received key with the hash it already has (e.g. received as part of connection invitation or server in NEW command). If the hash does not match, the client must terminate the connection.
|
||||
3. If the hash is the same, the client should generate a random symmetric AES key and IV that will be used as a session key both by the client and the server.
|
||||
4. The client then should encrypt this symmetric key with the public key that the server sent and send back to the server the result and the server ID also shared with the client in advance: `rsa-encrypt(aes-key, iv, server-id)`.
|
||||
5. The server should decrypt the received key, IV and server id with its private key.
|
||||
6. The server should compare the `server-id` sent by the client and if it does not match its ID terminate the connection.
|
||||
7. In case of successful decryption and matching server ID, the server should send encrypted welcome header.
|
||||
1. Once the connection is established, the server sends server_header and its public RSA key encoded in X509 binary format to the client.
|
||||
2. The client compares the hash of the received key with the hash it already has (e.g. received as part of connection invitation or server in NEW command). If the hash does not match, the client must terminate the connection. TODO as the hash is optional in server syntax at the moment, hash comparison will be optional as well. Probably it should become required.
|
||||
3. If the hash is the same, the client should generate random symmetric AES keys and base IVs that will be used as session keys/IVs by the client and the server.
|
||||
4. The client then should construct client_handshake block and send it to the server encrypted with the server public key: `rsa-encrypt(client_handshake)`. `snd_aes_key` and `snd_base_iv` will be used by the client to encrypt **sent** messages and by the server to decrypt them, `rcv_aes_key` and `rcv_base_iv` will be used by the client to decrypt **received** messages and by the server to encrypt them.
|
||||
5. The server should decrypt the received keys and base IVs with its private key.
|
||||
6. In case of successful decryption, the server should send encrypted welcome block (encrypted_welcome_block) that contains SMP protocol version.
|
||||
|
||||
```abnf
|
||||
aes_welcome_header = aes_header_auth_tag aes_encrypted_header
|
||||
welcome_header = smp_version ["," smp_mode] *SP ; decrypt(aes_encrypted_header) - 32 bytes
|
||||
smp_version = %s"v" 1*DIGIT "." 1*DIGIT "." 1*DIGIT ["-" 1*ALPHA "." 1*DIGIT] ; in semver format
|
||||
; for example: v123.456.789-alpha.7
|
||||
smp_mode = smp_public / smp_authenticated
|
||||
smp_public = %s"pub" ; public (default) - no auth to create and manage queues
|
||||
smp_authenticated = %s"auth" ; server authentication with AUTH command (TBD) is required to create and manage queues
|
||||
aes_header_auth_tag = aes_auth_tag
|
||||
aes_auth_tag = 16*16(OCTET)
|
||||
```
|
||||
|
||||
No payload should follow this header, it is only used to confirm successful handshake and send the SMP protocol version that the server supports.
|
||||
|
||||
All the subsequent data both from the client and from the server should be sent encrypted using symmetric AES key and IV sent by the client during the handshake.
|
||||
All the subsequent data both from the client and from the server should be sent encrypted using symmetric AES keys and base IVs (incremented by counters on both sides) sent by the client during the handshake.
|
||||
|
||||
Each transport block sent by the client and the server has this syntax:
|
||||
|
||||
```abnf
|
||||
transport_block = aes_header_auth_tag aes_encrypted_header aes_body_auth_tag aes_encrypted_body
|
||||
aes_encrypted_header = 32*32(OCTET)
|
||||
header = padded_body_size payload_size reserved ; decrypt(aes_encrypted_header) - 32 bytes
|
||||
server_header = block_size protocol key_size
|
||||
block_size = 4*4(OCTET) ; 4-byte block size sent by the server, currently the client rejects if > 65536 bytes
|
||||
protocol = 2*2(OCTET) ; currently it is 0, that means binary RSA key
|
||||
key_size = 2*2(OCTET) ; the encoded key size in bytes (binary encoded in X509 standard)
|
||||
|
||||
client_handshake = client_block_size client_protocol snd_aes_key snd_base_iv rcv_aes_key rcv_base_iv
|
||||
client_block_size = 4*4(OCTET) ; 4-byte block size sent by the client, currently it is ignored by the server - reserved
|
||||
client_protocol = 2*2(OCTET) ; currently it is 0 - reserved
|
||||
snd_aes_key = 32*32(OCTET)
|
||||
snd_base_iv = 16*16(OCTET)
|
||||
rcv_aes_key = 32*32(OCTET)
|
||||
rcv_base_iv = 16*16(OCTET)
|
||||
|
||||
transport_block = aes_body_auth_tag aes_encrypted_body
|
||||
; size is sent by server during handshake, usually 8192 bytes
|
||||
aes_encrypted_body = 1*OCTET
|
||||
body = payload pad
|
||||
padded_body_size = size ; body size in bytes
|
||||
payload_size = size ; payload_size in bytes
|
||||
size = 4*4(OCTET)
|
||||
reserved = 24*24(OCTET)
|
||||
aes_body_auth_tag = aes_auth_tag
|
||||
aes_body_auth_tag = 16*16(OCTET)
|
||||
|
||||
encrypted_welcome_block = transport_block
|
||||
welcome_block = smp_version SP pad ; decrypt(encrypted_welcome_block)
|
||||
smp_version = %s"v" 1*DIGIT "." 1*DIGIT "." 1*DIGIT ["-" 1*ALPHA "." 1*DIGIT] ; in semver format
|
||||
; for example: v123.456.789-alpha.7
|
||||
pad = 1*OCTET
|
||||
```
|
||||
|
||||
## Possible future improvements/changes
|
||||
|
||||
- server id (256 bits), so that only the users that have it can connect to the server. This ID will have to be passed to the server during the handshake
|
||||
- block size agreed during handshake
|
||||
- transport encryption protocol agreed during handshake
|
||||
- welcome block containing SMP mode (smp_mode)
|
||||
|
||||
```abnf
|
||||
smp_mode = smp_public / smp_authenticated
|
||||
smp_public = %s"pub" ; public (default) - no auth to create and manage queues
|
||||
smp_authenticated = %s"auth" ; server authentication with AUTH command (TBD) is required to create and manage queues
|
||||
```
|
||||
|
||||
## Initial handshake
|
||||
@@ -105,7 +116,9 @@ Symmetric keys are generated per message and encrypted with receiver's public ke
|
||||
The syntax of each encrypted message body is the following:
|
||||
|
||||
```abnf
|
||||
encrypted_message_body = rsa_encrypted_header aes_encrypted_body
|
||||
encrypted_message_body = rsa_signature encrypted_body
|
||||
encrypted_body = rsa_encrypted_header aes_encrypted_body
|
||||
rsa_signature = 256*256(OCTET) ; sign(encrypted_body) - assuming 2048 bit key size
|
||||
rsa_encrypted_header = 256*256(OCTET) ; encrypt(header) - assuming 2048 bit key size
|
||||
aes_encrypted_body = 1*OCTET ; encrypt(body)
|
||||
|
||||
@@ -120,7 +133,6 @@ body = payload pad
|
||||
|
||||
Future considerations:
|
||||
- Generation of symmetric keys per session and session rotation;
|
||||
- Signature and verification of messages.
|
||||
|
||||
## E2E implementation
|
||||
|
||||
@@ -133,9 +145,9 @@ For each message, the agent performs the following actions to encrypt it:
|
||||
3. Encrypt the message using this AES256 key with [AES-GCM-SIV](https://hackage.haskell.org/package/cryptonite-0.28/docs/Crypto-Cipher-AESGCMSIV.html) (AEAD scheme) using the function `encrypt` with a random nonce.
|
||||
4. Encrypt the AES256 key with public encryption key for the queue using RSA-OAEP [encrypt](https://hackage.haskell.org/package/cryptonite-0.28/docs/Crypto-PubKey-RSA-OAEP.html#v:encrypt) function parameterized with [SHA256](https://hackage.haskell.org/package/cryptonite-0.28/docs/Crypto-Hash-Algorithms.html#t:SHA256) algorithm.
|
||||
5. The message to send is concatenation of:
|
||||
1. encrypted AES256 key,
|
||||
2. AuthTag from encryption in step 3,
|
||||
3. encrypted message (in this order)
|
||||
1. encrypted AES256 key,
|
||||
2. AuthTag from encryption in step 3,
|
||||
3. encrypted message (in this order).
|
||||
|
||||
As there is no additional data that is sent e2e in clear text, we potentially need some simpler algorithm, possibly the one implemented in the tutorial, that implements authenticated encryption without additional (unencrypted) data.
|
||||
|
||||
@@ -143,7 +155,7 @@ Or we could use this algorithm to allow some data that is sent e2e in clear text
|
||||
|
||||
### Message decryption
|
||||
|
||||
Agent decrypt the message following the same steps in the opposite order
|
||||
Agent decrypts the message following the same steps in the opposite order:
|
||||
|
||||
1. Split encrypted message to AES256 key, AuthTag and encrypted message - tag and key have fixed size (?).
|
||||
2. Decrypt AES256 key using the decryption key (the private key that recipient has).
|
||||
@@ -0,0 +1,52 @@
|
||||
# Open connections
|
||||
|
||||
## Problem
|
||||
|
||||
This proposal describes how to create invitations that can be used multiple times.
|
||||
|
||||
It can be used for:
|
||||
- an open invitation to join a group.
|
||||
- an open invitation to connect to a person - e.g. QR code/invitation link on a person's website, or passed from one person to another.
|
||||
- part of the solution for public DNS-based addresses (when a directory server would map address in some domain name#example.tld to an open invitation).
|
||||
|
||||
## Solution
|
||||
|
||||
No changes to SMP protocol - a dedicated unsecured SMP queue is used to receive invitations to connect that are sent in encrypted agent message. An unsecured SMP queue is used as an out-of-band channel for establishing another SMP queue.
|
||||
|
||||
Additional parameters in commands in SMP agent protocol:
|
||||
|
||||
- `NEW` command will have a parameter `INV` or `CON` to create an invitation or a permanent contact connection.
|
||||
|
||||
`conn_id? OPEN` (or `PUB`, `NEWPUB`, tbc) - to create an "open"/"public" queue, the response is an invitation in a different format (TBC):
|
||||
- should allow multiple servers (probably the original invitation should be extended to support it)
|
||||
- should have a marker to indicate it's an open/public queue (probably the original invitation should be extended to include an invitation type).
|
||||
|
||||
e.g. `smp:<queue_type>::<server1>/<queue_id1>,<server2>/<queue_id2>::<key1>`
|
||||
|
||||
`queue_type`:
|
||||
- `prv` - original invitation, should be accepted with KEY SMP message
|
||||
- `pub` - open invitation, should be accepted with INV SMP message (to be added to SMP protocol)
|
||||
|
||||
```mmd
|
||||
A ->> AA: oidA? OPEN
|
||||
AA ->> A: oidA INV pub_inv
|
||||
|
||||
...
|
||||
|
||||
B ->> BA: cidBA? JOIN pub_inv len CRLF meta_binary CRLF ; change command to require meta, len can be 0 for the current usage ; meta is used to send user profile
|
||||
BA ->> B: cidBA OK
|
||||
BA ->> SA ->> AA: INV prv_inv CRLF meta_binary
|
||||
AA ->> A: oidA CONF invID len meta_binary
|
||||
A ->> AA cidAB? LET invID
|
||||
|
||||
establish connection as usual
|
||||
|
||||
BA ->> B: cidBA CON
|
||||
AA ->> A: cidAB CON
|
||||
```
|
||||
|
||||
That protocol requires addressing the current problem when an invitation cannot be accepted when the party that generated the invitation is not online.
|
||||
|
||||
Questions.
|
||||
|
||||
1. Do we need to differentiate the semantics of the invitation on the syntax level, or should we allow to just manage it outside of protocol when the receiving agent decides which SMP messages to accept and which to ignore (KEY / INV).
|
||||
@@ -0,0 +1,91 @@
|
||||
# Notification server
|
||||
|
||||
## Background and motivation
|
||||
|
||||
SimpleX Chat clients should receive message notifications when not being online and/or subscribed to SMP servers.
|
||||
|
||||
To avoid revealing identities of clients directly to SMP servers via any kind of push notification tokens, a new party called SimpleX Notification Server is introduced to act as a service for subscribing to SMP server queue notifications on behalf of clients and sending push notifications to them.
|
||||
|
||||
## Proposal
|
||||
|
||||
TCP service using the same TLS transport as SMP server, with the fixed size blocks (256 bytes?) and the following set of commands:
|
||||
|
||||
### Protocol
|
||||
|
||||
#### Create subscription
|
||||
|
||||
Command:
|
||||
|
||||
`%s"CREATE " ntfSmpQueueURI ntfPrivateKey token subPublicKey`
|
||||
|
||||
Response:
|
||||
|
||||
`s%"OK"`
|
||||
|
||||
#### Check subscription status
|
||||
|
||||
Command:
|
||||
|
||||
`%s"CHECK " ntfSmpQueueURI`
|
||||
|
||||
Response:
|
||||
|
||||
```abnf
|
||||
statusResp = %s"STAT " status
|
||||
status = %s"ERR AUTH" / "ERR SMP AUTH" / %s"ERR SMP TIMEOUT" / %s"ACTIVE" / %s"PENDING"
|
||||
```
|
||||
|
||||
#### Update subscription device token
|
||||
|
||||
Command:
|
||||
|
||||
`%s"TOKEN " ntfSmpQueueURI token`
|
||||
|
||||
Response:
|
||||
|
||||
`s%"OK" / %s"ERR"`
|
||||
|
||||
#### Delete subscription (e.g. when deleting the queue or moving to another notification server)
|
||||
|
||||
Command:
|
||||
|
||||
`%s"DELETE " SP ntfSmpQueueURI`
|
||||
|
||||
Response:
|
||||
|
||||
`s%"OK" / %s"ERR"`
|
||||
|
||||
### Agent schema changes
|
||||
|
||||
See [migration](../src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20220322_notifications.hs)
|
||||
|
||||
### Agent code
|
||||
|
||||
```haskell
|
||||
data NtfOptions = NtfOptions
|
||||
{ ntfServer :: Server, -- same type as for SMP servers, probably will be renamed
|
||||
ntfToken :: ByteString,
|
||||
ntfInitialCheckDelay :: Int, -- initial check delay after subscription is created, seconds
|
||||
ntfPeriodicCheckInterval :: Int -- subscription check interval, seconds
|
||||
}
|
||||
|
||||
data AgentConfig = AgentConfig {
|
||||
-- ...
|
||||
initialNtfOpts :: Maybe NtfOptions
|
||||
-- ...
|
||||
}
|
||||
|
||||
data AgentClient = AgentClient {
|
||||
-- ...
|
||||
ntfOpts :: TVar (Maybe NtfOptions)
|
||||
-- ...
|
||||
}
|
||||
```
|
||||
|
||||
A configuration parameter `initialNtfOpts :: Maybe NtfOptions` - if it is set or changes the agent would automatically manage subscriptions as SMP queues are subscribed/created/deleted and as the token or server changes.
|
||||
|
||||
There will be a method to update notifications configuration in case token or server changes.
|
||||
|
||||
All subscriptions will be managed in a separate subscription management loop, that would always take the earliest un-updated subscription that requires some action (ntf_sub_action column) and perform this action - the table of subscription would serve both as the table of existing subscriptions and required actions.
|
||||
|
||||
E.g. if the queue is subscribed and there is no notification subscription, it will be created in the table with "create" action, and the loop would create it and schedule "check" action on it.
|
||||
@@ -0,0 +1,34 @@
|
||||
# SMP protocol changes to support push notifications on iOS
|
||||
|
||||
## Problem
|
||||
|
||||
There are already commands/responses to allow subscriptions to message notifications - NKEY/NID, NSUB/NMSG. These commands will be used by SMP agent (NKEY/NID) and by notification server (NSUB/NMSG) to have message notifications delivered to notification server, so it can forward them to APNS server using device token.
|
||||
|
||||
There are two remaining problems that these commands do not solve.
|
||||
|
||||
1. Receiving the message when notification arrives.
|
||||
|
||||
iOS requires creating a bundled notification service extension (NSE) that runs in isolated container and, if we were to use the existing commands, would have SMP subscription to the same SMP servers as the main app, triggering resubscriptions every time the message reception switches between the app and NSE. That would cause a substantial increase in traffic and battery consumption to the users.
|
||||
|
||||
2. Showing notifications for service messages.
|
||||
|
||||
Users do not expect to see notifications for every single SMP messages - e.g., we currently do not show notifications when messages are edited and deleted, and users do not expect them. NSE requires that for every received push notification there should be some notification shown to the users. So only we would have to show a notification for message deletes and updates, we would have to show it for all service messages - e.g. user accepted file invitation, or file transmission has started, contact profile updates and so on.
|
||||
|
||||
We considered differentiating whether notifications are sent per queue, from the recipient side, so we do not send notifications for file queues. But it seems insufficient, particularly if we add such features as message receipts, status updates, etc.
|
||||
|
||||
## Proposal
|
||||
|
||||
1. To retrieve messages when push notifications arrive, we will add 2 SMP commands:
|
||||
|
||||
- GET: retrieve one message from the connection. Resonse could be either MSG (the same as when MSG is delivered, but with the correlation id) or GMSG (to simplify processing) – TBC during implementation. If message is not available, the response could be ERR NO_MSG
|
||||
- ACK or GACK: acknowledge that the message was processed by the client and can be removed - TBC which one is better. The response is OK or ERR NO_MSG if there was nothing to acknowledge (same as with ACK now)
|
||||
|
||||
This would allow receiving a single message from the queue without subscription, this way avoiding that the main app is unsubscribed from the queue.
|
||||
|
||||
2. The only way to avoid showing unnecessary notifications (status updates, service messages, etc.) is to avoid sending them. That requires instructing SMP server whether notification should be sent both per queue, from the recipient side, and per message - from the sender side. So the notification would only be sent if the queue has them enabled (via NKEY command) and the sender includes an additional flag in SEND command. The same flag should be included into MSG, so when the message is retrieved with GET command, the client knows, on the agent or chat level (or both), whether this message should have notification shown to the user, and if not - retrieve the next one(s) as well.
|
||||
|
||||
This is a substantial change to SMP protocol, that would require client and server upgrade for notifications to be supported.
|
||||
|
||||
We should consider whether to increase the SMP protocol version number to 2, so that the new clients can connect to the old clients but without notifications, or we could keep the old commands in the protocol and instead of adding flags to the existing commands, create new commands.
|
||||
|
||||
We can also consider making commands extensible so that the new flags can be added (and ignored by parsers if not supported) to at least some existing commands.
|
||||
@@ -0,0 +1,75 @@
|
||||
# DB access and processing messages for iOS notification service extension
|
||||
|
||||
## Problem
|
||||
|
||||
The only way to receive/process notificaitons is via a separate NSE process that requires a concurrent DB and network access.
|
||||
|
||||
SQLite concurenncy does not work, so we need to sync database access.
|
||||
|
||||
The problem is complex, as we do not directly control db access from the app, it can be triggered by the message arriving and it may fail to complete in case the app is suspended in the background.
|
||||
|
||||
So we need to prevent db access from starting when we know the app is about to be suspended.
|
||||
|
||||
The last problem is how to receive and process messages in NSE - should it use recently added GET command or should it subscribe to the connections that receive messages and process messages normally.
|
||||
|
||||
To summarize, 2 problems need to be solved:
|
||||
|
||||
1. sync db access between 2 (or more, if we add share extension) processes
|
||||
|
||||
2. prevent access from starting when the process is due to suspend, only complete operations.
|
||||
|
||||
3. Receiveing and processing agent messages in NSE
|
||||
|
||||
## Proposed solution
|
||||
|
||||
For problem 1, we can use Posix semaphores from our core code, in the same bracket that manages database connection - it would wait for semaphore to be free and unlock it once the db operation is complete.
|
||||
|
||||
For problem 2, we need to communicate from the app when it goes to the background to prevent database access starting and completing before the suspension. This would set some `aboutToSuspend` STM flag (or the opposite) that would prevent operations from progressing (using STM retry, that would block until the flag has the value allowing operation to progress).
|
||||
|
||||
Several possibilities can be considered:
|
||||
|
||||
- use this flag in the bracket that provides DB connections. While simple, it may lock some operations in the middle and may also lead to the situation when network operation succeeds but database access was blocked, and the database is not updated.
|
||||
- use this flag to stop network operations that would require database updates - like sending messages, subscriptions and ACK - all these operations would require database access once they succeed.
|
||||
- use two flags, for both cases above, but set them at different times since going to background - block new network operations as soon as the app goes to the background and block database access once the app is about to be suspended.
|
||||
|
||||
The last option seems more robust. To do it, there will be an api allowing the app to communicat its phase:
|
||||
|
||||
- app going to the background would trigger blocking new network operations and start a new background task - `background` phase.
|
||||
- background task receiving completion warning, or, maybe, some time after it is started - probably 20 seconds - or whatever happens earlier - would trigger call blocking db access - `suspended` phase.
|
||||
- app becoming active would trigger unblocking both flags - `active` phase.
|
||||
|
||||
`/_app phase <phase>` where `phase` can be one of the above values.
|
||||
|
||||
NSE would also use the same phases:
|
||||
|
||||
- sending `active` when it is started (the process starts as active, but it is possible that the new notification arrives to the same process, after the previous one sent background/suspension)
|
||||
- sending `background + suspended` (or `suspended` should set both flags) once it is finished processing the notification, provided no new notification arrived and started processing - this should be tracked in NSE.
|
||||
|
||||
For problem 3, NSE can do one of the following:
|
||||
|
||||
- use SUB and process messages normally - the downside is that the app will have to resubscribe and it has to be tracked.
|
||||
- use GET and process messages by pushing them through the processing function - the downside it that a rewiring of message processing is needed.
|
||||
- use GET but deliver messages through the same queue as when they arrive normally (in which case getMessage agent function should not return the message, but will return a flag showing whether the message was received, or, possibly, or, possibly will return a message but the message would also be sent to the queue?).
|
||||
- process messages in agent manually and in chat via the queue.
|
||||
|
||||
One of the downside of GET is that it requires calling GET again after ACK. We could have two variants of ACK (or additional ACK parameter) - one that never delivers a new message, and another one that does. In this case, if get needs to process the next message (when the current one has no notification flag), it can call ACK that delivers the next message. But, it is probably a premature optimization, and having general support of batched commands would add more value.
|
||||
|
||||
Additional problem is concurrency in NSE - if the new notification from the same queue arrives before the current one finishes processing in the same process one of the following can happen:
|
||||
|
||||
- the 2nd notification naively call GET and receives the same message.
|
||||
- the 2nd notification waits until the first finished processing, in which case it can run out of time.
|
||||
|
||||
The problem is that the app won't know it's the same queue, as nId is encrypted, so the agent should handle this scenario when the new call to getMessage is made before the previous one finished processing, and differentiate between calls made for additional messages (possibly, getMessage should include previous message ID, if it is available) and the first call.
|
||||
|
||||
EDIT: GETs have to be sent from UI to chat and from chat to agent as function calls, but the agent will have to queue get calls to make sure they return different messages. GET call would return message flags (incl. notification flag), so that the UI can send the next GET if needed without waiting.
|
||||
|
||||
Considered alternative: include notification content in the message and have NSE only perform decryption, without any network IO. In this case notification content would be in SEND and in NMSG, e2e encrypted.
|
||||
|
||||
While promising, as it solves network coordination issues and makes GET unnecessary, it creates mutliple other problems, so it was rejected:
|
||||
|
||||
- message content is exposed to centralized ntf and apns servers, creating additional attack vector.
|
||||
- it adds complexity in security critical parts of the stack - double ratchet encryption, as it requires either storing message keys and using different IVs for notifications, or initializing completely separate ratchet for notifications content.
|
||||
- it reduces the size of the message.
|
||||
- it makes user experience worse, as:
|
||||
- it would not accelerate handshake for new contacts and for file delivery - this approach only works for content messages.
|
||||
- it would open the app without the new messages - the users would have to wait until the messages are received. It is also bad for "security optics" - the users might think that the message content was exposed to notifications.
|
||||
@@ -0,0 +1,61 @@
|
||||
sequenceDiagram
|
||||
participant M as iOS message<br>notification
|
||||
participant S as iOS system
|
||||
participant N as iOS NSE
|
||||
participant U as iOS UI
|
||||
participant C as Core chat
|
||||
participant A as Core agent
|
||||
|
||||
M ->> N: notification
|
||||
S ->> N: get app pref
|
||||
note over N: ignore,<br>app is active
|
||||
|
||||
note over M, A: app going to background
|
||||
S ->> U: phase: background<br>(possibly, "will" method)
|
||||
U ->> S: set app pref "pausing"
|
||||
U ->> C: /_app phase paused, result CRCmdOk
|
||||
C ->> A: pauseAgent<br>(no new network IO)
|
||||
M ->> N: notification
|
||||
S ->> N: get app pref
|
||||
note over N: wait/poll for<br>"paused"/"suspending"/"suspended"<br>event/pref
|
||||
A ->> C: event "IO paused"<br>(after in-flight op completed)<br>PHASE PAUSED
|
||||
C ->> U: event "IO paused" (CRAppPaused)
|
||||
U ->> S: set shared pref "paused"
|
||||
|
||||
note over M, A: process notification
|
||||
M ->> N: notification
|
||||
S ->> N: get app pref<br>continue if<br>"paused"/"suspending"/"suspended"
|
||||
N ->> S: set NSE pref "active"
|
||||
N ->> C: /_get message
|
||||
C ->> A: getMessage
|
||||
A ->> C: msg flags
|
||||
C ->> N: msg flags
|
||||
note over N: get messages<br>until notification flag set
|
||||
A ->> C: MSG/CONF/INFO
|
||||
C ->> N: some event
|
||||
N ->> S: set NSE pref "completed"
|
||||
N ->> S: show notification
|
||||
|
||||
note over M, A: app about to be suspended<br>(or 15-20 sec after background)
|
||||
S ->> U: background task notice
|
||||
U ->> S: set app pref "suspending"
|
||||
U ->> C: /_app phase suspended, response ok
|
||||
C ->> A: suspendAgent<br>(no new DB)
|
||||
A ->> C: event "DB paused"<br>(after in-flight op completed)<br>PHASE SUSPENDED
|
||||
C ->> U: event "DB paused" (CRAppSuspended)
|
||||
U ->> S: set app pref "suspended"
|
||||
|
||||
note over M, A: app about to be activated
|
||||
S ->> U: phase: active<br>(or inactive?)<br><br>(possibly, "will" method)
|
||||
S ->> U: get NSE pref
|
||||
U ->> S: set app pref "activating"
|
||||
alt nse active?
|
||||
U ->> C: /_app phase inactive
|
||||
note over U: poll/wait till NSE pref is "completed"
|
||||
end
|
||||
|
||||
U ->> C: /_app phase active (response result)
|
||||
C ->> A: activateAgent<br>(allow IO/DB)
|
||||
A ->> C: result ()
|
||||
C ->> U: CRCmdOk
|
||||
U ->> S: set app pref "active"
|
||||
@@ -0,0 +1,26 @@
|
||||
# Accessing SMP servers via Tor
|
||||
|
||||
## Problem
|
||||
|
||||
While SMP protocol is focussed on minimizing application-level meta-data by using pair-wise identifiers instead of user profile identifiers, it is important for many users to protect their IP addresses.
|
||||
|
||||
Further, even if IP addresses are hidden by onion routing, clients should be able to choose to use a separate TCP connection to subscribe to each queue, even though it increases traffic and battery consumption, as otherwise the servers can observe multiple queues accessed by the same client.
|
||||
|
||||
## Solution and requirements
|
||||
|
||||
While some users may want to access SMP servers via tor, some other users (even their contacts) may want the opposite - e.g., if they use the network when accessing Tor would be suspicious (or blocked).
|
||||
|
||||
Therefore we need to support the connections when one of the user accesses the same server via Tor (and, possibly, via onion address), while another user accesses this server without Tor.
|
||||
|
||||
At the same time the user accessing the server via Tor may not want that their contacts access this server without Tor, and it also may be possible that the server is not available under a normal (not .onion) address.
|
||||
|
||||
The proposed options for connecting via Tor are:
|
||||
|
||||
1. Access servers via Socks proxy: no/yes (specify port?)
|
||||
2. Use .onion addresses: no/when available/warn/always
|
||||
3. Require senders to use .onion addresses: yes/no
|
||||
4. Use separate TCP connection for each queue
|
||||
|
||||
While it should be possible for SMP servers to have two addresses (with and without Tor), the queues should only use one server address - if the queue started being accessed via .onion address it should not be possible to access it via a normal address. Queue addresses in connection invitations should support dual server addresses (when senders are not required ot use .onion address).
|
||||
|
||||
At the same time, the queue with the normal addresses can be accessed with and without Tor, depending on the current device settings.
|
||||
@@ -0,0 +1,53 @@
|
||||
# SMP queue rotation and redundancy
|
||||
|
||||
## Problem
|
||||
|
||||
1. Long term usage of the queue allows the servers to analyze long term traffic patterns.
|
||||
|
||||
2. If the user changes the configured SMP server(s), the previously created contacts do not migrate to the new server(s).
|
||||
|
||||
3. Server can lose messages.
|
||||
|
||||
## Solution
|
||||
|
||||
Additional messages exchanged by SMP agents to negotiate addition and removal of queues to the connection.
|
||||
|
||||
This approach allows for both rotation and redundancy, as changing queue will be done be adding a queue and then removing the existing queue.
|
||||
|
||||
The reason for this approach is that otherwise it's non-trivial to switch from one queue to another without losing messages or delivering them out of order, it's easier to have messages delivered via both queues during the switch, however short or long time it is.
|
||||
|
||||
### Messages
|
||||
|
||||
Additional agent messages required for the protocol:
|
||||
|
||||
QADD_ -> "QA"
|
||||
QKEY_ -> "QK"
|
||||
QUSE_ -> "QU"
|
||||
QTEST_ -> "QT"
|
||||
|
||||
`QADD`: add the new queue address(es), the same format as `REPLY` message, encoded as `QA`.
|
||||
|
||||
`QKEY`: pass sender's key via existing connection (SMP confirmation message will not be used, to avoid the same "race" of the initial key exchange that would create the risk of intercepting the queue for the attacker), encoded as `QK`.
|
||||
|
||||
`QUSE`: instruct the sender to use the new queue with sender's queue ID as parameter, encoded as `QU`.
|
||||
|
||||
`QTEST`: send test message to the new connection, encoded as `QT`. Any other message can be sent if available to continue rotation, the absence of this message is not an error. Once this message is successfully sent the sender will stop using the old queue. Once this message (or any other message in the new queue) is received, the recipient will stop using the old queue and delete it.
|
||||
|
||||
### Protocol
|
||||
|
||||
```
|
||||
participant A as Alice
|
||||
participant B as Bob
|
||||
participant R as Server that has A's receive queue
|
||||
participant S as Server that has A's send queue (B's receive queue)
|
||||
participant R' as Server that hosts the new A's receive queue
|
||||
|
||||
A ->> R': create new queue
|
||||
A ->> S ->> B: QADD (R'): snd address of the new queue(s)
|
||||
B ->> S(R) ->> A: QKEY (R'): sender's key for the new queue(s) (to avoid the race of SMP confirmation for the initial exchange)
|
||||
A ->> S(R'): secure new queue
|
||||
A ->> S ->> B: QUSE (R'): instruction to use new queue(s)
|
||||
B ->> S(R') ->> A: QTEST
|
||||
A ->> S(R): DEL: delete the old queue
|
||||
B ->> S(R') ->> A: send all new messages to the new queue
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
# SMP Basic Auth
|
||||
|
||||
## Problem
|
||||
|
||||
Users who host their own servers do not want unknown people to be able to create messaging queues on their servers after discovering server address in groups or after making a connection. As the number of self-hosted servers is growing it became more important than it was when we excluded it from the original design.
|
||||
|
||||
## Solution
|
||||
|
||||
Single access password that can be optionally included in server address that is passed to app configuration. It will not be allowed in the existing contexts (and parsing will fail), to avoid accidentally leaking it. Server address with password will look this way: `smp://fingerprint:password@hosts`
|
||||
|
||||
## Implementation plan
|
||||
|
||||
1. A separate type to include server and password, so it can only be used where allowed.
|
||||
|
||||
2. Server password to create queues will be configured in TRANSPORT section of INI file, as `create_password` parameter.
|
||||
|
||||
3. The password will only be required in server configuration/address to create queues only, it won't be required for other receiving queue operations on already existing queues.
|
||||
|
||||
4. If new command is attempted in the session that does not allow creating queues, the server will send `ERR AUTH` response
|
||||
|
||||
5. Passing password to the server can be done in one of the several ways, we need to decide:
|
||||
|
||||
- as a parameter of NEW command. Pros: a local change, that only needs checking when queue is created. Cons: protocol version change.
|
||||
- as a separate command AUTH. Pros: allows to include additional parameters and potentially be extended beyond basic auth. Cons: more complex to manage server state, can be more difficult syntax in the future, if extended.
|
||||
- as part of handshake (we currently ignore the unparsed part of handshake block, so it can be extended). Pros: probably, the simplest, and independent of the commands protocol – establishes create permission for the current session. Cons: the client won't know about whether it is able to create the queue until it tries (same as in case 1).
|
||||
|
||||
My preference is the last option. As a variant of the last option, we can add a server response/message that includes permission to create queues - it will only be sent to the clients who pass credential in handshake - that might simplify testing server connection (we currently do not do it). It might be unnecessary, as we could simply create and delete queue in case credential is passed as part of testing connection (and even sending a message to it).
|
||||
@@ -0,0 +1,50 @@
|
||||
# SMP agent broadcast
|
||||
|
||||
## Problem
|
||||
|
||||
Support agent message broadcast to multiple connections.
|
||||
|
||||
It is done in ad-hoc way as part of the previous [groups proposal](./2021-05-23-groups2.md) - this proposal defines broadcast as a separate agent primitive to simplify group management.
|
||||
|
||||
It can also be used for other purposes when the same message needs to be sent to multiple recipients without creating groups.
|
||||
|
||||
## Solution
|
||||
|
||||
A minimal protocol of additional client commands to create, manage and use broadcasts.
|
||||
|
||||
From the point of view of the recipient this will look like a normal message, as if the sending agent executed multiple send commands (in fact, broadcast can be implemented by agent sending itself multiple SEND commands)
|
||||
|
||||
### Commands and messages
|
||||
|
||||
- command `B:bId? NEW` - create broadcast (response is `B:bId OK`, or `ERR` if broadcast already exists)
|
||||
- command `B:bId ADD C:cId` - add existing connection to a broadcast (response is `B:bId OK` or `ERR`, e.g. if connection already added or does not exist)
|
||||
- command `B:bId SEND msg` - broadcast message (response is multiple `B:cId SENT [C:bId] msgId` or ERR, separately for each connection and then for the broadcast)
|
||||
- message `B:bId SENT [C:bId] msgId` - notification that the message is sent to a specific or all recipients
|
||||
- command `B:bId REM C:cId` - remove connection from broadcast (response is `B:bId OK` or `ERR`)
|
||||
- message `B:bId EMPTY` - all connections were removed from the broadcast
|
||||
- command `B:bId DEL` - delete broadcast (response is `B:bId OK` and when the last connection is removed an additional `B:bId EMPTY` is sent)
|
||||
- command `B:bId LS` - list connections in broadcast, response is `B:Id MS space_separated_connections`
|
||||
- message `B:bId MS space_separated_connections`
|
||||
|
||||
## Questions
|
||||
|
||||
1. Should broadcast IDs use the same namespace as connection IDs (and as group IDs)? Having the same namespace for all abstractions that the agent can operate on can be helpful, as it can also allow implementing some queries to determine which type a given ID has, but it also increases implementation complexity.
|
||||
|
||||
2. Given that this abstraction would be used as internal abstraction for groups (same as connections internal to the group), it might be better to implement "agent users", each with its own connection namespace. In this case agent would use itself as one of the users.
|
||||
|
||||
3. There is a similarity of commands for connections, groups and broadcasts, they only differ on the single-letter prefix. We could do one of the following:
|
||||
- use the same command for different object types. This feels incorrect and error prone on its own.
|
||||
- extend transmission structure with the field defining the object type (connection, group, broadcast, etc.).
|
||||
|
||||
In this case, the transmission would look like:
|
||||
|
||||
```
|
||||
agentTransmission = [corrId] CRLF objectType:[objectID] CRLF agentCommand
|
||||
objectType = C | B | G ; this is the additional field
|
||||
```
|
||||
|
||||
This approach would allow reusing the existing command avoiding the unnecessary repetition.
|
||||
|
||||
In this case, the command type could be parameterized with the list of supported agent object types, so we can ensure on the type level that only allowed commands can be constructed.
|
||||
|
||||
EDIT: This approach is already implemented
|
||||
@@ -0,0 +1,41 @@
|
||||
# SMP agent introduction
|
||||
|
||||
## Problem
|
||||
|
||||
Allow an agent client to connect two connections it has directly, with the agent acting as an out-of-band channel.
|
||||
|
||||
It can be used both separately as part of some client functionality, and as part of group protocol.
|
||||
|
||||
## Solution
|
||||
|
||||
A protocol with commands and message envelopes to exchange the information between parties to establish connection.
|
||||
|
||||
### Commands and messages
|
||||
|
||||
Below commands are for the scenario when A introduces B to M.
|
||||
|
||||
- command `C:idAB INTRO C:idAM infoM` - initiate introduction of the connection cIdB to connection cIdM (response is `C:idAB OK`)
|
||||
- message `C:idBA REQ C:invId infoM` - notification to confirm introduction
|
||||
- command `C:idBM? ACPT C:invId` - accept offer to be introduced (response is `C:idBM OK`, followed by `C:idBM CON`)
|
||||
- message `C:idBM CON` - confirmation that connection is established to both introduced parties
|
||||
- message `C:idAB CON C:idAM` - confirmation that connection is established to the introducer
|
||||
|
||||
### Agent envelopes
|
||||
|
||||
- `INTRO C:extIntroIdM infoM` - new introduction offered by introducer
|
||||
- `INV C:extIntroIdB prv:invBM infoB` - invitation to join connection from B to M sent via A
|
||||
- `REQ C:extIntroIdB prv:invBM infoB` - new introduction forwarded by the introducer
|
||||
- `CON C:extIntroId` - confirmation that the connection is established sent by both introduced parties to the introducer
|
||||
|
||||
## Namespace
|
||||
|
||||
Given that the introduction objects are short lived, they should not reuse the same commands or share the same namespace as connections, broadcasts and groups, but they probably should share the namespace with group and connection invitations.
|
||||
|
||||
## Introduction protocol costs
|
||||
|
||||
5 messages + cost to establish a connection
|
||||
|
||||
|
||||
The [sequence digram for introduction](https://mermaid.ink/img/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG4gIHBhcnRpY2lwYW50IEEgYXMgQWxpY2UgKEEpIC0gdGhlIGludHJvZHVjZXJcbiAgcGFydGljaXBhbnQgQUEgYXMgQWxpY2Unczxicj5hZ2VudCAoQUEpXG4gIHBhcnRpY2lwYW50IEIgYXMgQm9iIChCKSAtIGludHJvZHVjZWRcbiAgcGFydGljaXBhbnQgQkEgYXMgQm9iJ3M8YnI-YWdlbnQgKEJBKVxuICBwYXJ0aWNpcGFudCBNIGFzIE1hcmsgKE0pIC0gaW50cm9kdWNlZCB0b1xuICBwYXJ0aWNpcGFudCBNQSBhcyBNYXJrJ3M8YnI-YWdlbnQgKE1BKVxuXG4gIG5vdGUgb3ZlciBBLCBBQTogMS4gY3JlYXRlIGludHJvZHVjdGlvblxuICBBIC0-PiBBQTogQzppZEFCIElOVFJPIEM6aWRBTSBpbmZvTTxicj4oaWRBQiAtIGNvbm4gYWxpYXMgQSBoYXMgZm9yIEIsPGJyPmlkQU0gLSBmb3IgTSlcbiAgQUEgLT4-IEE6IEM6aWRBQiBPS1xuXG4gIG5vdGUgb3ZlciBBLCBCQTogMi4gc2VuZCBpbnRybyB0byBCb2JcblxuICBBQSAtPj4gQkE6IHZpYSBpZEFCOiBJTlRSTyBDOmV4dEludHJvSWRNIGluZm9NXG4gIEJBIC0-PiBCOiBDOmlkQkEgUkVRIEM6aW50SW50cm9JZE0gaW5mb01cbiAgQiAtPj4gQkE6IEM6aWRCTT8gQUNQVCBDOmludEludHJvSWRNXG4gIEJBIC0-PiBCOiBDOmlkQk0gT0tcblxuICBub3RlIG92ZXIgQkE6IDMuIGNyZWF0ZSBjb25uZWN0aW9uIGZvciAgTSBpZEJNXG5cbiAgQkEgLT4-IEFBOiB2aWEgaWRCQTogSU5WIEM6ZXh0SW50cm9JZE0gcHJ2OmludkJNIGluZm9CXG5cbiAgbm90ZSBvdmVyIEFBLCBNOiA0LiBzZW5kIGludHJvIHRvIE1hcmtcblxuICBBQSAtPj4gTUE6IHZpYSBpZEFNOiBSRVEgQzpleHRJbnRyb0lkQiBwcnY6aW52Qk0gaW5mb0JcblxuICBub3RlIG92ZXIgTUEsIEI6IDUuIE1hcmsgY29ubmVjdHMgdG8gQm9iXG5cbiAgTUEgLT4-IE06IEM6aWRNQSBSRVEgQzppbnRJbnRyb0lkQiBpbmZvQlxuICBNIC0-PiBNQTogQzppZE1CPyBBQ1BUIEM6aW50SW50cm9JZEJcbiAgTUEgLT4-IE06IEM6aWRNQiBPS1xuXG4gIE1BIC0-PiBCQTogIGVzdGFibGlzaCBjb25uZWN0aW9uIGlkQk0gLT4gaWRNQlxuXG4gIG5vdGUgb3ZlciBBLCBNQTogNi4gbm90aWZ5IGFsbCBjbGllbnRzXG5cbiAgTUEgLT4-IE06IEM6aWRNQiBDT05cbiAgTUEgLT4-IEFBOiB2aWEgaWRNQTogQ09OIEM6ZXh0SW50cm9JZEJcbiAgQkEgLT4-IEI6IEM6aWRCTSBDT05cbiAgQkEgLT4-IEFBOiB2aWEgaWRCQTogQ09OIEM6ZXh0SW50cm9JZEJcbiAgQUEgLT4-IEE6IEM6aWRBQiBDT04gQzppZEFNXG4iLCJtZXJtYWlkIjp7fSwidXBkYXRlRWRpdG9yIjpmYWxzZX0), the source is [here](./intro.mmd).
|
||||
|
||||

|
||||
@@ -0,0 +1,135 @@
|
||||
sequenceDiagram
|
||||
participant M as Existing<br>member (M)
|
||||
participant MA as Existing<br>member<br>agent (MA)
|
||||
participant A as Alice (A)
|
||||
participant AA as Alice's<br>agent (AA)
|
||||
participant BA as Bob's<br>agent (BA)
|
||||
participant B as Bob (B)
|
||||
|
||||
note over A, AA: 1. create new group (no members)
|
||||
A ->> AA: G:gidA? NEW<br>(gidA - group ID of this group for A,<br>can be generated by the agent)
|
||||
|
||||
note over AA: create "internal" broadcast associated with the group<br>(B: NEW)
|
||||
|
||||
AA ->> A: G:gidA OK
|
||||
|
||||
note over A, BA: 2. add Bob to group
|
||||
|
||||
A ->> AA: C:idAB INTRO G:gidA gInfo<br>(idAB - conn alias A has for B)
|
||||
|
||||
note over AA: generate new random ID for member B (midB, unique per group)<br>initiate "internal" connection gidAB for B in group<br>(internal means that it is not visible to the clients<br>and cannot be used with client commands)
|
||||
|
||||
AA ->> BA: via idAB: GROUP C:midB g:gInvAB gInfo
|
||||
BA ->> B: C:idBA REQ G:invID gInfo<br>(invID - to refer to it in ACPT)
|
||||
B ->> BA: G:gidB? ACPT G:invID<br>(RJCT G:invID could be added)
|
||||
|
||||
note over BA: create group and "internal" broadcast associated with the group<br>(B: NEW)
|
||||
|
||||
BA ->> B: G:gidB OK
|
||||
|
||||
BA ->> AA: establish internal connection gidBA (using g:gInvAB) for A in group
|
||||
|
||||
note over BA, AA: add connections gidBA and gidAB to broadcasts<br>(B: ADD)
|
||||
|
||||
AA ->> A: G:gidA CON C:idAB
|
||||
BA ->> B: G:gidA CON C:idBA
|
||||
|
||||
note over M, B: For each existing member M:<br>create and accept internal introduction between connections, related to the group, via gidAB/BA/AM/MA, connections created are gidBM and gidMB<br>The fact that the introduction arrives via connection allocated for the group, allows agents identify it as a new group member, ID used in introductions is group-scoped member ID.
|
||||
|
||||
note over A, BA: once all members were sent to B
|
||||
AA ->> BA: via gidAB: MEM C:midB
|
||||
|
||||
note over BA, B: once all members are connected
|
||||
BA ->> B: G:gidB MEM
|
||||
|
||||
note over A, AA: once all members reported connection
|
||||
AA ->> A: G:gidA MEM C:idAB
|
||||
|
||||
note over M, AA: for each member M
|
||||
|
||||
AA ->> MA: via gidAM: MEM C:midB
|
||||
MA ->> M: G:gidM MEM C:idMB
|
||||
|
||||
note over M, B: 3. B sends message to the group
|
||||
|
||||
B ->> BA: G:gidB SEND msg
|
||||
|
||||
note over BA: send message via associated broadcast and respond to client with SENT notifications
|
||||
|
||||
BA ->> B: G:gidB SENT C:idBA intMsgID
|
||||
BA ->> B: G:gidB SENT C:idBM intMsgID
|
||||
|
||||
note over BA, B: once sent to all
|
||||
BA ->> B: G:gidB SENT intMsgID
|
||||
|
||||
AA ->> A: G:gidA MSG C:idAB intMsgID msgdata
|
||||
A ->> AA: G:gidA ACK intMsgID
|
||||
AA ->> BA: via gidAB: RCVD extMsgID hash sig
|
||||
BA ->> B: G:gidA RCVD C:idBA intMsgID status<br>(status - message integrity check)
|
||||
|
||||
MA ->> M: G:gidM MSG C:idMB intMsgID msgdata
|
||||
M ->> MA: G:gidM ACK intMsgID
|
||||
MA ->> BA: via gidMB: RCVD extMsgID hash sig
|
||||
BA ->> B: G:gidM RCVD C:idBM intMsgID status
|
||||
|
||||
note over BA, B: once received by all
|
||||
BA ->> B: G:gidM RCVD intMsgID status
|
||||
|
||||
note over M, B: 4a. A leaves group
|
||||
|
||||
A ->> AA: G:gidA LEAVE
|
||||
AA ->> A: G:gidA OK
|
||||
AA ->> BA: via gidAB: LEFT
|
||||
note over AA: remove gidAB, remove from broadcast
|
||||
note over BA: remove gidBA, remove from broadcast
|
||||
BA ->> B: G:gidB LEFT C:idBA
|
||||
|
||||
AA ->> MA: via gidAM: LEFT
|
||||
note over AA: remove gidAM, remove from broadcast
|
||||
note over MA: remove gidMA, remove from broadcast
|
||||
MA ->> M: G:gidM LEFT C:idMA
|
||||
|
||||
AA ->> A: G:gidA LEFT
|
||||
|
||||
note over B, BA: if all members left
|
||||
BA ->> B: G:gidB: EMPTY
|
||||
|
||||
note over M, B: 4b. A removes B from group
|
||||
|
||||
A ->> AA: G:gidA REM C:idAB
|
||||
AA ->> A: G:gidA OK
|
||||
AA ->> BA: via gidAB: OUT
|
||||
note over BA: remove gidBA, all gidBM
|
||||
BA ->> B: G:gidB OUT C:idBA
|
||||
|
||||
note over AA: remove gidAB
|
||||
AA ->> A: G:gidA OK
|
||||
|
||||
note over M, B: below steps happen for each existing member M
|
||||
|
||||
AA ->> MA: via gidAM: REM C:midB
|
||||
note over MA: remove gidMB
|
||||
MA ->> AA: via gidMA: REMD C:midB
|
||||
MA ->> M: G:gidM REMD C:idMB C:idMA<br>(B removed by A)
|
||||
|
||||
note over A, AA: once all members removed B
|
||||
|
||||
AA ->> A: G:gidA REMD C:idAB<br>(B removed by this agent)
|
||||
|
||||
note over M, B: 4c. A deletes group
|
||||
A ->> AA: G:gidA DEL
|
||||
AA ->> A: G:gidA OK
|
||||
|
||||
AA ->> BA: via gidAB: DEL
|
||||
note over BA: remove all group connections and messages
|
||||
BA ->> B: G:gidB DELD C:idBA<br>(group deleted by A)
|
||||
BA ->> AA: via gidBA: DELD
|
||||
AA ->> A: G:gidA DELD C:idAB
|
||||
|
||||
AA ->> MA: via gidAM: DEL
|
||||
note over MA: remove all group connections and messages
|
||||
MA ->> M: G:gidM DELD C:idMA<br>(group deleted by A)
|
||||
MA ->> AA: via gidMA: DELD
|
||||
AA ->> A: G:gidA DELD C:idAM
|
||||
|
||||
AA ->> A: G:gidA DELD<br>(group deleted by this agent - all confirmed)
|
||||
@@ -0,0 +1,42 @@
|
||||
sequenceDiagram
|
||||
participant A as Alice (A) - the introducer
|
||||
participant AA as Alice's<br>agent (AA)
|
||||
participant B as Bob (B) - introduced
|
||||
participant BA as Bob's<br>agent (BA)
|
||||
participant M as Mark (M) - introduced to
|
||||
participant MA as Mark's<br>agent (MA)
|
||||
|
||||
note over A, AA: 1. create introduction
|
||||
A ->> AA: C:idAB INTRO C:idAM infoM<br>(idAB - conn alias A has for B,<br>idAM - for M)
|
||||
AA ->> A: C:idAB OK
|
||||
|
||||
note over A, BA: 2. send intro to Bob
|
||||
|
||||
AA ->> BA: via idAB: INTRO C:extIntroIdM infoM
|
||||
BA ->> B: C:idBA REQ C:intIntroIdM infoM
|
||||
B ->> BA: C:idBM? ACPT C:intIntroIdM
|
||||
BA ->> B: C:idBM OK
|
||||
|
||||
note over BA: 3. create connection for M idBM
|
||||
|
||||
BA ->> AA: via idBA: INV C:extIntroIdM invBM infoB
|
||||
|
||||
note over AA, M: 4. send intro to Mark
|
||||
|
||||
AA ->> MA: via idAM: REQ C:extIntroIdB invBM infoB
|
||||
|
||||
note over MA, B: 5. Mark connects to Bob
|
||||
|
||||
MA ->> M: C:idMA REQ C:intIntroIdB infoB
|
||||
M ->> MA: C:idMB? ACPT C:intIntroIdB
|
||||
MA ->> M: C:idMB OK
|
||||
|
||||
MA ->> BA: establish connection idBM -> idMB
|
||||
|
||||
note over A, MA: 6. notify all clients
|
||||
|
||||
MA ->> M: C:idMB CON
|
||||
MA ->> AA: via idMA: CON C:extIntroIdB
|
||||
BA ->> B: C:idBM CON
|
||||
BA ->> AA: via idBA: CON C:extIntroIdB
|
||||
AA ->> A: C:idAB CON C:idAM
|
||||
@@ -0,0 +1,146 @@
|
||||
# Overview of SMP agent protocol commands
|
||||
|
||||
## Connections
|
||||
|
||||
### Commands and messages
|
||||
|
||||
A initiates connection, B accepts
|
||||
|
||||
- command `C:idB? NEW` - create connection
|
||||
- message `C:idB INV cInv`
|
||||
- command `C:idA? JOIN cInv replyMode` - join connection (response `OK`, followed by `CON`)
|
||||
- *message* `C:idB REQ prv:invId infoB` - request from B joining sent to A (not implemented)
|
||||
- *command* `C:idB ACPT prv:invId` - A confirms B joining (not implemented)
|
||||
- message `C:id CON` - connection is established
|
||||
- command `C:id SUB` - subscribe to connection
|
||||
- message `C:id END` - unsubscribed from connection
|
||||
- command `C:idB SEND msg` - send message
|
||||
- message `C:idA SENT msgId` - confirmation that the message is sent
|
||||
- message `C:id MSG msgId msgMeta msgIntegrity msgBody` - received message
|
||||
- *command* `C:idB ACK msgId` - acknowledge message reception (not implemented)
|
||||
- *message* `C:idA RCVD msgId msgIntegrity` - confirmation of message reception and integrity (not implemented)
|
||||
- command `C:id OFF` - suspend connection
|
||||
- command `C:id DEL` - delete connection
|
||||
- message `C:id? OK` - command confirmation
|
||||
- message `C:id? ERR e` - error
|
||||
|
||||
### Envelopes
|
||||
|
||||
- `MSG `
|
||||
- `HELLO verificationKey ackMode`
|
||||
- `REPLY replyInv`
|
||||
|
||||
## Broadcasts
|
||||
|
||||
### Commands & messages
|
||||
|
||||
- command `B:id? NEW` - create broadcast (response is `B:id OK`)
|
||||
- command `B:id SEND msg` - broadcast message (response is multiple `C:id SENT msgId` or ERR, separately for each connection, followed by `B:id SENT msgId` once sent to all)
|
||||
- message `B:id SENT msgId` - notification that the message is sent and its internal ID, same as SENT
|
||||
- command `B:id ADD cId` - add existing connection to a broadcast (response is `B:id OK` or `ERR`, e.g. if bId is used)
|
||||
- command `B:id REM cId` - remove connection from the broadcast (response is `REMD`)
|
||||
- message `B:id REMD cId` - connection removed from the broadcast
|
||||
- message `B:id EMPTY` - all connections were removed from the broadcast
|
||||
- command `B:id DEL` - delete broadcast (response is `B:id OK`)
|
||||
- command `B:id LS` - list connections in broadcast, response is `B:id MEM space_separated_connections`
|
||||
- message `B:id MEM space_separated_connections`
|
||||
|
||||
## Open/public connection
|
||||
|
||||
### Commands
|
||||
|
||||
- command `O:id? NEW` - create open connection
|
||||
- message `O:id INV oInv` - open invitation
|
||||
- command `C:id? JOIN oInv replyMode` - join connection (response `OK`, followed by `CON`)
|
||||
- message `O:id REQ open:invId infoB` - confirmation from B joining sent to A
|
||||
- command `C:idC? ACPT open:invId` - note, that it creates new connection, keeping OPEN connection
|
||||
- command `O:id SUB` - subscribe to open connection
|
||||
- message `O:id END` - unsubscribed from open connection
|
||||
- command `O:id OFF` - suspend open connection
|
||||
- command `O:id DEL` - delete open connection
|
||||
- message `O:id? OK` - command confirmation
|
||||
- message `O:id? ERR e` - error
|
||||
|
||||
## Introductions
|
||||
|
||||
### Commands
|
||||
|
||||
- command `C:idAB INTRO C:idAM infoM` - introduce connection cIdB to connection cIdM (response is `OK`)
|
||||
- message `C:idBA REQ C:invId infoM` - notification to confirm introduction
|
||||
- command `C:idBM? ACPT C:invId` - accept offer to be introduced (response is `cIdBM OK`, followed by `ICON`)
|
||||
- message `C:idBM CON` - confirmation that connection is established to both introduced parties
|
||||
- message `C:idAB CON C:idAM` - confirmation that connection is established to the introducer
|
||||
|
||||
### Envelopes
|
||||
|
||||
- `INTRO C:extIntroIdM infoM` - new introduction offered by introducer
|
||||
- `INV C:extIntroIdB prv:invBM infoB` - invitation to join connection from B to M sent via A (can be pub:)
|
||||
- `REQ C:extIntroIdB prv:invBM infoB` - new introduction forwarded by introducer
|
||||
- `CON C:extIntroIdM` - confirmation that the connection is established sent by both introduced parties to the introducer
|
||||
|
||||
## Groups
|
||||
|
||||
## Agent commands and messages syntax
|
||||
|
||||
- command `G:gId? NEW` - create group (response is `G:gId OK`)
|
||||
- command `C:cId INTRO G:gId gInfo` - add existing connection to a group
|
||||
- message `C:cId REQ g:invID gInfo` - invitation to join the group
|
||||
- command `G:gId? ACPT g:invId` - accept invitation (response is `G gId OK`)
|
||||
- message `G:gId CON C:cId` - 2 connections created with some group member (both for group and direct messages)
|
||||
- message `G:gId MEM [C:cId]` - connection created with all group members for a given member or current client
|
||||
- command `G:gId SEND msg` - send message to group
|
||||
- message `G:gId SENT msgId` - notification that the message is sent and its internal ID, same as SENT
|
||||
- message `G:gId MSG C:cId msgId msgdata` - received group message from cId, msgdata is the same set of parameters as in `MSG`
|
||||
- command `G:gId ACK msgId` - acknowledge message reception by the client
|
||||
- message `G:gId RCVD t:cId msgId status` - message delivery notification
|
||||
- command `G:gId LEAVE` - leave the group
|
||||
- message `G:gId LEFT [C:cId]` - connection cId left the group
|
||||
- command `G:gId REM C:cId` - remove group member (response is `gId OK`, followed by `GREMD` notification)
|
||||
- message `G:gId REMD C:cId [C:cId]` - member removed
|
||||
- message `G:gId OUT C:cId` - you are removed (see question below - should it be just a sequence of GLEFT?)
|
||||
- message `G:gId EMPTY` - all members left the group and it is now empty
|
||||
- command `G:gId DEL` - delete the group (response is `gId OK`)
|
||||
- message `G:gId DELD [C:cId]` - group deleted
|
||||
|
||||
## Agent message envelopes syntax
|
||||
|
||||
- `GROUP C:mid G:inv gInfo` - invitation to join the group
|
||||
- `MEM C:mid` - confirmation that member connected to all members
|
||||
- `LEFT` - notification that member left the group
|
||||
- `OUT` - you are removed from the group
|
||||
- `REM C:mid` - remove member mid from the group
|
||||
- `REMD C:mid` - confirmation that member is removed
|
||||
- `DEL` - group is deleted
|
||||
- `DELD` - confirmation that group is deleted
|
||||
|
||||
## Commands and objects
|
||||
|
||||
| Dir | Command / message | (C)onnection | (O)pen connection | (B)roadcast | (G)roup |
|
||||
|:---------:|:--------------------:|:------------:|:-----------------:|:-----------:|:-------:|
|
||||
| command | `t:id? NEW` | ✓ | ✓ | ✓ | ✓ |
|
||||
| command | `C:id INTRO t:id info` | ✓ | - | - | ✓ |
|
||||
| message | `t:id INV inv` | ✓ | ✓ | - | - |
|
||||
| command | `C:id? JOIN inv replyMode info` | ✓ | - | - | - |
|
||||
| message | `t:id REQ invId info` | ✓ | ✓ | - | ✓ |
|
||||
| command | `t:id? ACPT invId` | ✓ | - | - | ✓ |
|
||||
| message | `t:id CON [C:id]` | ✓ | - | - | ✓ |
|
||||
| message | `t:id MEM [C:id]` | - | - | - | ✓ |
|
||||
| command | `t:id SUB` | ✓ | ✓ | - | ✓ |
|
||||
| message | `t:id END` | ✓ | ✓ | - | ✓ |
|
||||
| command | `t:id OFF` | ✓ | ✓ | - | - |
|
||||
| command | `t:id DEL` | ✓ | ✓ | ✓ | ✓ |
|
||||
| message | `t:id DELD [C:Id]` | ✓ | ✓ | ✓ | ✓ |
|
||||
| command | `t:id SEND msg` | ✓ | - | ✓ | ✓ |
|
||||
| message | `t:id SENT [t':id] msgId` | ✓ | - | ✓ | ✓ |
|
||||
| message | `t:id MSG [C:id] msgId msgdata` | ✓ | - | - | ✓ |
|
||||
| command | `t:id ACK msgId` | ✓ | - | - | ✓ |
|
||||
| message | `t:id RCVD [t':id] msgId status` | ✓ | - | - | ✓ |
|
||||
| command | `t:id ADD C:id` | - | - | - | ✓ |
|
||||
| command | `t:id REM C:id` | - | - | ✓ | ✓ |
|
||||
| command | `t:id REMD C:id` | - | - | ✓ | ✓ |
|
||||
| message | `t:id EMPTY` | - | - | ✓ | ✓ |
|
||||
| message | `G:id OUT C:id` | - | - | - | ✓ |
|
||||
| command | `t:id LS` | - | - | ✓ | ✓ |
|
||||
| message | `t:id MS cIds` | - | - | ✓ | ✓ |
|
||||
| command | `G:id LEAVE` | - | - | - | ✓ |
|
||||
| message | `G:id LEFT [C:id]` | - | - | - | ✓ |
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env sh
|
||||
confd='/etc/opt/simplex'
|
||||
logd='/var/opt/simplex/'
|
||||
|
||||
# Check if server has been initialized
|
||||
if [ ! -f "${confd}/smp-server.ini" ]; then
|
||||
# If not, determine ip or domain
|
||||
case "${ADDR}" in
|
||||
'') printf 'Please specify $ADDR environment variable.\n'; exit 1 ;;
|
||||
*[a-zA-Z]*)
|
||||
case "${ADDR}" in
|
||||
*:*) set -- --ip "${ADDR}" ;;
|
||||
*) set -- -n "${ADDR}" ;;
|
||||
esac
|
||||
;;
|
||||
*) set -- --ip "${ADDR}" ;;
|
||||
esac
|
||||
|
||||
# Optionally, set password
|
||||
case "${PASS}" in
|
||||
'') set -- "$@" --no-password ;;
|
||||
*) set -- "$@" --password "${PASS}" ;;
|
||||
esac
|
||||
|
||||
# And init certificates and configs
|
||||
smp-server init -y -l "$@"
|
||||
fi
|
||||
|
||||
# Backup store log just in case
|
||||
#
|
||||
# Uses the UTC (universal) time zone and this
|
||||
# format: YYYY-mm-dd'T'HH:MM:SS
|
||||
# year, month, day, letter T, hour, minute, second
|
||||
#
|
||||
# This is the ISO 8601 format without the time zone at the end.
|
||||
#
|
||||
_file="${logd}/smp-server-store.log"
|
||||
if [ -f "${_file}" ]; then
|
||||
_backup_extension="$(date -u '+%Y-%m-%dT%H:%M:%S')"
|
||||
cp -v -p "${_file}" "${_file}.${_backup_extension:-date-failed}"
|
||||
unset -v _backup_extension
|
||||
fi
|
||||
unset -v _file
|
||||
|
||||
# Finally, run smp-sever. Notice that "exec" here is important:
|
||||
# smp-server replaces our helper script, so that it can catch INT signal
|
||||
exec smp-server start +RTS -N -RTS
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env sh
|
||||
confd='/etc/opt/simplex-xftp'
|
||||
logd='/var/opt/simplex-xftp'
|
||||
|
||||
# Check if server has been initialized
|
||||
if [ ! -f "${confd}/file-server.ini" ]; then
|
||||
# If not, determine ip or domain
|
||||
case "${ADDR}" in
|
||||
'') printf 'Please specify $ADDR environment variable.\n'; exit 1 ;;
|
||||
*[a-zA-Z]*)
|
||||
case "${ADDR}" in
|
||||
*:*) set -- --ip "${ADDR}" ;;
|
||||
*) set -- -n "${ADDR}" ;;
|
||||
esac
|
||||
;;
|
||||
*) set -- --ip "${ADDR}" ;;
|
||||
esac
|
||||
|
||||
# Set quota
|
||||
case "${QUOTA}" in
|
||||
'') printf 'Please specify $QUOTA environment variable.\n'; exit 1 ;;
|
||||
*) set -- "$@" --quota "${QUOTA}" ;;
|
||||
esac
|
||||
|
||||
# Init the certificates and configs
|
||||
xftp-server init -l -p /srv/xftp "$@"
|
||||
fi
|
||||
|
||||
# Backup store log just in case
|
||||
#
|
||||
# Uses the UTC (universal) time zone and this
|
||||
# format: YYYY-mm-dd'T'HH:MM:SS
|
||||
# year, month, day, letter T, hour, minute, second
|
||||
#
|
||||
# This is the ISO 8601 format without the time zone at the end.
|
||||
#
|
||||
_file="${logd}/file-server-store.log"
|
||||
if [ -f "${_file}" ]; then
|
||||
_backup_extension="$(date -u '+%Y-%m-%dT%H:%M:%S')"
|
||||
cp -v -p "${_file}" "${_file}.${_backup_extension:-date-failed}"
|
||||
unset -v _backup_extension
|
||||
fi
|
||||
unset -v _file
|
||||
|
||||
# Finally, run xftp-sever. Notice that "exec" here is important:
|
||||
# smp-server replaces our helper script, so that it can catch INT signal
|
||||
exec xftp-server start +RTS -N -RTS
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
path_conf_var="/var/opt"
|
||||
path_conf_smp="$path_conf_var/simplex"
|
||||
path_conf_xftp="$path_conf_var/simplex-xftp"
|
||||
path_conf_storelog_smp="$path_conf_smp/smp-server-store.log"
|
||||
path_conf_storelog_xftp="$path_conf_xftp/file-server-store.log"
|
||||
date="$(date -u '+%Y-%m-%dT%H:%M:%S')"
|
||||
|
||||
backup_smp() {
|
||||
if [ -e "$path_conf_storelog_smp" ]; then
|
||||
cp "$path_conf_storelog_smp" "${path_conf_storelog_smp}.${date:-date-failed}"
|
||||
fi
|
||||
}
|
||||
|
||||
backup_xftp() {
|
||||
if [ -e "$path_conf_storelog_xftp" ]; then
|
||||
cp "$path_conf_storelog_xftp" "${path_conf_storelog_xftp}.${date:-date-failed}"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$1" = 'smp-server' ]; then
|
||||
backup_smp
|
||||
elif [ "$1" = 'xftp-server' ]; then
|
||||
backup_xftp
|
||||
else
|
||||
backup_smp
|
||||
backup_xftp
|
||||
fi
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
GRN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
printf "This script is intended to be run with root privileges. Please re-run script using sudo."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "${RED}This action will permanently remove all configs, directories, binaries from Installation Script. Please backup any relevant configs if they are needed.${NC}\n\nPress ${GRN}ENTER${NC} to continue or ${RED}Ctrl+C${NC} to cancel installation"
|
||||
read ans
|
||||
|
||||
systemctl stop smp-server
|
||||
systemctl stop xftp-server
|
||||
|
||||
rm -rf /var/opt/simplex /etc/opt/simplex /var/opt/simplex-xftp /etc/opt/simplex-xftp /srv/xftp /etc/systemd/system/smp-server.service /etc/systemd/system/xftp-server.service /usr/local/bin/smp-server /usr/local/bin/xftp-server /usr/local/bin/simplex-servers-update /usr/local/bin/simplex-servers-uninstall
|
||||
|
||||
userdel smp && userdel xftp
|
||||
|
||||
printf "Uninstallation is complete! Thanks for trying out SimpleX!\n"
|
||||
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# Links to scripts/configs
|
||||
bin="https://github.com/simplex-chat/simplexmq/releases/latest/download"
|
||||
bin_smp="$bin/smp-server-ubuntu-20_04-x86-64"
|
||||
bin_xftp="$bin/xftp-server-ubuntu-20_04-x86-64"
|
||||
|
||||
scripts="https://raw.githubusercontent.com/simplex-chat/simplexmq/stable/scripts/main"
|
||||
scripts_systemd_smp="$scripts/smp-server.service"
|
||||
scripts_systemd_xftp="$scripts/xftp-server.service"
|
||||
scripts_update="$scripts/simplex-servers-update"
|
||||
scripts_uninstall="$scripts/simplex-servers-uninstall"
|
||||
scripts_stopscript="$scripts/simplex-servers-stopscript"
|
||||
|
||||
# Default installation paths
|
||||
path_bin="/usr/local/bin"
|
||||
path_bin_smp="$path_bin/smp-server"
|
||||
path_bin_xftp="$path_bin/xftp-server"
|
||||
path_bin_update="$path_bin/simplex-servers-update"
|
||||
path_bin_uninstall="$path_bin/simplex-servers-uninstall"
|
||||
path_bin_stopscript="$path_bin/simplex-servers-stopscript"
|
||||
|
||||
path_systemd="/etc/systemd/system"
|
||||
path_systemd_smp="$path_systemd/smp-server.service"
|
||||
path_systemd_xftp="$path_systemd/xftp-server.service"
|
||||
|
||||
# Temporary paths
|
||||
path_tmp_bin="$(mktemp -d)"
|
||||
path_tmp_bin_update="$path_tmp_bin/simplex-servers-update"
|
||||
path_tmp_bin_uninstall="$path_tmp_bin/simplex-servers-uninstall"
|
||||
path_tmp_bin_stopscript="$path_tmp_bin/simplex-servers-stopscript"
|
||||
path_tmp_systemd_smp="$path_tmp_bin/smp-server.service"
|
||||
path_tmp_systemd_xftp="$path_tmp_bin/xftp-server.service"
|
||||
|
||||
GRN='\033[0;32m'
|
||||
BLU='\033[1;36m'
|
||||
YLW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Currently, XFTP default to v0.1.0, so it doesn't make sense to check its version
|
||||
local_version="$($path_bin_smp -v | awk '{print $3}')"
|
||||
remote_version="$(curl --proto '=https' --tlsv1.2 -sSf -L https://api.github.com/repos/simplex-chat/simplexmq/releases/latest | grep -i "tag_name" | awk -F \" '{print $4}')"
|
||||
|
||||
update_scripts() {
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_update" -o "$path_tmp_bin_update" && chmod +x "$path_tmp_bin_update"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_uninstall" -o "$path_tmp_bin_uninstall" && chmod +x "$path_tmp_bin_uninstall"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_stopscript" -o "$path_tmp_bin_stopscript" && chmod +x "$path_tmp_bin_stopscript"
|
||||
|
||||
if diff -q "$path_bin_uninstall" "$path_tmp_bin_uninstall" > /dev/null 2>&1; then
|
||||
printf -- "- ${YLW}Uninstall script is up-to-date${NC}.\n"
|
||||
rm "$path_tmp_bin_uninstall"
|
||||
else
|
||||
printf -- "- Updating uninstall script..."
|
||||
mv "$path_tmp_bin_uninstall" "$path_bin_uninstall"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
fi
|
||||
if diff -q "$path_bin_stopscript" "$path_tmp_bin_stopscript" > /dev/null 2>&1; then
|
||||
printf -- "- ${YLW}Stopscript script is up-to-date${NC}.\n"
|
||||
rm "$path_tmp_bin_stopscript"
|
||||
else
|
||||
printf -- "- Updating stopscript script..."
|
||||
mv "$path_tmp_bin_stopscript" "$path_bin_stopscript"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
fi
|
||||
if diff -q "$path_bin_update" "$path_tmp_bin_update" > /dev/null 2>&1; then
|
||||
printf -- "- ${YLW}Update script is up-to-date${NC}.\n"
|
||||
rm "$path_tmp_bin_update"
|
||||
else
|
||||
printf -- "- Updating update script..."
|
||||
mv "$path_tmp_bin_update" "$path_bin_update"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
printf "Re-executing Update script with latest updates..."
|
||||
exec sh "$path_bin_update" "continue"
|
||||
fi
|
||||
}
|
||||
|
||||
update_systemd() {
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd_smp" -o "$path_tmp_systemd_smp"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$scripts_systemd_xftp" -o "$path_tmp_systemd_xftp"
|
||||
|
||||
if diff -q "$path_systemd_smp" "$path_tmp_systemd_smp" > /dev/null 2>&1; then
|
||||
printf -- "- ${YLW}smp-server service is up-to-date${NC}.\n"
|
||||
rm "$path_tmp_systemd_smp"
|
||||
else
|
||||
printf -- "- Updating smp-server service..."
|
||||
mv "$path_tmp_systemd_smp" "$path_systemd_smp"
|
||||
systemctl daemon-reload
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
fi
|
||||
if diff -q "$path_systemd_xftp" "$path_tmp_systemd_xftp" > /dev/null 2>&1; then
|
||||
printf -- "- ${YLW}xftp-server service is up-to-date${NC}.\n"
|
||||
rm "$path_tmp_systemd_xftp"
|
||||
else
|
||||
printf -- "- Updating xftp-server service..."
|
||||
mv "$path_tmp_systemd_xftp" "$path_systemd_xftp"
|
||||
systemctl daemon-reload
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
update_bins() {
|
||||
if [ "$local_version" != "$remote_version" ]; then
|
||||
if systemctl is-active --quiet smp-server; then
|
||||
printf -- "- Stopping smp-server service..."
|
||||
systemctl stop smp-server
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
|
||||
printf -- "- Updating smp-server bin to %s..." "$remote_version"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_smp" -o "$path_bin_smp" && chmod +x "$path_bin_smp"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
|
||||
printf -- "- Starting smp-server service..."
|
||||
systemctl start smp-server
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
else
|
||||
printf -- "- Updating smp-server bin..."
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_smp" -o "$path_bin_smp" && chmod +x "$path_bin_smp"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
fi
|
||||
|
||||
if systemctl is-active --quiet xftp-server; then
|
||||
printf -- "- Stopping xftp-server service..."
|
||||
systemctl stop xftp-server
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
|
||||
printf -- "- Updating xftp-server bin to %s..." "$remote_version"
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_xftp" -o "$path_bin_xftp" && chmod +x "$path_bin_xftp"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
|
||||
printf -- "- Starting xftp-server service..."
|
||||
systemctl start xftp-server
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
else
|
||||
printf -- "- Updating xftp-server bin..."
|
||||
curl --proto '=https' --tlsv1.2 -sSf -L "$bin_smp" -o "$path_bin_xftp" && chmod +x "$path_bin_xftp"
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
fi
|
||||
else
|
||||
printf -- "- ${YLW}smp-server and xftp-server binaries is up-to-date${NC}.\n"
|
||||
fi
|
||||
}
|
||||
|
||||
checks() {
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
printf "This script is intended to be run with root privileges. Please re-run script using sudo.\n"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
checks
|
||||
|
||||
set +u
|
||||
if [ "$1" != "continue" ]; then
|
||||
set -u
|
||||
printf "Updating scripts...\n"
|
||||
update_scripts
|
||||
else
|
||||
set -u
|
||||
printf "${GRN}Done!${NC}\n"
|
||||
fi
|
||||
|
||||
printf "Updating systemd services...\n"
|
||||
update_systemd
|
||||
|
||||
printf "Updating simplex server binaries...\n"
|
||||
update_bins
|
||||
|
||||
rm -rf "$path_tmp_bin"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=SMP server
|
||||
|
||||
[Service]
|
||||
User=smp
|
||||
Group=smp
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/smp-server start +RTS -N -RTS
|
||||
ExecStopPost=/usr/local/bin/simplex-servers-stopscript smp-server
|
||||
LimitNOFILE=65535
|
||||
KillSignal=SIGINT
|
||||
TimeoutStopSec=infinity
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=XFTP server
|
||||
|
||||
[Service]
|
||||
User=xftp
|
||||
Group=xftp
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/xftp-server start +RTS -N -RTS
|
||||
ExecStopPost=/usr/local/bin/simplex-servers-stopscript xftp-server
|
||||
LimitNOFILE=65535
|
||||
KillSignal=SIGINT
|
||||
TimeoutStopSec=infinity
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,178 @@
|
||||
#!/bin/bash
|
||||
|
||||
# <UDF name="enable_store_log" label="Store log - persist notification subscriptions to append only log and restore them upon server restart." default="on" oneof="on, off" />
|
||||
# <UDF name="api_token" label="Linode API token - enable Linode to create tags with server address, fingerprint and version. Note: minimal permissions token should have are read/write access to `linodes` (to create tags) and `domains` (to add A record for the third level domain if FQDN is provided)." default="" />
|
||||
# <UDF name="fqdn" label="FQDN (Fully Qualified Domain Name) - provide third level domain name (e.g. ntf.example.com). If provided use `ntf://fingerprint@FQDN` as server address in the client. If FQDN is not provided use `ntf://fingerprint@IP` instead." default="" />
|
||||
# <UDF name="apns_key_id" label="APNS key ID." default="" />
|
||||
|
||||
# Log all stdout output to stackscript.log
|
||||
exec &> >(tee -i /var/log/stackscript.log)
|
||||
|
||||
# Uncomment next line to enable debugging features
|
||||
# set -xeo pipefail
|
||||
|
||||
cd $HOME
|
||||
|
||||
# https://superuser.com/questions/1638779/automatic-yess-to-linux-update-upgrade
|
||||
# https://superuser.com/questions/1412054/non-interactive-apt-upgrade
|
||||
sudo DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get \
|
||||
-o Dpkg::Options::=--force-confold \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
update
|
||||
|
||||
sudo DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get \
|
||||
-o Dpkg::Options::=--force-confold \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
dist-upgrade
|
||||
|
||||
# TODO install unattended-upgrades
|
||||
sudo DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get \
|
||||
-o Dpkg::Options::=--force-confold \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
install jq
|
||||
|
||||
# Add firewall
|
||||
echo "y" | ufw enable
|
||||
|
||||
# Open ports
|
||||
ufw allow ssh
|
||||
ufw allow https
|
||||
ufw allow 5223
|
||||
|
||||
# Increase file descriptors limit
|
||||
echo 'fs.file-max = 1000000' >> /etc/sysctl.conf
|
||||
echo 'fs.inode-max = 1000000' >> /etc/sysctl.conf
|
||||
echo 'root soft nofile unlimited' >> /etc/security/limits.conf
|
||||
echo 'root hard nofile unlimited' >> /etc/security/limits.conf
|
||||
|
||||
# Download latest release
|
||||
bin_dir="/opt/simplex-notifications/bin"
|
||||
binary="$bin_dir/ntf-server"
|
||||
mkdir -p $bin_dir
|
||||
curl -L -o $binary https://github.com/simplex-chat/simplexmq/releases/latest/download/ntf-server-ubuntu-20_04-x86-64
|
||||
chmod +x $binary
|
||||
|
||||
# / Add to PATH
|
||||
cat > /etc/profile.d/simplex.sh << EOF
|
||||
#!/bin/bash
|
||||
|
||||
export PATH="$PATH:$bin_dir"
|
||||
|
||||
EOF
|
||||
# Add to PATH /
|
||||
|
||||
# Source and test PATH
|
||||
source /etc/profile.d/simplex.sh
|
||||
ntf-server --version
|
||||
|
||||
# Initialize server
|
||||
init_opts=()
|
||||
|
||||
[[ $ENABLE_STORE_LOG == "on" ]] && init_opts+=(-l)
|
||||
|
||||
ip_address=$(curl ifconfig.me)
|
||||
init_opts+=(--ip $ip_address)
|
||||
|
||||
[[ -n "$FQDN" ]] && init_opts+=(-n $FQDN)
|
||||
|
||||
ntf-server init "${init_opts[@]}"
|
||||
|
||||
# Server fingerprint
|
||||
fingerprint=$(cat /etc/opt/simplex-notifications/fingerprint)
|
||||
|
||||
# Determine server address to specify in welcome script and Linode tag
|
||||
if [[ -n "$FQDN" ]]; then
|
||||
server_address=$FQDN
|
||||
else
|
||||
server_address=$ip_address
|
||||
fi
|
||||
|
||||
# Set up welcome script
|
||||
on_login_script="/opt/simplex-notifications/on_login.sh"
|
||||
|
||||
# / Welcome script
|
||||
cat > $on_login_script << EOF
|
||||
#!/bin/bash
|
||||
|
||||
fingerprint=\$1
|
||||
server_address=\$2
|
||||
|
||||
cat << EOF2
|
||||
********************************************************************************
|
||||
|
||||
SimpleX notifications server address: ntf://\$fingerprint@\$server_address
|
||||
Check server status with: systemctl status ntf-server
|
||||
|
||||
To keep this server secure, the UFW firewall is enabled.
|
||||
All ports are BLOCKED except 22 (SSH), 443 (HTTPS), 5223 (notifications server).
|
||||
|
||||
********************************************************************************
|
||||
To stop seeing this message delete line - bash /opt/simplex-notifications/on_login.sh - from /root/.bashrc
|
||||
EOF2
|
||||
|
||||
EOF
|
||||
# Welcome script /
|
||||
|
||||
chmod +x $on_login_script
|
||||
echo "bash $on_login_script $fingerprint $server_address" >> /root/.bashrc
|
||||
|
||||
# Create A record and update Linode's tags
|
||||
if [[ -n "$API_TOKEN" ]]; then
|
||||
if [[ -n "$FQDN" ]]; then
|
||||
domain_address=$(echo $FQDN | rev | cut -d "." -f 1,2 | rev)
|
||||
domain_id=$(curl -H "Authorization: Bearer $API_TOKEN" https://api.linode.com/v4/domains \
|
||||
| jq --arg da "$domain_address" '.data[] | select( .domain == $da ) | .id')
|
||||
if [[ -n $domain_id ]]; then
|
||||
curl \
|
||||
-s -H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-X POST -d "{\"type\":\"A\",\"name\":\"$FQDN\",\"target\":\"$ip_address\"}" \
|
||||
https://api.linode.com/v4/domains/${domain_id}/records
|
||||
fi
|
||||
fi
|
||||
|
||||
version=$(ntf-server --version | cut -d ' ' -f 3-)
|
||||
|
||||
curl \
|
||||
-s -H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-X PUT -d "{\"tags\":[\"$server_address\",\"$fingerprint\",\"$version\"]}" \
|
||||
https://api.linode.com/v4/linode/instances/$LINODE_ID
|
||||
fi
|
||||
|
||||
# / Create systemd service
|
||||
cat > /etc/systemd/system/ntf-server.service << EOF
|
||||
[Unit]
|
||||
Description=SimpleX notifications server
|
||||
|
||||
[Service]
|
||||
Environment="APNS_KEY_FILE=/etc/opt/simplex-notifications/AuthKey.p8"
|
||||
Environment="APNS_KEY_ID=$APNS_KEY_ID"
|
||||
Type=simple
|
||||
ExecStart=/bin/sh -c "exec $binary start >> /var/opt/simplex-notifications/ntf-server.log 2>&1"
|
||||
KillSignal=SIGINT
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
LimitNOFILE=1000000
|
||||
LimitNOFILESoft=1000000
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
EOF
|
||||
# Create systemd service /
|
||||
|
||||
# Start systemd service
|
||||
chmod 644 /etc/systemd/system/ntf-server.service
|
||||
sudo systemctl enable ntf-server
|
||||
# ! APNS key file and certificate have to be copied manually
|
||||
# sudo systemctl start ntf-server
|
||||
|
||||
# Reboot Linode to apply upgrades
|
||||
# sudo reboot
|
||||
@@ -0,0 +1,23 @@
|
||||
# SMP server image for DigitalOcean
|
||||
|
||||
## How to build an image
|
||||
|
||||
1. [Create an API token](https://cloud.digitalocean.com/account/api/tokens) in vendor account in DigitalOcean.
|
||||
2. Install [packer](https://www.packer.io/downloads) downloading binary or with brew (on Mac):
|
||||
|
||||
```shell
|
||||
brew tap hashicorp/tap
|
||||
brew install hashicorp/tap/packer
|
||||
```
|
||||
|
||||
3. Run `packer build` in the `smp-server-digitalocean-droplet` repository:
|
||||
|
||||
```shell
|
||||
cd ./scripts/smp-server-digitalocean-droplet
|
||||
DIGITALOCEAN_TOKEN=$YOUR_TOKEN packer build -on-error=ask -color=false ./marketplace-image.json
|
||||
```
|
||||
|
||||
**TODO** (see Linode script)
|
||||
|
||||
- Increase file descriptors limit
|
||||
- Configure Restart for systemd service
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Download latest release
|
||||
bin_dir="/opt/simplex/bin"
|
||||
binary="$bin_dir/smp-server"
|
||||
mkdir -p $bin_dir
|
||||
curl -L -o $binary https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64
|
||||
chmod +x $binary
|
||||
|
||||
# / Add to PATH
|
||||
cat > /etc/profile.d/simplex.sh << EOF
|
||||
#!/bin/bash
|
||||
|
||||
export PATH="$PATH:$bin_dir"
|
||||
|
||||
EOF
|
||||
# Add to PATH /
|
||||
|
||||
# Source and test PATH
|
||||
source /etc/profile.d/simplex.sh
|
||||
smp-server --version
|
||||
|
||||
# Initialize server
|
||||
ip_address=$(curl ifconfig.me)
|
||||
smp-server init -l --ip $ip_address
|
||||
|
||||
# Server fingerprint
|
||||
fingerprint=$(cat /etc/opt/simplex/fingerprint)
|
||||
|
||||
# Set up welcome script
|
||||
echo "bash /opt/simplex/on_login.sh $fingerprint $ip_address" >> /root/.bashrc
|
||||
|
||||
# / Create systemd service for SMP server
|
||||
cat > /etc/systemd/system/smp-server.service << EOF
|
||||
[Unit]
|
||||
Description=SMP server
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/bin/sh -c "exec $binary start >> /var/opt/simplex/smp-server.log 2>&1"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
EOF
|
||||
# Create systemd service for SMP server /
|
||||
|
||||
# Start systemd service for SMP server
|
||||
chmod 644 /etc/systemd/system/smp-server.service
|
||||
sudo systemctl enable smp-server
|
||||
sudo systemctl start smp-server
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
fingerprint=$1
|
||||
server_address=$2
|
||||
|
||||
cat <<EOF
|
||||
********************************************************************************
|
||||
|
||||
SMP server address: smp://$fingerprint@$server_address
|
||||
Check SMP server status with: systemctl status smp-server
|
||||
|
||||
To keep this server secure, the UFW firewall is enabled.
|
||||
All ports are BLOCKED except 22 (SSH), 443 (HTTPS), 5223 (SMP server).
|
||||
|
||||
********************************************************************************
|
||||
To stop seeing this message delete line - bash /opt/simplex/on_login.sh - from /root/.bashrc
|
||||
EOF
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
if [[ ! -f /opt/simplex/do_initialize_server ]]; then
|
||||
touch /opt/simplex/do_initialize_server
|
||||
elif [[ ! -f /etc/opt/simplex/smp-server.ini ]]; then
|
||||
chmod +x /opt/simplex/initialize_server.sh
|
||||
/opt/simplex/initialize_server.sh
|
||||
else
|
||||
echo "SMP server already initialized"
|
||||
fi
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"variables": {
|
||||
"token": "{{env `DIGITALOCEAN_TOKEN`}}",
|
||||
"image_name": "smp-server-snapshot",
|
||||
"application_name": "SMP server"
|
||||
},
|
||||
"sensitive-variables": ["token"],
|
||||
"builders": [
|
||||
{
|
||||
"type": "digitalocean",
|
||||
"api_token": "{{user `token`}}",
|
||||
"image": "ubuntu-20-04-x64",
|
||||
"region": "fra1",
|
||||
"size": "s-1vcpu-1gb",
|
||||
"ssh_username": "root",
|
||||
"snapshot_name": "{{user `image_name`}}"
|
||||
}
|
||||
],
|
||||
"provisioners": [
|
||||
{
|
||||
"type": "shell",
|
||||
"inline": [
|
||||
"cloud-init status --wait",
|
||||
"mkdir -p /opt/simplex"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source": "files/opt/",
|
||||
"destination": "/opt/"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"environment_vars": [
|
||||
"application_name={{user `application_name`}}",
|
||||
"DEBIAN_FRONTEND=noninteractive"
|
||||
],
|
||||
"scripts": [
|
||||
"scripts/01-packages.sh",
|
||||
"scripts/02-firewall.sh",
|
||||
"scripts/03-init-start.sh",
|
||||
"scripts/90-cleanup.sh",
|
||||
"scripts/99-img-check.sh"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
|
||||
# https://superuser.com/questions/1638779/automatic-yess-to-linux-update-upgrade
|
||||
# https://superuser.com/questions/1412054/non-interactive-apt-upgrade
|
||||
sudo DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get \
|
||||
-o Dpkg::Options::=--force-confold \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
update
|
||||
|
||||
sudo DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get \
|
||||
-o Dpkg::Options::=--force-confold \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
dist-upgrade
|
||||
|
||||
# TODO install unattended-upgrades; jq is not needed on DigitalOcean
|
||||
# sudo DEBIAN_FRONTEND=noninteractive \
|
||||
# apt-get \
|
||||
# -o Dpkg::Options::=--force-confold \
|
||||
# -o Dpkg::Options::=--force-confdef \
|
||||
# -y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
# install jq
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Add firewall
|
||||
echo "y" | ufw enable
|
||||
|
||||
# Open ports
|
||||
ufw allow ssh
|
||||
ufw allow https
|
||||
ufw allow 5223
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
chmod +x /opt/simplex/server_bootstrap.sh
|
||||
|
||||
# / Create systemd service for server bootstrap script
|
||||
cat > /etc/systemd/system/server-bootstrap.service << EOF
|
||||
[Unit]
|
||||
Description=Server bootstrap script that downloads and initializes SMP server from the latest release
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/opt/simplex/server_bootstrap.sh
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
EOF
|
||||
# Create systemd service for server bootstrap script /
|
||||
|
||||
# Start systemd service for server bootstrap script
|
||||
chmod 644 /etc/systemd/system/server-bootstrap.service
|
||||
sudo systemctl enable server-bootstrap
|
||||
sudo systemctl start server-bootstrap
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
|
||||
# DigitalOcean Marketplace Image Validation Tool
|
||||
# © 2021 DigitalOcean LLC.
|
||||
# This code is licensed under Apache 2.0 license (see LICENSE.md for details)
|
||||
|
||||
set -o errexit
|
||||
|
||||
# Ensure /tmp exists and has the proper permissions before
|
||||
# checking for security updates
|
||||
# https://github.com/digitalocean/marketplace-partners/issues/94
|
||||
if [[ ! -d /tmp ]]; then
|
||||
mkdir /tmp
|
||||
fi
|
||||
chmod 1777 /tmp
|
||||
|
||||
if [ -n "$(command -v yum)" ]; then
|
||||
yum update -y
|
||||
yum clean all
|
||||
elif [ -n "$(command -v apt-get)" ]; then
|
||||
apt-get -y update
|
||||
apt-get -y upgrade
|
||||
apt-get -y autoremove
|
||||
apt-get -y autoclean
|
||||
fi
|
||||
|
||||
rm -rf /tmp/* /var/tmp/*
|
||||
history -c
|
||||
cat /dev/null > /root/.bash_history
|
||||
unset HISTFILE
|
||||
find /var/log -mtime -1 -type f -exec truncate -s 0 {} \;
|
||||
rm -rf /var/log/*.gz /var/log/*.[0-9] /var/log/*-????????
|
||||
rm -rf /var/lib/cloud/instances/*
|
||||
rm -f /root/.ssh/authorized_keys /etc/ssh/*key*
|
||||
touch /etc/ssh/revoked_keys
|
||||
chmod 600 /etc/ssh/revoked_keys
|
||||
|
||||
# Securely erase the unused portion of the filesystem
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
printf "\n${GREEN}Writing zeros to the remaining disk space to securely
|
||||
erase the unused portion of the file system.
|
||||
Depending on your disk size this may take several minutes.
|
||||
The secure erase will complete successfully when you see:${NC}
|
||||
dd: writing to '/zerofile': No space left on device\n
|
||||
Beginning secure erase now\n"
|
||||
|
||||
dd if=/dev/zero of=/zerofile &
|
||||
PID=$!
|
||||
while [ -d /proc/$PID ]
|
||||
do
|
||||
printf "."
|
||||
sleep 5
|
||||
done
|
||||
sync; rm /zerofile; sync
|
||||
cat /dev/null > /var/log/lastlog; cat /dev/null > /var/log/wtmp
|
||||
|
||||
rm /var/log/auth.log
|
||||
rm /var/log/kern.log
|
||||
rm /var/log/ufw.log
|
||||
@@ -0,0 +1,682 @@
|
||||
#!/bin/bash
|
||||
|
||||
# DigitalOcean Marketplace Image Validation Tool
|
||||
# © 2021 DigitalOcean LLC.
|
||||
# This code is licensed under Apache 2.0 license (see LICENSE.md for details)
|
||||
|
||||
VERSION="v. 1.6"
|
||||
RUNDATE=$( date )
|
||||
|
||||
# Script should be run with SUDO
|
||||
if [ "$EUID" -ne 0 ]
|
||||
then echo "[Error] - This script must be run with sudo or as the root user."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STATUS=0
|
||||
PASS=0
|
||||
WARN=0
|
||||
FAIL=0
|
||||
|
||||
# $1 == command to check for
|
||||
# returns: 0 == true, 1 == false
|
||||
cmdExists() {
|
||||
if command -v "$1" > /dev/null 2>&1; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
function getDistro {
|
||||
if [ -f /etc/os-release ]; then
|
||||
# freedesktop.org and systemd
|
||||
. /etc/os-release
|
||||
OS=$NAME
|
||||
VER=$VERSION_ID
|
||||
elif type lsb_release >/dev/null 2>&1; then
|
||||
# linuxbase.org
|
||||
OS=$(lsb_release -si)
|
||||
VER=$(lsb_release -sr)
|
||||
elif [ -f /etc/lsb-release ]; then
|
||||
# For some versions of Debian/Ubuntu without lsb_release command
|
||||
. /etc/lsb-release
|
||||
OS=$DISTRIB_ID
|
||||
VER=$DISTRIB_RELEASE
|
||||
elif [ -f /etc/debian_version ]; then
|
||||
# Older Debian/Ubuntu/etc.
|
||||
OS=Debian
|
||||
VER=$(cat /etc/debian_version)
|
||||
elif [ -f /etc/SuSe-release ]; then
|
||||
# Older SuSE/etc.
|
||||
:
|
||||
elif [ -f /etc/redhat-release ]; then
|
||||
# Older Red Hat, CentOS, etc.
|
||||
VER=$( cat /etc/redhat-release | cut -d" " -f3 | cut -d "." -f1)
|
||||
d=$( cat /etc/redhat-release | cut -d" " -f1 | cut -d "." -f1)
|
||||
if [[ $d == "CentOS" ]]; then
|
||||
OS="CentOS Linux"
|
||||
fi
|
||||
else
|
||||
# Fall back to uname, e.g. "Linux <version>", also works for BSD, etc.
|
||||
OS=$(uname -s)
|
||||
VER=$(uname -r)
|
||||
fi
|
||||
}
|
||||
function loadPasswords {
|
||||
SHADOW=$(cat /etc/shadow)
|
||||
}
|
||||
|
||||
function checkAgent {
|
||||
# Check for the presence of the do-agent in the filesystem
|
||||
if [ -d /var/opt/digitalocean/do-agent ];then
|
||||
echo -en "\e[41m[FAIL]\e[0m DigitalOcean Monitoring Agent detected.\n"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
if [[ $OS == "CentOS Linux" ]]; then
|
||||
echo "The agent can be removed with 'sudo yum remove do-agent' "
|
||||
elif [[ $OS == "Ubuntu" ]]; then
|
||||
echo "The agent can be removed with 'sudo apt-get purge do-agent' "
|
||||
fi
|
||||
else
|
||||
echo -en "\e[32m[PASS]\e[0m DigitalOcean Monitoring agent was not found\n"
|
||||
((PASS++))
|
||||
fi
|
||||
}
|
||||
|
||||
function checkLogs {
|
||||
cp_ignore="/var/log/cpanel-install.log"
|
||||
echo -en "\nChecking for log files in /var/log\n\n"
|
||||
# Check if there are log archives or log files that have not been recently cleared.
|
||||
for f in /var/log/*-????????; do
|
||||
[[ -e $f ]] || break
|
||||
if [ $f != $cp_ignore ]; then
|
||||
echo -en "\e[93m[WARN]\e[0m Log archive ${f} found\n"
|
||||
((WARN++))
|
||||
if [[ $STATUS != 2 ]]; then
|
||||
STATUS=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
for f in /var/log/*.[0-9];do
|
||||
[[ -e $f ]] || break
|
||||
echo -en "\e[93m[WARN]\e[0m Log archive ${f} found\n"
|
||||
((WARN++))
|
||||
if [[ $STATUS != 2 ]]; then
|
||||
STATUS=1
|
||||
fi
|
||||
done
|
||||
for f in /var/log/*.log; do
|
||||
[[ -e $f ]] || break
|
||||
if [[ "${f}" = '/var/log/lfd.log' && "$( cat "${f}" | egrep -v '/var/log/messages has been reset| Watching /var/log/messages' | wc -c)" -gt 50 ]]; then
|
||||
if [ $f != $cp_ignore ]; then
|
||||
echo -en "\e[93m[WARN]\e[0m un-cleared log file, ${f} found\n"
|
||||
((WARN++))
|
||||
if [[ $STATUS != 2 ]]; then
|
||||
STATUS=1
|
||||
fi
|
||||
fi
|
||||
elif [[ "${f}" != '/var/log/lfd.log' && "$( cat "${f}" | wc -c)" -gt 50 ]]; then
|
||||
if [ $f != $cp_ignore ]; then
|
||||
echo -en "\e[93m[WARN]\e[0m un-cleared log file, ${f} found\n"
|
||||
((WARN++))
|
||||
if [[ $STATUS != 2 ]]; then
|
||||
STATUS=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
function checkTMP {
|
||||
# Check the /tmp directory to ensure it is empty. Warn on any files found.
|
||||
return 1
|
||||
}
|
||||
function checkRoot {
|
||||
user="root"
|
||||
uhome="/root"
|
||||
for usr in $SHADOW
|
||||
do
|
||||
IFS=':' read -r -a u <<< "$usr"
|
||||
if [[ "${u[0]}" == "${user}" ]]; then
|
||||
if [[ ${u[1]} == "!" ]] || [[ ${u[1]} == "!!" ]] || [[ ${u[1]} == "*" ]]; then
|
||||
echo -en "\e[32m[PASS]\e[0m User ${user} has no password set.\n"
|
||||
((PASS++))
|
||||
else
|
||||
echo -en "\e[41m[FAIL]\e[0m User ${user} has a password set on their account.\n"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [ -d ${uhome}/ ]; then
|
||||
if [ -d ${uhome}/.ssh/ ]; then
|
||||
if ls ${uhome}/.ssh/*> /dev/null 2>&1; then
|
||||
for key in ${uhome}/.ssh/*
|
||||
do
|
||||
if [ "${key}" == "${uhome}/.ssh/authorized_keys" ]; then
|
||||
|
||||
if [ "$( cat "${key}" | wc -c)" -gt 50 ]; then
|
||||
echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a populated authorized_keys file in \e[93m${key}\e[0m\n"
|
||||
akey=$(cat ${key})
|
||||
echo "File Contents:"
|
||||
echo $akey
|
||||
echo "--------------"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
fi
|
||||
elif [ "${key}" == "${uhome}/.ssh/id_rsa" ]; then
|
||||
if [ "$( cat "${key}" | wc -c)" -gt 0 ]; then
|
||||
echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a private key file in \e[93m${key}\e[0m\n"
|
||||
akey=$(cat ${key})
|
||||
echo "File Contents:"
|
||||
echo $akey
|
||||
echo "--------------"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
else
|
||||
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has empty private key file in \e[93m${key}\e[0m\n"
|
||||
((WARN++))
|
||||
if [[ $STATUS != 2 ]]; then
|
||||
STATUS=1
|
||||
fi
|
||||
fi
|
||||
elif [ "${key}" != "${uhome}/.ssh/known_hosts" ]; then
|
||||
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a file in their .ssh directory at \e[93m${key}\e[0m\n"
|
||||
((WARN++))
|
||||
if [[ $STATUS != 2 ]]; then
|
||||
STATUS=1
|
||||
fi
|
||||
else
|
||||
if [ "$( cat "${key}" | wc -c)" -gt 50 ]; then
|
||||
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a populated known_hosts file in \e[93m${key}\e[0m\n"
|
||||
((WARN++))
|
||||
if [[ $STATUS != 2 ]]; then
|
||||
STATUS=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m has no SSH keys present\n"
|
||||
fi
|
||||
else
|
||||
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have an .ssh directory\n"
|
||||
fi
|
||||
if [ -f /root/.bash_history ];then
|
||||
|
||||
BH_S=$( cat /root/.bash_history | wc -c)
|
||||
|
||||
if [[ $BH_S -lt 200 ]]; then
|
||||
echo -en "\e[32m[PASS]\e[0m ${user}'s Bash History appears to have been cleared\n"
|
||||
((PASS++))
|
||||
else
|
||||
echo -en "\e[41m[FAIL]\e[0m ${user}'s Bash History should be cleared to prevent sensitive information from leaking\n"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
fi
|
||||
|
||||
return 1;
|
||||
else
|
||||
echo -en "\e[32m[PASS]\e[0m The Root User's Bash History is not present\n"
|
||||
((PASS++))
|
||||
fi
|
||||
else
|
||||
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have a directory in /home\n"
|
||||
fi
|
||||
echo -en "\n\n"
|
||||
return 1
|
||||
}
|
||||
|
||||
function checkUsers {
|
||||
# Check each user-created account
|
||||
for user in $(awk -F: '$3 >= 1000 && $1 != "nobody" {print $1}' /etc/passwd;)
|
||||
do
|
||||
# Skip some other non-user system accounts
|
||||
if [[ $user == "centos" ]]; then
|
||||
:
|
||||
elif [[ $user == "nfsnobody" ]]; then
|
||||
:
|
||||
else
|
||||
echo -en "\nChecking user: ${user}...\n"
|
||||
for usr in $SHADOW
|
||||
do
|
||||
IFS=':' read -r -a u <<< "$usr"
|
||||
if [[ "${u[0]}" == "${user}" ]]; then
|
||||
if [[ ${u[1]} == "!" ]] || [[ ${u[1]} == "!!" ]] || [[ ${u[1]} == "*" ]]; then
|
||||
echo -en "\e[32m[PASS]\e[0m User ${user} has no password set.\n"
|
||||
((PASS++))
|
||||
else
|
||||
echo -en "\e[41m[FAIL]\e[0m User ${user} has a password set on their account. Only system users are allowed on the image.\n"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
#echo "User Found: ${user}"
|
||||
uhome="/home/${user}"
|
||||
if [ -d "${uhome}/" ]; then
|
||||
if [ -d "${uhome}/.ssh/" ]; then
|
||||
if ls "${uhome}/.ssh/*"> /dev/null 2>&1; then
|
||||
for key in ${uhome}/.ssh/*
|
||||
do
|
||||
if [ "${key}" == "${uhome}/.ssh/authorized_keys" ]; then
|
||||
if [ "$( cat "${key}" | wc -c)" -gt 50 ]; then
|
||||
echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a populated authorized_keys file in \e[93m${key}\e[0m\n"
|
||||
akey=$(cat ${key})
|
||||
echo "File Contents:"
|
||||
echo $akey
|
||||
echo "--------------"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
fi
|
||||
elif [ "${key}" == "${uhome}/.ssh/id_rsa" ]; then
|
||||
if [ "$( cat "${key}" | wc -c)" -gt 0 ]; then
|
||||
echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a private key file in \e[93m${key}\e[0m\n"
|
||||
akey=$(cat ${key})
|
||||
echo "File Contents:"
|
||||
echo $akey
|
||||
echo "--------------"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
else
|
||||
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has empty private key file in \e[93m${key}\e[0m\n"
|
||||
((WARN++))
|
||||
if [[ $STATUS != 2 ]]; then
|
||||
STATUS=1
|
||||
fi
|
||||
fi
|
||||
elif [ "${key}" != "${uhome}/.ssh/known_hosts" ]; then
|
||||
|
||||
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a file in their .ssh directory named \e[93m${key}\e[0m\n"
|
||||
((WARN++))
|
||||
if [[ $STATUS != 2 ]]; then
|
||||
STATUS=1
|
||||
fi
|
||||
|
||||
else
|
||||
if [ "$( cat "${key}" | wc -c)" -gt 50 ]; then
|
||||
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a known_hosts file in \e[93m${key}\e[0m\n"
|
||||
((WARN++))
|
||||
if [[ $STATUS != 2 ]]; then
|
||||
STATUS=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
done
|
||||
else
|
||||
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m has no SSH keys present\n"
|
||||
fi
|
||||
else
|
||||
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have an .ssh directory\n"
|
||||
fi
|
||||
else
|
||||
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have a directory in /home\n"
|
||||
fi
|
||||
|
||||
# Check for an uncleared .bash_history for this user
|
||||
if [ -f "${uhome}/.bash_history" ]; then
|
||||
BH_S=$( cat "${uhome}/.bash_history" | wc -c )
|
||||
|
||||
if [[ $BH_S -lt 200 ]]; then
|
||||
echo -en "\e[32m[PASS]\e[0m ${user}'s Bash History appears to have been cleared\n"
|
||||
((PASS++))
|
||||
else
|
||||
echo -en "\e[41m[FAIL]\e[0m ${user}'s Bash History should be cleared to prevent sensitive information from leaking\n"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
|
||||
fi
|
||||
echo -en "\n\n"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
function checkFirewall {
|
||||
|
||||
if [[ $OS == "Ubuntu" ]]; then
|
||||
fw="ufw"
|
||||
ufwa=$(ufw status |head -1| sed -e "s/^Status:\ //")
|
||||
if [[ $ufwa == "active" ]]; then
|
||||
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
|
||||
((PASS++))
|
||||
else
|
||||
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
|
||||
((WARN++))
|
||||
fi
|
||||
elif [[ $OS == "CentOS Linux" ]]; then
|
||||
if [ -f /usr/lib/systemd/system/csf.service ]; then
|
||||
fw="csf"
|
||||
if [[ $(systemctl status $fw >/dev/null 2>&1) ]]; then
|
||||
|
||||
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
|
||||
((PASS++))
|
||||
elif cmdExists "firewall-cmd"; then
|
||||
if [[ $(systemctl is-active firewalld >/dev/null 2>&1 && echo 1 || echo 0) ]]; then
|
||||
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
|
||||
((PASS++))
|
||||
else
|
||||
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
|
||||
((WARN++))
|
||||
fi
|
||||
else
|
||||
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
|
||||
((WARN++))
|
||||
fi
|
||||
else
|
||||
fw="firewalld"
|
||||
if [[ $(systemctl is-active firewalld >/dev/null 2>&1 && echo 1 || echo 0) ]]; then
|
||||
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
|
||||
((PASS++))
|
||||
else
|
||||
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
|
||||
((WARN++))
|
||||
fi
|
||||
fi
|
||||
elif [[ "$OS" =~ Debian.* ]]; then
|
||||
# user could be using a number of different services for managing their firewall
|
||||
# we will check some of the most common
|
||||
if cmdExists 'ufw'; then
|
||||
fw="ufw"
|
||||
ufwa=$(ufw status |head -1| sed -e "s/^Status:\ //")
|
||||
if [[ $ufwa == "active" ]]; then
|
||||
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
|
||||
((PASS++))
|
||||
else
|
||||
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
|
||||
((WARN++))
|
||||
fi
|
||||
elif cmdExists "firewall-cmd"; then
|
||||
fw="firewalld"
|
||||
if [[ $(systemctl is-active --quiet $fw) ]]; then
|
||||
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
|
||||
((PASS++))
|
||||
else
|
||||
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
|
||||
((WARN++))
|
||||
fi
|
||||
else
|
||||
# user could be using vanilla iptables, check if kernel module is loaded
|
||||
fw="iptables"
|
||||
if [[ $(lsmod | grep -q '^ip_tables' 2>/dev/null) ]]; then
|
||||
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
|
||||
((PASS++))
|
||||
else
|
||||
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
|
||||
((WARN++))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
}
|
||||
function checkUpdates {
|
||||
if [[ $OS == "Ubuntu" ]] || [[ "$OS" =~ Debian.* ]]; then
|
||||
# Ensure /tmp exists and has the proper permissions before
|
||||
# checking for security updates
|
||||
# https://github.com/digitalocean/marketplace-partners/issues/94
|
||||
if [[ ! -d /tmp ]]; then
|
||||
mkdir /tmp
|
||||
fi
|
||||
chmod 1777 /tmp
|
||||
|
||||
echo -en "\nUpdating apt package database to check for security updates, this may take a minute...\n\n"
|
||||
apt-get -y update > /dev/null
|
||||
|
||||
uc=$(apt-get --just-print upgrade | grep -i "security" | wc -l)
|
||||
if [[ $uc -gt 0 ]]; then
|
||||
update_count=$(( ${uc} / 2 ))
|
||||
else
|
||||
update_count=0
|
||||
fi
|
||||
|
||||
if [[ $update_count -gt 0 ]]; then
|
||||
echo -en "\e[41m[FAIL]\e[0m There are ${update_count} security updates available for this image that have not been installed.\n"
|
||||
echo -en
|
||||
echo -en "Here is a list of the security updates that are not installed:\n"
|
||||
sleep 2
|
||||
apt-get --just-print upgrade | grep -i security | awk '{print $2}' | awk '!seen[$0]++'
|
||||
echo -en
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
else
|
||||
echo -en "\e[32m[PASS]\e[0m There are no pending security updates for this image.\n\n"
|
||||
fi
|
||||
elif [[ $OS == "CentOS Linux" ]]; then
|
||||
echo -en "\nChecking for available security updates, this may take a minute...\n\n"
|
||||
|
||||
update_count=$(yum check-update --security --quiet | wc -l)
|
||||
if [[ $update_count -gt 0 ]]; then
|
||||
echo -en "\e[41m[FAIL]\e[0m There are ${update_count} security updates available for this image that have not been installed.\n"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
else
|
||||
echo -en "\e[32m[PASS]\e[0m There are no pending security updates for this image.\n"
|
||||
((PASS++))
|
||||
fi
|
||||
else
|
||||
echo "Error encountered"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
return 1;
|
||||
}
|
||||
function checkCloudInit {
|
||||
|
||||
if hash cloud-init 2>/dev/null; then
|
||||
CI="\e[32m[PASS]\e[0m Cloud-init is installed.\n"
|
||||
((PASS++))
|
||||
else
|
||||
CI="\e[41m[FAIL]\e[0m No valid verison of cloud-init was found.\n"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
function checkMongoDB {
|
||||
# Check if MongoDB is installed
|
||||
# If it is, verify the version is allowed (non-SSPL)
|
||||
|
||||
if [[ $OS == "Ubuntu" ]] || [[ "$OS" =~ Debian.* ]]; then
|
||||
|
||||
if [[ -f "/usr/bin/mongod" ]]; then
|
||||
version=$(/usr/bin/mongod --version --quiet | grep "db version" | sed -e "s/^db\ version\ v//")
|
||||
|
||||
if version_gt $version 4.0.0; then
|
||||
if version_gt $version 4.0.3; then
|
||||
echo -en "\e[41m[FAIL]\e[0m An SSPL version of MongoDB is present, ${version}"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
else
|
||||
echo -en "\e[32m[PASS]\e[0m The version of MongoDB installed, ${version} is not under the SSPL"
|
||||
((PASS++))
|
||||
fi
|
||||
else
|
||||
if version_gt $version 3.6.8; then
|
||||
echo -en "\e[41m[FAIL]\e[0m An SSPL version of MongoDB is present, ${version}"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
else
|
||||
echo -en "\e[32m[PASS]\e[0m The version of MongoDB installed, ${version} is not under the SSPL"
|
||||
((PASS++))
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
else
|
||||
echo -en "\e[32m[PASS]\e[0m MongoDB is not installed"
|
||||
((PASS++))
|
||||
fi
|
||||
|
||||
elif [[ $OS == "CentOS Linux" ]]; then
|
||||
|
||||
if [[ -f "/usr/bin/mongod" ]]; then
|
||||
version=$(/usr/bin/mongod --version --quiet | grep "db version" | sed -e "s/^db\ version\ v//")
|
||||
|
||||
|
||||
if version_gt $version 4.0.0; then
|
||||
if version_gt $version 4.0.3; then
|
||||
echo -en "\e[41m[FAIL]\e[0m An SSPL version of MongoDB is present"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
else
|
||||
echo -en "\e[32m[PASS]\e[0m The version of MongoDB installed is not under the SSPL"
|
||||
((PASS++))
|
||||
fi
|
||||
else
|
||||
if version_gt $version 3.6.8; then
|
||||
echo -en "\e[41m[FAIL]\e[0m An SSPL version of MongoDB is present"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
else
|
||||
echo -en "\e[32m[PASS]\e[0m The version of MongoDB installed is not under the SSPL"
|
||||
((PASS++))
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
|
||||
else
|
||||
echo -en "\e[32m[PASS]\e[0m MongoDB is not installed"
|
||||
((PASS++))
|
||||
fi
|
||||
|
||||
else
|
||||
echo "ERROR: Unable to identify distribution"
|
||||
((FAIL++))
|
||||
STATUS 2
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
||||
}
|
||||
|
||||
function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }
|
||||
|
||||
|
||||
clear
|
||||
echo "DigitalOcean Marketplace Image Validation Tool ${VERSION}"
|
||||
echo "Executed on: ${RUNDATE}"
|
||||
echo "Checking local system for Marketplace compatibility..."
|
||||
|
||||
getDistro
|
||||
|
||||
echo -en "\n\e[1mDistribution:\e[0m ${OS}\n"
|
||||
echo -en "\e[1mVersion:\e[0m ${VER}\n\n"
|
||||
|
||||
ost=0
|
||||
osv=0
|
||||
|
||||
if [[ $OS == "Ubuntu" ]]; then
|
||||
ost=1
|
||||
if [[ $VER == "20.04" ]]; then
|
||||
osv=1
|
||||
elif [[ $VER == "18.04" ]]; then
|
||||
osv=1
|
||||
elif [[ $VER == "16.04" ]]; then
|
||||
osv=1
|
||||
else
|
||||
osv=0
|
||||
fi
|
||||
|
||||
elif [[ "$OS" =~ Debian.* ]]; then
|
||||
ost=1
|
||||
case "$VER" in
|
||||
9)
|
||||
osv=1
|
||||
;;
|
||||
10)
|
||||
osv=1
|
||||
;;
|
||||
*)
|
||||
osv=2
|
||||
;;
|
||||
esac
|
||||
|
||||
elif [[ $OS == "CentOS Linux" ]]; then
|
||||
ost=1
|
||||
if [[ $VER == "8" ]]; then
|
||||
osv=1
|
||||
elif [[ $VER == "7" ]]; then
|
||||
osv=1
|
||||
elif [[ $VER == "6" ]]; then
|
||||
osv=1
|
||||
else
|
||||
osv=2
|
||||
fi
|
||||
else
|
||||
ost=0
|
||||
fi
|
||||
|
||||
if [[ $ost == 1 ]]; then
|
||||
echo -en "\e[32m[PASS]\e[0m Supported Operating System Detected: ${OS}\n"
|
||||
((PASS++))
|
||||
else
|
||||
echo -en "\e[41m[FAIL]\e[0m ${OS} is not a supported Operating System\n"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
fi
|
||||
|
||||
if [[ $osv == 1 ]]; then
|
||||
echo -en "\e[32m[PASS]\e[0m Supported Release Detected: ${VER}\n"
|
||||
((PASS++))
|
||||
elif [[ $ost == 1 ]]; then
|
||||
echo -en "\e[41m[FAIL]\e[0m ${OS} ${VER} is not a supported Operating System Version\n"
|
||||
((FAIL++))
|
||||
STATUS=2
|
||||
else
|
||||
echo "Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
checkCloudInit
|
||||
|
||||
echo -en "${CI}"
|
||||
|
||||
checkFirewall
|
||||
|
||||
echo -en "${FW_VER}"
|
||||
|
||||
checkUpdates
|
||||
|
||||
loadPasswords
|
||||
|
||||
checkLogs
|
||||
|
||||
echo -en "\n\nChecking all user-created accounts...\n"
|
||||
checkUsers
|
||||
|
||||
echo -en "\n\nChecking the root account...\n"
|
||||
checkRoot
|
||||
|
||||
checkAgent
|
||||
|
||||
checkMongoDB
|
||||
|
||||
|
||||
# Summary
|
||||
echo -en "\n\n---------------------------------------------------------------------------------------------------\n"
|
||||
|
||||
if [[ $STATUS == 0 ]]; then
|
||||
echo -en "Scan Complete.\n\e[32mAll Tests Passed!\e[0m\n"
|
||||
elif [[ $STATUS == 1 ]]; then
|
||||
echo -en "Scan Complete. \n\e[93mSome non-critical tests failed. Please review these items.\e[0m\e[0m\n"
|
||||
else
|
||||
echo -en "Scan Complete. \n\e[41mOne or more tests failed. Please review these items and re-test.\e[0m\n"
|
||||
fi
|
||||
echo "---------------------------------------------------------------------------------------------------"
|
||||
echo -en "\e[1m${PASS} Tests PASSED\e[0m\n"
|
||||
echo -en "\e[1m${WARN} WARNINGS\e[0m\n"
|
||||
echo -en "\e[1m${FAIL} Tests FAILED\e[0m\n"
|
||||
echo -en "---------------------------------------------------------------------------------------------------\n"
|
||||
|
||||
if [[ $STATUS == 0 ]]; then
|
||||
echo -en "We did not detect any issues with this image. Please be sure to manually ensure that all software installed on the base system is functional, secure and properly configured (or facilities for configuration on first-boot have been created).\n\n"
|
||||
exit 0
|
||||
elif [[ $STATUS == 1 ]]; then
|
||||
echo -en "Please review all [WARN] items above and ensure they are intended or resolved. If you do not have a specific requirement, we recommend resolving these items before image submission\n\n"
|
||||
exit 0
|
||||
else
|
||||
echo -en "Some critical tests failed. These items must be resolved and this scan re-run before you submit your image to the DigitalOcean Marketplace.\n\n"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,175 @@
|
||||
#!/bin/bash
|
||||
|
||||
# <UDF name="enable_store_log" label="Store log - persist SMP queues to append only log and restore them upon server restart." default="on" oneof="on, off" />
|
||||
# <UDF name="api_token" label="Linode API token - enable Linode to create tags with server address, fingerprint and version. Note: minimal permissions token should have are read/write access to `linodes` (to create tags) and `domains` (to add A record for the third level domain if FQDN is provided)." default="" />
|
||||
# <UDF name="fqdn" label="FQDN (Fully Qualified Domain Name) - provide third level domain name (e.g. smp.example.com). If provided use `smp://fingerprint@FQDN` as server address in the client. If FQDN is not provided use `smp://fingerprint@IP` instead." default="" />
|
||||
|
||||
# Log all stdout output to stackscript.log
|
||||
exec &> >(tee -i /var/log/stackscript.log)
|
||||
|
||||
# Uncomment next line to enable debugging features
|
||||
# set -xeo pipefail
|
||||
|
||||
cd $HOME
|
||||
|
||||
# https://superuser.com/questions/1638779/automatic-yess-to-linux-update-upgrade
|
||||
# https://superuser.com/questions/1412054/non-interactive-apt-upgrade
|
||||
sudo DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get \
|
||||
-o Dpkg::Options::=--force-confold \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
update
|
||||
|
||||
sudo DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get \
|
||||
-o Dpkg::Options::=--force-confold \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
dist-upgrade
|
||||
|
||||
# TODO install unattended-upgrades
|
||||
sudo DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get \
|
||||
-o Dpkg::Options::=--force-confold \
|
||||
-o Dpkg::Options::=--force-confdef \
|
||||
-y --allow-downgrades --allow-remove-essential --allow-change-held-packages \
|
||||
install jq
|
||||
|
||||
# Add firewall
|
||||
echo "y" | ufw enable
|
||||
|
||||
# Open ports
|
||||
ufw allow ssh
|
||||
ufw allow https
|
||||
ufw allow 5223
|
||||
|
||||
# Increase file descriptors limit
|
||||
echo 'fs.file-max = 1000000' >> /etc/sysctl.conf
|
||||
echo 'fs.inode-max = 1000000' >> /etc/sysctl.conf
|
||||
echo 'root soft nofile unlimited' >> /etc/security/limits.conf
|
||||
echo 'root hard nofile unlimited' >> /etc/security/limits.conf
|
||||
|
||||
# Download latest release
|
||||
bin_dir="/opt/simplex/bin"
|
||||
binary="$bin_dir/smp-server"
|
||||
mkdir -p $bin_dir
|
||||
curl -L -o $binary https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64
|
||||
chmod +x $binary
|
||||
|
||||
# / Add to PATH
|
||||
cat > /etc/profile.d/simplex.sh << EOF
|
||||
#!/bin/bash
|
||||
|
||||
export PATH="$PATH:$bin_dir"
|
||||
|
||||
EOF
|
||||
# Add to PATH /
|
||||
|
||||
# Source and test PATH
|
||||
source /etc/profile.d/simplex.sh
|
||||
smp-server --version
|
||||
|
||||
# Initialize server
|
||||
init_opts=()
|
||||
|
||||
[[ $ENABLE_STORE_LOG == "on" ]] && init_opts+=(-l)
|
||||
|
||||
ip_address=$(curl ifconfig.me)
|
||||
init_opts+=(--ip $ip_address)
|
||||
|
||||
[[ -n "$FQDN" ]] && init_opts+=(-n $FQDN)
|
||||
|
||||
smp-server init "${init_opts[@]}"
|
||||
|
||||
# Server fingerprint
|
||||
fingerprint=$(cat /etc/opt/simplex/fingerprint)
|
||||
|
||||
# Determine server address to specify in welcome script and Linode tag
|
||||
if [[ -n "$FQDN" ]]; then
|
||||
server_address=$FQDN
|
||||
else
|
||||
server_address=$ip_address
|
||||
fi
|
||||
|
||||
# Set up welcome script
|
||||
on_login_script="/opt/simplex/on_login.sh"
|
||||
|
||||
# / Welcome script
|
||||
cat > $on_login_script << EOF
|
||||
#!/bin/bash
|
||||
|
||||
fingerprint=\$1
|
||||
server_address=\$2
|
||||
|
||||
cat << EOF2
|
||||
********************************************************************************
|
||||
|
||||
SMP server address: smp://\$fingerprint@\$server_address
|
||||
Check SMP server status with: systemctl status smp-server
|
||||
|
||||
To keep this server secure, the UFW firewall is enabled.
|
||||
All ports are BLOCKED except 22 (SSH), 443 (HTTPS), 5223 (SMP server).
|
||||
|
||||
********************************************************************************
|
||||
To stop seeing this message delete line - bash /opt/simplex/on_login.sh - from /root/.bashrc
|
||||
EOF2
|
||||
|
||||
EOF
|
||||
# Welcome script /
|
||||
|
||||
chmod +x $on_login_script
|
||||
echo "bash $on_login_script $fingerprint $server_address" >> /root/.bashrc
|
||||
|
||||
# Create A record and update Linode's tags
|
||||
if [[ -n "$API_TOKEN" ]]; then
|
||||
if [[ -n "$FQDN" ]]; then
|
||||
domain_address=$(echo $FQDN | rev | cut -d "." -f 1,2 | rev)
|
||||
domain_id=$(curl -H "Authorization: Bearer $API_TOKEN" https://api.linode.com/v4/domains \
|
||||
| jq --arg da "$domain_address" '.data[] | select( .domain == $da ) | .id')
|
||||
if [[ -n $domain_id ]]; then
|
||||
curl \
|
||||
-s -H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-X POST -d "{\"type\":\"A\",\"name\":\"$FQDN\",\"target\":\"$ip_address\"}" \
|
||||
https://api.linode.com/v4/domains/${domain_id}/records
|
||||
fi
|
||||
fi
|
||||
|
||||
version=$(smp-server --version | cut -d ' ' -f 3-)
|
||||
|
||||
curl \
|
||||
-s -H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-X PUT -d "{\"tags\":[\"$server_address\",\"$fingerprint\",\"$version\"]}" \
|
||||
https://api.linode.com/v4/linode/instances/$LINODE_ID
|
||||
fi
|
||||
|
||||
# / Create systemd service
|
||||
cat > /etc/systemd/system/smp-server.service << EOF
|
||||
[Unit]
|
||||
Description=SMP server
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/bin/sh -c "exec $binary start >> /var/opt/simplex/smp-server.log 2>&1"
|
||||
KillSignal=SIGINT
|
||||
TimeoutStopSec=infinity
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
LimitNOFILE=1000000
|
||||
LimitNOFILESoft=1000000
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
EOF
|
||||
# Create systemd service /
|
||||
|
||||
# Start systemd service
|
||||
chmod 644 /etc/systemd/system/smp-server.service
|
||||
sudo systemctl enable smp-server
|
||||
sudo systemctl start smp-server
|
||||
|
||||
# Reboot Linode to apply upgrades
|
||||
sudo reboot
|
||||
@@ -0,0 +1,37 @@
|
||||
1. Install `tor` following [official guide](https://community.torproject.org/onion-services/setup/install/).
|
||||
|
||||
2. Modify `/etc/tor/torrc` configuration file:
|
||||
|
||||
```sh
|
||||
...
|
||||
# Disable anonymous mode for better connectivity
|
||||
## Needed for HiddenServiceNonAnonymousMode
|
||||
SOCKSPort 0
|
||||
## Needed for HiddenServiceSingleHopMode
|
||||
HiddenServiceNonAnonymousMode 1
|
||||
## Flag to disable anonymous mode
|
||||
## This option reduces the latency of server connection, but it makes server itself not anonymous,
|
||||
## it only protects the anonymity of the users connecting to the server.
|
||||
## In case your server address has both public and onion hostnames it is not anonymous anyway,
|
||||
## so this is what you want.
|
||||
HiddenServiceSingleHopMode 1
|
||||
|
||||
# Specify folder for smp-tor
|
||||
## Folder for keys, address, etc.
|
||||
HiddenServiceDir /var/lib/tor/simplex-smp/
|
||||
## Map smp port (5223) to tor
|
||||
HiddenServicePort 5223 localhost:5223
|
||||
...
|
||||
```
|
||||
|
||||
3. Restart `tor` system service:
|
||||
|
||||
```
|
||||
sudo systemctl restart tor
|
||||
```
|
||||
|
||||
4. Onion address can be obtained from following file:
|
||||
|
||||
```sh
|
||||
sudo cat /var/lib/tor/simplex-smp/hostname
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
# systemd has to be configured to use SIGINT to save and restore undelivered messages after restart.
|
||||
# Add this to [Service] section:
|
||||
# KillSignal=SIGINT
|
||||
curl -L -o /opt/simplex/bin/smp-server-new https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64
|
||||
systemctl stop smp-server
|
||||
cp /var/opt/simplex/smp-server-store.log /var/opt/simplex/smp-server-store.log.bak
|
||||
mv /opt/simplex/bin/smp-server /opt/simplex/bin/smp-server-old
|
||||
mv /opt/simplex/bin/smp-server-new /opt/simplex/bin/smp-server
|
||||
chmod +x /opt/simplex/bin/smp-server
|
||||
systemctl start smp-server
|
||||
@@ -0,0 +1,614 @@
|
||||
cabal-version: 1.12
|
||||
|
||||
-- This file has been generated from package.yaml by hpack version 0.35.1.
|
||||
--
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplexmq
|
||||
version: 5.2.0
|
||||
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.
|
||||
category: Chat, Network, Web, System, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplexmq#readme
|
||||
author: simplex.chat
|
||||
maintainer: chat@simplex.chat
|
||||
copyright: 2020-2022 simplex.chat
|
||||
license: AGPL-3
|
||||
license-file: LICENSE
|
||||
build-type: Simple
|
||||
extra-source-files:
|
||||
README.md
|
||||
CHANGELOG.md
|
||||
|
||||
flag swift
|
||||
description: Enable swift JSON format
|
||||
manual: True
|
||||
default: False
|
||||
|
||||
library
|
||||
exposed-modules:
|
||||
Simplex.FileTransfer
|
||||
Simplex.FileTransfer.Agent
|
||||
Simplex.FileTransfer.Client
|
||||
Simplex.FileTransfer.Client.Agent
|
||||
Simplex.FileTransfer.Client.Main
|
||||
Simplex.FileTransfer.Client.Presets
|
||||
Simplex.FileTransfer.Crypto
|
||||
Simplex.FileTransfer.Description
|
||||
Simplex.FileTransfer.Protocol
|
||||
Simplex.FileTransfer.Server
|
||||
Simplex.FileTransfer.Server.Env
|
||||
Simplex.FileTransfer.Server.Main
|
||||
Simplex.FileTransfer.Server.Stats
|
||||
Simplex.FileTransfer.Server.Store
|
||||
Simplex.FileTransfer.Server.StoreLog
|
||||
Simplex.FileTransfer.Transport
|
||||
Simplex.FileTransfer.Types
|
||||
Simplex.FileTransfer.Util
|
||||
Simplex.Messaging.Agent
|
||||
Simplex.Messaging.Agent.Client
|
||||
Simplex.Messaging.Agent.Env.SQLite
|
||||
Simplex.Messaging.Agent.Lock
|
||||
Simplex.Messaging.Agent.NtfSubSupervisor
|
||||
Simplex.Messaging.Agent.Protocol
|
||||
Simplex.Messaging.Agent.QueryString
|
||||
Simplex.Messaging.Agent.RetryInterval
|
||||
Simplex.Messaging.Agent.Server
|
||||
Simplex.Messaging.Agent.Store
|
||||
Simplex.Messaging.Agent.Store.SQLite
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220101_initial
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220301_snd_queue_keys
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220322_notifications
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220608_v2
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220625_v2_ntf_mode
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220811_onion_hosts
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220817_connection_ntfs
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220905_commands
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20220915_connection_queues
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230110_users
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230117_fkey_indexes
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230120_delete_errors
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230217_server_key_hash
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230223_files
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230320_retry_state
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230401_snd_files
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230510_files_pending_replicas_indexes
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230516_encrypted_rcv_message_hashes
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230531_switch_status
|
||||
Simplex.Messaging.Agent.Store.SQLite.Migrations.M20230615_ratchet_sync
|
||||
Simplex.Messaging.Agent.TAsyncs
|
||||
Simplex.Messaging.Agent.TRcvQueues
|
||||
Simplex.Messaging.Client
|
||||
Simplex.Messaging.Client.Agent
|
||||
Simplex.Messaging.Crypto
|
||||
Simplex.Messaging.Crypto.Lazy
|
||||
Simplex.Messaging.Crypto.Ratchet
|
||||
Simplex.Messaging.Encoding
|
||||
Simplex.Messaging.Encoding.String
|
||||
Simplex.Messaging.Notifications.Client
|
||||
Simplex.Messaging.Notifications.Protocol
|
||||
Simplex.Messaging.Notifications.Server
|
||||
Simplex.Messaging.Notifications.Server.Env
|
||||
Simplex.Messaging.Notifications.Server.Main
|
||||
Simplex.Messaging.Notifications.Server.Push.APNS
|
||||
Simplex.Messaging.Notifications.Server.Stats
|
||||
Simplex.Messaging.Notifications.Server.Store
|
||||
Simplex.Messaging.Notifications.Server.StoreLog
|
||||
Simplex.Messaging.Notifications.Transport
|
||||
Simplex.Messaging.Notifications.Types
|
||||
Simplex.Messaging.Parsers
|
||||
Simplex.Messaging.Protocol
|
||||
Simplex.Messaging.Server
|
||||
Simplex.Messaging.Server.CLI
|
||||
Simplex.Messaging.Server.Env.STM
|
||||
Simplex.Messaging.Server.Expiration
|
||||
Simplex.Messaging.Server.Main
|
||||
Simplex.Messaging.Server.MsgStore
|
||||
Simplex.Messaging.Server.MsgStore.STM
|
||||
Simplex.Messaging.Server.QueueStore
|
||||
Simplex.Messaging.Server.QueueStore.STM
|
||||
Simplex.Messaging.Server.Stats
|
||||
Simplex.Messaging.Server.StoreLog
|
||||
Simplex.Messaging.TMap
|
||||
Simplex.Messaging.Transport
|
||||
Simplex.Messaging.Transport.Buffer
|
||||
Simplex.Messaging.Transport.Client
|
||||
Simplex.Messaging.Transport.HTTP2
|
||||
Simplex.Messaging.Transport.HTTP2.Client
|
||||
Simplex.Messaging.Transport.HTTP2.Server
|
||||
Simplex.Messaging.Transport.KeepAlive
|
||||
Simplex.Messaging.Transport.Server
|
||||
Simplex.Messaging.Transport.WebSockets
|
||||
Simplex.Messaging.Util
|
||||
Simplex.Messaging.Version
|
||||
other-modules:
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
src
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns
|
||||
build-depends:
|
||||
QuickCheck ==2.14.*
|
||||
, aeson ==2.0.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, asn1-encoding ==0.9.*
|
||||
, asn1-types ==0.3.*
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.10.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, cryptostore ==0.2.*
|
||||
, data-default ==0.7.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, filepath ==1.4.*
|
||||
, generic-random >=1.3 && <1.5
|
||||
, http-types ==0.12.*
|
||||
, http2 ==4.1.*
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-transport ==0.5.4
|
||||
, 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.*
|
||||
, template-haskell ==2.16.*
|
||||
, temporary ==1.3.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, transformers ==0.5.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
|
||||
executable ntf-server
|
||||
main-is: Main.hs
|
||||
other-modules:
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
apps/ntf-server
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
|
||||
build-depends:
|
||||
QuickCheck ==2.14.*
|
||||
, aeson ==2.0.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, asn1-encoding ==0.9.*
|
||||
, asn1-types ==0.3.*
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.10.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, cryptostore ==0.2.*
|
||||
, data-default ==0.7.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, filepath ==1.4.*
|
||||
, generic-random >=1.3 && <1.5
|
||||
, http-types ==0.12.*
|
||||
, http2 ==4.1.*
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-transport ==0.5.4
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
, simple-logger ==0.1.*
|
||||
, simplexmq
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.16.*
|
||||
, temporary ==1.3.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, transformers ==0.5.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
|
||||
executable smp-agent
|
||||
main-is: Main.hs
|
||||
other-modules:
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
apps/smp-agent
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
|
||||
build-depends:
|
||||
QuickCheck ==2.14.*
|
||||
, aeson ==2.0.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, asn1-encoding ==0.9.*
|
||||
, asn1-types ==0.3.*
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.10.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, cryptostore ==0.2.*
|
||||
, data-default ==0.7.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, filepath ==1.4.*
|
||||
, generic-random >=1.3 && <1.5
|
||||
, http-types ==0.12.*
|
||||
, http2 ==4.1.*
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-transport ==0.5.4
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
, simple-logger ==0.1.*
|
||||
, simplexmq
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.16.*
|
||||
, temporary ==1.3.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, transformers ==0.5.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
|
||||
executable smp-server
|
||||
main-is: Main.hs
|
||||
other-modules:
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
apps/smp-server
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
|
||||
build-depends:
|
||||
QuickCheck ==2.14.*
|
||||
, aeson ==2.0.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, asn1-encoding ==0.9.*
|
||||
, asn1-types ==0.3.*
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.10.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, cryptostore ==0.2.*
|
||||
, data-default ==0.7.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, filepath ==1.4.*
|
||||
, generic-random >=1.3 && <1.5
|
||||
, http-types ==0.12.*
|
||||
, http2 ==4.1.*
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-transport ==0.5.4
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
, simple-logger ==0.1.*
|
||||
, simplexmq
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.16.*
|
||||
, temporary ==1.3.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, transformers ==0.5.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
|
||||
executable xftp
|
||||
main-is: Main.hs
|
||||
other-modules:
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
apps/xftp
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
|
||||
build-depends:
|
||||
QuickCheck ==2.14.*
|
||||
, aeson ==2.0.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, asn1-encoding ==0.9.*
|
||||
, asn1-types ==0.3.*
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.10.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, cryptostore ==0.2.*
|
||||
, data-default ==0.7.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, filepath ==1.4.*
|
||||
, generic-random >=1.3 && <1.5
|
||||
, http-types ==0.12.*
|
||||
, http2 ==4.1.*
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-transport ==0.5.4
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
, simple-logger ==0.1.*
|
||||
, simplexmq
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.16.*
|
||||
, temporary ==1.3.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, transformers ==0.5.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
|
||||
executable xftp-server
|
||||
main-is: Main.hs
|
||||
other-modules:
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
apps/xftp-server
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded
|
||||
build-depends:
|
||||
QuickCheck ==2.14.*
|
||||
, aeson ==2.0.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, asn1-encoding ==0.9.*
|
||||
, asn1-types ==0.3.*
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.10.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, cryptostore ==0.2.*
|
||||
, data-default ==0.7.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, filepath ==1.4.*
|
||||
, generic-random >=1.3 && <1.5
|
||||
, http-types ==0.12.*
|
||||
, http2 ==4.1.*
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-transport ==0.5.4
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
, simple-logger ==0.1.*
|
||||
, simplexmq
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.16.*
|
||||
, temporary ==1.3.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time-manager ==0.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, transformers ==0.5.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
|
||||
test-suite simplexmq-test
|
||||
type: exitcode-stdio-1.0
|
||||
main-is: Test.hs
|
||||
other-modules:
|
||||
AgentTests
|
||||
AgentTests.ConnectionRequestTests
|
||||
AgentTests.DoubleRatchetTests
|
||||
AgentTests.FunctionalAPITests
|
||||
AgentTests.MigrationTests
|
||||
AgentTests.NotificationTests
|
||||
AgentTests.SchemaDump
|
||||
AgentTests.SQLiteTests
|
||||
CLITests
|
||||
CoreTests.CryptoTests
|
||||
CoreTests.EncodingTests
|
||||
CoreTests.ProtocolErrorTests
|
||||
CoreTests.RetryIntervalTests
|
||||
CoreTests.VersionRangeTests
|
||||
FileDescriptionTests
|
||||
NtfClient
|
||||
NtfServerTests
|
||||
ServerTests
|
||||
SMPAgentClient
|
||||
SMPClient
|
||||
XFTPAgent
|
||||
XFTPCLI
|
||||
XFTPClient
|
||||
XFTPServerTests
|
||||
Paths_simplexmq
|
||||
hs-source-dirs:
|
||||
tests
|
||||
ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns
|
||||
build-depends:
|
||||
HUnit ==1.6.*
|
||||
, QuickCheck ==2.14.*
|
||||
, aeson ==2.0.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, asn1-encoding ==0.9.*
|
||||
, asn1-types ==0.3.*
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.14.*
|
||||
, base >=4.14 && <5
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.10.*
|
||||
, case-insensitive ==1.2.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, cryptostore ==0.2.*
|
||||
, data-default ==0.7.*
|
||||
, deepseq ==1.4.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, filepath ==1.4.*
|
||||
, generic-random >=1.3 && <1.5
|
||||
, hspec ==2.7.*
|
||||
, hspec-core ==2.7.*
|
||||
, http-types ==0.12.*
|
||||
, http2 ==4.1.*
|
||||
, ini ==0.4.1
|
||||
, iproute ==1.7.*
|
||||
, iso8601-time ==0.1.*
|
||||
, main-tester ==0.2.*
|
||||
, memory ==0.15.*
|
||||
, mtl ==2.2.*
|
||||
, network >=3.1.2.7 && <3.2
|
||||
, network-transport ==0.5.4
|
||||
, optparse-applicative >=0.15 && <0.17
|
||||
, process ==1.6.*
|
||||
, random >=1.1 && <1.3
|
||||
, silently ==1.2.*
|
||||
, simple-logger ==0.1.*
|
||||
, simplexmq
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
, template-haskell ==2.16.*
|
||||
, temporary ==1.3.*
|
||||
, text ==1.2.*
|
||||
, time ==1.9.*
|
||||
, time-compat ==1.9.*
|
||||
, time-manager ==0.0.*
|
||||
, timeit ==2.0.*
|
||||
, tls >=1.6.0 && <1.7
|
||||
, transformers ==0.5.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, x509 ==1.7.*
|
||||
, x509-store ==1.6.*
|
||||
, x509-validation ==1.6.*
|
||||
, yaml ==0.11.*
|
||||
default-language: Haskell2010
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
@@ -0,0 +1,14 @@
|
||||
module Simplex.FileTransfer where
|
||||
|
||||
-- TODO
|
||||
-- Protocol
|
||||
-- Store (in memory storage)
|
||||
-- StoreLog (append only log)
|
||||
-- FileDescription
|
||||
-- Server
|
||||
-- Client
|
||||
-- Server/Main (server CLI)
|
||||
-- Client/Main (client CLI)
|
||||
--
|
||||
-- Transport for HTTP2 ?
|
||||
-- streaming Crypto
|
||||
@@ -0,0 +1,646 @@
|
||||
{-# LANGUAGE ConstraintKinds #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Simplex.FileTransfer.Agent
|
||||
( startWorkers,
|
||||
closeXFTPAgent,
|
||||
toFSFilePath,
|
||||
-- Receiving files
|
||||
receiveFile,
|
||||
deleteRcvFile,
|
||||
-- Sending files
|
||||
sendFileExperimental,
|
||||
sendFile,
|
||||
deleteSndFileInternal,
|
||||
deleteSndFileRemote,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent.STM (stateTVar)
|
||||
import Control.Logger.Simple (logError)
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Reader
|
||||
import Crypto.Random (ChaChaDRG, randomBytesGenerate)
|
||||
import Data.Bifunctor (first)
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString.Base64.URL as U
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Composition ((.:))
|
||||
import Data.Int (Int64)
|
||||
import Data.List (foldl', isSuffixOf, partition, sortOn)
|
||||
import Data.List.NonEmpty (nonEmpty)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Text (Text)
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import Data.Time.Format (defaultTimeLocale, formatTime)
|
||||
import Simplex.FileTransfer.Client (XFTPChunkSpec (..))
|
||||
import Simplex.FileTransfer.Client.Main
|
||||
import Simplex.FileTransfer.Crypto
|
||||
import Simplex.FileTransfer.Description
|
||||
import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI, SFileParty (..))
|
||||
import Simplex.FileTransfer.Transport (XFTPRcvChunkSpec (..))
|
||||
import Simplex.FileTransfer.Types
|
||||
import Simplex.FileTransfer.Util (removePath, uniqueCombine)
|
||||
import Simplex.Messaging.Agent.Client
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Agent.Store.SQLite
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (EntityId, XFTPServer, XFTPServerWithAuth)
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Util (liftError, liftIOEither, tshow, unlessM, whenM)
|
||||
import System.FilePath (takeFileName, (</>))
|
||||
import UnliftIO
|
||||
import UnliftIO.Concurrent
|
||||
import UnliftIO.Directory
|
||||
import qualified UnliftIO.Exception as E
|
||||
|
||||
startWorkers :: AgentMonad m => AgentClient -> Maybe FilePath -> m ()
|
||||
startWorkers c workDir = do
|
||||
wd <- asks $ xftpWorkDir . xftpAgent
|
||||
atomically $ writeTVar wd workDir
|
||||
startRcvFiles
|
||||
startSndFiles
|
||||
startDelFiles
|
||||
where
|
||||
startRcvFiles = do
|
||||
rcvFilesTTL <- asks $ rcvFilesTTL . config
|
||||
pendingRcvServers <- withStore' c (`getPendingRcvFilesServers` rcvFilesTTL)
|
||||
forM_ pendingRcvServers $ \s -> addXFTPRcvWorker c (Just s)
|
||||
-- start local worker for files pending decryption,
|
||||
-- no need to make an extra query for the check
|
||||
-- as the worker will check the store anyway
|
||||
addXFTPRcvWorker c Nothing
|
||||
startSndFiles = do
|
||||
sndFilesTTL <- asks $ sndFilesTTL . config
|
||||
-- start worker for files pending encryption/creation
|
||||
addXFTPSndWorker c Nothing
|
||||
pendingSndServers <- withStore' c (`getPendingSndFilesServers` sndFilesTTL)
|
||||
forM_ pendingSndServers $ \s -> addXFTPSndWorker c (Just s)
|
||||
startDelFiles = do
|
||||
rcvFilesTTL <- asks $ rcvFilesTTL . config
|
||||
pendingDelServers <- withStore' c (`getPendingDelFilesServers` rcvFilesTTL)
|
||||
forM_ pendingDelServers $ addXFTPDelWorker c
|
||||
|
||||
closeXFTPAgent :: MonadUnliftIO m => XFTPAgent -> m ()
|
||||
closeXFTPAgent XFTPAgent {xftpRcvWorkers, xftpSndWorkers} = do
|
||||
stopWorkers xftpRcvWorkers
|
||||
stopWorkers xftpSndWorkers
|
||||
where
|
||||
stopWorkers wsSel = do
|
||||
ws <- atomically $ stateTVar wsSel (,M.empty)
|
||||
mapM_ (uninterruptibleCancel . snd) ws
|
||||
|
||||
receiveFile :: AgentMonad m => AgentClient -> UserId -> ValidFileDescription 'FRecipient -> m RcvFileId
|
||||
receiveFile c userId (ValidFileDescription fd@FileDescription {chunks}) = do
|
||||
g <- asks idsDrg
|
||||
prefixPath <- getPrefixPath "rcv.xftp"
|
||||
createDirectory prefixPath
|
||||
let relPrefixPath = takeFileName prefixPath
|
||||
relTmpPath = relPrefixPath </> "xftp.encrypted"
|
||||
relSavePath = relPrefixPath </> "xftp.decrypted"
|
||||
createDirectory =<< toFSFilePath relTmpPath
|
||||
createEmptyFile =<< toFSFilePath relSavePath
|
||||
fId <- withStore c $ \db -> createRcvFile db g userId fd relPrefixPath relTmpPath relSavePath
|
||||
forM_ chunks downloadChunk
|
||||
pure fId
|
||||
where
|
||||
downloadChunk :: AgentMonad m => FileChunk -> m ()
|
||||
downloadChunk FileChunk {replicas = (FileChunkReplica {server} : _)} = do
|
||||
addXFTPRcvWorker c (Just server)
|
||||
downloadChunk _ = throwError $ INTERNAL "no replicas"
|
||||
|
||||
getPrefixPath :: AgentMonad m => String -> m FilePath
|
||||
getPrefixPath suffix = do
|
||||
workPath <- getXFTPWorkPath
|
||||
ts <- liftIO getCurrentTime
|
||||
let isoTime = formatTime defaultTimeLocale "%Y%m%d_%H%M%S_%6q" ts
|
||||
uniqueCombine workPath (isoTime <> "_" <> suffix)
|
||||
|
||||
toFSFilePath :: AgentMonad m => FilePath -> m FilePath
|
||||
toFSFilePath f = (</> f) <$> getXFTPWorkPath
|
||||
|
||||
createEmptyFile :: AgentMonad m => FilePath -> m ()
|
||||
createEmptyFile fPath = do
|
||||
h <- openFile fPath AppendMode
|
||||
liftIO $ B.hPut h "" >> hFlush h
|
||||
|
||||
addXFTPRcvWorker :: AgentMonad m => AgentClient -> Maybe XFTPServer -> m ()
|
||||
addXFTPRcvWorker c = addWorker c xftpRcvWorkers runXFTPRcvWorker runXFTPRcvLocalWorker
|
||||
|
||||
addWorker ::
|
||||
AgentMonad m =>
|
||||
AgentClient ->
|
||||
(XFTPAgent -> TMap (Maybe XFTPServer) (TMVar (), Async ())) ->
|
||||
(AgentClient -> XFTPServer -> TMVar () -> m ()) ->
|
||||
(AgentClient -> TMVar () -> m ()) ->
|
||||
Maybe XFTPServer ->
|
||||
m ()
|
||||
addWorker c wsSel runWorker runWorkerNoSrv srv_ = do
|
||||
ws <- asks $ wsSel . xftpAgent
|
||||
atomically (TM.lookup srv_ ws) >>= \case
|
||||
Nothing -> do
|
||||
doWork <- newTMVarIO ()
|
||||
let runWorker' = case srv_ of
|
||||
Just srv -> runWorker c srv doWork
|
||||
Nothing -> runWorkerNoSrv c doWork
|
||||
worker <- async $ runWorker' `E.finally` atomically (TM.delete srv_ ws)
|
||||
atomically $ TM.insert srv_ (doWork, worker) ws
|
||||
Just (doWork, _) ->
|
||||
void . atomically $ tryPutTMVar doWork ()
|
||||
|
||||
runXFTPRcvWorker :: forall m. AgentMonad m => AgentClient -> XFTPServer -> TMVar () -> m ()
|
||||
runXFTPRcvWorker c srv doWork = do
|
||||
forever $ do
|
||||
void . atomically $ readTMVar doWork
|
||||
atomically $ assertAgentForeground c
|
||||
runXFTPOperation
|
||||
where
|
||||
noWorkToDo = void . atomically $ tryTakeTMVar doWork
|
||||
runXFTPOperation :: m ()
|
||||
runXFTPOperation = do
|
||||
rcvFilesTTL <- asks (rcvFilesTTL . config)
|
||||
nextChunk <- withStore' c $ \db -> getNextRcvChunkToDownload db srv rcvFilesTTL
|
||||
case nextChunk of
|
||||
Nothing -> noWorkToDo
|
||||
Just RcvFileChunk {rcvFileId, rcvFileEntityId, fileTmpPath, replicas = []} -> rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) "chunk has no replicas"
|
||||
Just fc@RcvFileChunk {userId, rcvFileId, rcvFileEntityId, digest, fileTmpPath, replicas = replica@RcvFileChunkReplica {rcvChunkReplicaId, server, delay} : _} -> do
|
||||
ri <- asks $ reconnectInterval . config
|
||||
let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay
|
||||
withRetryInterval ri' $ \delay' loop ->
|
||||
downloadFileChunk fc replica
|
||||
`catchError` \e -> retryOnError "XFTP rcv worker" (retryLoop loop e delay') (retryDone e) e
|
||||
where
|
||||
retryLoop loop e replicaDelay = do
|
||||
flip catchError (\_ -> pure ()) $ do
|
||||
notifyOnRetry <- asks (xftpNotifyErrsOnRetry . config)
|
||||
when notifyOnRetry $ notify c rcvFileEntityId $ RFERR e
|
||||
closeXFTPServerClient c userId server digest
|
||||
withStore' c $ \db -> updateRcvChunkReplicaDelay db rcvChunkReplicaId replicaDelay
|
||||
atomically $ assertAgentForeground c
|
||||
loop
|
||||
retryDone e = rcvWorkerInternalError c rcvFileId rcvFileEntityId (Just fileTmpPath) (show e)
|
||||
downloadFileChunk :: RcvFileChunk -> RcvFileChunkReplica -> m ()
|
||||
downloadFileChunk RcvFileChunk {userId, rcvFileId, rcvFileEntityId, rcvChunkId, chunkNo, chunkSize, digest, fileTmpPath} replica = do
|
||||
fsFileTmpPath <- toFSFilePath fileTmpPath
|
||||
chunkPath <- uniqueCombine fsFileTmpPath $ show chunkNo
|
||||
let chunkSpec = XFTPRcvChunkSpec chunkPath (unFileSize chunkSize) (unFileDigest digest)
|
||||
relChunkPath = fileTmpPath </> takeFileName chunkPath
|
||||
agentXFTPDownloadChunk c userId digest replica chunkSpec
|
||||
(complete, progress) <- withStore c $ \db -> runExceptT $ do
|
||||
liftIO $ updateRcvFileChunkReceived db (rcvChunkReplicaId replica) rcvChunkId relChunkPath
|
||||
RcvFile {size = FileSize total, chunks} <- ExceptT $ getRcvFile db rcvFileId
|
||||
let rcvd = receivedSize chunks
|
||||
complete = all chunkReceived chunks
|
||||
liftIO . when complete $ updateRcvFileStatus db rcvFileId RFSReceived
|
||||
pure (complete, RFPROG rcvd total)
|
||||
notify c rcvFileEntityId progress
|
||||
when complete $ addXFTPRcvWorker c Nothing
|
||||
where
|
||||
receivedSize :: [RcvFileChunk] -> Int64
|
||||
receivedSize = foldl' (\sz ch -> sz + receivedChunkSize ch) 0
|
||||
receivedChunkSize ch@RcvFileChunk {chunkSize = s}
|
||||
| chunkReceived ch = fromIntegral (unFileSize s)
|
||||
| otherwise = 0
|
||||
chunkReceived RcvFileChunk {replicas} = any received replicas
|
||||
|
||||
retryOnError :: AgentMonad m => Text -> m a -> m a -> AgentErrorType -> m a
|
||||
retryOnError name loop done e = do
|
||||
logError $ name <> " error: " <> tshow e
|
||||
if temporaryAgentError e
|
||||
then loop
|
||||
else done
|
||||
|
||||
rcvWorkerInternalError :: AgentMonad m => AgentClient -> DBRcvFileId -> RcvFileId -> Maybe FilePath -> String -> m ()
|
||||
rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath internalErrStr = do
|
||||
forM_ tmpPath (removePath <=< toFSFilePath)
|
||||
withStore' c $ \db -> updateRcvFileError db rcvFileId internalErrStr
|
||||
notify c rcvFileEntityId $ RFERR $ INTERNAL internalErrStr
|
||||
|
||||
runXFTPRcvLocalWorker :: forall m. AgentMonad m => AgentClient -> TMVar () -> m ()
|
||||
runXFTPRcvLocalWorker c doWork = do
|
||||
forever $ do
|
||||
void . atomically $ readTMVar doWork
|
||||
atomically $ assertAgentForeground c
|
||||
runXFTPOperation
|
||||
where
|
||||
runXFTPOperation :: m ()
|
||||
runXFTPOperation = do
|
||||
rcvFilesTTL <- asks (rcvFilesTTL . config)
|
||||
nextFile <- withStore' c (`getNextRcvFileToDecrypt` rcvFilesTTL)
|
||||
case nextFile of
|
||||
Nothing -> noWorkToDo
|
||||
Just f@RcvFile {rcvFileId, rcvFileEntityId, tmpPath} ->
|
||||
decryptFile f `catchError` (rcvWorkerInternalError c rcvFileId rcvFileEntityId tmpPath . show)
|
||||
noWorkToDo = void . atomically $ tryTakeTMVar doWork
|
||||
decryptFile :: RcvFile -> m ()
|
||||
decryptFile RcvFile {rcvFileId, rcvFileEntityId, key, nonce, tmpPath, savePath, status, chunks} = do
|
||||
fsSavePath <- toFSFilePath savePath
|
||||
when (status == RFSDecrypting) $
|
||||
whenM (doesFileExist fsSavePath) (removeFile fsSavePath >> createEmptyFile fsSavePath)
|
||||
withStore' c $ \db -> updateRcvFileStatus db rcvFileId RFSDecrypting
|
||||
chunkPaths <- getChunkPaths chunks
|
||||
encSize <- liftIO $ foldM (\s path -> (s +) . fromIntegral <$> getFileSize path) 0 chunkPaths
|
||||
void $ liftError (INTERNAL . show) $ decryptChunks encSize chunkPaths key nonce $ \_ -> pure fsSavePath
|
||||
notify c rcvFileEntityId $ RFDONE fsSavePath
|
||||
forM_ tmpPath (removePath <=< toFSFilePath)
|
||||
withStore' c (`updateRcvFileComplete` rcvFileId)
|
||||
where
|
||||
getChunkPaths :: [RcvFileChunk] -> m [FilePath]
|
||||
getChunkPaths [] = pure []
|
||||
getChunkPaths (RcvFileChunk {chunkTmpPath = Just path} : cs) = do
|
||||
ps <- getChunkPaths cs
|
||||
fsPath <- toFSFilePath path
|
||||
pure $ fsPath : ps
|
||||
getChunkPaths (RcvFileChunk {chunkTmpPath = Nothing} : _cs) =
|
||||
throwError $ INTERNAL "no chunk path"
|
||||
|
||||
deleteRcvFile :: AgentMonad m => AgentClient -> RcvFileId -> m ()
|
||||
deleteRcvFile c rcvFileEntityId = do
|
||||
RcvFile {rcvFileId, prefixPath, status} <- withStore c $ \db -> getRcvFileByEntityId db rcvFileEntityId
|
||||
if status == RFSComplete || status == RFSError
|
||||
then do
|
||||
removePath prefixPath
|
||||
withStore' c (`deleteRcvFile'` rcvFileId)
|
||||
else withStore' c (`updateRcvFileDeleted` rcvFileId)
|
||||
|
||||
sendFileExperimental :: forall m. AgentMonad m => AgentClient -> UserId -> FilePath -> Int -> m SndFileId
|
||||
sendFileExperimental c@AgentClient {xftpServers} userId filePath numRecipients = do
|
||||
g <- asks idsDrg
|
||||
sndFileId <- liftIO $ randomId g 12
|
||||
xftpSrvs <- atomically $ TM.lookup userId xftpServers
|
||||
void $ forkIO $ sendCLI sndFileId $ maybe [] L.toList xftpSrvs
|
||||
pure sndFileId
|
||||
where
|
||||
randomId :: TVar ChaChaDRG -> Int -> IO ByteString
|
||||
randomId gVar n = U.encode <$> (atomically . stateTVar gVar $ randomBytesGenerate n)
|
||||
sendCLI :: SndFileId -> [XFTPServerWithAuth] -> m ()
|
||||
sendCLI sndFileId xftpSrvs = do
|
||||
let fileName = takeFileName filePath
|
||||
workPath <- getXFTPWorkPath
|
||||
outputDir <- uniqueCombine workPath $ fileName <> ".descr"
|
||||
createDirectory outputDir
|
||||
let tempPath = workPath </> "snd"
|
||||
createDirectoryIfMissing False tempPath
|
||||
runSend fileName outputDir tempPath `catchError` \e -> do
|
||||
cleanup outputDir tempPath
|
||||
notify c sndFileId $ SFERR e
|
||||
where
|
||||
runSend :: String -> FilePath -> FilePath -> m ()
|
||||
runSend fileName outputDir tempPath = do
|
||||
let sendOptions =
|
||||
SendOptions
|
||||
{ filePath,
|
||||
outputDir = Just outputDir,
|
||||
numRecipients,
|
||||
xftpServers = xftpSrvs,
|
||||
retryCount = 3,
|
||||
tempPath = Just tempPath,
|
||||
verbose = False
|
||||
}
|
||||
liftCLI $ cliSendFileOpts sendOptions False $ notify c sndFileId .: SFPROG
|
||||
(sndDescr, rcvDescrs) <- readDescrs outputDir fileName
|
||||
cleanup outputDir tempPath
|
||||
notify c sndFileId $ SFDONE sndDescr rcvDescrs
|
||||
cleanup :: FilePath -> FilePath -> m ()
|
||||
cleanup outputDir tempPath = do
|
||||
removePath tempPath
|
||||
removePath outputDir
|
||||
liftCLI :: ExceptT CLIError IO () -> m ()
|
||||
liftCLI = either (throwError . INTERNAL . show) pure <=< liftIO . runExceptT
|
||||
readDescrs :: FilePath -> FilePath -> m (ValidFileDescription 'FSender, [ValidFileDescription 'FRecipient])
|
||||
readDescrs outDir fileName = do
|
||||
let descrDir = outDir </> (fileName <> ".xftp")
|
||||
files <- listDirectory descrDir
|
||||
let (sdFiles, rdFiles) = partition ("snd.xftp.private" `isSuffixOf`) files
|
||||
sdFile = maybe "" (\l -> descrDir </> L.head l) (nonEmpty sdFiles)
|
||||
rdFiles' = map (descrDir </>) rdFiles
|
||||
(,) <$> readDescr sdFile <*> mapM readDescr rdFiles'
|
||||
readDescr :: FilePartyI p => FilePath -> m (ValidFileDescription p)
|
||||
readDescr f = liftIOEither $ first INTERNAL . strDecode <$> B.readFile f
|
||||
|
||||
notify :: forall m e. (MonadUnliftIO m, AEntityI e) => AgentClient -> EntityId -> ACommand 'Agent e -> m ()
|
||||
notify c entId cmd = atomically $ writeTBQueue (subQ c) ("", entId, APC (sAEntity @e) cmd)
|
||||
|
||||
sendFile :: AgentMonad m => AgentClient -> UserId -> FilePath -> Int -> m SndFileId
|
||||
sendFile c userId filePath numRecipients = do
|
||||
g <- asks idsDrg
|
||||
prefixPath <- getPrefixPath "snd.xftp"
|
||||
createDirectory prefixPath
|
||||
let relPrefixPath = takeFileName prefixPath
|
||||
key <- liftIO C.randomSbKey
|
||||
nonce <- liftIO C.randomCbNonce
|
||||
-- saving absolute filePath will not allow to restore file encryption after app update, but it's a short window
|
||||
fId <- withStore c $ \db -> createSndFile db g userId numRecipients filePath relPrefixPath key nonce
|
||||
addXFTPSndWorker c Nothing
|
||||
pure fId
|
||||
|
||||
addXFTPSndWorker :: AgentMonad m => AgentClient -> Maybe XFTPServer -> m ()
|
||||
addXFTPSndWorker c = addWorker c xftpSndWorkers runXFTPSndWorker runXFTPSndPrepareWorker
|
||||
|
||||
runXFTPSndPrepareWorker :: forall m. AgentMonad m => AgentClient -> TMVar () -> m ()
|
||||
runXFTPSndPrepareWorker c doWork = do
|
||||
forever $ do
|
||||
void . atomically $ readTMVar doWork
|
||||
atomically $ assertAgentForeground c
|
||||
runXFTPOperation
|
||||
where
|
||||
runXFTPOperation :: m ()
|
||||
runXFTPOperation = do
|
||||
sndFilesTTL <- asks (sndFilesTTL . config)
|
||||
nextFile <- withStore' c (`getNextSndFileToPrepare` sndFilesTTL)
|
||||
case nextFile of
|
||||
Nothing -> noWorkToDo
|
||||
Just f@SndFile {sndFileId, sndFileEntityId, prefixPath} ->
|
||||
prepareFile f `catchError` (sndWorkerInternalError c sndFileId sndFileEntityId prefixPath . show)
|
||||
noWorkToDo = void . atomically $ tryTakeTMVar doWork
|
||||
prepareFile :: SndFile -> m ()
|
||||
prepareFile SndFile {prefixPath = Nothing} =
|
||||
throwError $ INTERNAL "no prefix path"
|
||||
prepareFile sndFile@SndFile {sndFileId, userId, prefixPath = Just ppath, status} = do
|
||||
SndFile {numRecipients, chunks} <-
|
||||
if status /= SFSEncrypted -- status is SFSNew or SFSEncrypting
|
||||
then do
|
||||
fsEncPath <- toFSFilePath $ sndFileEncPath ppath
|
||||
when (status == SFSEncrypting) $
|
||||
whenM (doesFileExist fsEncPath) $ removeFile fsEncPath
|
||||
withStore' c $ \db -> updateSndFileStatus db sndFileId SFSEncrypting
|
||||
(digest, chunkSpecsDigests) <- encryptFileForUpload sndFile fsEncPath
|
||||
withStore c $ \db -> do
|
||||
updateSndFileEncrypted db sndFileId digest chunkSpecsDigests
|
||||
getSndFile db sndFileId
|
||||
else pure sndFile
|
||||
maxRecipients <- asks (xftpMaxRecipientsPerRequest . config)
|
||||
let numRecipients' = min numRecipients maxRecipients
|
||||
-- concurrently?
|
||||
forM_ (filter (not . chunkCreated) chunks) $ createChunk numRecipients'
|
||||
withStore' c $ \db -> updateSndFileStatus db sndFileId SFSUploading
|
||||
where
|
||||
encryptFileForUpload :: SndFile -> FilePath -> m (FileDigest, [(XFTPChunkSpec, FileDigest)])
|
||||
encryptFileForUpload SndFile {key, nonce, filePath} fsEncPath = do
|
||||
let fileName = takeFileName filePath
|
||||
fileSize <- fromInteger <$> getFileSize filePath
|
||||
when (fileSize > maxFileSize) $ throwError $ INTERNAL "max file size exceeded"
|
||||
let fileHdr = smpEncode FileHeader {fileName, fileExtra = Nothing}
|
||||
fileSize' = fromIntegral (B.length fileHdr) + fileSize
|
||||
chunkSizes = prepareChunkSizes $ fileSize' + fileSizeLen + authTagSize
|
||||
chunkSizes' = map fromIntegral chunkSizes
|
||||
encSize = sum chunkSizes'
|
||||
void $ liftError (INTERNAL . show) $ encryptFile filePath fileHdr key nonce fileSize' encSize fsEncPath
|
||||
digest <- liftIO $ LC.sha512Hash <$> LB.readFile fsEncPath
|
||||
let chunkSpecs = prepareChunkSpecs fsEncPath chunkSizes
|
||||
chunkDigests <- map FileDigest <$> mapM (liftIO . getChunkDigest) chunkSpecs
|
||||
pure (FileDigest digest, zip chunkSpecs chunkDigests)
|
||||
chunkCreated :: SndFileChunk -> Bool
|
||||
chunkCreated SndFileChunk {replicas} =
|
||||
any (\SndFileChunkReplica {replicaStatus} -> replicaStatus == SFRSCreated) replicas
|
||||
createChunk :: Int -> SndFileChunk -> m ()
|
||||
createChunk numRecipients' ch = do
|
||||
atomically $ assertAgentForeground c
|
||||
(replica, ProtoServerWithAuth srv _) <- tryCreate
|
||||
withStore' c $ \db -> createSndFileReplica db ch replica
|
||||
addXFTPSndWorker c $ Just srv
|
||||
where
|
||||
tryCreate = do
|
||||
ri <- asks $ messageRetryInterval . config
|
||||
usedSrvs <- newTVarIO ([] :: [XFTPServer])
|
||||
withRetryInterval (riFast ri) $ \_ loop ->
|
||||
createWithNextSrv usedSrvs
|
||||
`catchError` \e -> retryOnError "XFTP prepare worker" (retryLoop loop) (throwError e) e
|
||||
where
|
||||
retryLoop loop = atomically (assertAgentForeground c) >> loop
|
||||
createWithNextSrv usedSrvs = do
|
||||
deleted <- withStore' c $ \db -> getSndFileDeleted db sndFileId
|
||||
when deleted $ throwError $ INTERNAL "file deleted, aborting chunk creation"
|
||||
withNextSrv c userId usedSrvs [] $ \srvAuth -> do
|
||||
replica <- agentXFTPNewChunk c ch numRecipients' srvAuth
|
||||
pure (replica, srvAuth)
|
||||
|
||||
sndWorkerInternalError :: AgentMonad m => AgentClient -> DBSndFileId -> SndFileId -> Maybe FilePath -> String -> m ()
|
||||
sndWorkerInternalError c sndFileId sndFileEntityId prefixPath internalErrStr = do
|
||||
forM_ prefixPath $ removePath <=< toFSFilePath
|
||||
withStore' c $ \db -> updateSndFileError db sndFileId internalErrStr
|
||||
notify c sndFileEntityId $ SFERR $ INTERNAL internalErrStr
|
||||
|
||||
runXFTPSndWorker :: forall m. AgentMonad m => AgentClient -> XFTPServer -> TMVar () -> m ()
|
||||
runXFTPSndWorker c srv doWork = do
|
||||
forever $ do
|
||||
void . atomically $ readTMVar doWork
|
||||
atomically $ assertAgentForeground c
|
||||
runXFTPOperation
|
||||
where
|
||||
noWorkToDo = void . atomically $ tryTakeTMVar doWork
|
||||
runXFTPOperation :: m ()
|
||||
runXFTPOperation = do
|
||||
sndFilesTTL <- asks (sndFilesTTL . config)
|
||||
nextChunk <- withStore' c $ \db -> getNextSndChunkToUpload db srv sndFilesTTL
|
||||
case nextChunk of
|
||||
Nothing -> noWorkToDo
|
||||
Just SndFileChunk {sndFileId, sndFileEntityId, filePrefixPath, replicas = []} -> sndWorkerInternalError c sndFileId sndFileEntityId (Just filePrefixPath) "chunk has no replicas"
|
||||
Just fc@SndFileChunk {userId, sndFileId, sndFileEntityId, filePrefixPath, digest, replicas = replica@SndFileChunkReplica {sndChunkReplicaId, server, delay} : _} -> do
|
||||
ri <- asks $ reconnectInterval . config
|
||||
let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay
|
||||
withRetryInterval ri' $ \delay' loop ->
|
||||
uploadFileChunk fc replica
|
||||
`catchError` \e -> retryOnError "XFTP snd worker" (retryLoop loop e delay') (retryDone e) e
|
||||
where
|
||||
retryLoop loop e replicaDelay = do
|
||||
flip catchError (\_ -> pure ()) $ do
|
||||
notifyOnRetry <- asks (xftpNotifyErrsOnRetry . config)
|
||||
when notifyOnRetry $ notify c sndFileEntityId $ SFERR e
|
||||
closeXFTPServerClient c userId server digest
|
||||
withStore' c $ \db -> updateSndChunkReplicaDelay db sndChunkReplicaId replicaDelay
|
||||
atomically $ assertAgentForeground c
|
||||
loop
|
||||
retryDone e = sndWorkerInternalError c sndFileId sndFileEntityId (Just filePrefixPath) (show e)
|
||||
uploadFileChunk :: SndFileChunk -> SndFileChunkReplica -> m ()
|
||||
uploadFileChunk sndFileChunk@SndFileChunk {sndFileId, userId, chunkSpec = chunkSpec@XFTPChunkSpec {filePath}, digest = chunkDigest} replica = do
|
||||
replica'@SndFileChunkReplica {sndChunkReplicaId} <- addRecipients sndFileChunk replica
|
||||
fsFilePath <- toFSFilePath filePath
|
||||
unlessM (doesFileExist fsFilePath) $ throwError $ INTERNAL "encrypted file doesn't exist on upload"
|
||||
let chunkSpec' = chunkSpec {filePath = fsFilePath} :: XFTPChunkSpec
|
||||
atomically $ assertAgentForeground c
|
||||
agentXFTPUploadChunk c userId chunkDigest replica' chunkSpec'
|
||||
sf@SndFile {sndFileEntityId, prefixPath, chunks} <- withStore c $ \db -> do
|
||||
updateSndChunkReplicaStatus db sndChunkReplicaId SFRSUploaded
|
||||
getSndFile db sndFileId
|
||||
let uploaded = uploadedSize chunks
|
||||
total = totalSize chunks
|
||||
complete = all chunkUploaded chunks
|
||||
notify c sndFileEntityId $ SFPROG uploaded total
|
||||
when complete $ do
|
||||
(sndDescr, rcvDescrs) <- sndFileToDescrs sf
|
||||
notify c sndFileEntityId $ SFDONE sndDescr rcvDescrs
|
||||
forM_ prefixPath $ removePath <=< toFSFilePath
|
||||
withStore' c $ \db -> updateSndFileComplete db sndFileId
|
||||
where
|
||||
addRecipients :: SndFileChunk -> SndFileChunkReplica -> m SndFileChunkReplica
|
||||
addRecipients ch@SndFileChunk {numRecipients} cr@SndFileChunkReplica {rcvIdsKeys}
|
||||
| length rcvIdsKeys > numRecipients = throwError $ INTERNAL "too many recipients"
|
||||
| length rcvIdsKeys == numRecipients = pure cr
|
||||
| otherwise = do
|
||||
maxRecipients <- asks $ xftpMaxRecipientsPerRequest . config
|
||||
let numRecipients' = min (numRecipients - length rcvIdsKeys) maxRecipients
|
||||
rcvIdsKeys' <- agentXFTPAddRecipients c userId chunkDigest cr numRecipients'
|
||||
cr' <- withStore' c $ \db -> addSndChunkReplicaRecipients db cr $ L.toList rcvIdsKeys'
|
||||
addRecipients ch cr'
|
||||
sndFileToDescrs :: SndFile -> m (ValidFileDescription 'FSender, [ValidFileDescription 'FRecipient])
|
||||
sndFileToDescrs SndFile {digest = Nothing} = throwError $ INTERNAL "snd file has no digest"
|
||||
sndFileToDescrs SndFile {chunks = []} = throwError $ INTERNAL "snd file has no chunks"
|
||||
sndFileToDescrs SndFile {digest = Just digest, key, nonce, chunks = chunks@(fstChunk : _)} = do
|
||||
let chunkSize = FileSize $ sndChunkSize fstChunk
|
||||
size = FileSize $ sum $ map (fromIntegral . sndChunkSize) chunks
|
||||
-- snd description
|
||||
sndDescrChunks <- mapM toSndDescrChunk chunks
|
||||
let fdSnd = FileDescription {party = SFSender, size, digest, key, nonce, chunkSize, chunks = sndDescrChunks}
|
||||
validFdSnd <- either (throwError . INTERNAL) pure $ validateFileDescription fdSnd
|
||||
-- rcv descriptions
|
||||
let fdRcv = FileDescription {party = SFRecipient, size, digest, key, nonce, chunkSize, chunks = []}
|
||||
fdRcvs = createRcvFileDescriptions fdRcv chunks
|
||||
validFdRcvs <- either (throwError . INTERNAL) pure $ mapM validateFileDescription fdRcvs
|
||||
pure (validFdSnd, validFdRcvs)
|
||||
toSndDescrChunk :: SndFileChunk -> m FileChunk
|
||||
toSndDescrChunk SndFileChunk {replicas = []} = throwError $ INTERNAL "snd file chunk has no replicas"
|
||||
toSndDescrChunk ch@SndFileChunk {chunkNo, digest = chDigest, replicas = (SndFileChunkReplica {server, replicaId, replicaKey} : _)} = do
|
||||
let chunkSize = FileSize $ sndChunkSize ch
|
||||
replicas = [FileChunkReplica {server, replicaId, replicaKey}]
|
||||
pure FileChunk {chunkNo, digest = chDigest, chunkSize, replicas}
|
||||
createRcvFileDescriptions :: FileDescription 'FRecipient -> [SndFileChunk] -> [FileDescription 'FRecipient]
|
||||
createRcvFileDescriptions fd sndChunks = map (\chunks -> (fd :: (FileDescription 'FRecipient)) {chunks}) rcvChunks
|
||||
where
|
||||
rcvReplicas :: [SentRecipientReplica]
|
||||
rcvReplicas = concatMap toSentRecipientReplicas sndChunks
|
||||
toSentRecipientReplicas :: SndFileChunk -> [SentRecipientReplica]
|
||||
toSentRecipientReplicas ch@SndFileChunk {chunkNo, digest, replicas} =
|
||||
let chunkSize = FileSize $ sndChunkSize ch
|
||||
in concatMap
|
||||
( \SndFileChunkReplica {server, rcvIdsKeys} ->
|
||||
zipWith
|
||||
(\rcvNo (replicaId, replicaKey) -> SentRecipientReplica {chunkNo, server, rcvNo, replicaId, replicaKey, digest, chunkSize})
|
||||
[1 ..]
|
||||
rcvIdsKeys
|
||||
)
|
||||
replicas
|
||||
rcvChunks :: [[FileChunk]]
|
||||
rcvChunks = map (sortChunks . M.elems) $ M.elems $ foldl' addRcvChunk M.empty rcvReplicas
|
||||
sortChunks :: [FileChunk] -> [FileChunk]
|
||||
sortChunks = map reverseReplicas . sortOn (chunkNo :: FileChunk -> Int)
|
||||
reverseReplicas ch@FileChunk {replicas} = (ch :: FileChunk) {replicas = reverse replicas}
|
||||
addRcvChunk :: Map Int (Map Int FileChunk) -> SentRecipientReplica -> Map Int (Map Int FileChunk)
|
||||
addRcvChunk m SentRecipientReplica {chunkNo, server, rcvNo, replicaId, replicaKey, digest, chunkSize} =
|
||||
M.alter (Just . addOrChangeRecipient) rcvNo m
|
||||
where
|
||||
addOrChangeRecipient :: Maybe (Map Int FileChunk) -> Map Int FileChunk
|
||||
addOrChangeRecipient = \case
|
||||
Just m' -> M.alter (Just . addOrChangeChunk) chunkNo m'
|
||||
_ -> M.singleton chunkNo $ FileChunk {chunkNo, digest, chunkSize, replicas = [replica']}
|
||||
addOrChangeChunk :: Maybe FileChunk -> FileChunk
|
||||
addOrChangeChunk = \case
|
||||
Just ch@FileChunk {replicas} -> ch {replicas = replica' : replicas}
|
||||
_ -> FileChunk {chunkNo, digest, chunkSize, replicas = [replica']}
|
||||
replica' = FileChunkReplica {server, replicaId, replicaKey}
|
||||
uploadedSize :: [SndFileChunk] -> Int64
|
||||
uploadedSize = foldl' (\sz ch -> sz + uploadedChunkSize ch) 0
|
||||
uploadedChunkSize ch
|
||||
| chunkUploaded ch = fromIntegral (sndChunkSize ch)
|
||||
| otherwise = 0
|
||||
totalSize :: [SndFileChunk] -> Int64
|
||||
totalSize = foldl' (\sz ch -> sz + fromIntegral (sndChunkSize ch)) 0
|
||||
chunkUploaded :: SndFileChunk -> Bool
|
||||
chunkUploaded SndFileChunk {replicas} =
|
||||
any (\SndFileChunkReplica {replicaStatus} -> replicaStatus == SFRSUploaded) replicas
|
||||
|
||||
deleteSndFileInternal :: AgentMonad m => AgentClient -> SndFileId -> m ()
|
||||
deleteSndFileInternal c sndFileEntityId = do
|
||||
SndFile {sndFileId, prefixPath, status} <- withStore c $ \db -> getSndFileByEntityId db sndFileEntityId
|
||||
if status == SFSComplete || status == SFSError
|
||||
then do
|
||||
forM_ prefixPath $ removePath <=< toFSFilePath
|
||||
withStore' c (`deleteSndFile'` sndFileId)
|
||||
else withStore' c (`updateSndFileDeleted` sndFileId)
|
||||
|
||||
deleteSndFileRemote :: forall m. AgentMonad m => AgentClient -> UserId -> SndFileId -> ValidFileDescription 'FSender -> m ()
|
||||
deleteSndFileRemote c userId sndFileEntityId (ValidFileDescription FileDescription {chunks}) = do
|
||||
deleteSndFileInternal c sndFileEntityId `catchError` (notify c sndFileEntityId . SFERR)
|
||||
forM_ chunks $ \ch -> deleteFileChunk ch `catchError` (notify c sndFileEntityId . SFERR)
|
||||
where
|
||||
deleteFileChunk :: FileChunk -> m ()
|
||||
deleteFileChunk FileChunk {digest, replicas = replica@FileChunkReplica {server} : _} = do
|
||||
withStore' c $ \db -> createDeletedSndChunkReplica db userId replica digest
|
||||
addXFTPDelWorker c server
|
||||
deleteFileChunk _ = pure ()
|
||||
|
||||
addXFTPDelWorker :: AgentMonad m => AgentClient -> XFTPServer -> m ()
|
||||
addXFTPDelWorker c srv = do
|
||||
ws <- asks $ xftpDelWorkers . xftpAgent
|
||||
atomically (TM.lookup srv ws) >>= \case
|
||||
Nothing -> do
|
||||
doWork <- newTMVarIO ()
|
||||
worker <- async $ runXFTPDelWorker c srv doWork `E.finally` atomically (TM.delete srv ws)
|
||||
atomically $ TM.insert srv (doWork, worker) ws
|
||||
Just (doWork, _) ->
|
||||
void . atomically $ tryPutTMVar doWork ()
|
||||
|
||||
runXFTPDelWorker :: forall m. AgentMonad m => AgentClient -> XFTPServer -> TMVar () -> m ()
|
||||
runXFTPDelWorker c srv doWork = do
|
||||
forever $ do
|
||||
void . atomically $ readTMVar doWork
|
||||
atomically $ assertAgentForeground c
|
||||
runXFTPOperation
|
||||
where
|
||||
noWorkToDo = void . atomically $ tryTakeTMVar doWork
|
||||
runXFTPOperation :: m ()
|
||||
runXFTPOperation = do
|
||||
-- no point in deleting files older than rcv ttl, as they will be expired on server
|
||||
rcvFilesTTL <- asks (rcvFilesTTL . config)
|
||||
nextReplica <- withStore' c $ \db -> getNextDeletedSndChunkReplica db srv rcvFilesTTL
|
||||
case nextReplica of
|
||||
Nothing -> noWorkToDo
|
||||
Just replica@DeletedSndChunkReplica {deletedSndChunkReplicaId, userId, server, chunkDigest, delay} -> do
|
||||
ri <- asks $ reconnectInterval . config
|
||||
let ri' = maybe ri (\d -> ri {initialInterval = d, increaseAfter = 0}) delay
|
||||
withRetryInterval ri' $ \delay' loop ->
|
||||
deleteChunkReplica replica
|
||||
`catchError` \e -> retryOnError "XFTP del worker" (retryLoop loop e delay') (retryDone e) e
|
||||
where
|
||||
retryLoop loop e replicaDelay = do
|
||||
flip catchError (\_ -> pure ()) $ do
|
||||
notifyOnRetry <- asks (xftpNotifyErrsOnRetry . config)
|
||||
when notifyOnRetry $ notify c "" $ SFERR e
|
||||
closeXFTPServerClient c userId server chunkDigest
|
||||
withStore' c $ \db -> updateDeletedSndChunkReplicaDelay db deletedSndChunkReplicaId replicaDelay
|
||||
atomically $ assertAgentForeground c
|
||||
loop
|
||||
retryDone e = delWorkerInternalError c deletedSndChunkReplicaId e
|
||||
deleteChunkReplica :: DeletedSndChunkReplica -> m ()
|
||||
deleteChunkReplica replica@DeletedSndChunkReplica {userId, deletedSndChunkReplicaId} = do
|
||||
agentXFTPDeleteChunk c userId replica
|
||||
withStore' c $ \db -> deleteDeletedSndChunkReplica db deletedSndChunkReplicaId
|
||||
|
||||
delWorkerInternalError :: AgentMonad m => AgentClient -> Int64 -> AgentErrorType -> m ()
|
||||
delWorkerInternalError c deletedSndChunkReplicaId e = do
|
||||
withStore' c $ \db -> deleteDeletedSndChunkReplica db deletedSndChunkReplicaId
|
||||
notify c "" $ SFERR e
|
||||
|
||||
assertAgentForeground :: AgentClient -> STM ()
|
||||
assertAgentForeground c = do
|
||||
throwWhenInactive c
|
||||
waitUntilForeground c
|
||||
@@ -0,0 +1,219 @@
|
||||
{-# LANGUAGE BlockArguments #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Simplex.FileTransfer.Client where
|
||||
|
||||
import Control.Monad.Except
|
||||
import Data.Bifunctor (first)
|
||||
import Data.ByteString.Builder (Builder, byteString)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Int (Int64)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import Data.Time (UTCTime)
|
||||
import Data.Word (Word32)
|
||||
import qualified Network.HTTP.Types as N
|
||||
import qualified Network.HTTP2.Client as H
|
||||
import Simplex.FileTransfer.Description (mb)
|
||||
import Simplex.FileTransfer.Protocol
|
||||
import Simplex.FileTransfer.Transport
|
||||
import Simplex.Messaging.Client
|
||||
( NetworkConfig (..),
|
||||
ProtocolClientError (..),
|
||||
TransportSession,
|
||||
chooseTransportHost,
|
||||
defaultNetworkConfig,
|
||||
proxyUsername,
|
||||
transportClientConfig,
|
||||
)
|
||||
import Simplex.Messaging.Client.Agent ()
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol
|
||||
( BasicAuth,
|
||||
Protocol (..),
|
||||
ProtocolServer (..),
|
||||
RecipientId,
|
||||
SenderId,
|
||||
)
|
||||
import Simplex.Messaging.Transport (supportedParameters)
|
||||
import Simplex.Messaging.Transport.Client (TransportClientConfig, TransportHost)
|
||||
import Simplex.Messaging.Transport.HTTP2
|
||||
import Simplex.Messaging.Transport.HTTP2.Client
|
||||
import Simplex.Messaging.Util (bshow, liftEitherError, whenM)
|
||||
import UnliftIO
|
||||
import UnliftIO.Directory
|
||||
|
||||
data XFTPClient = XFTPClient
|
||||
{ http2Client :: HTTP2Client,
|
||||
transportSession :: TransportSession FileResponse,
|
||||
config :: XFTPClientConfig
|
||||
}
|
||||
|
||||
data XFTPClientConfig = XFTPClientConfig
|
||||
{ xftpNetworkConfig :: NetworkConfig,
|
||||
uploadTimeoutPerMb :: Int64
|
||||
}
|
||||
|
||||
data XFTPChunkBody = XFTPChunkBody
|
||||
{ chunkSize :: Int,
|
||||
chunkPart :: Int -> IO ByteString,
|
||||
http2Body :: HTTP2Body
|
||||
}
|
||||
|
||||
data XFTPChunkSpec = XFTPChunkSpec
|
||||
{ filePath :: FilePath,
|
||||
chunkOffset :: Int64,
|
||||
chunkSize :: Word32
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
type XFTPClientError = ProtocolClientError XFTPErrorType
|
||||
|
||||
defaultXFTPClientConfig :: XFTPClientConfig
|
||||
defaultXFTPClientConfig =
|
||||
XFTPClientConfig
|
||||
{ xftpNetworkConfig = defaultNetworkConfig,
|
||||
uploadTimeoutPerMb = 10000000 -- 10 seconds
|
||||
}
|
||||
|
||||
getXFTPClient :: TransportSession FileResponse -> XFTPClientConfig -> (XFTPClient -> IO ()) -> IO (Either XFTPClientError XFTPClient)
|
||||
getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {xftpNetworkConfig} disconnected = runExceptT $ do
|
||||
let tcConfig = transportClientConfig xftpNetworkConfig
|
||||
http2Config = xftpHTTP2Config tcConfig config
|
||||
username = proxyUsername transportSession
|
||||
ProtocolServer _ host port keyHash = srv
|
||||
useHost <- liftEither $ chooseTransportHost xftpNetworkConfig host
|
||||
clientVar <- newTVarIO Nothing
|
||||
let usePort = if null port then "443" else port
|
||||
clientDisconnected = readTVarIO clientVar >>= mapM_ disconnected
|
||||
http2Client <- liftEitherError xftpClientError $ getVerifiedHTTP2Client (Just username) useHost usePort (Just keyHash) Nothing http2Config clientDisconnected
|
||||
let c = XFTPClient {http2Client, transportSession, config}
|
||||
atomically $ writeTVar clientVar $ Just c
|
||||
pure c
|
||||
|
||||
closeXFTPClient :: XFTPClient -> IO ()
|
||||
closeXFTPClient XFTPClient {http2Client} = closeHTTP2Client http2Client
|
||||
|
||||
xftpClientServer :: XFTPClient -> String
|
||||
xftpClientServer = B.unpack . strEncode . snd3 . transportSession
|
||||
where
|
||||
snd3 (_, s, _) = s
|
||||
|
||||
xftpTransportHost :: XFTPClient -> TransportHost
|
||||
xftpTransportHost = (host :: HClient -> TransportHost) . client_ . http2Client
|
||||
|
||||
xftpSessionTs :: XFTPClient -> UTCTime
|
||||
xftpSessionTs = sessionTs . http2Client
|
||||
|
||||
xftpHTTP2Config :: TransportClientConfig -> XFTPClientConfig -> HTTP2ClientConfig
|
||||
xftpHTTP2Config transportConfig XFTPClientConfig {xftpNetworkConfig = NetworkConfig {tcpConnectTimeout}} =
|
||||
defaultHTTP2ClientConfig
|
||||
{ bodyHeadSize = xftpBlockSize,
|
||||
suportedTLSParams = supportedParameters,
|
||||
connTimeout = tcpConnectTimeout,
|
||||
transportConfig
|
||||
}
|
||||
|
||||
xftpClientError :: HTTP2ClientError -> XFTPClientError
|
||||
xftpClientError = \case
|
||||
HCResponseTimeout -> PCEResponseTimeout
|
||||
HCNetworkError -> PCENetworkError
|
||||
HCIOError e -> PCEIOError e
|
||||
|
||||
sendXFTPCommand :: forall p. FilePartyI p => XFTPClient -> C.APrivateSignKey -> XFTPFileId -> FileCommand p -> Maybe XFTPChunkSpec -> ExceptT XFTPClientError IO (FileResponse, HTTP2Body)
|
||||
sendXFTPCommand XFTPClient {config, http2Client = http2@HTTP2Client {sessionId}} pKey fId cmd chunkSpec_ = do
|
||||
t <-
|
||||
liftEither . first PCETransportError $
|
||||
xftpEncodeTransmission sessionId (Just pKey) ("", fId, FileCmd (sFileParty @p) cmd)
|
||||
let req = H.requestStreaming N.methodPost "/" [] $ streamBody t
|
||||
reqTimeout = (\XFTPChunkSpec {chunkSize} -> chunkTimeout config chunkSize) <$> chunkSpec_
|
||||
HTTP2Response {respBody = body@HTTP2Body {bodyHead}} <- liftEitherError xftpClientError $ sendRequest http2 req reqTimeout
|
||||
when (B.length bodyHead /= xftpBlockSize) $ throwError $ PCEResponseError BLOCK
|
||||
-- TODO validate that the file ID is the same as in the request?
|
||||
(_, _, (_, _fId, respOrErr)) <- liftEither . first PCEResponseError $ xftpDecodeTransmission sessionId bodyHead
|
||||
case respOrErr of
|
||||
Right r -> case protocolError r of
|
||||
Just e -> throwError $ PCEProtocolError e
|
||||
_ -> pure (r, body)
|
||||
Left e -> throwError $ PCEResponseError e
|
||||
where
|
||||
streamBody :: ByteString -> (Builder -> IO ()) -> IO () -> IO ()
|
||||
streamBody t send done = do
|
||||
send $ byteString t
|
||||
forM_ chunkSpec_ $ \XFTPChunkSpec {filePath, chunkOffset, chunkSize} ->
|
||||
withFile filePath ReadMode $ \h -> do
|
||||
hSeek h AbsoluteSeek $ fromIntegral chunkOffset
|
||||
sendFile h send $ fromIntegral chunkSize
|
||||
done
|
||||
|
||||
createXFTPChunk ::
|
||||
XFTPClient ->
|
||||
C.APrivateSignKey ->
|
||||
FileInfo ->
|
||||
NonEmpty C.APublicVerifyKey ->
|
||||
Maybe BasicAuth ->
|
||||
ExceptT XFTPClientError IO (SenderId, NonEmpty RecipientId)
|
||||
createXFTPChunk c spKey file rcps auth_ =
|
||||
sendXFTPCommand c spKey "" (FNEW file rcps auth_) Nothing >>= \case
|
||||
(FRSndIds sId rIds, body) -> noFile body (sId, rIds)
|
||||
(r, _) -> throwError . PCEUnexpectedResponse $ bshow r
|
||||
|
||||
addXFTPRecipients :: XFTPClient -> C.APrivateSignKey -> XFTPFileId -> NonEmpty C.APublicVerifyKey -> ExceptT XFTPClientError IO (NonEmpty RecipientId)
|
||||
addXFTPRecipients c spKey fId rcps =
|
||||
sendXFTPCommand c spKey fId (FADD rcps) Nothing >>= \case
|
||||
(FRRcvIds rIds, body) -> noFile body rIds
|
||||
(r, _) -> throwError . PCEUnexpectedResponse $ bshow r
|
||||
|
||||
uploadXFTPChunk :: XFTPClient -> C.APrivateSignKey -> XFTPFileId -> XFTPChunkSpec -> ExceptT XFTPClientError IO ()
|
||||
uploadXFTPChunk c spKey fId chunkSpec =
|
||||
sendXFTPCommand c spKey fId FPUT (Just chunkSpec) >>= okResponse
|
||||
|
||||
downloadXFTPChunk :: XFTPClient -> C.APrivateSignKey -> XFTPFileId -> XFTPRcvChunkSpec -> ExceptT XFTPClientError IO ()
|
||||
downloadXFTPChunk c@XFTPClient {config} rpKey fId chunkSpec@XFTPRcvChunkSpec {filePath, chunkSize} = do
|
||||
(rDhKey, rpDhKey) <- liftIO C.generateKeyPair'
|
||||
sendXFTPCommand c rpKey fId (FGET rDhKey) Nothing >>= \case
|
||||
(FRFile sDhKey cbNonce, HTTP2Body {bodyHead = _bg, bodySize = _bs, bodyPart}) -> case bodyPart of
|
||||
-- TODO atm bodySize is set to 0, so chunkSize will be incorrect - validate once set
|
||||
Just chunkPart -> do
|
||||
let dhSecret = C.dh' sDhKey rpDhKey
|
||||
cbState <- liftEither . first PCECryptoError $ LC.cbInit dhSecret cbNonce
|
||||
let t = chunkTimeout config chunkSize
|
||||
t `timeout` download cbState >>= maybe (throwError PCEResponseTimeout) pure
|
||||
where
|
||||
download cbState =
|
||||
withExceptT PCEResponseError $
|
||||
receiveEncFile chunkPart cbState chunkSpec `catchError` \e ->
|
||||
whenM (doesFileExist filePath) (removeFile filePath) >> throwError e
|
||||
_ -> throwError $ PCEResponseError NO_FILE
|
||||
(r, _) -> throwError . PCEUnexpectedResponse $ bshow r
|
||||
|
||||
chunkTimeout :: XFTPClientConfig -> Word32 -> Int
|
||||
chunkTimeout config chunkSize = fromIntegral $ (fromIntegral chunkSize * uploadTimeoutPerMb config) `div` mb 1
|
||||
|
||||
deleteXFTPChunk :: XFTPClient -> C.APrivateSignKey -> SenderId -> ExceptT XFTPClientError IO ()
|
||||
deleteXFTPChunk c spKey sId = sendXFTPCommand c spKey sId FDEL Nothing >>= okResponse
|
||||
|
||||
ackXFTPChunk :: XFTPClient -> C.APrivateSignKey -> RecipientId -> ExceptT XFTPClientError IO ()
|
||||
ackXFTPChunk c rpKey rId = sendXFTPCommand c rpKey rId FACK Nothing >>= okResponse
|
||||
|
||||
okResponse :: (FileResponse, HTTP2Body) -> ExceptT XFTPClientError IO ()
|
||||
okResponse = \case
|
||||
(FROk, body) -> noFile body ()
|
||||
(r, _) -> throwError . PCEUnexpectedResponse $ bshow r
|
||||
|
||||
-- TODO this currently does not check anything because response size is not set and bodyPart is always Just
|
||||
noFile :: HTTP2Body -> a -> ExceptT XFTPClientError IO a
|
||||
noFile HTTP2Body {bodyPart} a = case bodyPart of
|
||||
Just _ -> pure a -- throwError $ PCEResponseError HAS_FILE
|
||||
_ -> pure a
|
||||
|
||||
-- FACK :: FileCommand Recipient
|
||||
-- PING :: FileCommand Recipient
|
||||
@@ -0,0 +1,127 @@
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE NumericUnderscores #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Simplex.FileTransfer.Client.Agent where
|
||||
|
||||
import Control.Logger.Simple (logInfo)
|
||||
import Control.Monad.Except
|
||||
import Data.Bifunctor (first)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (decodeUtf8)
|
||||
import Simplex.FileTransfer.Client
|
||||
import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Client (NetworkConfig (..), ProtocolClientError (..), temporaryClientError)
|
||||
import Simplex.Messaging.Client.Agent ()
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Protocol (ProtocolServer (..), XFTPServer)
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Util (catchAll_, tryError)
|
||||
import UnliftIO
|
||||
|
||||
type XFTPClientVar = TMVar (Either XFTPClientAgentError XFTPClient)
|
||||
|
||||
data XFTPClientAgent = XFTPClientAgent
|
||||
{ xftpClients :: TMap XFTPServer XFTPClientVar,
|
||||
config :: XFTPClientAgentConfig
|
||||
}
|
||||
|
||||
data XFTPClientAgentConfig = XFTPClientAgentConfig
|
||||
{ xftpConfig :: XFTPClientConfig,
|
||||
reconnectInterval :: RetryInterval
|
||||
}
|
||||
|
||||
defaultXFTPClientAgentConfig :: XFTPClientAgentConfig
|
||||
defaultXFTPClientAgentConfig =
|
||||
XFTPClientAgentConfig
|
||||
{ xftpConfig = defaultXFTPClientConfig,
|
||||
reconnectInterval =
|
||||
RetryInterval
|
||||
{ initialInterval = 5_000000,
|
||||
increaseAfter = 10_000000,
|
||||
maxInterval = 60_000000
|
||||
}
|
||||
}
|
||||
|
||||
data XFTPClientAgentError = XFTPClientAgentError XFTPServer XFTPClientError
|
||||
deriving (Show, Exception)
|
||||
|
||||
newXFTPAgent :: XFTPClientAgentConfig -> STM XFTPClientAgent
|
||||
newXFTPAgent config = do
|
||||
xftpClients <- TM.empty
|
||||
pure XFTPClientAgent {xftpClients, config}
|
||||
|
||||
type ME a = ExceptT XFTPClientAgentError IO a
|
||||
|
||||
getXFTPServerClient :: XFTPClientAgent -> XFTPServer -> ME XFTPClient
|
||||
getXFTPServerClient XFTPClientAgent {xftpClients, config} srv = do
|
||||
atomically getClientVar >>= either newXFTPClient waitForXFTPClient
|
||||
where
|
||||
connectClient :: ME XFTPClient
|
||||
connectClient =
|
||||
ExceptT $
|
||||
first (XFTPClientAgentError srv)
|
||||
<$> getXFTPClient (1, srv, Nothing) (xftpConfig config) clientDisconnected
|
||||
|
||||
clientDisconnected :: XFTPClient -> IO ()
|
||||
clientDisconnected _ = do
|
||||
atomically $ TM.delete srv xftpClients
|
||||
logInfo $ "disconnected from " <> showServer srv
|
||||
|
||||
getClientVar :: STM (Either XFTPClientVar XFTPClientVar)
|
||||
getClientVar = maybe (Left <$> newClientVar) (pure . Right) =<< TM.lookup srv xftpClients
|
||||
where
|
||||
newClientVar :: STM XFTPClientVar
|
||||
newClientVar = do
|
||||
var <- newEmptyTMVar
|
||||
TM.insert srv var xftpClients
|
||||
pure var
|
||||
|
||||
waitForXFTPClient :: XFTPClientVar -> ME XFTPClient
|
||||
waitForXFTPClient clientVar = do
|
||||
let XFTPClientConfig {xftpNetworkConfig = NetworkConfig {tcpConnectTimeout}} = xftpConfig config
|
||||
client_ <- tcpConnectTimeout `timeout` atomically (readTMVar clientVar)
|
||||
liftEither $ case client_ of
|
||||
Just (Right c) -> Right c
|
||||
Just (Left e) -> Left e
|
||||
Nothing -> Left $ XFTPClientAgentError srv PCEResponseTimeout
|
||||
|
||||
newXFTPClient :: XFTPClientVar -> ME XFTPClient
|
||||
newXFTPClient clientVar = tryConnectClient tryConnectAsync
|
||||
where
|
||||
tryConnectClient :: ME () -> ME XFTPClient
|
||||
tryConnectClient retryAction =
|
||||
tryError connectClient >>= \r -> case r of
|
||||
Right client -> do
|
||||
logInfo $ "connected to " <> showServer srv
|
||||
atomically $ putTMVar clientVar r
|
||||
pure client
|
||||
Left e@(XFTPClientAgentError _ e') -> do
|
||||
if temporaryClientError e'
|
||||
then retryAction
|
||||
else atomically $ do
|
||||
putTMVar clientVar r
|
||||
TM.delete srv xftpClients
|
||||
throwError e
|
||||
tryConnectAsync :: ME ()
|
||||
tryConnectAsync = void . async $ do
|
||||
withRetryInterval (reconnectInterval config) $ \_ loop -> void $ tryConnectClient loop
|
||||
|
||||
showServer :: XFTPServer -> Text
|
||||
showServer ProtocolServer {host, port} =
|
||||
decodeUtf8 $ strEncode host <> B.pack (if null port then "" else ':' : port)
|
||||
|
||||
closeXFTPServerClient :: XFTPClientAgent -> XFTPServer -> IO ()
|
||||
closeXFTPServerClient XFTPClientAgent {xftpClients, config} srv =
|
||||
atomically (TM.lookupDelete srv xftpClients) >>= mapM_ closeClient
|
||||
where
|
||||
closeClient cVar = do
|
||||
let NetworkConfig {tcpConnectTimeout} = xftpNetworkConfig $ xftpConfig config
|
||||
tcpConnectTimeout `timeout` atomically (readTMVar cVar) >>= \case
|
||||
Just (Right client) -> closeXFTPClient client `catchAll_` pure ()
|
||||
_ -> pure ()
|
||||
@@ -0,0 +1,601 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
module Simplex.FileTransfer.Client.Main
|
||||
( SendOptions (..),
|
||||
CLIError (..),
|
||||
xftpClientCLI,
|
||||
cliSendFile,
|
||||
cliSendFileOpts,
|
||||
prepareChunkSizes,
|
||||
prepareChunkSpecs,
|
||||
chunkSize1,
|
||||
chunkSize2,
|
||||
chunkSize3,
|
||||
maxFileSize,
|
||||
fileSizeLen,
|
||||
getChunkDigest,
|
||||
SentRecipientReplica (..),
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Concurrent.STM (stateTVar)
|
||||
import Control.Logger.Simple
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Crypto.Random (getRandomBytes)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bifunctor (first)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Char (toLower)
|
||||
import Data.Int (Int64)
|
||||
import Data.List (foldl', sortOn)
|
||||
import Data.List.NonEmpty (NonEmpty (..), nonEmpty)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map (Map)
|
||||
import qualified Data.Map as M
|
||||
import Data.Maybe (fromMaybe)
|
||||
import qualified Data.Text as T
|
||||
import Data.Word (Word32)
|
||||
import GHC.Records (HasField (getField))
|
||||
import Options.Applicative
|
||||
import Simplex.FileTransfer.Client
|
||||
import Simplex.FileTransfer.Client.Agent
|
||||
import Simplex.FileTransfer.Client.Presets
|
||||
import Simplex.FileTransfer.Crypto
|
||||
import Simplex.FileTransfer.Description
|
||||
import Simplex.FileTransfer.Protocol
|
||||
import Simplex.FileTransfer.Transport (XFTPRcvChunkSpec (..))
|
||||
import Simplex.FileTransfer.Types
|
||||
import Simplex.FileTransfer.Util (uniqueCombine)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String (StrEncoding (..))
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), SenderId, SndPrivateSignKey, XFTPServer, XFTPServerWithAuth)
|
||||
import Simplex.Messaging.Server.CLI (getCliCommand')
|
||||
import Simplex.Messaging.Util (groupAllOn, ifM, tshow, whenM)
|
||||
import System.Exit (exitFailure)
|
||||
import System.FilePath (splitFileName, (</>))
|
||||
import System.IO.Temp (getCanonicalTemporaryDirectory)
|
||||
import System.Random (StdGen, newStdGen, randomR)
|
||||
import UnliftIO
|
||||
import UnliftIO.Directory
|
||||
|
||||
xftpClientVersion :: String
|
||||
xftpClientVersion = "0.1.0"
|
||||
|
||||
chunkSize1 :: Word32
|
||||
chunkSize1 = kb 256
|
||||
|
||||
chunkSize2 :: Word32
|
||||
chunkSize2 = mb 1
|
||||
|
||||
chunkSize3 :: Word32
|
||||
chunkSize3 = mb 4
|
||||
|
||||
maxFileSize :: Int64
|
||||
maxFileSize = gb 1
|
||||
|
||||
maxFileSizeStr :: String
|
||||
maxFileSizeStr = B.unpack . strEncode $ FileSize maxFileSize
|
||||
|
||||
fileSizeLen :: Int64
|
||||
fileSizeLen = 8
|
||||
|
||||
newtype CLIError = CLIError String
|
||||
deriving (Eq, Show, Exception)
|
||||
|
||||
cliCryptoError :: FTCryptoError -> CLIError
|
||||
cliCryptoError = \case
|
||||
FTCECryptoError e -> CLIError $ "Error decrypting file: " <> show e
|
||||
FTCEInvalidHeader e -> CLIError $ "Invalid file header: " <> e
|
||||
FTCEInvalidAuthTag -> CLIError "Error decrypting file: incorrect auth tag"
|
||||
FTCEFileIOError e -> CLIError $ "File IO error: " <> show e
|
||||
|
||||
data CliCommand
|
||||
= SendFile SendOptions
|
||||
| ReceiveFile ReceiveOptions
|
||||
| DeleteFile DeleteOptions
|
||||
| RandomFile RandomFileOptions
|
||||
| FileDescrInfo InfoOptions
|
||||
|
||||
data SendOptions = SendOptions
|
||||
{ filePath :: FilePath,
|
||||
outputDir :: Maybe FilePath,
|
||||
numRecipients :: Int,
|
||||
xftpServers :: [XFTPServerWithAuth],
|
||||
retryCount :: Int,
|
||||
tempPath :: Maybe FilePath,
|
||||
verbose :: Bool
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data ReceiveOptions = ReceiveOptions
|
||||
{ fileDescription :: FilePath,
|
||||
filePath :: Maybe FilePath,
|
||||
retryCount :: Int,
|
||||
tempPath :: Maybe FilePath,
|
||||
verbose :: Bool,
|
||||
yes :: Bool
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data DeleteOptions = DeleteOptions
|
||||
{ fileDescription :: FilePath,
|
||||
retryCount :: Int,
|
||||
verbose :: Bool,
|
||||
yes :: Bool
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
newtype InfoOptions = InfoOptions
|
||||
{ fileDescription :: FilePath
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
data RandomFileOptions = RandomFileOptions
|
||||
{ filePath :: FilePath,
|
||||
fileSize :: FileSize Int64
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
defaultRetryCount :: Int
|
||||
defaultRetryCount = 3
|
||||
|
||||
cliCommandP :: Parser CliCommand
|
||||
cliCommandP =
|
||||
hsubparser
|
||||
( command "send" (info (SendFile <$> sendP) (progDesc "Send file"))
|
||||
<> command "recv" (info (ReceiveFile <$> receiveP) (progDesc "Receive file"))
|
||||
<> command "del" (info (DeleteFile <$> deleteP) (progDesc "Delete file from server(s)"))
|
||||
<> command "info" (info (FileDescrInfo <$> infoP) (progDesc "Show file description"))
|
||||
)
|
||||
<|> hsubparser (command "rand" (info (RandomFile <$> randomP) (progDesc "Generate a random file of a given size")) <> internal)
|
||||
where
|
||||
sendP :: Parser SendOptions
|
||||
sendP =
|
||||
SendOptions
|
||||
<$> argument str (metavar "FILE" <> help "File to send")
|
||||
<*> optional (argument str $ metavar "DIR" <> help "Directory to save file descriptions (default: current directory)")
|
||||
<*> option auto (short 'n' <> metavar "COUNT" <> help "Number of recipients" <> value 1 <> showDefault)
|
||||
<*> xftpServers
|
||||
<*> retryCountP
|
||||
<*> temp
|
||||
<*> verboseP
|
||||
receiveP :: Parser ReceiveOptions
|
||||
receiveP =
|
||||
ReceiveOptions
|
||||
<$> fileDescrArg
|
||||
<*> optional (argument str $ metavar "DIR" <> help "Directory to save file (default: system Downloads directory)")
|
||||
<*> retryCountP
|
||||
<*> temp
|
||||
<*> verboseP
|
||||
<*> yesP
|
||||
deleteP :: Parser DeleteOptions
|
||||
deleteP =
|
||||
DeleteOptions
|
||||
<$> fileDescrArg
|
||||
<*> retryCountP
|
||||
<*> verboseP
|
||||
<*> yesP
|
||||
infoP :: Parser InfoOptions
|
||||
infoP = InfoOptions <$> fileDescrArg
|
||||
randomP :: Parser RandomFileOptions
|
||||
randomP =
|
||||
RandomFileOptions
|
||||
<$> argument str (metavar "FILE" <> help "Path to save file")
|
||||
<*> argument str (metavar "SIZE" <> help "File size (bytes/kb/mb/gb)")
|
||||
fileDescrArg = argument str (metavar "FILE" <> help "File description file")
|
||||
retryCountP = option auto (long "retry" <> short 'r' <> metavar "RETRY" <> help "Number of network retries" <> value defaultRetryCount <> showDefault)
|
||||
temp = optional (strOption $ long "tmp" <> metavar "TMP" <> help "Directory for temporary encrypted file (default: system temp directory)")
|
||||
verboseP = switch (long "verbose" <> short 'v' <> help "Verbose output")
|
||||
yesP = switch (long "yes" <> short 'y' <> help "Yes to questions")
|
||||
xftpServers =
|
||||
option
|
||||
parseXFTPServers
|
||||
( long "servers"
|
||||
<> short 's'
|
||||
<> metavar "SERVER"
|
||||
<> help "Semicolon-separated list of XFTP server(s) to use (each server can have more than one hostname)"
|
||||
<> value []
|
||||
)
|
||||
parseXFTPServers = eitherReader $ parseAll xftpServersP . B.pack
|
||||
xftpServersP = strP `A.sepBy1` A.char ';'
|
||||
|
||||
data SentFileChunk = SentFileChunk
|
||||
{ chunkNo :: Int,
|
||||
sndId :: SenderId,
|
||||
sndPrivateKey :: SndPrivateSignKey,
|
||||
chunkSize :: FileSize Word32,
|
||||
digest :: FileDigest,
|
||||
replicas :: [SentFileChunkReplica]
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data SentFileChunkReplica = SentFileChunkReplica
|
||||
{ server :: XFTPServer,
|
||||
recipients :: [(ChunkReplicaId, C.APrivateSignKey)]
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data SentRecipientReplica = SentRecipientReplica
|
||||
{ chunkNo :: Int,
|
||||
server :: XFTPServer,
|
||||
rcvNo :: Int,
|
||||
replicaId :: ChunkReplicaId,
|
||||
replicaKey :: C.APrivateSignKey,
|
||||
digest :: FileDigest,
|
||||
chunkSize :: FileSize Word32
|
||||
}
|
||||
|
||||
logCfg :: LogConfig
|
||||
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
xftpClientCLI :: IO ()
|
||||
xftpClientCLI =
|
||||
getCliCommand' cliCommandP clientVersion >>= \case
|
||||
SendFile opts -> runLogE opts $ cliSendFile opts
|
||||
ReceiveFile opts -> runLogE opts $ cliReceiveFile opts
|
||||
DeleteFile opts -> runLogE opts $ cliDeleteFile opts
|
||||
FileDescrInfo opts -> runE $ cliFileDescrInfo opts
|
||||
RandomFile opts -> cliRandomFile opts
|
||||
where
|
||||
clientVersion = "SimpleX XFTP client v" <> xftpClientVersion
|
||||
|
||||
runLogE :: HasField "verbose" a Bool => a -> ExceptT CLIError IO () -> IO ()
|
||||
runLogE opts a
|
||||
| getField @"verbose" opts = setLogLevel LogDebug >> withGlobalLogging logCfg (runE a)
|
||||
| otherwise = runE a
|
||||
|
||||
runE :: ExceptT CLIError IO () -> IO ()
|
||||
runE a =
|
||||
runExceptT a >>= \case
|
||||
Left (CLIError e) -> putStrLn e >> exitFailure
|
||||
_ -> pure ()
|
||||
|
||||
cliSendFile :: SendOptions -> ExceptT CLIError IO ()
|
||||
cliSendFile opts = cliSendFileOpts opts True $ printProgress "Uploaded"
|
||||
|
||||
cliSendFileOpts :: SendOptions -> Bool -> (Int64 -> Int64 -> IO ()) -> ExceptT CLIError IO ()
|
||||
cliSendFileOpts SendOptions {filePath, outputDir, numRecipients, xftpServers, retryCount, tempPath, verbose} printInfo notifyProgress = do
|
||||
let (_, fileName) = splitFileName filePath
|
||||
liftIO $ when printInfo $ printNoNewLine "Encrypting file..."
|
||||
(encPath, fdRcv, fdSnd, chunkSpecs, encSize) <- encryptFileForUpload fileName
|
||||
liftIO $ when printInfo $ printNoNewLine "Uploading file..."
|
||||
uploadedChunks <- newTVarIO []
|
||||
sentChunks <- uploadFile chunkSpecs uploadedChunks encSize
|
||||
whenM (doesFileExist encPath) $ removeFile encPath
|
||||
-- TODO if only small chunks, use different default size
|
||||
liftIO $ do
|
||||
let fdRcvs = createRcvFileDescriptions fdRcv sentChunks
|
||||
fdSnd' = createSndFileDescription fdSnd sentChunks
|
||||
(fdRcvPaths, fdSndPath) <- writeFileDescriptions fileName fdRcvs fdSnd'
|
||||
when printInfo $ do
|
||||
printNoNewLine "File uploaded!"
|
||||
putStrLn $ "\nSender file description: " <> fdSndPath
|
||||
putStrLn "Pass file descriptions to the recipient(s):"
|
||||
forM_ fdRcvPaths putStrLn
|
||||
where
|
||||
encryptFileForUpload :: String -> ExceptT CLIError IO (FilePath, FileDescription 'FRecipient, FileDescription 'FSender, [XFTPChunkSpec], Int64)
|
||||
encryptFileForUpload fileName = do
|
||||
fileSize <- fromInteger <$> getFileSize filePath
|
||||
when (fileSize > maxFileSize) $ throwError $ CLIError $ "Files bigger than " <> maxFileSizeStr <> " are not supported"
|
||||
encPath <- getEncPath tempPath "xftp"
|
||||
key <- liftIO C.randomSbKey
|
||||
nonce <- liftIO C.randomCbNonce
|
||||
let fileHdr = smpEncode FileHeader {fileName, fileExtra = Nothing}
|
||||
fileSize' = fromIntegral (B.length fileHdr) + fileSize
|
||||
chunkSizes = prepareChunkSizes $ fileSize' + fileSizeLen + authTagSize
|
||||
defChunkSize = head chunkSizes
|
||||
chunkSizes' = map fromIntegral chunkSizes
|
||||
encSize = sum chunkSizes'
|
||||
withExceptT (CLIError . show) $ encryptFile filePath fileHdr key nonce fileSize' encSize encPath
|
||||
digest <- liftIO $ LC.sha512Hash <$> LB.readFile encPath
|
||||
let chunkSpecs = prepareChunkSpecs encPath chunkSizes
|
||||
fdRcv = FileDescription {party = SFRecipient, size = FileSize encSize, digest = FileDigest digest, key, nonce, chunkSize = FileSize defChunkSize, chunks = []}
|
||||
fdSnd = FileDescription {party = SFSender, size = FileSize encSize, digest = FileDigest digest, key, nonce, chunkSize = FileSize defChunkSize, chunks = []}
|
||||
logInfo $ "encrypted file to " <> tshow encPath
|
||||
pure (encPath, fdRcv, fdSnd, chunkSpecs, encSize)
|
||||
uploadFile :: [XFTPChunkSpec] -> TVar [Int64] -> Int64 -> ExceptT CLIError IO [SentFileChunk]
|
||||
uploadFile chunks uploadedChunks encSize = do
|
||||
a <- atomically $ newXFTPAgent defaultXFTPClientAgentConfig
|
||||
gen <- newTVarIO =<< liftIO newStdGen
|
||||
let xftpSrvs = fromMaybe defaultXFTPServers (nonEmpty xftpServers)
|
||||
srvs <- liftIO $ replicateM (length chunks) $ getXFTPServer gen xftpSrvs
|
||||
let thd3 (_, _, x) = x
|
||||
chunks' = groupAllOn thd3 $ zip3 [1 ..] chunks srvs
|
||||
-- TODO shuffle/unshuffle chunks
|
||||
-- the reason we don't do pooled downloads here within one server is that http2 library doesn't handle cleint concurrency, even though
|
||||
-- upload doesn't allow other requests within the same client until complete (but download does allow).
|
||||
logInfo $ "uploading " <> tshow (length chunks) <> " chunks..."
|
||||
map snd . sortOn fst . concat <$> pooledForConcurrentlyN 16 chunks' (mapM $ uploadFileChunk a)
|
||||
where
|
||||
uploadFileChunk :: XFTPClientAgent -> (Int, XFTPChunkSpec, XFTPServerWithAuth) -> ExceptT CLIError IO (Int, SentFileChunk)
|
||||
uploadFileChunk a (chunkNo, chunkSpec@XFTPChunkSpec {chunkSize}, ProtoServerWithAuth xftpServer auth) = do
|
||||
logInfo $ "uploading chunk " <> tshow chunkNo <> " to " <> showServer xftpServer <> "..."
|
||||
(sndKey, spKey) <- liftIO $ C.generateSignatureKeyPair C.SEd25519
|
||||
rKeys <- liftIO $ L.fromList <$> replicateM numRecipients (C.generateSignatureKeyPair C.SEd25519)
|
||||
digest <- liftIO $ getChunkDigest chunkSpec
|
||||
let ch = FileInfo {sndKey, size = fromIntegral chunkSize, digest}
|
||||
c <- withRetry retryCount $ getXFTPServerClient a xftpServer
|
||||
(sndId, rIds) <- withRetry retryCount $ createXFTPChunk c spKey ch (L.map fst rKeys) auth
|
||||
withReconnect a xftpServer retryCount $ \c' -> uploadXFTPChunk c' spKey sndId chunkSpec
|
||||
logInfo $ "uploaded chunk " <> tshow chunkNo
|
||||
uploaded <- atomically . stateTVar uploadedChunks $ \cs ->
|
||||
let cs' = fromIntegral chunkSize : cs in (sum cs', cs')
|
||||
liftIO $ do
|
||||
notifyProgress uploaded encSize
|
||||
when verbose $ putStrLn ""
|
||||
let recipients = L.toList $ L.map ChunkReplicaId rIds `L.zip` L.map snd rKeys
|
||||
replicas = [SentFileChunkReplica {server = xftpServer, recipients}]
|
||||
pure (chunkNo, SentFileChunk {chunkNo, sndId, sndPrivateKey = spKey, chunkSize = FileSize $ fromIntegral chunkSize, digest = FileDigest digest, replicas})
|
||||
getXFTPServer :: TVar StdGen -> NonEmpty XFTPServerWithAuth -> IO XFTPServerWithAuth
|
||||
getXFTPServer gen = \case
|
||||
srv :| [] -> pure srv
|
||||
servers -> do
|
||||
atomically $ (servers L.!!) <$> stateTVar gen (randomR (0, L.length servers - 1))
|
||||
|
||||
-- M chunks, R replicas, N recipients
|
||||
-- rcvReplicas: M[SentFileChunk] -> M * R * N [SentRecipientReplica]
|
||||
-- rcvChunks: M * R * N [SentRecipientReplica] -> N[ M[FileChunk] ]
|
||||
createRcvFileDescriptions :: FileDescription 'FRecipient -> [SentFileChunk] -> [FileDescription 'FRecipient]
|
||||
createRcvFileDescriptions fd sentChunks = map (\chunks -> (fd :: (FileDescription 'FRecipient)) {chunks}) rcvChunks
|
||||
where
|
||||
rcvReplicas :: [SentRecipientReplica]
|
||||
rcvReplicas =
|
||||
concatMap
|
||||
( \SentFileChunk {chunkNo, digest, chunkSize, replicas} ->
|
||||
concatMap
|
||||
( \SentFileChunkReplica {server, recipients} ->
|
||||
zipWith (\rcvNo (replicaId, replicaKey) -> SentRecipientReplica {chunkNo, server, rcvNo, replicaId, replicaKey, digest, chunkSize}) [1 ..] recipients
|
||||
)
|
||||
replicas
|
||||
)
|
||||
sentChunks
|
||||
rcvChunks :: [[FileChunk]]
|
||||
rcvChunks = map (sortChunks . M.elems) $ M.elems $ foldl' addRcvChunk M.empty rcvReplicas
|
||||
sortChunks :: [FileChunk] -> [FileChunk]
|
||||
sortChunks = map reverseReplicas . sortOn (chunkNo :: FileChunk -> Int)
|
||||
reverseReplicas ch@FileChunk {replicas} = (ch :: FileChunk) {replicas = reverse replicas}
|
||||
addRcvChunk :: Map Int (Map Int FileChunk) -> SentRecipientReplica -> Map Int (Map Int FileChunk)
|
||||
addRcvChunk m SentRecipientReplica {chunkNo, server, rcvNo, replicaId, replicaKey, digest, chunkSize} =
|
||||
M.alter (Just . addOrChangeRecipient) rcvNo m
|
||||
where
|
||||
addOrChangeRecipient :: Maybe (Map Int FileChunk) -> Map Int FileChunk
|
||||
addOrChangeRecipient = \case
|
||||
Just m' -> M.alter (Just . addOrChangeChunk) chunkNo m'
|
||||
_ -> M.singleton chunkNo $ FileChunk {chunkNo, digest, chunkSize, replicas = [replica]}
|
||||
addOrChangeChunk :: Maybe FileChunk -> FileChunk
|
||||
addOrChangeChunk = \case
|
||||
Just ch@FileChunk {replicas} -> ch {replicas = replica : replicas}
|
||||
_ -> FileChunk {chunkNo, digest, chunkSize, replicas = [replica]}
|
||||
replica = FileChunkReplica {server, replicaId, replicaKey}
|
||||
createSndFileDescription :: FileDescription 'FSender -> [SentFileChunk] -> FileDescription 'FSender
|
||||
createSndFileDescription fd sentChunks = fd {chunks = sndChunks}
|
||||
where
|
||||
sndChunks :: [FileChunk]
|
||||
sndChunks =
|
||||
map
|
||||
( \SentFileChunk {chunkNo, sndId, sndPrivateKey, chunkSize, digest, replicas} ->
|
||||
FileChunk {chunkNo, digest, chunkSize, replicas = sndReplicas replicas (ChunkReplicaId sndId) sndPrivateKey}
|
||||
)
|
||||
sentChunks
|
||||
-- SentFileChunk having sndId and sndPrivateKey represents the current implementation's limitation
|
||||
-- that sender uploads each chunk only to one server, so we can use the first replica's server for FileChunkReplica
|
||||
sndReplicas :: [SentFileChunkReplica] -> ChunkReplicaId -> C.APrivateSignKey -> [FileChunkReplica]
|
||||
sndReplicas [] _ _ = []
|
||||
sndReplicas (SentFileChunkReplica {server} : _) replicaId replicaKey = [FileChunkReplica {server, replicaId, replicaKey}]
|
||||
writeFileDescriptions :: String -> [FileDescription 'FRecipient] -> FileDescription 'FSender -> IO ([FilePath], FilePath)
|
||||
writeFileDescriptions fileName fdRcvs fdSnd = do
|
||||
outDir <- uniqueCombine (fromMaybe "." outputDir) (fileName <> ".xftp")
|
||||
createDirectoryIfMissing True outDir
|
||||
fdRcvPaths <- forM (zip [1 ..] fdRcvs) $ \(i :: Int, fd) -> do
|
||||
let fdPath = outDir </> ("rcv" <> show i <> ".xftp")
|
||||
B.writeFile fdPath $ strEncode fd
|
||||
pure fdPath
|
||||
let fdSndPath = outDir </> "snd.xftp.private"
|
||||
B.writeFile fdSndPath $ strEncode fdSnd
|
||||
pure (fdRcvPaths, fdSndPath)
|
||||
|
||||
getChunkDigest :: XFTPChunkSpec -> IO ByteString
|
||||
getChunkDigest XFTPChunkSpec {filePath = chunkPath, chunkOffset, chunkSize} =
|
||||
withFile chunkPath ReadMode $ \h -> do
|
||||
hSeek h AbsoluteSeek $ fromIntegral chunkOffset
|
||||
LC.sha256Hash <$> LB.hGet h (fromIntegral chunkSize)
|
||||
|
||||
cliReceiveFile :: ReceiveOptions -> ExceptT CLIError IO ()
|
||||
cliReceiveFile ReceiveOptions {fileDescription, filePath, retryCount, tempPath, verbose, yes} =
|
||||
getFileDescription' fileDescription >>= receiveFile
|
||||
where
|
||||
receiveFile :: ValidFileDescription 'FRecipient -> ExceptT CLIError IO ()
|
||||
receiveFile (ValidFileDescription FileDescription {size, digest, key, nonce, chunks}) = do
|
||||
encPath <- getEncPath tempPath "xftp"
|
||||
createDirectory encPath
|
||||
a <- atomically $ newXFTPAgent defaultXFTPClientAgentConfig
|
||||
liftIO $ printNoNewLine "Downloading file..."
|
||||
downloadedChunks <- newTVarIO []
|
||||
let srv FileChunk {replicas} = server (head replicas :: FileChunkReplica)
|
||||
srvChunks = groupAllOn srv chunks
|
||||
chunkPaths <- map snd . sortOn fst . concat <$> pooledForConcurrentlyN 16 srvChunks (mapM $ downloadFileChunk a encPath size downloadedChunks)
|
||||
encDigest <- liftIO $ LC.sha512Hash <$> readChunks chunkPaths
|
||||
when (encDigest /= unFileDigest digest) $ throwError $ CLIError "File digest mismatch"
|
||||
encSize <- liftIO $ foldM (\s path -> (s +) . fromIntegral <$> getFileSize path) 0 chunkPaths
|
||||
when (FileSize encSize /= size) $ throwError $ CLIError "File size mismatch"
|
||||
liftIO $ printNoNewLine "Decrypting file..."
|
||||
path <- withExceptT cliCryptoError $ decryptChunks encSize chunkPaths key nonce getFilePath
|
||||
forM_ chunks $ acknowledgeFileChunk a
|
||||
whenM (doesPathExist encPath) $ removeDirectoryRecursive encPath
|
||||
liftIO $ do
|
||||
printNoNewLine $ "File downloaded: " <> path
|
||||
removeFD yes fileDescription
|
||||
downloadFileChunk :: XFTPClientAgent -> FilePath -> FileSize Int64 -> TVar [Int64] -> FileChunk -> ExceptT CLIError IO (Int, FilePath)
|
||||
downloadFileChunk a encPath (FileSize encSize) downloadedChunks FileChunk {chunkNo, chunkSize, digest, replicas = replica : _} = do
|
||||
let FileChunkReplica {server, replicaId, replicaKey} = replica
|
||||
logInfo $ "downloading chunk " <> tshow chunkNo <> " from " <> showServer server <> "..."
|
||||
chunkPath <- uniqueCombine encPath $ show chunkNo
|
||||
let chunkSpec = XFTPRcvChunkSpec chunkPath (unFileSize chunkSize) (unFileDigest digest)
|
||||
withReconnect a server retryCount $ \c -> downloadXFTPChunk c replicaKey (unChunkReplicaId replicaId) chunkSpec
|
||||
logInfo $ "downloaded chunk " <> tshow chunkNo <> " to " <> T.pack chunkPath
|
||||
downloaded <- atomically . stateTVar downloadedChunks $ \cs ->
|
||||
let cs' = fromIntegral (unFileSize chunkSize) : cs in (sum cs', cs')
|
||||
liftIO $ do
|
||||
printProgress "Downloaded" downloaded encSize
|
||||
when verbose $ putStrLn ""
|
||||
pure (chunkNo, chunkPath)
|
||||
downloadFileChunk _ _ _ _ _ = throwError $ CLIError "chunk has no replicas"
|
||||
getFilePath :: String -> ExceptT String IO FilePath
|
||||
getFilePath name =
|
||||
case filePath of
|
||||
Just path ->
|
||||
ifM (doesDirectoryExist path) (uniqueCombine path name) $
|
||||
ifM (doesFileExist path) (throwError "File already exists") (pure path)
|
||||
_ -> (`uniqueCombine` name) . (</> "Downloads") =<< getHomeDirectory
|
||||
acknowledgeFileChunk :: XFTPClientAgent -> FileChunk -> ExceptT CLIError IO ()
|
||||
acknowledgeFileChunk a FileChunk {replicas = replica : _} = do
|
||||
let FileChunkReplica {server, replicaId, replicaKey} = replica
|
||||
c <- withRetry retryCount $ getXFTPServerClient a server
|
||||
withRetry retryCount $ ackXFTPChunk c replicaKey (unChunkReplicaId replicaId)
|
||||
acknowledgeFileChunk _ _ = throwError $ CLIError "chunk has no replicas"
|
||||
|
||||
printProgress :: String -> Int64 -> Int64 -> IO ()
|
||||
printProgress s part total = printNoNewLine $ s <> " " <> show ((part * 100) `div` total) <> "%"
|
||||
|
||||
printNoNewLine :: String -> IO ()
|
||||
printNoNewLine s = do
|
||||
putStr $ s <> replicate (max 0 $ 25 - length s) ' ' <> "\r"
|
||||
hFlush stdout
|
||||
|
||||
cliDeleteFile :: DeleteOptions -> ExceptT CLIError IO ()
|
||||
cliDeleteFile DeleteOptions {fileDescription, retryCount, yes} = do
|
||||
getFileDescription' fileDescription >>= deleteFile
|
||||
where
|
||||
deleteFile :: ValidFileDescription 'FSender -> ExceptT CLIError IO ()
|
||||
deleteFile (ValidFileDescription FileDescription {chunks}) = do
|
||||
a <- atomically $ newXFTPAgent defaultXFTPClientAgentConfig
|
||||
forM_ chunks $ deleteFileChunk a
|
||||
liftIO $ do
|
||||
printNoNewLine "File deleted!"
|
||||
removeFD yes fileDescription
|
||||
deleteFileChunk :: XFTPClientAgent -> FileChunk -> ExceptT CLIError IO ()
|
||||
deleteFileChunk a FileChunk {chunkNo, replicas = replica : _} = do
|
||||
let FileChunkReplica {server, replicaId, replicaKey} = replica
|
||||
withReconnect a server retryCount $ \c -> deleteXFTPChunk c replicaKey (unChunkReplicaId replicaId)
|
||||
logInfo $ "deleted chunk " <> tshow chunkNo <> " from " <> showServer server
|
||||
deleteFileChunk _ _ = throwError $ CLIError "chunk has no replicas"
|
||||
|
||||
cliFileDescrInfo :: InfoOptions -> ExceptT CLIError IO ()
|
||||
cliFileDescrInfo InfoOptions {fileDescription} = do
|
||||
getFileDescription fileDescription >>= \case
|
||||
AVFD (ValidFileDescription FileDescription {party, size, chunkSize, chunks}) -> do
|
||||
let replicas = groupReplicasByServer chunkSize chunks
|
||||
liftIO $ do
|
||||
printParty
|
||||
putStrLn $ "File download size: " <> strEnc size
|
||||
putStrLn "File server(s):"
|
||||
forM_ replicas $ \srvReplicas -> do
|
||||
let srv = replicaServer $ head srvReplicas
|
||||
chSizes = map (\FileServerReplica {chunkSize = chSize_} -> unFileSize $ fromMaybe chunkSize chSize_) srvReplicas
|
||||
putStrLn $ strEnc srv <> ": " <> strEnc (FileSize $ sum chSizes)
|
||||
where
|
||||
printParty :: IO ()
|
||||
printParty = case party of
|
||||
SFRecipient -> putStrLn "Recipient file description"
|
||||
SFSender -> putStrLn "Sender file description"
|
||||
|
||||
strEnc :: StrEncoding a => a -> String
|
||||
strEnc = B.unpack . strEncode
|
||||
|
||||
getFileDescription :: FilePath -> ExceptT CLIError IO AValidFileDescription
|
||||
getFileDescription path =
|
||||
ExceptT $ first (CLIError . ("Failed to parse file description: " <>)) . strDecode <$> B.readFile path
|
||||
|
||||
getFileDescription' :: FilePartyI p => FilePath -> ExceptT CLIError IO (ValidFileDescription p)
|
||||
getFileDescription' path =
|
||||
getFileDescription path >>= \case
|
||||
AVFD fd -> either (throwError . CLIError) pure $ checkParty fd
|
||||
|
||||
prepareChunkSizes :: Int64 -> [Word32]
|
||||
prepareChunkSizes size' = prepareSizes size'
|
||||
where
|
||||
(smallSize, bigSize) = if size' > size34 chunkSize3 then (chunkSize2, chunkSize3) else (chunkSize1, chunkSize2)
|
||||
size34 sz = (fromIntegral sz * 3) `div` 4
|
||||
prepareSizes 0 = []
|
||||
prepareSizes size
|
||||
| size >= fromIntegral bigSize = replicate (fromIntegral n1) bigSize <> prepareSizes remSz
|
||||
| size > size34 bigSize = [bigSize]
|
||||
| otherwise = replicate (fromIntegral n2') smallSize
|
||||
where
|
||||
(n1, remSz) = size `divMod` fromIntegral bigSize
|
||||
n2' = let (n2, remSz2) = (size `divMod` fromIntegral smallSize) in if remSz2 == 0 then n2 else n2 + 1
|
||||
|
||||
prepareChunkSpecs :: FilePath -> [Word32] -> [XFTPChunkSpec]
|
||||
prepareChunkSpecs filePath chunkSizes = reverse . snd $ foldl' addSpec (0, []) chunkSizes
|
||||
where
|
||||
addSpec :: (Int64, [XFTPChunkSpec]) -> Word32 -> (Int64, [XFTPChunkSpec])
|
||||
addSpec (chunkOffset, specs) sz =
|
||||
let spec = XFTPChunkSpec {filePath, chunkOffset, chunkSize = fromIntegral sz}
|
||||
in (chunkOffset + fromIntegral sz, spec : specs)
|
||||
|
||||
getEncPath :: MonadIO m => Maybe FilePath -> String -> m FilePath
|
||||
getEncPath path name = (`uniqueCombine` (name <> ".encrypted")) =<< maybe (liftIO getCanonicalTemporaryDirectory) pure path
|
||||
|
||||
withReconnect :: Show e => XFTPClientAgent -> XFTPServer -> Int -> (XFTPClient -> ExceptT e IO a) -> ExceptT CLIError IO a
|
||||
withReconnect a srv n run = withRetry n $ do
|
||||
c <- withRetry n $ getXFTPServerClient a srv
|
||||
withExceptT (CLIError . show) (run c) `catchError` \e -> do
|
||||
liftIO $ closeXFTPServerClient a srv
|
||||
throwError e
|
||||
|
||||
withRetry :: Show e => Int -> ExceptT e IO a -> ExceptT CLIError IO a
|
||||
withRetry retryCount = withRetry' retryCount . withExceptT (CLIError . show)
|
||||
where
|
||||
withRetry' :: Int -> ExceptT CLIError IO a -> ExceptT CLIError IO a
|
||||
withRetry' 0 _ = throwError $ CLIError "internal: no retry attempts"
|
||||
withRetry' 1 a = a
|
||||
withRetry' n a =
|
||||
a `catchError` \e -> do
|
||||
logWarn ("retrying: " <> tshow e)
|
||||
withRetry' (n - 1) a
|
||||
|
||||
removeFD :: Bool -> FilePath -> IO ()
|
||||
removeFD yes fd
|
||||
| yes = do
|
||||
removeFile fd
|
||||
putStrLn $ "\nFile description " <> fd <> " is deleted."
|
||||
| otherwise = do
|
||||
y <- liftIO . getConfirmation $ "\nFile description " <> fd <> " can't be used again. Delete it"
|
||||
when y $ removeFile fd
|
||||
|
||||
getConfirmation :: String -> IO Bool
|
||||
getConfirmation prompt = do
|
||||
putStr $ prompt <> " (Y/n): "
|
||||
hFlush stdout
|
||||
s <- getLine
|
||||
case map toLower s of
|
||||
"y" -> pure True
|
||||
"" -> pure True
|
||||
"n" -> pure False
|
||||
_ -> getConfirmation prompt
|
||||
|
||||
cliRandomFile :: RandomFileOptions -> IO ()
|
||||
cliRandomFile RandomFileOptions {filePath, fileSize = FileSize size} = do
|
||||
withFile filePath WriteMode (`saveRandomFile` size)
|
||||
putStrLn $ "File created: " <> filePath
|
||||
where
|
||||
saveRandomFile h sz = do
|
||||
bytes <- getRandomBytes $ fromIntegral $ min mb' sz
|
||||
B.hPut h bytes
|
||||
when (sz > mb') $ saveRandomFile h (sz - mb')
|
||||
mb' = mb 1
|
||||
@@ -0,0 +1,17 @@
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
module Simplex.FileTransfer.Client.Presets where
|
||||
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import Simplex.Messaging.Protocol (XFTPServerWithAuth)
|
||||
|
||||
defaultXFTPServers :: NonEmpty XFTPServerWithAuth
|
||||
defaultXFTPServers =
|
||||
[ "xftp://da1aH3nOT-9G8lV7bWamhxpDYdJ1xmW7j3JpGaDR5Ug=@xftp1.simplex.im,blisztokwh6alt5oem45f2eksm2xdowxszmhjesntcyolmhutoon7sid.onion",
|
||||
"xftp://5vog2Imy1ExJB_7zDZrkV1KDWi96jYFyy9CL6fndBVw=@xftp2.simplex.im,jk6jybnel5hin2am5omd7g3hyvb4tfkyl7ea4vx5ldi7gpvz5ogqtcqd.onion",
|
||||
"xftp://PYa32DdYNFWi0uZZOprWQoQpIk5qyjRJ3EF7bVpbsn8=@xftp3.simplex.im,pqhchr4bpkzef5wra4hnmzvppn5j5mjrpzkh5ixx6mhylnxxd6lmwnad.onion",
|
||||
"xftp://k_GgQl40UZVV0Y4BX9ZTyMVqX5ZewcLW0waQIl7AYDE=@xftp4.simplex.im,q2lrjgiyo2efyweksavwxh3dlsrxetaxjztde2x5nateugrtrwzvmdid.onion",
|
||||
"xftp://-bIo6o8wuVc4wpZkZD3tH-rCeYaeER_0lz1ffQcSJDs=@xftp5.simplex.im,2kikdbdhlrluburwt6q6rnoxvezcm2ifx2hj463jlunkeftlzm7hf3qd.onion",
|
||||
"xftp://6nSvtY9pJn6PXWTAIMNl95E1Kk1vD7FM2TeOA64CFLg=@xftp6.simplex.im,tg3fmu6houq7vfqlsipm2qqzhtpgxesnuydqozx3khojxydhs53sbbyd.onion"
|
||||
]
|
||||
@@ -0,0 +1,115 @@
|
||||
{-# LANGUAGE BangPatterns #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.FileTransfer.Crypto where
|
||||
|
||||
import Control.Monad.Except
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bifunctor (first)
|
||||
import qualified Data.ByteArray as BA
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Int (Int64)
|
||||
import Simplex.FileTransfer.Types (FileHeader (..), authTagSize)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.Lazy (LazyByteString)
|
||||
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Util (liftEitherWith)
|
||||
import UnliftIO
|
||||
import UnliftIO.Directory (removeFile)
|
||||
|
||||
encryptFile :: FilePath -> ByteString -> C.SbKey -> C.CbNonce -> Int64 -> Int64 -> FilePath -> ExceptT FTCryptoError IO ()
|
||||
encryptFile filePath fileHdr key nonce fileSize' encSize encFile = do
|
||||
sb <- liftEitherWith FTCECryptoError $ LC.sbInit key nonce
|
||||
withFile filePath ReadMode $ \r -> withFile encFile WriteMode $ \w -> do
|
||||
let lenStr = smpEncode fileSize'
|
||||
(hdr, !sb') = LC.sbEncryptChunk sb $ lenStr <> fileHdr
|
||||
padLen = encSize - authTagSize - fileSize' - 8
|
||||
liftIO $ B.hPut w hdr
|
||||
sb2 <- encryptChunks r w (sb', fileSize' - fromIntegral (B.length fileHdr))
|
||||
sb3 <- encryptPad w (sb2, padLen)
|
||||
let tag = BA.convert $ LC.sbAuth sb3
|
||||
liftIO $ B.hPut w tag
|
||||
where
|
||||
encryptChunks r = encryptChunks_ $ liftIO . B.hGet r . fromIntegral
|
||||
encryptPad = encryptChunks_ $ \sz -> pure $ B.replicate (fromIntegral sz) '#'
|
||||
encryptChunks_ :: (Int64 -> IO ByteString) -> Handle -> (LC.SbState, Int64) -> ExceptT FTCryptoError IO LC.SbState
|
||||
encryptChunks_ get w (!sb, !len)
|
||||
| len == 0 = pure sb
|
||||
| otherwise = do
|
||||
let chSize = min len 65536
|
||||
ch <- liftIO $ get chSize
|
||||
when (B.length ch /= fromIntegral chSize) $ throwError $ FTCEFileIOError "encrypting file: unexpected EOF"
|
||||
let (ch', sb') = LC.sbEncryptChunk sb ch
|
||||
liftIO $ B.hPut w ch'
|
||||
encryptChunks_ get w (sb', len - chSize)
|
||||
|
||||
decryptChunks :: Int64 -> [FilePath] -> C.SbKey -> C.CbNonce -> (String -> ExceptT String IO String) -> ExceptT FTCryptoError IO FilePath
|
||||
decryptChunks _ [] _ _ _ = throwError $ FTCEInvalidHeader "empty"
|
||||
decryptChunks encSize (chPath : chPaths) key nonce getFilePath = case reverse chPaths of
|
||||
[] -> do
|
||||
(!authOk, !f) <- liftEither . first FTCECryptoError . LC.sbDecryptTailTag key nonce (encSize - authTagSize) =<< liftIO (LB.readFile chPath)
|
||||
unless authOk $ throwError FTCEInvalidAuthTag
|
||||
(FileHeader {fileName}, !f') <- parseFileHeader f
|
||||
path <- withExceptT FTCEFileIOError $ getFilePath fileName
|
||||
liftIO $ LB.writeFile path f'
|
||||
pure path
|
||||
lastPath : chPaths' -> do
|
||||
(state, expectedLen, ch) <- decryptFirstChunk
|
||||
(FileHeader {fileName}, ch') <- parseFileHeader ch
|
||||
path <- withExceptT FTCEFileIOError $ getFilePath fileName
|
||||
authOk <- liftIO . withFile path WriteMode $ \h -> do
|
||||
liftIO $ LB.hPut h ch'
|
||||
state' <- foldM (decryptChunk h) state $ reverse chPaths'
|
||||
decryptLastChunk h state' expectedLen
|
||||
unless authOk $ do
|
||||
removeFile path
|
||||
throwError FTCEInvalidAuthTag
|
||||
pure path
|
||||
where
|
||||
decryptFirstChunk = do
|
||||
sb <- liftEitherWith FTCECryptoError $ LC.sbInit key nonce
|
||||
ch <- liftIO $ LB.readFile chPath
|
||||
let (ch1, !sb') = LC.sbDecryptChunkLazy sb ch
|
||||
(!expectedLen, ch2) <- liftEitherWith FTCECryptoError $ LC.splitLen ch1
|
||||
let len1 = LB.length ch2
|
||||
pure ((sb', len1), expectedLen, ch2)
|
||||
decryptChunk h (!sb, !len) chPth = do
|
||||
ch <- LB.readFile chPth
|
||||
let len' = len + LB.length ch
|
||||
(ch', sb') = LC.sbDecryptChunkLazy sb ch
|
||||
LB.hPut h ch'
|
||||
pure (sb', len')
|
||||
decryptLastChunk h (!sb, !len) expectedLen = do
|
||||
ch <- LB.readFile lastPath
|
||||
let (ch1, tag') = LB.splitAt (LB.length ch - authTagSize) ch
|
||||
tag'' = LB.toStrict tag'
|
||||
(ch2, sb') = LC.sbDecryptChunkLazy sb ch1
|
||||
len' = len + LB.length ch2
|
||||
ch3 = LB.take (LB.length ch2 - len' + expectedLen) ch2
|
||||
tag :: ByteString = BA.convert (LC.sbAuth sb')
|
||||
LB.hPut h ch3
|
||||
pure $ B.length tag'' == 16 && BA.constEq tag'' tag
|
||||
where
|
||||
parseFileHeader :: LazyByteString -> ExceptT FTCryptoError IO (FileHeader, LazyByteString)
|
||||
parseFileHeader s = do
|
||||
let (hdrStr, s') = LB.splitAt 1024 s
|
||||
case A.parse smpP $ LB.toStrict hdrStr of
|
||||
A.Fail _ _ e -> throwError $ FTCEInvalidHeader e
|
||||
A.Partial _ -> throwError $ FTCEInvalidHeader "incomplete"
|
||||
A.Done rest hdr -> pure (hdr, LB.fromStrict rest <> s')
|
||||
|
||||
readChunks :: [FilePath] -> IO LB.ByteString
|
||||
readChunks = foldM (\s path -> (s <>) <$> LB.readFile path) ""
|
||||
|
||||
data FTCryptoError
|
||||
= FTCECryptoError C.CryptoError
|
||||
| FTCEInvalidHeader String
|
||||
| FTCEInvalidAuthTag
|
||||
| FTCEFileIOError String
|
||||
deriving (Show, Eq, Exception)
|
||||
@@ -0,0 +1,356 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE PatternSynonyms #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
module Simplex.FileTransfer.Description
|
||||
( FileDescription (..),
|
||||
AFileDescription (..),
|
||||
ValidFileDescription, -- constructor is not exported, use pattern
|
||||
pattern ValidFileDescription,
|
||||
AValidFileDescription (..),
|
||||
FileDigest (..),
|
||||
FileChunk (..),
|
||||
FileChunkReplica (..),
|
||||
FileServerReplica (..),
|
||||
FileSize (..),
|
||||
ChunkReplicaId (..),
|
||||
YAMLFileDescription (..), -- for tests
|
||||
YAMLServerReplicas (..), -- for tests
|
||||
validateFileDescription,
|
||||
groupReplicasByServer,
|
||||
replicaServer,
|
||||
fdSeparator,
|
||||
kb,
|
||||
mb,
|
||||
gb,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Applicative (optional)
|
||||
import Control.Monad ((<=<))
|
||||
import Data.Aeson (FromJSON, ToJSON)
|
||||
import qualified Data.Aeson as J
|
||||
import Data.Attoparsec.ByteString.Char8 (Parser)
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.Bifunctor (first)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Int (Int64)
|
||||
import Data.List (foldl', sortOn)
|
||||
import Data.Map (Map)
|
||||
import qualified Data.Map as M
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.String
|
||||
import Data.Word (Word32)
|
||||
import qualified Data.Yaml as Y
|
||||
import Database.SQLite.Simple.FromField (FromField (..))
|
||||
import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import GHC.Generics (Generic)
|
||||
import Simplex.FileTransfer.Protocol
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
import Simplex.Messaging.Protocol (XFTPServer)
|
||||
import Simplex.Messaging.Util (bshow, groupAllOn, (<$?>))
|
||||
|
||||
data FileDescription (p :: FileParty) = FileDescription
|
||||
{ party :: SFileParty p,
|
||||
size :: FileSize Int64,
|
||||
digest :: FileDigest,
|
||||
key :: C.SbKey,
|
||||
nonce :: C.CbNonce,
|
||||
chunkSize :: FileSize Word32,
|
||||
chunks :: [FileChunk]
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data AFileDescription = forall p. FilePartyI p => AFD (FileDescription p)
|
||||
|
||||
newtype ValidFileDescription p = ValidFD (FileDescription p)
|
||||
deriving (Eq, Show)
|
||||
|
||||
pattern ValidFileDescription :: FileDescription p -> ValidFileDescription p
|
||||
pattern ValidFileDescription fd = ValidFD fd
|
||||
|
||||
{-# COMPLETE ValidFileDescription #-}
|
||||
|
||||
data AValidFileDescription = forall p. FilePartyI p => AVFD (ValidFileDescription p)
|
||||
|
||||
fdSeparator :: IsString s => s
|
||||
fdSeparator = "################################\n"
|
||||
|
||||
newtype FileDigest = FileDigest {unFileDigest :: ByteString}
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding FileDigest where
|
||||
strEncode (FileDigest fd) = strEncode fd
|
||||
strDecode s = FileDigest <$> strDecode s
|
||||
strP = FileDigest <$> strP
|
||||
|
||||
instance FromJSON FileDigest where
|
||||
parseJSON = strParseJSON "FileDigest"
|
||||
|
||||
instance ToJSON FileDigest where
|
||||
toJSON = strToJSON
|
||||
toEncoding = strToJEncoding
|
||||
|
||||
instance FromField FileDigest where fromField f = FileDigest <$> fromField f
|
||||
|
||||
instance ToField FileDigest where toField (FileDigest s) = toField s
|
||||
|
||||
data FileChunk = FileChunk
|
||||
{ chunkNo :: Int,
|
||||
chunkSize :: FileSize Word32,
|
||||
digest :: FileDigest,
|
||||
replicas :: [FileChunkReplica]
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data FileChunkReplica = FileChunkReplica
|
||||
{ server :: XFTPServer,
|
||||
replicaId :: ChunkReplicaId,
|
||||
replicaKey :: C.APrivateSignKey
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
newtype ChunkReplicaId = ChunkReplicaId {unChunkReplicaId :: ByteString}
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding ChunkReplicaId where
|
||||
strEncode (ChunkReplicaId fid) = strEncode fid
|
||||
strP = ChunkReplicaId <$> strP
|
||||
|
||||
instance FromJSON ChunkReplicaId where
|
||||
parseJSON = strParseJSON "ChunkReplicaId"
|
||||
|
||||
instance ToJSON ChunkReplicaId where
|
||||
toJSON = strToJSON
|
||||
toEncoding = strToJEncoding
|
||||
|
||||
instance FromField ChunkReplicaId where fromField f = ChunkReplicaId <$> fromField f
|
||||
|
||||
instance ToField ChunkReplicaId where toField (ChunkReplicaId s) = toField s
|
||||
|
||||
data YAMLFileDescription = YAMLFileDescription
|
||||
{ party :: FileParty,
|
||||
size :: String,
|
||||
digest :: FileDigest,
|
||||
key :: C.SbKey,
|
||||
nonce :: C.CbNonce,
|
||||
chunkSize :: String,
|
||||
replicas :: [YAMLServerReplicas]
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
instance ToJSON YAMLFileDescription where
|
||||
toJSON = J.genericToJSON J.defaultOptions
|
||||
toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
data YAMLServerReplicas = YAMLServerReplicas
|
||||
{ server :: XFTPServer,
|
||||
chunks :: [String]
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
instance ToJSON YAMLServerReplicas where
|
||||
toJSON = J.genericToJSON J.defaultOptions
|
||||
toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
data FileServerReplica = FileServerReplica
|
||||
{ chunkNo :: Int,
|
||||
server :: XFTPServer,
|
||||
replicaId :: ChunkReplicaId,
|
||||
replicaKey :: C.APrivateSignKey,
|
||||
digest :: Maybe FileDigest,
|
||||
chunkSize :: Maybe (FileSize Word32)
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
instance FilePartyI p => StrEncoding (ValidFileDescription p) where
|
||||
strEncode (ValidFD fd) = strEncode fd
|
||||
strDecode s = strDecode s >>= (\(AVFD fd) -> checkParty fd)
|
||||
strP = strDecode <$?> A.takeByteString
|
||||
|
||||
instance StrEncoding AValidFileDescription where
|
||||
strEncode (AVFD fd) = strEncode fd
|
||||
strDecode = (\(AFD fd) -> AVFD <$> validateFileDescription fd) <=< strDecode
|
||||
strP = strDecode <$?> A.takeByteString
|
||||
|
||||
instance FilePartyI p => StrEncoding (FileDescription p) where
|
||||
strEncode = Y.encode . encodeFileDescription
|
||||
strDecode s = strDecode s >>= (\(AFD fd) -> checkParty fd)
|
||||
strP = strDecode <$?> A.takeByteString
|
||||
|
||||
instance StrEncoding AFileDescription where
|
||||
strEncode (AFD fd) = strEncode fd
|
||||
strDecode = decodeFileDescription <=< first show . Y.decodeEither'
|
||||
strP = strDecode <$?> A.takeByteString
|
||||
|
||||
validateFileDescription :: FileDescription p -> Either String (ValidFileDescription p)
|
||||
validateFileDescription fd@FileDescription {size, chunks}
|
||||
| chunkNos /= [1 .. length chunks] = Left "chunk numbers are not sequential"
|
||||
| chunksSize chunks /= unFileSize size = Left "chunks total size is different than file size"
|
||||
| otherwise = Right $ ValidFD fd
|
||||
where
|
||||
chunkNos = map (chunkNo :: FileChunk -> Int) chunks
|
||||
chunksSize = fromIntegral . foldl' (\s FileChunk {chunkSize} -> s + unFileSize chunkSize) 0
|
||||
|
||||
encodeFileDescription :: FileDescription p -> YAMLFileDescription
|
||||
encodeFileDescription FileDescription {party, size, digest, key, nonce, chunkSize, chunks} =
|
||||
YAMLFileDescription
|
||||
{ party = toFileParty party,
|
||||
size = B.unpack $ strEncode size,
|
||||
digest,
|
||||
key,
|
||||
nonce,
|
||||
chunkSize = B.unpack $ strEncode chunkSize,
|
||||
replicas = encodeFileReplicas chunkSize chunks
|
||||
}
|
||||
|
||||
newtype FileSize a = FileSize {unFileSize :: a}
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance (Integral a, Show a) => StrEncoding (FileSize a) where
|
||||
strEncode (FileSize b)
|
||||
| b' /= 0 = bshow b
|
||||
| ks' /= 0 = bshow ks <> "kb"
|
||||
| ms' /= 0 = bshow ms <> "mb"
|
||||
| otherwise = bshow gs <> "gb"
|
||||
where
|
||||
(ks, b') = b `divMod` 1024
|
||||
(ms, ks') = ks `divMod` 1024
|
||||
(gs, ms') = ms `divMod` 1024
|
||||
strP =
|
||||
FileSize
|
||||
<$> A.choice
|
||||
[ gb <$> A.decimal <* "gb",
|
||||
mb <$> A.decimal <* "mb",
|
||||
kb <$> A.decimal <* "kb",
|
||||
A.decimal
|
||||
]
|
||||
|
||||
kb :: Integral a => a -> a
|
||||
kb n = 1024 * n
|
||||
{-# INLINE kb #-}
|
||||
|
||||
mb :: Integral a => a -> a
|
||||
mb n = 1024 * kb n
|
||||
{-# INLINE mb #-}
|
||||
|
||||
gb :: Integral a => a -> a
|
||||
gb n = 1024 * mb n
|
||||
{-# INLINE gb #-}
|
||||
|
||||
instance (Integral a, Show a) => IsString (FileSize a) where
|
||||
fromString = either error id . strDecode . B.pack
|
||||
|
||||
instance (FromField a) => FromField (FileSize a) where fromField f = FileSize <$> fromField f
|
||||
|
||||
instance (ToField a) => ToField (FileSize a) where toField (FileSize s) = toField s
|
||||
|
||||
groupReplicasByServer :: FileSize Word32 -> [FileChunk] -> [[FileServerReplica]]
|
||||
groupReplicasByServer defChunkSize =
|
||||
groupAllOn replicaServer . unfoldChunksToReplicas defChunkSize
|
||||
|
||||
encodeFileReplicas :: FileSize Word32 -> [FileChunk] -> [YAMLServerReplicas]
|
||||
encodeFileReplicas defChunkSize =
|
||||
map encodeServerReplicas . groupReplicasByServer defChunkSize
|
||||
where
|
||||
encodeServerReplicas fs =
|
||||
YAMLServerReplicas
|
||||
{ server = replicaServer $ head fs, -- groupAllOn guarantees that fs is not empty
|
||||
chunks = map (B.unpack . encodeServerReplica) fs
|
||||
}
|
||||
|
||||
replicaServer :: FileServerReplica -> XFTPServer
|
||||
replicaServer = server
|
||||
|
||||
encodeServerReplica :: FileServerReplica -> ByteString
|
||||
encodeServerReplica FileServerReplica {chunkNo, replicaId, replicaKey, digest, chunkSize} =
|
||||
bshow chunkNo
|
||||
<> ":"
|
||||
<> strEncode replicaId
|
||||
<> ":"
|
||||
<> strEncode replicaKey
|
||||
<> maybe "" ((":" <>) . strEncode) digest
|
||||
<> maybe "" ((":" <>) . strEncode) chunkSize
|
||||
|
||||
serverReplicaP :: XFTPServer -> Parser FileServerReplica
|
||||
serverReplicaP server = do
|
||||
chunkNo <- A.decimal
|
||||
replicaId <- A.char ':' *> strP
|
||||
replicaKey <- A.char ':' *> strP
|
||||
digest <- optional (A.char ':' *> strP)
|
||||
chunkSize <- optional (A.char ':' *> strP)
|
||||
pure FileServerReplica {chunkNo, server, replicaId, replicaKey, digest, chunkSize}
|
||||
|
||||
unfoldChunksToReplicas :: FileSize Word32 -> [FileChunk] -> [FileServerReplica]
|
||||
unfoldChunksToReplicas defChunkSize = concatMap chunkReplicas
|
||||
where
|
||||
chunkReplicas c@FileChunk {replicas} = zipWith (replicaToServerReplica c) [1 ..] replicas
|
||||
replicaToServerReplica :: FileChunk -> Int -> FileChunkReplica -> FileServerReplica
|
||||
replicaToServerReplica FileChunk {chunkNo, digest, chunkSize} replicaNo FileChunkReplica {server, replicaId, replicaKey} =
|
||||
let chunkSize' = if chunkSize /= defChunkSize && replicaNo == 1 then Just chunkSize else Nothing
|
||||
digest' = if replicaNo == 1 then Just digest else Nothing
|
||||
in FileServerReplica {chunkNo, server, replicaId, replicaKey, digest = digest', chunkSize = chunkSize'}
|
||||
|
||||
decodeFileDescription :: YAMLFileDescription -> Either String AFileDescription
|
||||
decodeFileDescription YAMLFileDescription {party, size, digest, key, nonce, chunkSize, replicas} = do
|
||||
size' <- strDecode $ B.pack size
|
||||
chunkSize' <- strDecode $ B.pack chunkSize
|
||||
replicas' <- decodeFileParts replicas
|
||||
chunks <- foldReplicasToChunks chunkSize' replicas'
|
||||
pure $ case aFileParty party of
|
||||
AFP party' -> AFD FileDescription {party = party', size = size', digest, key, nonce, chunkSize = chunkSize', chunks}
|
||||
where
|
||||
decodeFileParts = fmap concat . mapM decodeYAMLServerReplicas
|
||||
|
||||
decodeYAMLServerReplicas :: YAMLServerReplicas -> Either String [FileServerReplica]
|
||||
decodeYAMLServerReplicas YAMLServerReplicas {server, chunks} =
|
||||
mapM (parseAll (serverReplicaP server) . B.pack) chunks
|
||||
|
||||
-- this function should fail if:
|
||||
-- 1. no replica has digest or two replicas have different digests
|
||||
-- 2. two replicas have different chunk sizes
|
||||
foldReplicasToChunks :: FileSize Word32 -> [FileServerReplica] -> Either String [FileChunk]
|
||||
foldReplicasToChunks defChunkSize fs = do
|
||||
sd <- foldSizesDigests fs
|
||||
-- TODO validate (check that chunks match) or in separate function
|
||||
sortOn (chunkNo :: FileChunk -> Int) . map reverseReplicas . M.elems <$> foldChunks sd fs
|
||||
where
|
||||
foldSizesDigests :: [FileServerReplica] -> Either String (Map Int (FileSize Word32), Map Int FileDigest)
|
||||
foldSizesDigests = foldl' addSizeDigest $ Right (M.empty, M.empty)
|
||||
addSizeDigest :: Either String (Map Int (FileSize Word32), Map Int FileDigest) -> FileServerReplica -> Either String (Map Int (FileSize Word32), Map Int FileDigest)
|
||||
addSizeDigest (Left e) _ = Left e
|
||||
addSizeDigest (Right (ms, md)) FileServerReplica {chunkNo, chunkSize, digest} =
|
||||
(,) <$> combineChunk ms chunkNo chunkSize <*> combineChunk md chunkNo digest
|
||||
combineChunk :: Eq a => Map Int a -> Int -> Maybe a -> Either String (Map Int a)
|
||||
combineChunk m _ Nothing = Right m
|
||||
combineChunk m chunkNo (Just value) = case M.lookup chunkNo m of
|
||||
Nothing -> Right $ M.insert chunkNo value m
|
||||
Just v -> if v == value then Right m else Left "different size or digest in chunk replicas"
|
||||
foldChunks :: (Map Int (FileSize Word32), Map Int FileDigest) -> [FileServerReplica] -> Either String (Map Int FileChunk)
|
||||
foldChunks sd = foldl' (addReplica sd) (Right M.empty)
|
||||
addReplica :: (Map Int (FileSize Word32), Map Int FileDigest) -> Either String (Map Int FileChunk) -> FileServerReplica -> Either String (Map Int FileChunk)
|
||||
addReplica _ (Left e) _ = Left e
|
||||
addReplica (ms, md) (Right cs) FileServerReplica {chunkNo, server, replicaId, replicaKey} = do
|
||||
case M.lookup chunkNo cs of
|
||||
Just chunk@FileChunk {replicas} ->
|
||||
let replica = FileChunkReplica {server, replicaId, replicaKey}
|
||||
in Right $ M.insert chunkNo ((chunk :: FileChunk) {replicas = replica : replicas}) cs
|
||||
_ -> do
|
||||
case M.lookup chunkNo md of
|
||||
Just digest' ->
|
||||
let replica = FileChunkReplica {server, replicaId, replicaKey}
|
||||
chunkSize' = fromMaybe defChunkSize $ M.lookup chunkNo ms
|
||||
chunk = FileChunk {chunkNo, digest = digest', chunkSize = chunkSize', replicas = [replica]}
|
||||
in Right $ M.insert chunkNo chunk cs
|
||||
_ -> Left "no digest for chunk"
|
||||
reverseReplicas c@FileChunk {replicas} = (c :: FileChunk) {replicas = reverse replicas}
|
||||