mirror of
https://github.com/signalapp/Signal-Server.git
synced 2025-12-05 01:10:13 +00:00
Compare commits
1986 Commits
v8.94.0
...
aa2f9e5a65
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa2f9e5a65 | ||
|
|
f13837d2f2 | ||
|
|
3ff2af47cb | ||
|
|
c719da3527 | ||
|
|
0ea52b785e | ||
|
|
1ce1c298d3 | ||
|
|
4d5cc4dc22 | ||
|
|
640274108e | ||
|
|
dd17ddc98c | ||
|
|
4c4a954c1c | ||
|
|
65ce9af366 | ||
|
|
4af0de2ab2 | ||
|
|
ccf72a45db | ||
|
|
f3744fbcb1 | ||
|
|
ec08731e6d | ||
|
|
852591df40 | ||
|
|
9ff9b3a7b3 | ||
|
|
dd4e058cd7 | ||
|
|
85226bdd87 | ||
|
|
faa74469ea | ||
|
|
bb94975d74 | ||
|
|
298b0d8d28 | ||
|
|
9da643dd69 | ||
|
|
6dc4bfe5fa | ||
|
|
23a3e32eb8 | ||
|
|
bf6939ec00 | ||
|
|
3b310463c3 | ||
|
|
f7eb6fab33 | ||
|
|
ce945ff245 | ||
|
|
4dbd564442 | ||
|
|
24f8f48a26 | ||
|
|
a2ce37fd53 | ||
|
|
c4d55e099e | ||
|
|
0f950917d8 | ||
|
|
3116913378 | ||
|
|
d7a9e3c9f3 | ||
|
|
14010a4f83 | ||
|
|
ad0bcd5436 | ||
|
|
88d458cf79 | ||
|
|
342c8a1b28 | ||
|
|
ad2500d4fd | ||
|
|
c2ebabad58 | ||
|
|
1105753aab | ||
|
|
9378b9a6e6 | ||
|
|
c68e3103c4 | ||
|
|
c9760f4c38 | ||
|
|
73765fc4ec | ||
|
|
9e1b716548 | ||
|
|
a2f2fc93b0 | ||
|
|
9751569dc7 | ||
|
|
d6c15ef1d5 | ||
|
|
e0eaa76ebf | ||
|
|
0d4d9c0af5 | ||
|
|
4ffd768aac | ||
|
|
b8a720c5b4 | ||
|
|
ab1ec86cd2 | ||
|
|
e9d6b91416 | ||
|
|
63f892add1 | ||
|
|
6e42b2898c | ||
|
|
9384813752 | ||
|
|
70ac4ad139 | ||
|
|
4ab58e950b | ||
|
|
7f301cbf95 | ||
|
|
5a7660d3ae | ||
|
|
850172f6a3 | ||
|
|
4e73162055 | ||
|
|
4127cf90ab | ||
|
|
08cc7bc462 | ||
|
|
2cf2391f38 | ||
|
|
007dde8d45 | ||
|
|
f80e30f9f2 | ||
|
|
ff5201a58e | ||
|
|
8954708d77 | ||
|
|
3cf194e476 | ||
|
|
bad2602491 | ||
|
|
a647c1bfdf | ||
|
|
1bba30a81e | ||
|
|
827d8388d0 | ||
|
|
70efb19a86 | ||
|
|
35ffb208e3 | ||
|
|
e50dcd185d | ||
|
|
1cd446ee31 | ||
|
|
1847c6d4ca | ||
|
|
1770558d5e | ||
|
|
e0d39212ec | ||
|
|
31ebe9071a | ||
|
|
8f65486fab | ||
|
|
7e3540bda0 | ||
|
|
c544628dfe | ||
|
|
61b162d0a1 | ||
|
|
8c2d738924 | ||
|
|
efde8a31f9 | ||
|
|
b2dd315177 | ||
|
|
dc3920a99c | ||
|
|
b8e8fd3313 | ||
|
|
a5423b6e21 | ||
|
|
89b37015c6 | ||
|
|
2af3088571 | ||
|
|
774cc52b61 | ||
|
|
f52a262741 | ||
|
|
cd957e0347 | ||
|
|
8060d74899 | ||
|
|
0e1e248564 | ||
|
|
d33761f107 | ||
|
|
89c7521be0 | ||
|
|
8e429e267f | ||
|
|
8fe87b77e4 | ||
|
|
194e43926a | ||
|
|
4c0281f540 | ||
|
|
75391785f8 | ||
|
|
ebdc5a30f8 | ||
|
|
f57093a94a | ||
|
|
53f9c7b31f | ||
|
|
bc20aee7c9 | ||
|
|
807e03ca2b | ||
|
|
a747afb487 | ||
|
|
eb42d22155 | ||
|
|
8825396fc1 | ||
|
|
f616612104 | ||
|
|
a8c6fa93e0 | ||
|
|
32cf12e9af | ||
|
|
1cc933b6bc | ||
|
|
dbbd913445 | ||
|
|
50e7301dd8 | ||
|
|
552edbc1eb | ||
|
|
7f5ea6608c | ||
|
|
78a7112675 | ||
|
|
be8b44d645 | ||
|
|
7ca3604601 | ||
|
|
c5af8f3a9e | ||
|
|
d67847e5b1 | ||
|
|
3a5704a5cc | ||
|
|
50a54136e8 | ||
|
|
a1d9c4c062 | ||
|
|
4acb3b5ac7 | ||
|
|
b76eaa1098 | ||
|
|
c883cd8148 | ||
|
|
1e77d0471e | ||
|
|
a94ce72894 | ||
|
|
0f2a4d02e0 | ||
|
|
4d521cea42 | ||
|
|
7d10209198 | ||
|
|
fc8d180f7c | ||
|
|
470e17963a | ||
|
|
4c5dc118aa | ||
|
|
ad84cd848a | ||
|
|
127bd56f73 | ||
|
|
ad3721acf1 | ||
|
|
085127326b | ||
|
|
2e32ab3282 | ||
|
|
8bb125597b | ||
|
|
8ca7f85bf0 | ||
|
|
9d4d36939c | ||
|
|
dabe1c157b | ||
|
|
36d0c4422e | ||
|
|
1429efd573 | ||
|
|
93c03c5676 | ||
|
|
bd1bd007c0 | ||
|
|
f485406c4d | ||
|
|
d96c360016 | ||
|
|
6d71f43b6c | ||
|
|
d6824fcaf6 | ||
|
|
61f388f5f4 | ||
|
|
6955e1ee20 | ||
|
|
7bb4a4bc90 | ||
|
|
6fa01a5d48 | ||
|
|
5a65a46fc1 | ||
|
|
c255355e5c | ||
|
|
8d3781db90 | ||
|
|
f6a87b2ec0 | ||
|
|
e8a1854c5e | ||
|
|
f8d27d8fab | ||
|
|
94dd2712b1 | ||
|
|
a4804f6501 | ||
|
|
5cb3a053fb | ||
|
|
4923b6da68 | ||
|
|
b7e64e09a3 | ||
|
|
76e65a47a2 | ||
|
|
ef644ce319 | ||
|
|
08c4179a7a | ||
|
|
5c3be9c3d6 | ||
|
|
8fc0b49994 | ||
|
|
db4c71368c | ||
|
|
30774bbc40 | ||
|
|
3b48c82c72 | ||
|
|
038c68c594 | ||
|
|
cf222e1105 | ||
|
|
541c87e262 | ||
|
|
5f5c345f94 | ||
|
|
8aa408a3c1 | ||
|
|
4f0337021c | ||
|
|
00062fdd5c | ||
|
|
2bc91c1f21 | ||
|
|
c92a29db1e | ||
|
|
37d67f110a | ||
|
|
bf779f30ab | ||
|
|
4d81124dfa | ||
|
|
ccf8840fa3 | ||
|
|
267aafe861 | ||
|
|
1bdcfb1d83 | ||
|
|
0c69ef9381 | ||
|
|
c99b1cada1 | ||
|
|
83d19ac8ed | ||
|
|
5f77d7f582 | ||
|
|
96f6e75702 | ||
|
|
74c7e49cea | ||
|
|
876bf15a11 | ||
|
|
de60752219 | ||
|
|
5d80ac73da | ||
|
|
0ff32d5cae | ||
|
|
4618b47141 | ||
|
|
94361b2d5d | ||
|
|
d4429ebce1 | ||
|
|
b5711ead25 | ||
|
|
522ddd4e61 | ||
|
|
73365369df | ||
|
|
631b9a5290 | ||
|
|
4ccd39fd55 | ||
|
|
50bc6b2c62 | ||
|
|
609b86acb9 | ||
|
|
1ea84483da | ||
|
|
ed5086823c | ||
|
|
73748a6341 | ||
|
|
1f60300555 | ||
|
|
85a13a9dc0 | ||
|
|
803e73bd1e | ||
|
|
a3356d0188 | ||
|
|
5c21aa2ad4 | ||
|
|
6116830da9 | ||
|
|
650bc2598b | ||
|
|
a36fba061a | ||
|
|
e62b3d390f | ||
|
|
1a8ebf80b5 | ||
|
|
2564f706d8 | ||
|
|
c97c00bf5f | ||
|
|
702b125a48 | ||
|
|
ca9f29f984 | ||
|
|
58b9fa100d | ||
|
|
656b08f3b6 | ||
|
|
3f62677176 | ||
|
|
ae2d98750c | ||
|
|
7d41c1219b | ||
|
|
65e1f1b3a9 | ||
|
|
437b823c84 | ||
|
|
c9f21d5970 | ||
|
|
80c11e7eda | ||
|
|
0745cabc87 | ||
|
|
3e80669f4e | ||
|
|
b81cd9ec61 | ||
|
|
da6ed94443 | ||
|
|
96d41b3716 | ||
|
|
7dddc4d759 | ||
|
|
a87690d817 | ||
|
|
18ef3da261 | ||
|
|
f4698dd5b2 | ||
|
|
d4322a2ed4 | ||
|
|
7260a9d5b4 | ||
|
|
12b4ceb4aa | ||
|
|
fa1cd5c263 | ||
|
|
f8da13912d | ||
|
|
a3b3bf86ba | ||
|
|
a99f7bb87d | ||
|
|
d6f14d02dd | ||
|
|
d18671eaf9 | ||
|
|
87c30d00e8 | ||
|
|
c8f45685b8 | ||
|
|
bb90d80d22 | ||
|
|
dcc541f86e | ||
|
|
aaa36fd8f5 | ||
|
|
2bb14892af | ||
|
|
6d8701665e | ||
|
|
c2b8fdac0d | ||
|
|
059caa4c57 | ||
|
|
51773f5709 | ||
|
|
483404a67f | ||
|
|
68b84dd56b | ||
|
|
7709e1313c | ||
|
|
c952baa672 | ||
|
|
9dfe51eac4 | ||
|
|
5de848bf38 | ||
|
|
295cedc075 | ||
|
|
4f1cab407f | ||
|
|
626a7fdad7 | ||
|
|
9a1da23bdb | ||
|
|
4ffd164461 | ||
|
|
904cc63a72 | ||
|
|
177c36b0d6 | ||
|
|
5fc6bdd478 | ||
|
|
ca6e5fb0a8 | ||
|
|
1a7a446150 | ||
|
|
981d929f50 | ||
|
|
4a3eb642c0 | ||
|
|
a1b0c1a4aa | ||
|
|
0f185a528d | ||
|
|
aef7f3fef8 | ||
|
|
1767586797 | ||
|
|
60be6de9af | ||
|
|
2a7551cca5 | ||
|
|
36439b5252 | ||
|
|
bbee80dbd0 | ||
|
|
a7ea42adc3 | ||
|
|
4dc3b19d2a | ||
|
|
030d8e8dd4 | ||
|
|
401165d0d6 | ||
|
|
ccb209ad37 | ||
|
|
c1a66e0418 | ||
|
|
8491d18413 | ||
|
|
9b835633ab | ||
|
|
fbbc4b8b27 | ||
|
|
74ee1c8c4f | ||
|
|
35604cf151 | ||
|
|
aafcd63a9f | ||
|
|
43a534f05b | ||
|
|
9ec66dac7f | ||
|
|
13fc0ffbca | ||
|
|
93ba6616d1 | ||
|
|
a4b98f38a6 | ||
|
|
b95d08aaea | ||
|
|
b400d49e77 | ||
|
|
e43487155f | ||
|
|
dee3723d97 | ||
|
|
b7e986f43c | ||
|
|
664fb23e97 | ||
|
|
714ef128a1 | ||
|
|
7cf3fce624 | ||
|
|
0cc5431867 | ||
|
|
b8d5b2c8ea | ||
|
|
894ca6d290 | ||
|
|
847b25f695 | ||
|
|
703a05cb15 | ||
|
|
30c194c557 | ||
|
|
cc7b030a41 | ||
|
|
7a91c4d5b7 | ||
|
|
287da6e7e3 | ||
|
|
7cf89764e7 | ||
|
|
d316c72beb | ||
|
|
82d187cc45 | ||
|
|
0c240d21d2 | ||
|
|
009252c831 | ||
|
|
0c1146aaa5 | ||
|
|
4fd06594a0 | ||
|
|
4e175be88f | ||
|
|
771a700acd | ||
|
|
e9bd5da2c3 | ||
|
|
f64244f33a | ||
|
|
ed1417c3e3 | ||
|
|
0398e02690 | ||
|
|
e285bf1a52 | ||
|
|
2c9219d4f7 | ||
|
|
26b3b75054 | ||
|
|
cdb651b68f | ||
|
|
91a36f4421 | ||
|
|
21c1d71551 | ||
|
|
38befdb260 | ||
|
|
63c79173b2 | ||
|
|
d2ad003891 | ||
|
|
eb89773819 | ||
|
|
403abd84f6 | ||
|
|
f62f79c95c | ||
|
|
144c4c9223 | ||
|
|
ab4fc4f459 | ||
|
|
51569ce0a5 | ||
|
|
f191c68efc | ||
|
|
bb8ce6d981 | ||
|
|
e0ee75e0d0 | ||
|
|
1ef3a230a1 | ||
|
|
b1805d4bf1 | ||
|
|
cac979c7fd | ||
|
|
4072dcdda5 | ||
|
|
ed382fff6d | ||
|
|
23bb8277d5 | ||
|
|
8099d6465c | ||
|
|
28a0b9e84e | ||
|
|
9287aaf7ce | ||
|
|
0585f862cb | ||
|
|
7cac6f6f72 | ||
|
|
57be4d798b | ||
|
|
05c74f1997 | ||
|
|
f5e49b6db7 | ||
|
|
3c40e72d27 | ||
|
|
2f2ae7cec5 | ||
|
|
b236b53dc3 | ||
|
|
eb71e30046 | ||
|
|
aa5fd52302 | ||
|
|
d6bc2765b6 | ||
|
|
01258de560 | ||
|
|
3af2cc5c70 | ||
|
|
2278842531 | ||
|
|
60ab00ecc6 | ||
|
|
1fb6d23500 | ||
|
|
8d8a2a5583 | ||
|
|
caa81b4885 | ||
|
|
37c4a0451a | ||
|
|
11df8fcc6c | ||
|
|
5a7f4d8381 | ||
|
|
1f1e4c72ec | ||
|
|
02a5a6b55f | ||
|
|
58ad647d29 | ||
|
|
099073356c | ||
|
|
37038c4a63 | ||
|
|
ffa98e5b34 | ||
|
|
6013d00654 | ||
|
|
c03d63acb8 | ||
|
|
c6689ca07a | ||
|
|
1d0e2d29a7 | ||
|
|
d83d826236 | ||
|
|
2efe687b4b | ||
|
|
7cabc8f328 | ||
|
|
b57bdcaaea | ||
|
|
041643783d | ||
|
|
ed75ef917a | ||
|
|
7ea0885474 | ||
|
|
488e7c4913 | ||
|
|
6af4d41322 | ||
|
|
2b07a21477 | ||
|
|
4a42ff562d | ||
|
|
d4031893cc | ||
|
|
c2e3ab832c | ||
|
|
8c2f3c839f | ||
|
|
df188e6f15 | ||
|
|
12576daf1f | ||
|
|
46aefc0cbe | ||
|
|
9d4f9b4c12 | ||
|
|
3591e6bebd | ||
|
|
e12ba6b15b | ||
|
|
744b05244d | ||
|
|
c22b8fafa6 | ||
|
|
2194cf46e1 | ||
|
|
1a46ac122a | ||
|
|
55f85a81c6 | ||
|
|
5a8cb8a312 | ||
|
|
d641d1fc39 | ||
|
|
df56c65b54 | ||
|
|
1346fcb59e | ||
|
|
50f681ffe8 | ||
|
|
faef614d80 | ||
|
|
db2cd20dcb | ||
|
|
9ef6024291 | ||
|
|
a643a6c0f0 | ||
|
|
82e21b0c21 | ||
|
|
50e298a4f4 | ||
|
|
a0b9c40f6c | ||
|
|
359cf02161 | ||
|
|
6a1f01f876 | ||
|
|
0595acc48f | ||
|
|
46ff8d51dc | ||
|
|
dcd80e11f4 | ||
|
|
e1b1c7db8d | ||
|
|
99041bc593 | ||
|
|
dbd14481ed | ||
|
|
e3160bc717 | ||
|
|
6798958650 | ||
|
|
3a90c572b4 | ||
|
|
eab3c36d83 | ||
|
|
b7fee7b426 | ||
|
|
469955aec9 | ||
|
|
d1c9dff2c5 | ||
|
|
9491ebbe90 | ||
|
|
09b50383d7 | ||
|
|
8517eef3fe | ||
|
|
8955e31a1e | ||
|
|
f7a3971c64 | ||
|
|
552079d3c2 | ||
|
|
59d984e25d | ||
|
|
d17b9322b7 | ||
|
|
12bc175776 | ||
|
|
376458efa8 | ||
|
|
886984861f | ||
|
|
b248b6bc12 | ||
|
|
8d0d0d61f1 | ||
|
|
26c348520f | ||
|
|
ec79386306 | ||
|
|
093ac6fb16 | ||
|
|
68e2c511b7 | ||
|
|
47c82b42d9 | ||
|
|
e1a3b48c6e | ||
|
|
b8b17ae473 | ||
|
|
b203344ed4 | ||
|
|
6d30a45017 | ||
|
|
3f9863c441 | ||
|
|
47294ef6b8 | ||
|
|
2356d7c629 | ||
|
|
4908a0aa9e | ||
|
|
6032764052 | ||
|
|
908a41814b | ||
|
|
3ae145bd60 | ||
|
|
b086a73353 | ||
|
|
794e254d90 | ||
|
|
760c5737f9 | ||
|
|
ea17eee320 | ||
|
|
09ce79bd43 | ||
|
|
2dfd17af4a | ||
|
|
a9975e524b | ||
|
|
5d062285c2 | ||
|
|
e4b0f3ced5 | ||
|
|
6545bb9edb | ||
|
|
70ce6eff9e | ||
|
|
c84d96abee | ||
|
|
09eb42e5c6 | ||
|
|
06388b514c | ||
|
|
7c17a4067c | ||
|
|
48ada8e8ca | ||
|
|
282bcf6f34 | ||
|
|
1446d1acf8 | ||
|
|
aae94ffae3 | ||
|
|
ebd906a45d | ||
|
|
e30beb9c9f | ||
|
|
5a2e297991 | ||
|
|
ae1e7fbaa0 | ||
|
|
7e616a4056 | ||
|
|
c9e192564c | ||
|
|
47550d48e7 | ||
|
|
5cc76f48aa | ||
|
|
3ceaa8bd20 | ||
|
|
790b9bbf01 | ||
|
|
ee1016523f | ||
|
|
2f51778421 | ||
|
|
d3d68c2a60 | ||
|
|
0628b3e41c | ||
|
|
eac183495a | ||
|
|
0ae02da9be | ||
|
|
90fe634ddd | ||
|
|
1cae841ed6 | ||
|
|
a88560e557 | ||
|
|
3ca9a66323 | ||
|
|
3a4a55c245 | ||
|
|
f4a243861c | ||
|
|
68209f270e | ||
|
|
8af939d320 | ||
|
|
16a50935ea | ||
|
|
24ea10c451 | ||
|
|
95abda4870 | ||
|
|
c8993c4da8 | ||
|
|
d096798340 | ||
|
|
f2f5e0e26f | ||
|
|
9121032114 | ||
|
|
541bf968e5 | ||
|
|
8c3ebdcbab | ||
|
|
0593e9e89f | ||
|
|
0d412c88fd | ||
|
|
8280106493 | ||
|
|
a3e106fe04 | ||
|
|
981a04f33b | ||
|
|
26025e5abd | ||
|
|
4839a5ba70 | ||
|
|
85a1550485 | ||
|
|
77658415b2 | ||
|
|
68f27be7cd | ||
|
|
6460327372 | ||
|
|
a96c0ec7a3 | ||
|
|
33c0a27b85 | ||
|
|
d5b39cd496 | ||
|
|
5a35d69ed0 | ||
|
|
e1e94a33e2 | ||
|
|
18c9b177f3 | ||
|
|
1970741049 | ||
|
|
f16428ce2a | ||
|
|
14427523ae | ||
|
|
236b0496d3 | ||
|
|
f68ddf66e9 | ||
|
|
20685b6d69 | ||
|
|
4988b4e0f5 | ||
|
|
651e444875 | ||
|
|
2093fed554 | ||
|
|
cc7bb8b549 | ||
|
|
a99ac14c6a | ||
|
|
2c163352c3 | ||
|
|
fb6c4eca34 | ||
|
|
142e2cbe9d | ||
|
|
c70dd119d3 | ||
|
|
9e312cbdfa | ||
|
|
4d87b741cd | ||
|
|
d08bc4c413 | ||
|
|
f5d3d1e65d | ||
|
|
916314233f | ||
|
|
c7e6ee7297 | ||
|
|
557a6ecd4f | ||
|
|
0e04cac800 | ||
|
|
1db9258d39 | ||
|
|
2803c2acdb | ||
|
|
8be43566a4 | ||
|
|
d865cec2a4 | ||
|
|
6f0370a073 | ||
|
|
5b9f8177f2 | ||
|
|
6967e4e54b | ||
|
|
96fb0ac3ae | ||
|
|
7201938793 | ||
|
|
cdd6f78c73 | ||
|
|
ab94d3045d | ||
|
|
ff4e2bdfb7 | ||
|
|
ffed19d198 | ||
|
|
49d6a5e32d | ||
|
|
3ba7ba4f92 | ||
|
|
43ffc996db | ||
|
|
27f5f94c60 | ||
|
|
1d9734c824 | ||
|
|
717fb57a14 | ||
|
|
af1d21c225 | ||
|
|
3c8b2a82a3 | ||
|
|
0cb6f662c6 | ||
|
|
739ed56b4c | ||
|
|
9a19ef82fd | ||
|
|
5627ed141b | ||
|
|
e4a2af67b1 | ||
|
|
cba56f3263 | ||
|
|
1c3cf39b8a | ||
|
|
6421438f64 | ||
|
|
8a63682c16 | ||
|
|
13a8c6256d | ||
|
|
8c9cc4cce5 | ||
|
|
0023cb2521 | ||
|
|
010ed77345 | ||
|
|
00c11f5dd0 | ||
|
|
637e424506 | ||
|
|
eb55b80bdc | ||
|
|
73812b06be | ||
|
|
d135957f0d | ||
|
|
815fd44ab3 | ||
|
|
4e8a48ab3d | ||
|
|
ea75c39b58 | ||
|
|
085f013bf9 | ||
|
|
c6b8d890e5 | ||
|
|
66783c9381 | ||
|
|
b32e67ff9e | ||
|
|
ba55d6caeb | ||
|
|
1eba04d37b | ||
|
|
ecbb2f1399 | ||
|
|
93f4a91ebf | ||
|
|
a1ac5bd74c | ||
|
|
0ca9e973ad | ||
|
|
ee5df0e11c | ||
|
|
d53a6e4c42 | ||
|
|
744042e8c8 | ||
|
|
444f6ca826 | ||
|
|
0ea13ec528 | ||
|
|
6845ba9b90 | ||
|
|
2f890f7bb3 | ||
|
|
3fefb24d71 | ||
|
|
d6f890c7b9 | ||
|
|
59e0137816 | ||
|
|
dd4bc23e4f | ||
|
|
09fd5e8819 | ||
|
|
81f3ba17c7 | ||
|
|
3288d3d538 | ||
|
|
7e861f388f | ||
|
|
1323b42169 | ||
|
|
a843f1af6c | ||
|
|
52b759c009 | ||
|
|
f2f5815316 | ||
|
|
2da00e162a | ||
|
|
e53a9f3f1a | ||
|
|
767f6a90e0 | ||
|
|
562b495a18 | ||
|
|
084607f359 | ||
|
|
eeeb565313 | ||
|
|
1fa31b3974 | ||
|
|
7158a504fa | ||
|
|
3ccd7508ac | ||
|
|
fc6075f19c | ||
|
|
776c147ea4 | ||
|
|
5d9641ae86 | ||
|
|
88a1f951c2 | ||
|
|
1726a1d5f4 | ||
|
|
6c563a3f13 | ||
|
|
6a1f4906c5 | ||
|
|
ef716aacc2 | ||
|
|
c91242ed60 | ||
|
|
e536a40740 | ||
|
|
5aaf4cad20 | ||
|
|
1c167ec150 | ||
|
|
9898e18ae2 | ||
|
|
b182c3d86d | ||
|
|
96a4d4c8ac | ||
|
|
9d19fc9ecc | ||
|
|
aad12670b2 | ||
|
|
7ca7fe7c13 | ||
|
|
ca7e7c288e | ||
|
|
3a604464b5 | ||
|
|
e277281d18 | ||
|
|
3d339696dc | ||
|
|
3e36a49142 | ||
|
|
d8f53954d0 | ||
|
|
8c984cbf42 | ||
|
|
60cdcf5f0c | ||
|
|
5afcd634b6 | ||
|
|
00ca58ec13 | ||
|
|
bbb6d448db | ||
|
|
45fad7a6a9 | ||
|
|
7633a9b07a | ||
|
|
00d0dba62c | ||
|
|
c9a396b9e3 | ||
|
|
fc0a7b7657 | ||
|
|
c1e870d8f5 | ||
|
|
190f2a7fc2 | ||
|
|
ce0ccf4fd0 | ||
|
|
0018e0bec6 | ||
|
|
63021e0ca3 | ||
|
|
bf741df38e | ||
|
|
e627d4e2c4 | ||
|
|
9b5a62e60f | ||
|
|
3bb1eab48c | ||
|
|
4af576668c | ||
|
|
f5a93574f6 | ||
|
|
74f8889bfa | ||
|
|
a8da0f64ac | ||
|
|
44c3b046dd | ||
|
|
f2cb04817b | ||
|
|
a5f60b1522 | ||
|
|
0e3dccd9f6 | ||
|
|
b21b50873f | ||
|
|
d335b7a033 | ||
|
|
f3b22e04e8 | ||
|
|
712f3affd9 | ||
|
|
89292e238b | ||
|
|
3287085ef9 | ||
|
|
17dfd914d5 | ||
|
|
c65fe49983 | ||
|
|
6552d90dc9 | ||
|
|
d925e8af9e | ||
|
|
2c0fc43137 | ||
|
|
ccdbec088f | ||
|
|
9822d17ab9 | ||
|
|
9573d9e385 | ||
|
|
155f3d6231 | ||
|
|
1959ca2d96 | ||
|
|
324913d2da | ||
|
|
5c4cafcb6f | ||
|
|
1ea8d69b40 | ||
|
|
013e45596e | ||
|
|
3fdb691702 | ||
|
|
997129871c | ||
|
|
39b1935350 | ||
|
|
dbb9a8dcf6 | ||
|
|
cacd4afbbb | ||
|
|
9c5877aa31 | ||
|
|
bda4788a34 | ||
|
|
5abfef50fc | ||
|
|
e3ee5c1f2e | ||
|
|
e6eb702a88 | ||
|
|
1447819198 | ||
|
|
adf5795dff | ||
|
|
584fd06b88 | ||
|
|
1faa1a5abc | ||
|
|
b5db8eba06 | ||
|
|
c6843c1eae | ||
|
|
93b7fd589e | ||
|
|
eb80305f87 | ||
|
|
865e3c5bde | ||
|
|
c2270e57df | ||
|
|
d48c031548 | ||
|
|
830a07012b | ||
|
|
46227295ff | ||
|
|
73fb1fc2ed | ||
|
|
7ff48155d6 | ||
|
|
0adaa331a1 | ||
|
|
30ec06ca76 | ||
|
|
9b5c6e538b | ||
|
|
240a406964 | ||
|
|
38d25f9a9b | ||
|
|
7b1b6fa1cf | ||
|
|
a3d9af132f | ||
|
|
5d8b566a27 | ||
|
|
8c30a359e7 | ||
|
|
087c2b61ee | ||
|
|
a9117010f9 | ||
|
|
9d980f36b0 | ||
|
|
93515e5a0f | ||
|
|
d550c69f7f | ||
|
|
087e192fac | ||
|
|
63e45563ec | ||
|
|
961d6d0a5c | ||
|
|
f7aacefc40 | ||
|
|
42e920cd5c | ||
|
|
02ff3f2ff4 | ||
|
|
ca2845bcb0 | ||
|
|
80cd5d9ccc | ||
|
|
0eb7db8de5 | ||
|
|
100955a7db | ||
|
|
68814813c3 | ||
|
|
39590f1b28 | ||
|
|
92698efd39 | ||
|
|
b693cb98d0 | ||
|
|
3ed142d0a9 | ||
|
|
581e61a85b | ||
|
|
764b200289 | ||
|
|
26503dffdf | ||
|
|
b284e95394 | ||
|
|
b2211de8d8 | ||
|
|
1bb0eb0e70 | ||
|
|
0a1161048f | ||
|
|
7a6ce00fed | ||
|
|
c0aa9ced8d | ||
|
|
6b36df3f8f | ||
|
|
df3caeb04a | ||
|
|
0e267509da | ||
|
|
bbe41278ed | ||
|
|
d91a6b0c38 | ||
|
|
694a93db6d | ||
|
|
2f2dec87b1 | ||
|
|
098288c290 | ||
|
|
ab2e6bb9a3 | ||
|
|
513f19370a | ||
|
|
e20a4c1f77 | ||
|
|
946a486c4b | ||
|
|
78b40397f9 | ||
|
|
48e8d1c12f | ||
|
|
b115e95da4 | ||
|
|
ab0892cc41 | ||
|
|
e25291c74c | ||
|
|
0e552bd602 | ||
|
|
e9b3e15556 | ||
|
|
237d0fd4e2 | ||
|
|
50bd30fb1f | ||
|
|
9fb4e2d272 | ||
|
|
3e51366921 | ||
|
|
2d184b1ab6 | ||
|
|
befcdf55fe | ||
|
|
ba12d39121 | ||
|
|
bf0f553ced | ||
|
|
cd68a674bb | ||
|
|
315fc00eac | ||
|
|
6142dcc7e6 | ||
|
|
c47141ffda | ||
|
|
6c8566db60 | ||
|
|
aa60fae3b1 | ||
|
|
8cb9c60a3c | ||
|
|
dd7a20a774 | ||
|
|
374fe087bc | ||
|
|
11691c3122 | ||
|
|
8d129b10ca | ||
|
|
d6e03f50b9 | ||
|
|
f60c9f2a15 | ||
|
|
1c617284f3 | ||
|
|
5bc6ff0e77 | ||
|
|
020c21f4ef | ||
|
|
bd57c1c7e7 | ||
|
|
f4b94a7a89 | ||
|
|
b666b66160 | ||
|
|
8bafb1a641 | ||
|
|
42d4574213 | ||
|
|
9ef6f8aec9 | ||
|
|
556eec649d | ||
|
|
e160025cfc | ||
|
|
0602149c52 | ||
|
|
ad17c6e40d | ||
|
|
b95a766888 | ||
|
|
a0770db179 | ||
|
|
0601f6a35c | ||
|
|
d6acfa56c2 | ||
|
|
11601fd091 | ||
|
|
d78c8370b6 | ||
|
|
0b752409d5 | ||
|
|
46e0f5da74 | ||
|
|
d835a2a450 | ||
|
|
f09cc03164 | ||
|
|
4c628b1cd9 | ||
|
|
5122271750 | ||
|
|
7df978390f | ||
|
|
564dba3053 | ||
|
|
3b4d445ca8 | ||
|
|
176a15dace | ||
|
|
9249cf240e | ||
|
|
a049eda7e6 | ||
|
|
9eafa118d5 | ||
|
|
8699d94de6 | ||
|
|
4c0a5ac3b2 | ||
|
|
fa51793379 | ||
|
|
8b99df3169 | ||
|
|
7cbbf73cc9 | ||
|
|
0b1ec1e50b | ||
|
|
2744d33ef8 | ||
|
|
659ac2c107 | ||
|
|
5892dc71fa | ||
|
|
fc3e547dce | ||
|
|
e4f9f949f0 | ||
|
|
7605462d48 | ||
|
|
fd10b9723d | ||
|
|
6cdfb7ab63 | ||
|
|
e5fdab1bc8 | ||
|
|
2aa1eee29d | ||
|
|
97e566d470 | ||
|
|
a8eaf2d0ad | ||
|
|
b5f9564e13 | ||
|
|
7e353f8ea0 | ||
|
|
0075e94a42 | ||
|
|
1ea9e38fea | ||
|
|
3b405a53d0 | ||
|
|
84c329e911 | ||
|
|
4349ceaf0e | ||
|
|
acdf37561f | ||
|
|
9128d4cc49 | ||
|
|
206e97d374 | ||
|
|
f682af2fe0 | ||
|
|
ecf7e60d98 | ||
|
|
68ddc070ca | ||
|
|
d661da8d7e | ||
|
|
5d2e8cb000 | ||
|
|
096bb8e6e5 | ||
|
|
1af8bb494e | ||
|
|
46d04d9d1a | ||
|
|
c0ca4ffbcc | ||
|
|
8720b6db95 | ||
|
|
8c61d45206 | ||
|
|
0e4625ef88 | ||
|
|
10d559bbb5 | ||
|
|
65b2892de5 | ||
|
|
6fa6c3c81c | ||
|
|
e4ffc932a9 | ||
|
|
8afc0e6ab2 | ||
|
|
822092044b | ||
|
|
f1c153f39f | ||
|
|
7e62dc64dc | ||
|
|
2104a60703 | ||
|
|
97785fa570 | ||
|
|
9341fe9584 | ||
|
|
3a582721cf | ||
|
|
3d96d73169 | ||
|
|
542422b7b8 | ||
|
|
c835d85256 | ||
|
|
56ada7f0e9 | ||
|
|
56fdebde75 | ||
|
|
4ee67064bb | ||
|
|
045ec9689d | ||
|
|
4ebad2c473 | ||
|
|
1fe6dac760 | ||
|
|
f12a6ff73f | ||
|
|
6eed458ceb | ||
|
|
54fb0a6acb | ||
|
|
5147d9cb6d | ||
|
|
37369929f3 | ||
|
|
4f10014902 | ||
|
|
0ef3e00ba7 | ||
|
|
2408590430 | ||
|
|
b7f4fe4d73 | ||
|
|
b811492acd | ||
|
|
a63e0e0390 | ||
|
|
5e8a0b2cfa | ||
|
|
eac75aad03 | ||
|
|
b05fbc2102 | ||
|
|
6d166fdfc5 | ||
|
|
2e36673702 | ||
|
|
0c81ffe8b7 | ||
|
|
02b9ceb4c7 | ||
|
|
775889c0b6 | ||
|
|
98f2cdaf5a | ||
|
|
ff5cc3cb4f | ||
|
|
ebecb1caec | ||
|
|
73e0aea85c | ||
|
|
1a09f5807b | ||
|
|
ec009a2bba | ||
|
|
f52c40a492 | ||
|
|
1959c059ed | ||
|
|
2d1610b075 | ||
|
|
2f76738b50 | ||
|
|
1cf174a613 | ||
|
|
d743454d07 | ||
|
|
1cd16eaa08 | ||
|
|
90e622b307 | ||
|
|
cb5cd64c05 | ||
|
|
2619569549 | ||
|
|
d306cafbcc | ||
|
|
f5ce34fb69 | ||
|
|
dbeba4f173 | ||
|
|
86f83635bc | ||
|
|
fceda00d83 | ||
|
|
9b7af00cf5 | ||
|
|
fa1281ae86 | ||
|
|
f5de4d7b71 | ||
|
|
1134df88e2 | ||
|
|
4aadabfac0 | ||
|
|
c27898a993 | ||
|
|
daa897db93 | ||
|
|
7a907bb44d | ||
|
|
d7cb219577 | ||
|
|
b28f8b0e7f | ||
|
|
51721dde50 | ||
|
|
09547ba788 | ||
|
|
3dc8acc385 | ||
|
|
0414da8c32 | ||
|
|
155450380e | ||
|
|
09bc4ef1d6 | ||
|
|
3aa4d8713c | ||
|
|
5fc926271f | ||
|
|
f435b612c9 | ||
|
|
5b78c0d3e0 | ||
|
|
6a14bf70e0 | ||
|
|
138b368951 | ||
|
|
0871d6ebc1 | ||
|
|
ad5ef76e8e | ||
|
|
2f55747601 | ||
|
|
b376458963 | ||
|
|
64ac22a918 | ||
|
|
ffb81e4ff7 | ||
|
|
01743e5c88 | ||
|
|
4ef6266e8f | ||
|
|
478a8362b8 | ||
|
|
afa1899dc9 | ||
|
|
cea2abcf6e | ||
|
|
c7d1ad56ff | ||
|
|
a5f490cc53 | ||
|
|
abe29fa6ee | ||
|
|
f6d1e566e7 | ||
|
|
9ec4f0b2f5 | ||
|
|
1678045ce4 | ||
|
|
7286e724dc | ||
|
|
e59a1e9efd | ||
|
|
097bedcb9b | ||
|
|
907ff89011 | ||
|
|
08faa0c009 | ||
|
|
dd4759487b | ||
|
|
7980da9ce5 | ||
|
|
0e43524dac | ||
|
|
c5c5f642e8 | ||
|
|
e096c608ee | ||
|
|
9a2bfe1180 | ||
|
|
9e36cabef0 | ||
|
|
ce1c5be940 | ||
|
|
1182d159aa | ||
|
|
7d95926f02 | ||
|
|
101ecf342f | ||
|
|
4efba94662 | ||
|
|
1855d661e8 | ||
|
|
438abc4cf9 | ||
|
|
40639f70f4 | ||
|
|
a80c020146 | ||
|
|
2ce3270d21 | ||
|
|
4d8fe0b6b2 | ||
|
|
411087ff1a | ||
|
|
10bb2a6a10 | ||
|
|
7aff81547a | ||
|
|
fc097db2a0 | ||
|
|
843151859d | ||
|
|
854ab353b3 | ||
|
|
cc6ec1d351 | ||
|
|
cf307db31d | ||
|
|
dcfca4d95e | ||
|
|
567c368a81 | ||
|
|
223b2fc263 | ||
|
|
4a28ab6317 | ||
|
|
0986ce12e6 | ||
|
|
37aa3b8e49 | ||
|
|
d7f14339fe | ||
|
|
0e4be0c85a | ||
|
|
b6f8bca361 | ||
|
|
354c72968e | ||
|
|
9d3e3c7312 | ||
|
|
88e2687e23 | ||
|
|
19944bfdb2 | ||
|
|
c8efcf5105 | ||
|
|
7f6da52349 | ||
|
|
8999f0104f | ||
|
|
516c481e94 | ||
|
|
3266c2cd8f | ||
|
|
f0dcd8e07b | ||
|
|
9ef1fee172 | ||
|
|
b3bd4ccc17 | ||
|
|
fba7686390 | ||
|
|
2d314e5309 | ||
|
|
ed72d7f9ec | ||
|
|
b8f64fe3d4 | ||
|
|
ab64828661 | ||
|
|
10dfa18e81 | ||
|
|
a38bf25e68 | ||
|
|
419ec6e308 | ||
|
|
ada589d0c3 | ||
|
|
7068d27a8b | ||
|
|
a302275187 | ||
|
|
b734d58ab7 | ||
|
|
2046b02bd8 | ||
|
|
1df824db7c | ||
|
|
9cad2c6b7d | ||
|
|
82881c030a | ||
|
|
00ca7d5942 | ||
|
|
d36df3eaa9 | ||
|
|
e5d654f0c7 | ||
|
|
fc1f471369 | ||
|
|
be6f4e38b8 | ||
|
|
faa8674f39 | ||
|
|
2dc707d86e | ||
|
|
05a92494bb | ||
|
|
39fd955f13 | ||
|
|
4863e1d227 | ||
|
|
44ad9d4f5f | ||
|
|
2b652fe2a9 | ||
|
|
cdd2082b07 | ||
|
|
5c74aed8f6 | ||
|
|
5b97bc04e0 | ||
|
|
63c8b275d1 | ||
|
|
1ebc17352f | ||
|
|
268c8382ee | ||
|
|
498dcbbfe8 | ||
|
|
3a1ecb342f | ||
|
|
bb0da69c9e | ||
|
|
796dce3cd3 | ||
|
|
f59c34004d | ||
|
|
c4cbf0d618 | ||
|
|
d002e5dda8 | ||
|
|
a9d0ab271d | ||
|
|
89cb821c97 | ||
|
|
ef8c520b59 | ||
|
|
8897fd75ad | ||
|
|
fd748c1dc3 | ||
|
|
c95dbf7508 | ||
|
|
ed64c38950 | ||
|
|
0b5be8cdcd | ||
|
|
fcc77052a6 | ||
|
|
831c9ff5bf | ||
|
|
de37141812 | ||
|
|
c35a648734 | ||
|
|
a550caf63f | ||
|
|
de9eaa98db | ||
|
|
37b657cbbd | ||
|
|
a733f5c615 | ||
|
|
8a587d1d12 | ||
|
|
75bb22f08b | ||
|
|
d10da39e5b | ||
|
|
54e9b839bd | ||
|
|
aec6ac019f | ||
|
|
075a08884b | ||
|
|
6fcb2ab5dd | ||
|
|
7f0f045f29 | ||
|
|
e7d1eadf8e | ||
|
|
a9b5359f7c | ||
|
|
9df6e19204 | ||
|
|
5eaae184c9 | ||
|
|
459882e6fa | ||
|
|
2c2b5d555e | ||
|
|
0ab2428d87 | ||
|
|
8574494573 | ||
|
|
a4d4a9c686 | ||
|
|
3d32b68bb2 | ||
|
|
fd9eb462cc | ||
|
|
f9533e016f | ||
|
|
85b15fa63b | ||
|
|
e236842888 | ||
|
|
3dadaf9334 | ||
|
|
9e510a678c | ||
|
|
2dc0ea2b89 | ||
|
|
7d364ca7ce | ||
|
|
9f6a6d7f5b | ||
|
|
3cc740cda3 | ||
|
|
47b24b5dff | ||
|
|
8f100a792e | ||
|
|
84c6731ddf | ||
|
|
1f1de353de | ||
|
|
40eb82adbe | ||
|
|
d9240e1e2e | ||
|
|
9abaed8385 | ||
|
|
95e83c52fa | ||
|
|
be377dcda8 | ||
|
|
88a68e883e | ||
|
|
2ad5d33251 | ||
|
|
4e5dd914dd | ||
|
|
2adf1e5017 | ||
|
|
55ca4e93c4 | ||
|
|
d1d03f45c5 | ||
|
|
436bd891bd | ||
|
|
a7c28fe5ed | ||
|
|
60814d1ff0 | ||
|
|
d018efe2a5 | ||
|
|
6fd0cba06a | ||
|
|
86f9322036 | ||
|
|
12c6af23ee | ||
|
|
69330f47fd | ||
|
|
4f40c128bf | ||
|
|
30b5ad1515 | ||
|
|
665a26d164 | ||
|
|
a5774bf6ff | ||
|
|
d2716fe5cf | ||
|
|
279f877bf2 | ||
|
|
d51e6a43e7 | ||
|
|
6a96756c87 | ||
|
|
df69d9f195 | ||
|
|
26ffa19f36 | ||
|
|
29ef3f0b41 | ||
|
|
106d5e54c7 | ||
|
|
6ac2460eb0 | ||
|
|
79c030b138 | ||
|
|
c8d649e8c2 | ||
|
|
1fdf82dd6c | ||
|
|
4aa4246695 | ||
|
|
1bebceb29c | ||
|
|
a2139ee236 | ||
|
|
8c55f39cdf | ||
|
|
0329184c94 | ||
|
|
cd64390141 | ||
|
|
3e12a8780d | ||
|
|
11e6ff1bbe | ||
|
|
36f85fc97e | ||
|
|
9040cfd200 | ||
|
|
757da3b15a | ||
|
|
d162590a32 | ||
|
|
f41e1716c6 | ||
|
|
4dce0f1b9d | ||
|
|
fef57dce0d | ||
|
|
d884700b61 | ||
|
|
ff9ad4bd1d | ||
|
|
9ce2b7555c | ||
|
|
f90ccd3391 | ||
|
|
5ff092e541 | ||
|
|
dcdf401f64 | ||
|
|
e4fb80b39b | ||
|
|
9745854ab8 | ||
|
|
7124621f66 | ||
|
|
47fd8f5793 | ||
|
|
40d698f2db | ||
|
|
74abe98706 | ||
|
|
86787f3bc8 | ||
|
|
699b0c775a | ||
|
|
ff59ef8094 | ||
|
|
089af7cc1f | ||
|
|
1591a2d9a3 | ||
|
|
f7984ed642 | ||
|
|
be634c6043 | ||
|
|
d1f68eacd9 | ||
|
|
4f45f23094 | ||
|
|
c5dc01ee11 | ||
|
|
587c385936 | ||
|
|
3a641a58b0 | ||
|
|
e944306a28 | ||
|
|
3b44ed6d16 | ||
|
|
0965ab8063 | ||
|
|
fcae100df1 | ||
|
|
24a7762873 | ||
|
|
e441ab60a2 | ||
|
|
50c2bc5edb | ||
|
|
2ab14ca59e | ||
|
|
4475d65780 | ||
|
|
b1d10f5817 | ||
|
|
36664f37de | ||
|
|
c838df90ef | ||
|
|
fb39af67e5 | ||
|
|
2d4d37f96a | ||
|
|
84af984c4b | ||
|
|
26adf20ee8 | ||
|
|
72668ed0a2 | ||
|
|
50f1ed7851 | ||
|
|
cf8f2a3463 | ||
|
|
b483159b3a | ||
|
|
480abebf7e | ||
|
|
b924dea045 | ||
|
|
2c1e7e5ed6 | ||
|
|
4dfd74906c | ||
|
|
fdae6ad94f | ||
|
|
c80225a18c | ||
|
|
0e6242373e | ||
|
|
4305db5579 | ||
|
|
36e7772f74 | ||
|
|
ca05df5172 | ||
|
|
422e8e6f3e | ||
|
|
852b285d84 | ||
|
|
6c13193623 | ||
|
|
61809107c8 | ||
|
|
6bda9d8604 | ||
|
|
1428ca73de | ||
|
|
498ace0488 | ||
|
|
f082b95efb | ||
|
|
4b8fc2950f | ||
|
|
595cc55578 | ||
|
|
91b0c368b4 | ||
|
|
21d0ffc990 | ||
|
|
55b9d84956 | ||
|
|
ffdb0db6c6 | ||
|
|
a5ed07a666 | ||
|
|
da02c90bad | ||
|
|
3820a231ec | ||
|
|
408b065b9e | ||
|
|
238ab84749 | ||
|
|
6894015986 | ||
|
|
f5080f9bd6 | ||
|
|
db4aa99ce0 | ||
|
|
70134507f8 | ||
|
|
360a4793ae | ||
|
|
47bfb25f2c | ||
|
|
b048b0bf65 | ||
|
|
394f9929ad | ||
|
|
bf39be3320 | ||
|
|
4a2cbb9ec7 | ||
|
|
cc6cf8194f | ||
|
|
e934ead85c | ||
|
|
323bfd9a6e | ||
|
|
bf05e47e26 | ||
|
|
d18f576239 | ||
|
|
7d483c711a | ||
|
|
61256d49cd | ||
|
|
184cdc0331 | ||
|
|
ed972a0037 | ||
|
|
a62a6c1cb6 | ||
|
|
ba0c6be3e3 | ||
|
|
f66566aa17 | ||
|
|
b6ecfc7131 | ||
|
|
460dc6224c | ||
|
|
2b688b1a60 | ||
|
|
3c64d9292f | ||
|
|
0f52d2e464 | ||
|
|
1e5fadc440 | ||
|
|
f495ff483a | ||
|
|
4e3b1509a8 | ||
|
|
d1a80cc880 | ||
|
|
e1ad25cee0 | ||
|
|
195f23c347 | ||
|
|
ad6b99be6a | ||
|
|
b9dd9fc47d | ||
|
|
19a8a80a30 | ||
|
|
637792c6d4 | ||
|
|
4d1bca2d97 | ||
|
|
f33a2eba50 | ||
|
|
5d6bea5ec9 | ||
|
|
057d1f07a8 | ||
|
|
25c3f55672 | ||
|
|
c9d4091c1e | ||
|
|
1d55562dc3 | ||
|
|
95bb9a9780 | ||
|
|
06c391cbf6 | ||
|
|
d90dff95b1 | ||
|
|
c93972a322 | ||
|
|
ca47a7b663 | ||
|
|
9d3d4a3698 | ||
|
|
3b509bf820 | ||
|
|
5b7f91827a | ||
|
|
a44491714c | ||
|
|
06800043a9 | ||
|
|
3090de56b8 | ||
|
|
a37acd1f42 | ||
|
|
372e3f83d2 | ||
|
|
de260a2bef | ||
|
|
e9a130f976 | ||
|
|
43f17414ff | ||
|
|
b259eea8ce | ||
|
|
9cfc2ba09a | ||
|
|
bb347999ce | ||
|
|
3548c3df15 | ||
|
|
1167d0ac2e | ||
|
|
f738bc97e7 | ||
|
|
3f9edfe597 | ||
|
|
a024949311 | ||
|
|
609c901867 | ||
|
|
4ce060a963 | ||
|
|
c4ca0fee40 | ||
|
|
8d4acf0330 | ||
|
|
28a981f29f | ||
|
|
c29113d17a | ||
|
|
951f978447 | ||
|
|
07899f35bd | ||
|
|
3cbbf37468 | ||
|
|
6c7a3df5ae | ||
|
|
2054ab2771 | ||
|
|
44145073f1 | ||
|
|
feb933b4df | ||
|
|
c7cc3002d5 | ||
|
|
049b901d63 | ||
|
|
3cf1b92dfc | ||
|
|
5b0fcbe854 | ||
|
|
cca747a1f6 | ||
|
|
417d99a17e | ||
|
|
e9708b9259 | ||
|
|
e5d3be16b0 | ||
|
|
2ab3c97ee8 | ||
|
|
f20d3043d6 | ||
|
|
4efda89358 | ||
|
|
1fb88271e5 | ||
|
|
a843780f68 | ||
|
|
5ad83da4e0 | ||
|
|
949cc9e214 | ||
|
|
50d92265ea | ||
|
|
e084a9f2b6 | ||
|
|
664f9f36e1 | ||
|
|
4c9efdb936 | ||
|
|
45848e7bfe | ||
|
|
4fa10e5783 | ||
|
|
fc0bc85f4d | ||
|
|
5ae2e5281a | ||
|
|
34a943832a | ||
|
|
db17693ba7 | ||
|
|
6cdf8ebd2c | ||
|
|
072b470f46 | ||
|
|
78b2df2ecc | ||
|
|
51a825f25c | ||
|
|
00e72a30c9 | ||
|
|
69990c23a5 | ||
|
|
df421e0182 | ||
|
|
ede9297139 | ||
|
|
85383fe581 | ||
|
|
4cca7aa4bd | ||
|
|
e2037dea6c | ||
|
|
f10f772e94 | ||
|
|
9ecfe15ac4 | ||
|
|
5f0726af8a | ||
|
|
331bbdd4e6 | ||
|
|
37e3bcfc3e | ||
|
|
4f42c10d60 | ||
|
|
20392a567b | ||
|
|
c03249b411 | ||
|
|
22e6584402 | ||
|
|
c18aca9215 | ||
|
|
aa23a5422a | ||
|
|
01fde4f9ca | ||
|
|
3980dec123 | ||
|
|
c97f837f45 | ||
|
|
9c54d2407b | ||
|
|
a027c4ce1f | ||
|
|
b1fd025ea6 | ||
|
|
a05a230085 | ||
|
|
8fbc1dac74 | ||
|
|
f46842c6c9 | ||
|
|
8b95bb0c03 | ||
|
|
202dd8e92d | ||
|
|
1da3f96d10 | ||
|
|
5f6fe4d670 | ||
|
|
a74438d1ee | ||
|
|
c8033f875d | ||
|
|
07c04006df | ||
|
|
521900c048 | ||
|
|
9069c5abb6 | ||
|
|
ff7a5f471b | ||
|
|
42a47406cc | ||
|
|
de10b6de7b | ||
|
|
d6ade0e1ac | ||
|
|
e04b5e5c9f | ||
|
|
15a6c46d47 | ||
|
|
cb1fc734c2 | ||
|
|
db7f18aae7 | ||
|
|
7fbc327591 | ||
|
|
84b56ae1b2 | ||
|
|
041aa8639a | ||
|
|
216ac72ad0 | ||
|
|
c85ddaeb9c | ||
|
|
e09dec330a | ||
|
|
8f7bae54fe | ||
|
|
ce60f13320 | ||
|
|
1ac0140666 | ||
|
|
6cc8b147a9 | ||
|
|
e078161e2f | ||
|
|
7764185c57 | ||
|
|
d4ef2adf0a | ||
|
|
a83378a44e | ||
|
|
a4a4204762 | ||
|
|
acd1140ef6 | ||
|
|
fbf71c93ff | ||
|
|
38bc0c466a | ||
|
|
71e4351743 | ||
|
|
387e4b94b4 | ||
|
|
201c76b861 | ||
|
|
1c3aa87ca6 | ||
|
|
db63ff6b88 | ||
|
|
115431a486 | ||
|
|
d47ff9b7c7 | ||
|
|
b0818148cf | ||
|
|
2bc4412d66 | ||
|
|
6a428b4da9 | ||
|
|
7299067829 | ||
|
|
5659cb2820 | ||
|
|
570aa4b9e2 | ||
|
|
c4079a3b11 | ||
|
|
6b38b538f1 | ||
|
|
ba139dddd8 | ||
|
|
38b581a231 | ||
|
|
3c2675b41a | ||
|
|
0f5c62ade5 | ||
|
|
54bc3bce96 | ||
|
|
3d92e5b8a9 | ||
|
|
325d145ac3 | ||
|
|
b0654a416a | ||
|
|
19930ec2e4 | ||
|
|
e4de6bf4a7 | ||
|
|
21125c2f5a | ||
|
|
6f166425fe | ||
|
|
cf2353bcf9 | ||
|
|
744eb58071 | ||
|
|
9d47a6f41f | ||
|
|
4f4c23b12f | ||
|
|
fb02815c27 | ||
|
|
fd19299ae0 | ||
|
|
9c053e20da | ||
|
|
19d7b5c65d | ||
|
|
7b9d8829da | ||
|
|
3505ac498c | ||
|
|
f0ab52eb5d | ||
|
|
e8cebad27e | ||
|
|
6441d5838d | ||
|
|
ac0c8b1e9a | ||
|
|
8ec062fbef | ||
|
|
5990a100db | ||
|
|
bc35278684 | ||
|
|
c3c7329ebb | ||
|
|
6fd1c84126 | ||
|
|
0100f0fcc9 | ||
|
|
0cdc32cf65 | ||
|
|
601e9eebbd | ||
|
|
eaa868cf06 | ||
|
|
f55504c665 | ||
|
|
b2ff016cc1 | ||
|
|
33b4f17945 | ||
|
|
e310a3560b | ||
|
|
162b27323e | ||
|
|
ae976ef8d6 | ||
|
|
c6b4e2b71d | ||
|
|
33c8bbd0ce | ||
|
|
f2a3b8dba4 | ||
|
|
207ae6129b | ||
|
|
e1aa734c40 | ||
|
|
9b1b03bbfa | ||
|
|
bb7e0528c4 | ||
|
|
010eadcd10 | ||
|
|
c43e0b54f2 | ||
|
|
6522b74e20 | ||
|
|
8c7975d89a | ||
|
|
407070c9fc | ||
|
|
7821a3cd61 | ||
|
|
a00c2fcfdb | ||
|
|
9cd21d1326 | ||
|
|
aaba95f9b8 | ||
|
|
8d1135a2a3 | ||
|
|
f9fabbedce | ||
|
|
16012e6ffe | ||
|
|
d10a132b0c | ||
|
|
0b3af7d824 | ||
|
|
d0fdae3df7 | ||
|
|
a263611746 | ||
|
|
0e989419c6 | ||
|
|
0fa8276d2d | ||
|
|
b594986241 | ||
|
|
9f3ffa3707 | ||
|
|
8e598c19dc | ||
|
|
2601d6e906 | ||
|
|
de41088051 | ||
|
|
f2752b2a02 | ||
|
|
f0544fab89 | ||
|
|
1b9bf01ab1 | ||
|
|
9945367fa1 | ||
|
|
cbc3887226 | ||
|
|
c11b74e9c0 | ||
|
|
2b764c2abd | ||
|
|
845fc338d7 | ||
|
|
977243ebfd | ||
|
|
29ca544c95 | ||
|
|
94b41d3a2c | ||
|
|
92bb783cbb | ||
|
|
8348263fab | ||
|
|
48f633de11 | ||
|
|
b3b9a629f3 | ||
|
|
5934b7344a | ||
|
|
a9a2e40fed | ||
|
|
656326355a | ||
|
|
b89e2e5355 | ||
|
|
2d187abf13 | ||
|
|
b701412295 | ||
|
|
b4dad81220 | ||
|
|
6bccdad998 | ||
|
|
ecd6b0174a | ||
|
|
a1e534a515 | ||
|
|
ebbe19ba63 | ||
|
|
6a37b73463 | ||
|
|
dd18fcaea2 | ||
|
|
5afc058f90 | ||
|
|
5e221fa9a3 | ||
|
|
0e0cb4d422 | ||
|
|
09f6d60ae9 | ||
|
|
9577d552c6 | ||
|
|
093f17dce2 | ||
|
|
6089f49b9c | ||
|
|
cfb910e87e | ||
|
|
376cffc61d | ||
|
|
d338ba5152 | ||
|
|
f181397664 | ||
|
|
708f23a2ee | ||
|
|
2d1a979eba | ||
|
|
ee0be92967 | ||
|
|
7536b75508 | ||
|
|
7237ae6c54 | ||
|
|
ca05753a3e | ||
|
|
9ca8503eac | ||
|
|
754f71ce00 | ||
|
|
619b05e56c | ||
|
|
8b13826949 | ||
|
|
a96ee57c7e | ||
|
|
ff1ef90a6d | ||
|
|
22905fa8ee | ||
|
|
9e218ddd1c | ||
|
|
6f0462622b | ||
|
|
2f17161163 | ||
|
|
17d48b95ac | ||
|
|
eeea97e2fe | ||
|
|
360e101660 | ||
|
|
3501a944a3 | ||
|
|
76305190a2 | ||
|
|
ab83990170 | ||
|
|
8103a22026 | ||
|
|
1f8e4713ef | ||
|
|
ff9fe2c1be | ||
|
|
7f37c8ee5e | ||
|
|
ed0a723fef | ||
|
|
5c31ef43c9 | ||
|
|
43fd8518c0 | ||
|
|
19a08f01e8 | ||
|
|
33498cf147 | ||
|
|
beeb85cf8d | ||
|
|
ccd860207b | ||
|
|
2c835b5c51 | ||
|
|
5caa951c61 | ||
|
|
4d8c4d6693 | ||
|
|
a9d0574ea8 | ||
|
|
3954494eae | ||
|
|
ed6a2c55eb | ||
|
|
b6ee074149 | ||
|
|
f6b3500e92 | ||
|
|
a71dc48b9b | ||
|
|
bc5eed48c3 | ||
|
|
2ecf3cb303 | ||
|
|
bed33d042a | ||
|
|
d7975626be | ||
|
|
3ac7aba6b2 | ||
|
|
1dde612855 | ||
|
|
4ec97cf006 | ||
|
|
d51c6fd2f8 | ||
|
|
d868e3075c | ||
|
|
ae61ee5486 | ||
|
|
58fd9ddb27 | ||
|
|
a953cb33b7 | ||
|
|
95b90e7c5a | ||
|
|
6a3ecb2881 | ||
|
|
6cf4241283 | ||
|
|
42141e51a1 | ||
|
|
b01945ff50 | ||
|
|
a131f2116f | ||
|
|
625637b888 | ||
|
|
c873f62025 | ||
|
|
43d91e5bd6 | ||
|
|
5c4c729703 | ||
|
|
308da3343d | ||
|
|
48c7572dd5 | ||
|
|
dc5f35460b | ||
|
|
69ea9b0296 | ||
|
|
969c6884c0 | ||
|
|
fcf311aab3 | ||
|
|
888879dfb2 | ||
|
|
e003197f77 | ||
|
|
f57910cd97 | ||
|
|
d85e25dba0 | ||
|
|
89a4034fc6 | ||
|
|
f53743d287 | ||
|
|
2d132128e1 | ||
|
|
6e5ffbe7b5 | ||
|
|
a81c9681a0 | ||
|
|
baf98accd0 | ||
|
|
901c950ee6 | ||
|
|
50ac7f9dc2 | ||
|
|
c2ea4a5290 | ||
|
|
b691b8d37d | ||
|
|
4ead8527c8 | ||
|
|
6f4801fd6f | ||
|
|
10689843b0 | ||
|
|
60cc0c482e | ||
|
|
e1a5105c28 | ||
|
|
ed8a1ed579 | ||
|
|
c3fd2e2284 | ||
|
|
872ef5d0a0 | ||
|
|
b44599cd59 | ||
|
|
7a5dcc700e | ||
|
|
705fb93e45 | ||
|
|
9df923d916 | ||
|
|
dc1cb9093a | ||
|
|
e32043ae79 | ||
|
|
881c921d56 | ||
|
|
abb32bd919 | ||
|
|
4a6c7152cf | ||
|
|
cf92007f66 | ||
|
|
f5c57e5741 | ||
|
|
5627209fdd | ||
|
|
0188d314ce | ||
|
|
67343f6bdc | ||
|
|
ade2e9c6cf | ||
|
|
352e1b2249 | ||
|
|
b8d8d349f4 | ||
|
|
e87468fbe0 | ||
|
|
e38a713ccc | ||
|
|
82ed783a2d | ||
|
|
d17c7aaba6 | ||
|
|
8c93368b20 | ||
|
|
41f61c66a3 | ||
|
|
1b7a20619e | ||
|
|
7d19e58953 | ||
|
|
1605676509 | ||
|
|
a0d6146ff5 | ||
|
|
f709b00be3 | ||
|
|
5847300290 | ||
|
|
9aaac0eefd | ||
|
|
c5ae9913fe | ||
|
|
fc2ad20c63 | ||
|
|
6db97f5541 | ||
|
|
adf6c751ee | ||
|
|
c315b34395 | ||
|
|
f592201e4c | ||
|
|
8bf5ee45ed | ||
|
|
25f759dd07 | ||
|
|
e5f4c17148 | ||
|
|
098b177bd3 | ||
|
|
ef1a8fc50f | ||
|
|
76f2e93a2c | ||
|
|
25ea1df299 | ||
|
|
5ced86af1d | ||
|
|
62e02a49df | ||
|
|
540550d72a | ||
|
|
8cb83fb6e4 | ||
|
|
56db925f0e | ||
|
|
2c0fc8fe3e | ||
|
|
08c7baafac | ||
|
|
8edb450d73 | ||
|
|
fedeef4da5 | ||
|
|
b593d49399 | ||
|
|
4a91fc3c3d | ||
|
|
bb9605d7c3 | ||
|
|
1049326a70 | ||
|
|
457ecf145f | ||
|
|
463dd9d7d8 | ||
|
|
bdcd055aaf | ||
|
|
30ae2037e8 | ||
|
|
ce4fdbfb3c | ||
|
|
2d154eb0cf | ||
|
|
a3e82dfae8 | ||
|
|
97a7469432 | ||
|
|
1a1defb055 | ||
|
|
93c78b6e40 | ||
|
|
b852d6681d | ||
|
|
8e48ac4ede | ||
|
|
859f646c55 | ||
|
|
fb39b2edaf | ||
|
|
d7bf815bd5 | ||
|
|
c93af9e31e | ||
|
|
b81a0e99d4 | ||
|
|
f8fefe2e5e | ||
|
|
f26bc70b59 | ||
|
|
b5fd131aba | ||
|
|
06997e19e0 | ||
|
|
97710540c0 | ||
|
|
c78c109577 | ||
|
|
8d995e456e | ||
|
|
cc3cab9c88 | ||
|
|
0122b410be | ||
|
|
2ddd2b9476 | ||
|
|
a768498250 | ||
|
|
a45aadae16 | ||
|
|
25802432c2 | ||
|
|
98578b18aa | ||
|
|
6d81f69785 | ||
|
|
7dce183170 | ||
|
|
f1962a03ef | ||
|
|
cb26bfd807 | ||
|
|
befd336372 | ||
|
|
8501e61eb1 | ||
|
|
ae489e5a52 | ||
|
|
13afdbda97 | ||
|
|
9cfd88a23f | ||
|
|
13456bad3a | ||
|
|
45be85c5ef | ||
|
|
861dc0d021 | ||
|
|
128d709c99 | ||
|
|
e8f01be8ef | ||
|
|
7f1ee015d1 | ||
|
|
17aa5d8e74 | ||
|
|
b27334b0ff | ||
|
|
7fc6b1e802 | ||
|
|
25b7c8f802 | ||
|
|
8ec6a24a2d | ||
|
|
234707169e | ||
|
|
1c8443210a | ||
|
|
aaf43a592f | ||
|
|
2b08742c0a | ||
|
|
cac04146de | ||
|
|
2b266c7beb | ||
|
|
099932ae68 | ||
|
|
8579babde6 | ||
|
|
9c93d379a8 | ||
|
|
085c7a67c8 | ||
|
|
e6917d8427 | ||
|
|
47cc7fd615 | ||
|
|
ecd207f0a1 | ||
|
|
0ab66f2f14 | ||
|
|
d1e38737ce | ||
|
|
f17de58a71 | ||
|
|
dd552e8e8f | ||
|
|
18480e9d18 | ||
|
|
7ffccd9c3a | ||
|
|
0edd99e9cf | ||
|
|
defdc14d5e | ||
|
|
5dcf8edd38 | ||
|
|
a320766bb6 | ||
|
|
91805caa9a | ||
|
|
48d39dccbd | ||
|
|
fc9e1f59a5 | ||
|
|
e7bc8bd6b9 | ||
|
|
23337d7992 | ||
|
|
f513dc0398 | ||
|
|
184969336e | ||
|
|
1534f1aa6a | ||
|
|
cd8f74e60b | ||
|
|
d832eaa759 | ||
|
|
796863341d | ||
|
|
217b68a1e0 | ||
|
|
4a8ad3103c | ||
|
|
a5f853c67a | ||
|
|
70b54e227e | ||
|
|
1ab6bff54e | ||
|
|
c2317e8493 | ||
|
|
b034a088b1 | ||
|
|
ae7cb8036e | ||
|
|
1a5327aece | ||
|
|
8ce2b04fe4 | ||
|
|
a3c37aed47 | ||
|
|
fa8f19fd43 | ||
|
|
c9a9409b9a | ||
|
|
d3e0ba6d44 | ||
|
|
300ac16cf1 | ||
|
|
3e53884979 | ||
|
|
859fbe9ab1 | ||
|
|
6043c1a4e8 | ||
|
|
0d9fd043a4 | ||
|
|
f06eaf13d1 | ||
|
|
66a619a378 | ||
|
|
fb1b1e1c04 | ||
|
|
9450f88c8c | ||
|
|
0706171264 | ||
|
|
287e2fa89a | ||
|
|
8d1c26d07d | ||
|
|
caae27c44c | ||
|
|
34d77e73ff | ||
|
|
0889741f34 | ||
|
|
8c42199baf | ||
|
|
7395b5760a | ||
|
|
c8f97ed065 | ||
|
|
d2baa8b8fb | ||
|
|
1beee5fd04 | ||
|
|
281b91a59a | ||
|
|
2be2b4ff23 | ||
|
|
a83fd1d3fe | ||
|
|
cb72e4f426 | ||
|
|
3214852a41 | ||
|
|
1057bd7e1f | ||
|
|
33903553ab | ||
|
|
c309afc04b | ||
|
|
f6c4ba898b | ||
|
|
7ba86b40aa | ||
|
|
b2b0aee4b7 | ||
|
|
919cc7e5eb | ||
|
|
e38911b2c5 | ||
|
|
bc68b67cdf | ||
|
|
42a9f1b3e4 | ||
|
|
08333d5989 | ||
|
|
0e0c0c5dfe | ||
|
|
59ebe65643 | ||
|
|
4fd2422e4d | ||
|
|
6181d439f6 | ||
|
|
57b6c10dd1 | ||
|
|
3ee5ac4514 | ||
|
|
be176f98ad | ||
|
|
12b58a31a1 | ||
|
|
8d468d17e3 | ||
|
|
30df4c3d29 | ||
|
|
5122a1c466 | ||
|
|
e135d50d82 | ||
|
|
487b5edc75 | ||
|
|
47ad5779ad | ||
|
|
4fb89360ce | ||
|
|
6dfdbeb7bb | ||
|
|
d0ccbd5526 | ||
|
|
031ee57371 | ||
|
|
2043678739 | ||
|
|
dd27e3b0c8 | ||
|
|
1083d8bde0 | ||
|
|
d1eb247d8c | ||
|
|
fd5e9ea016 | ||
|
|
11829d1f9f | ||
|
|
ae70d1113c | ||
|
|
c485d317fb | ||
|
|
350682b83a | ||
|
|
0fe6485038 | ||
|
|
a553093046 | ||
|
|
af0d5adcdc | ||
|
|
61af1ba029 | ||
|
|
8847cb92ac | ||
|
|
5242514874 | ||
|
|
33a6577b6e | ||
|
|
23d5006f70 | ||
|
|
2697872bdd | ||
|
|
7b331edcde | ||
|
|
e4da59c236 | ||
|
|
48ebafa4e0 | ||
|
|
f5726f63bd | ||
|
|
391b070cff | ||
|
|
781cd0ca3f | ||
|
|
84355963f9 | ||
|
|
3ccfeb490b | ||
|
|
0cc84131de | ||
|
|
4fa08fb189 | ||
|
|
2a551d1d41 | ||
|
|
391aa9c518 | ||
|
|
39d9fd0317 | ||
|
|
18b1fcd724 | ||
|
|
f5c62a3d85 | ||
|
|
6075d5137b | ||
|
|
890293e429 | ||
|
|
05b43a878b | ||
|
|
fe9c3982a1 | ||
|
|
82baa892f7 | ||
|
|
ee53260d72 | ||
|
|
a8eb27940d | ||
|
|
fd8918eaff | ||
|
|
a3a7d7108b | ||
|
|
cd27fe0409 | ||
|
|
35606a9afd | ||
|
|
2052e62c01 | ||
|
|
8ccab5c1e0 | ||
|
|
292f69256e | ||
|
|
fbdcb942e8 | ||
|
|
c14ef7e6cf | ||
|
|
a04fe133b6 | ||
|
|
483e444174 | ||
|
|
ebf8aa7b15 | ||
|
|
7c52be2ac1 | ||
|
|
203a49975c | ||
|
|
7d45838a1e | ||
|
|
2683f1c6e7 | ||
|
|
d13413aff2 | ||
|
|
4c85e7ba66 | ||
|
|
46fef4082c | ||
|
|
c06313dd2e | ||
|
|
59bc2c5535 | ||
|
|
437bc1358b | ||
|
|
99e651e902 | ||
|
|
757ce42a35 | ||
|
|
179f3df847 | ||
|
|
8a889516b0 | ||
|
|
7de5c0a27d | ||
|
|
71d234e1e4 | ||
|
|
b5fb33e21e | ||
|
|
2be22c2a8e | ||
|
|
db198237f3 | ||
|
|
d0ccae129a | ||
|
|
ecbef9c6ee | ||
|
|
ef2cc6620e | ||
|
|
b8f363b187 | ||
|
|
c3f4956ead | ||
|
|
047f4a1c00 | ||
|
|
41c0fe9ffa | ||
|
|
6edb0d49e9 | ||
|
|
a5e3b81a50 | ||
|
|
b9b4e3fdd8 | ||
|
|
6ee9c6ad46 | ||
|
|
6d6556eee5 | ||
|
|
7529c35013 | ||
|
|
378b32d44d | ||
|
|
e1fcd3e3f6 | ||
|
|
d7ad8dd448 | ||
|
|
859f2302a9 | ||
|
|
a6d11789e9 | ||
|
|
43f83076fa | ||
|
|
71c0fc8d4a | ||
|
|
d2f723de12 | ||
|
|
1f4f926ce6 | ||
|
|
35286f838e | ||
|
|
e1ea3795bb | ||
|
|
95237a22a9 | ||
|
|
11c93c5f53 | ||
|
|
b59b8621c5 | ||
|
|
44c61d9a58 | ||
|
|
63a17bc14b | ||
|
|
f4f93bb24d | ||
|
|
7561622bc8 | ||
|
|
b041566aba | ||
|
|
cb72158abc | ||
|
|
5c432d094f | ||
|
|
24ac48b3b1 | ||
|
|
c03060fe3c | ||
|
|
3ebd5141ae | ||
|
|
c16006dc4b | ||
|
|
8fc465b3e8 | ||
|
|
ce689bdff3 | ||
|
|
e23386ddc7 | ||
|
|
0f17d63774 | ||
|
|
4fc3949367 | ||
|
|
e19c04377b | ||
|
|
7c3f429c56 | ||
|
|
7558489ad0 | ||
|
|
4a3880b5ae | ||
|
|
ca7a4abd30 | ||
|
|
a4a45de161 |
@@ -158,7 +158,7 @@ ij_java_keep_indents_on_empty_lines = false
|
||||
ij_java_keep_line_breaks = true
|
||||
ij_java_keep_multiple_expressions_in_one_line = false
|
||||
ij_java_keep_simple_blocks_in_one_line = false
|
||||
ij_java_keep_simple_classes_in_one_line = false
|
||||
ij_java_keep_simple_classes_in_one_line = true
|
||||
ij_java_keep_simple_lambdas_in_one_line = false
|
||||
ij_java_keep_simple_methods_in_one_line = false
|
||||
ij_java_label_indent_absolute = false
|
||||
|
||||
33
.github/workflows/documentation.yml
vendored
Normal file
33
.github/workflows/documentation.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Update Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version-file: .java-version
|
||||
cache: 'maven'
|
||||
- name: Compile and Build OpenAPI file
|
||||
run: ./mvnw compile
|
||||
- name: Update Documentation
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
cp -r api-doc/target/openapi/signal-server-openapi.yaml /tmp/
|
||||
git config user.email "github@signal.org"
|
||||
git config user.name "Documentation Updater"
|
||||
git fetch origin gh-pages
|
||||
git checkout gh-pages
|
||||
cp /tmp/signal-server-openapi.yaml .
|
||||
git diff --quiet || git commit -a -m "Updating documentation"
|
||||
git push origin gh-pages -q
|
||||
37
.github/workflows/integration-tests.yml
vendored
Normal file
37
.github/workflows/integration-tests.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Integration Tests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 19 * * MON-FRI'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
# This may seem a little redundant, but copying the configuration to an environment variable makes it easier and safer
|
||||
# to then write its contents to a file
|
||||
INTEGRATION_TEST_CONFIG: ${{ vars.INTEGRATION_TEST_CONFIG }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ vars.INTEGRATION_TEST_CONFIG != '' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version-file: .java-version
|
||||
cache: 'maven'
|
||||
- uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v5.0.0
|
||||
name: Configure AWS credentials from Test account
|
||||
with:
|
||||
role-to-assume: ${{ vars.AWS_ROLE }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
- name: Write integration test configuration
|
||||
run: |
|
||||
mkdir -p integration-tests/src/main/resources
|
||||
echo "${INTEGRATION_TEST_CONFIG}" > integration-tests/src/main/resources/config.yml
|
||||
- name: Run and verify integration tests
|
||||
run: ./mvnw clean compile test-compile failsafe:integration-test failsafe:verify -P aws-sso
|
||||
58
.github/workflows/test.yml
vendored
58
.github/workflows/test.yml
vendored
@@ -1,23 +1,67 @@
|
||||
name: Service CI
|
||||
|
||||
on: [push]
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches-ignore:
|
||||
- gh-pages
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:22.04
|
||||
container: ubuntu:24.04
|
||||
timeout-minutes: 20
|
||||
|
||||
services:
|
||||
foundationdb0:
|
||||
# Note: this should generally match the version of the FoundationDB SERVER deployed in production; it's okay if
|
||||
# it's a little behind the CLIENT version.
|
||||
image: foundationdb/foundationdb:7.3.62
|
||||
options: --name foundationdb0
|
||||
foundationdb1:
|
||||
# Note: this should generally match the version of the FoundationDB SERVER deployed in production; it's okay if
|
||||
# it's a little behind the CLIENT version.
|
||||
image: foundationdb/foundationdb:7.3.62
|
||||
options: --name foundationdb1
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@de1bb2b0c5634f0fc4438d7aa9944e68f9bf86cc # v3.6.0
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
java-version-file: .java-version
|
||||
cache: 'maven'
|
||||
env:
|
||||
# work around an issue with actions/runner setting an incorrect HOME in containers, which breaks maven caching
|
||||
# https://github.com/actions/setup-java/issues/356
|
||||
HOME: /root
|
||||
- name: Install APT packages
|
||||
# ca-certificates: required for AWS CRT client
|
||||
run: |
|
||||
# Add Docker's official GPG key:
|
||||
apt update
|
||||
apt install -y ca-certificates curl
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
||||
chmod a+r /etc/apt/keyrings/docker.asc
|
||||
|
||||
# Add Docker repository to apt sources:
|
||||
echo \
|
||||
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
|
||||
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
|
||||
tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
|
||||
# ca-certificates: required for AWS CRT client
|
||||
apt update && apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin ca-certificates
|
||||
- name: Configure FoundationDB0 database
|
||||
run: docker exec foundationdb0 /usr/bin/fdbcli --exec 'configure new single memory'
|
||||
- name: Configure FoundationDB1 database
|
||||
run: docker exec foundationdb1 /usr/bin/fdbcli --exec 'configure new single memory'
|
||||
- name: Download and install FoundationDB client
|
||||
run: |
|
||||
./mvnw -e -B -Pexclude-spam-filter clean prepare-package -DskipTests=true
|
||||
cp service/target/jib-extra/usr/lib/libfdb_c.x86_64.so /usr/lib/libfdb_c.x86_64.so
|
||||
ldconfig
|
||||
- name: Build with Maven
|
||||
run: ./mvnw -e -B verify
|
||||
run: ./mvnw -e -B clean verify -DfoundationDb.serviceContainerNamePrefix=foundationdb
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -16,7 +16,6 @@ config/deploy.properties
|
||||
/service/config/testing.yml
|
||||
/service/config/deploy.properties
|
||||
/service/dependency-reduced-pom.xml
|
||||
.java-version
|
||||
.opsmanage
|
||||
put.sh
|
||||
deployer-staging.properties
|
||||
@@ -29,3 +28,4 @@ deployer.log
|
||||
.project
|
||||
.classpath
|
||||
.settings
|
||||
.DS_Store
|
||||
|
||||
1
.java-version
Normal file
1
.java-version
Normal file
@@ -0,0 +1 @@
|
||||
temurin-24
|
||||
@@ -4,6 +4,6 @@
|
||||
<extension>
|
||||
<groupId>fr.brouillard.oss</groupId>
|
||||
<artifactId>jgitver-maven-plugin</artifactId>
|
||||
<version>1.7.1</version>
|
||||
<version>1.9.0</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
|
||||
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
Binary file not shown.
6
.mvn/wrapper/maven-wrapper.properties
vendored
6
.mvn/wrapper/maven-wrapper.properties
vendored
@@ -14,5 +14,7 @@
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar
|
||||
distributionSha256Sum=0d7125e8c91097b36edb990ea5934e6c68b4440eef4ea96510a0f6815e7eeadb
|
||||
wrapperSha256Sum=4e2fbf6554bc8a4702cdfdd3bef464f423393d784ddbb037216320ce55d5e4e1
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -296,7 +296,7 @@ commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
procedures, authorization keysManager, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
|
||||
29
README.md
29
README.md
@@ -8,9 +8,28 @@ Looking for protocol documentation? Check out the website!
|
||||
|
||||
https://signal.org/docs/
|
||||
|
||||
Cryptography Notice
|
||||
How to Build
|
||||
------------
|
||||
|
||||
This project uses [FoundationDB](https://www.foundationdb.org/) and requires the FoundationDB client library to be installed on the host system. With that in place, the server can be built and tested with:
|
||||
|
||||
```shell script
|
||||
$ ./mvnw clean test
|
||||
```
|
||||
|
||||
Security
|
||||
--------
|
||||
|
||||
Security issues should be sent to <a href=mailto:security@signal.org>security@signal.org</a>.
|
||||
|
||||
Help
|
||||
----
|
||||
|
||||
We cannot provide direct technical support. Get help running this software in your own environment in our [unofficial community forum][community forum].
|
||||
|
||||
Cryptography Notice
|
||||
-------------------
|
||||
|
||||
This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of encryption software.
|
||||
BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted.
|
||||
See <https://www.wassenaar.org/> for more information.
|
||||
@@ -19,8 +38,10 @@ The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS
|
||||
The form and manner of this distribution makes it eligible for export under the License Exception ENC Technology Software Unrestricted (TSU) exception (see the BIS Export Administration Regulations, Section 740.13) for both object code and source code.
|
||||
|
||||
License
|
||||
---------------------
|
||||
-------
|
||||
|
||||
Copyright 2013-2022 Signal Messenger, LLC
|
||||
Copyright 2013 Signal Messenger, LLC
|
||||
|
||||
Licensed under the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html
|
||||
Licensed under the GNU AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
[community forum]: https://community.signalusers.org
|
||||
|
||||
30
TESTING.md
Normal file
30
TESTING.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Testing
|
||||
|
||||
## Automated tests
|
||||
|
||||
The full suite of automated tests can be run using Maven from the project root:
|
||||
|
||||
```sh
|
||||
./mvnw verify
|
||||
```
|
||||
|
||||
## Test server
|
||||
|
||||
The service can be run in a feature-limited test mode by running the Maven `integration-test`
|
||||
goal with the `test-server` profile activated:
|
||||
|
||||
```sh
|
||||
./mvnw integration-test -Ptest-server [-DskipTests=true]
|
||||
```
|
||||
|
||||
This runs [`LocalWhisperServerService`][lwss] with [test configuration][test.yml] and [secrets][test secrets]. External
|
||||
registration clients are stubbed so that:
|
||||
|
||||
- a captcha requirement can be satisfied with `noop.noop.registration.noop`
|
||||
- any string will be accepted for a phone verification code
|
||||
|
||||
[lwss]: service/src/test/java/org/whispersystems/textsecuregcm/LocalWhisperServerService.java
|
||||
|
||||
[test.yml]: service/src/test/resources/config/test.yml
|
||||
|
||||
[test secrets]: service/src/test/resources/config/test-secrets-bundle.yml
|
||||
53
api-doc/pom.xml
Normal file
53
api-doc/pom.xml
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>TextSecureServer</artifactId>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<version>JGITVER</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>api-doc</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<artifactId>service</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-maven-plugin-jakarta</artifactId>
|
||||
<version>${swagger.version}</version>
|
||||
<configuration>
|
||||
<outputFileName>signal-server-openapi</outputFileName>
|
||||
<outputPath>${project.build.directory}/openapi</outputPath>
|
||||
<outputFormat>YAML</outputFormat>
|
||||
<configurationFilePath>${project.basedir}/src/main/resources/openapi/openapi-configuration.yaml
|
||||
</configurationFilePath>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>resolve</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.google.cloud.tools</groupId>
|
||||
<artifactId>jib-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<!-- we don't want jib to execute on this module -->
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.openapi;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
import com.fasterxml.jackson.databind.JavaType;
|
||||
import com.fasterxml.jackson.databind.type.SimpleType;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import io.swagger.v3.jaxrs2.ResolvedParameter;
|
||||
import io.swagger.v3.jaxrs2.ext.AbstractOpenAPIExtension;
|
||||
import io.swagger.v3.jaxrs2.ext.OpenAPIExtension;
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.Set;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||
|
||||
/**
|
||||
* One of the extension mechanisms of Swagger Core library (OpenAPI processor) is via custom implementations
|
||||
* of the {@link AbstractOpenAPIExtension} class.
|
||||
* <p/>
|
||||
* The purpose of this extension is to customize certain aspects of the OpenAPI model generation on a lower level.
|
||||
* This extension works in coordination with {@link OpenApiReader} that has access to the model on a higher level.
|
||||
* <p/>
|
||||
* The extension is enabled by being listed in {@code META-INF/services/io.swagger.v3.jaxrs2.ext.OpenAPIExtension} file.
|
||||
* @see ServiceLoader
|
||||
* @see OpenApiReader
|
||||
* @see <a href="https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Extensions">Swagger 2.X Extensions</a>
|
||||
*/
|
||||
public class OpenApiExtension extends AbstractOpenAPIExtension {
|
||||
|
||||
public static final ResolvedParameter AUTHENTICATED_ACCOUNT = new ResolvedParameter();
|
||||
|
||||
public static final ResolvedParameter OPTIONAL_AUTHENTICATED_ACCOUNT = new ResolvedParameter();
|
||||
|
||||
/**
|
||||
* When parsing endpoint methods, Swagger will treat the first parameter not annotated as header/path/query param
|
||||
* as a request body (and will ignore other not annotated parameters). In our case, this behavior conflicts with
|
||||
* the {@code @Auth}-annotated parameters. Here we're checking if parameters are known to be anything other than
|
||||
* a body and return an appropriate {@link ResolvedParameter} representation.
|
||||
*/
|
||||
@Override
|
||||
public ResolvedParameter extractParameters(
|
||||
final List<Annotation> annotations,
|
||||
final Type type,
|
||||
final Set<Type> typesToSkip,
|
||||
final Components components,
|
||||
final Consumes classConsumes,
|
||||
final Consumes methodConsumes,
|
||||
final boolean includeRequestBody,
|
||||
final JsonView jsonViewAnnotation,
|
||||
final Iterator<OpenAPIExtension> chain) {
|
||||
|
||||
if (annotations.stream().anyMatch(a -> a.annotationType().equals(Auth.class))) {
|
||||
// this is the case of authenticated endpoint,
|
||||
if (type instanceof SimpleType simpleType
|
||||
&& simpleType.getRawClass().equals(AuthenticatedDevice.class)) {
|
||||
return AUTHENTICATED_ACCOUNT;
|
||||
}
|
||||
if (type instanceof SimpleType simpleType
|
||||
&& isOptionalOfType(simpleType, AuthenticatedDevice.class)) {
|
||||
return OPTIONAL_AUTHENTICATED_ACCOUNT;
|
||||
}
|
||||
}
|
||||
|
||||
return super.extractParameters(
|
||||
annotations,
|
||||
type,
|
||||
typesToSkip,
|
||||
components,
|
||||
classConsumes,
|
||||
methodConsumes,
|
||||
includeRequestBody,
|
||||
jsonViewAnnotation,
|
||||
chain);
|
||||
}
|
||||
|
||||
private static boolean isOptionalOfType(final SimpleType simpleType, final Class<?> expectedType) {
|
||||
if (!simpleType.getRawClass().equals(Optional.class)) {
|
||||
return false;
|
||||
}
|
||||
final List<JavaType> typeParameters = simpleType.getBindings().getTypeParameters();
|
||||
if (typeParameters.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return typeParameters.get(0) instanceof SimpleType optionalParameterType
|
||||
&& optionalParameterType.getRawClass().equals(expectedType);
|
||||
}
|
||||
}
|
||||
71
api-doc/src/main/java/org/signal/openapi/OpenApiReader.java
Normal file
71
api-doc/src/main/java/org/signal/openapi/OpenApiReader.java
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.openapi;
|
||||
|
||||
import static com.google.common.base.MoreObjects.firstNonNull;
|
||||
import static org.signal.openapi.OpenApiExtension.AUTHENTICATED_ACCOUNT;
|
||||
import static org.signal.openapi.OpenApiExtension.OPTIONAL_AUTHENTICATED_ACCOUNT;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import io.swagger.v3.jaxrs2.Reader;
|
||||
import io.swagger.v3.jaxrs2.ResolvedParameter;
|
||||
import io.swagger.v3.oas.models.Operation;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* One of the extension mechanisms of Swagger Core library (OpenAPI processor) is via custom implementations
|
||||
* of the {@link Reader} class.
|
||||
* <p/>
|
||||
* The purpose of this extension is to customize certain aspects of the OpenAPI model generation on a higher level.
|
||||
* This extension works in coordination with {@link OpenApiExtension} that has access to the model on a lower level.
|
||||
* <p/>
|
||||
* The extension is enabled by being listed in {@code resources/openapi/openapi-configuration.yaml} file.
|
||||
* @see OpenApiExtension
|
||||
* @see <a href="https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Extensions">Swagger 2.X Extensions</a>
|
||||
*/
|
||||
public class OpenApiReader extends Reader {
|
||||
|
||||
private static final String AUTHENTICATED_ACCOUNT_AUTH_SCHEMA = "authenticatedAccount";
|
||||
|
||||
|
||||
/**
|
||||
* Overriding this method allows converting a resolved parameter into other operation entities,
|
||||
* in this case, into security requirements.
|
||||
*/
|
||||
@Override
|
||||
protected ResolvedParameter getParameters(
|
||||
final Type type,
|
||||
final List<Annotation> annotations,
|
||||
final Operation operation,
|
||||
final Consumes classConsumes,
|
||||
final Consumes methodConsumes,
|
||||
final JsonView jsonViewAnnotation) {
|
||||
final ResolvedParameter resolved = super.getParameters(
|
||||
type, annotations, operation, classConsumes, methodConsumes, jsonViewAnnotation);
|
||||
|
||||
if (resolved == AUTHENTICATED_ACCOUNT) {
|
||||
operation.setSecurity(ImmutableList.<SecurityRequirement>builder()
|
||||
.addAll(firstNonNull(operation.getSecurity(), Collections.emptyList()))
|
||||
.add(new SecurityRequirement().addList(AUTHENTICATED_ACCOUNT_AUTH_SCHEMA))
|
||||
.build());
|
||||
}
|
||||
if (resolved == OPTIONAL_AUTHENTICATED_ACCOUNT) {
|
||||
operation.setSecurity(ImmutableList.<SecurityRequirement>builder()
|
||||
.addAll(firstNonNull(operation.getSecurity(), Collections.emptyList()))
|
||||
.add(new SecurityRequirement().addList(AUTHENTICATED_ACCOUNT_AUTH_SCHEMA))
|
||||
.add(new SecurityRequirement())
|
||||
.build());
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.signal.openapi.OpenApiExtension
|
||||
@@ -0,0 +1,25 @@
|
||||
resourcePackages:
|
||||
- org.whispersystems.textsecuregcm
|
||||
prettyPrint: true
|
||||
cacheTTL: 0
|
||||
readerClass: org.signal.openapi.OpenApiReader
|
||||
openAPI:
|
||||
info:
|
||||
title: Signal Server API
|
||||
license:
|
||||
name: AGPL-3.0-only
|
||||
url: https://www.gnu.org/licenses/agpl-3.0.txt
|
||||
servers:
|
||||
- url: https://chat.signal.org
|
||||
description: Production service
|
||||
- url: https://chat.staging.signal.org
|
||||
description: Staging service
|
||||
components:
|
||||
securitySchemes:
|
||||
authenticatedAccount:
|
||||
type: http
|
||||
scheme: basic
|
||||
description: |
|
||||
Account authentication is based on Basic authentication schema,
|
||||
where `username` has a format of `<user_id>[.<device_id>]`. If `device_id` is not specified,
|
||||
user's `main` device is assumed.
|
||||
@@ -1,86 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2022 Signal Messenger, LLC
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>TextSecureServer</artifactId>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<version>JGITVER</version>
|
||||
</parent>
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>event-logger</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.google.cloud</groupId>
|
||||
<artifactId>google-cloud-logging</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<!--
|
||||
depends on an outdated version (13.0) for JDK 6 compatibility, but it’s safe to override
|
||||
https://youtrack.jetbrains.com/issue/KT-25047
|
||||
-->
|
||||
<artifactId>annotations</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlinx</groupId>
|
||||
<artifactId>kotlinx-serialization-json</artifactId>
|
||||
<version>${kotlinx-serialization.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
|
||||
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
|
||||
<execution>
|
||||
<id>test-compile</id>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<compilerPlugins>
|
||||
<plugin>kotlinx-serialization</plugin>
|
||||
</compilerPlugins>
|
||||
</configuration>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-serialization</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.event
|
||||
|
||||
import java.util.Collections
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
import kotlinx.serialization.modules.subclass
|
||||
|
||||
val module = SerializersModule {
|
||||
polymorphic(Event::class) {
|
||||
subclass(RemoteConfigSetEvent::class)
|
||||
subclass(RemoteConfigDeleteEvent::class)
|
||||
}
|
||||
}
|
||||
val jsonFormat = Json { serializersModule = module }
|
||||
|
||||
sealed interface Event
|
||||
|
||||
@Serializable
|
||||
data class RemoteConfigSetEvent(
|
||||
val token: String,
|
||||
val name: String,
|
||||
val percentage: Int,
|
||||
val defaultValue: String? = null,
|
||||
val value: String? = null,
|
||||
val hashKey: String? = null,
|
||||
val uuids: Collection<String> = Collections.emptyList(),
|
||||
) : Event
|
||||
|
||||
@Serializable
|
||||
data class RemoteConfigDeleteEvent(
|
||||
val token: String,
|
||||
val name: String,
|
||||
) : Event
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.event
|
||||
|
||||
import com.google.cloud.logging.LogEntry
|
||||
import com.google.cloud.logging.Logging
|
||||
import com.google.cloud.logging.MonitoredResourceUtil
|
||||
import com.google.cloud.logging.Payload.JsonPayload
|
||||
import com.google.cloud.logging.Severity
|
||||
import com.google.protobuf.Struct
|
||||
import com.google.protobuf.util.JsonFormat
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
interface AdminEventLogger {
|
||||
fun logEvent(event: Event, labels: Map<String, String>?)
|
||||
fun logEvent(event: Event) = logEvent(event, null)
|
||||
}
|
||||
|
||||
class NoOpAdminEventLogger : AdminEventLogger {
|
||||
override fun logEvent(event: Event, labels: Map<String, String>?) {}
|
||||
}
|
||||
|
||||
class GoogleCloudAdminEventLogger(private val logging: Logging, private val projectId: String, private val logName: String) : AdminEventLogger {
|
||||
override fun logEvent(event: Event, labels: Map<String, String>?) {
|
||||
val structBuilder = Struct.newBuilder()
|
||||
JsonFormat.parser().merge(jsonFormat.encodeToString(event), structBuilder)
|
||||
val struct = structBuilder.build()
|
||||
|
||||
val logEntryBuilder = LogEntry.newBuilder(JsonPayload.of(struct))
|
||||
.setLogName(logName)
|
||||
.setSeverity(Severity.NOTICE)
|
||||
.setResource(MonitoredResourceUtil.getResource(projectId, "project"));
|
||||
if (labels != null) {
|
||||
logEntryBuilder.setLabels(labels);
|
||||
}
|
||||
logging.write(listOf(logEntryBuilder.build()))
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.event
|
||||
|
||||
import com.google.cloud.logging.Logging
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.mock
|
||||
|
||||
class GoogleCloudAdminEventLoggerTest {
|
||||
|
||||
@Test
|
||||
fun logEvent() {
|
||||
val logging = mock(Logging::class.java)
|
||||
val logger = GoogleCloudAdminEventLogger(logging, "my-project", "test")
|
||||
|
||||
val event = RemoteConfigDeleteEvent("token", "test")
|
||||
logger.logEvent(event)
|
||||
}
|
||||
}
|
||||
2
integration-tests/.gitignore
vendored
Normal file
2
integration-tests/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.libs
|
||||
src/main/resources/config.yml
|
||||
73
integration-tests/pom.xml
Normal file
73
integration-tests/pom.xml
Normal file
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>TextSecureServer</artifactId>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<version>JGITVER</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>integration-tests</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<artifactId>service</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>dynamodb</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>**</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<configuration>
|
||||
<additionalClasspathElements>
|
||||
<additionalClasspathElement>${project.basedir}/.libs/software.amazon.awssdk-sso.jar</additionalClasspathElement>
|
||||
</additionalClasspathElements>
|
||||
<includes>
|
||||
<include>**/*.java</include>
|
||||
</includes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.google.cloud.tools</groupId>
|
||||
<artifactId>jib-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<!-- we don't want jib to execute on this module -->
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>aws-sso</id>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>sso</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.integration;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
|
||||
public final class Codecs {
|
||||
|
||||
private Codecs() {
|
||||
// utility class
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface CheckedFunction<T, R> {
|
||||
R apply(T t) throws Exception;
|
||||
}
|
||||
|
||||
public static class Base64BasedSerializer<T> extends JsonSerializer<T> {
|
||||
|
||||
private final CheckedFunction<T, byte[]> mapper;
|
||||
|
||||
public Base64BasedSerializer(final CheckedFunction<T, byte[]> mapper) {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(final T value, final JsonGenerator gen, final SerializerProvider serializers) throws IOException {
|
||||
try {
|
||||
gen.writeString(Base64.getEncoder().withoutPadding().encodeToString(mapper.apply(value)));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Base64BasedDeserializer<T> extends JsonDeserializer<T> {
|
||||
|
||||
private final CheckedFunction<byte[], T> mapper;
|
||||
|
||||
public Base64BasedDeserializer(final CheckedFunction<byte[], T> mapper) {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T deserialize(final JsonParser p, final DeserializationContext ctxt) throws IOException {
|
||||
try {
|
||||
return mapper.apply(Base64.getDecoder().decode(p.getValueAsString()));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ByteArraySerializer extends Base64BasedSerializer<byte[]> {
|
||||
public ByteArraySerializer() {
|
||||
super(bytes -> bytes);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ByteArrayDeserializer extends Base64BasedDeserializer<byte[]> {
|
||||
public ByteArrayDeserializer() {
|
||||
super(bytes -> bytes);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ECPublicKeySerializer extends Base64BasedSerializer<ECPublicKey> {
|
||||
public ECPublicKeySerializer() {
|
||||
super(ECPublicKey::serialize);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ECPublicKeyDeserializer extends Base64BasedDeserializer<ECPublicKey> {
|
||||
public ECPublicKeyDeserializer() {
|
||||
super(ECPublicKey::new);
|
||||
}
|
||||
}
|
||||
|
||||
public static class IdentityKeySerializer extends Base64BasedSerializer<IdentityKey> {
|
||||
public IdentityKeySerializer() {
|
||||
super(IdentityKey::serialize);
|
||||
}
|
||||
}
|
||||
|
||||
public static class IdentityKeyDeserializer extends Base64BasedDeserializer<IdentityKey> {
|
||||
public IdentityKeyDeserializer() {
|
||||
super(bytes -> new IdentityKey(bytes, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.integration;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import org.signal.integration.config.Config;
|
||||
import org.whispersystems.textsecuregcm.metrics.NoopAwsSdkMetricPublisher;
|
||||
import org.whispersystems.textsecuregcm.registration.VerificationSession;
|
||||
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
|
||||
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswords;
|
||||
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.VerificationSessionManager;
|
||||
import org.whispersystems.textsecuregcm.storage.VerificationSessions;
|
||||
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
|
||||
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||
|
||||
public class IntegrationTools {
|
||||
|
||||
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
|
||||
|
||||
private final VerificationSessionManager verificationSessionManager;
|
||||
|
||||
private final PhoneNumberIdentifiers phoneNumberIdentifiers;
|
||||
|
||||
|
||||
public static IntegrationTools create(final Config config) {
|
||||
final AwsCredentialsProvider credentialsProvider = DefaultCredentialsProvider.builder().build();
|
||||
|
||||
final DynamoDbAsyncClient dynamoDbAsyncClient =
|
||||
config.dynamoDbClient().buildAsyncClient(credentialsProvider, new NoopAwsSdkMetricPublisher());
|
||||
|
||||
final RegistrationRecoveryPasswords registrationRecoveryPasswords = new RegistrationRecoveryPasswords(
|
||||
config.dynamoDbTables().registrationRecovery(), Duration.ofDays(1), dynamoDbAsyncClient, Clock.systemUTC());
|
||||
|
||||
final VerificationSessions verificationSessions = new VerificationSessions(
|
||||
dynamoDbAsyncClient, config.dynamoDbTables().verificationSessions(), Clock.systemUTC());
|
||||
|
||||
return new IntegrationTools(
|
||||
new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords),
|
||||
new VerificationSessionManager(verificationSessions),
|
||||
new PhoneNumberIdentifiers(dynamoDbAsyncClient, config.dynamoDbTables().phoneNumberIdentifiers())
|
||||
);
|
||||
}
|
||||
|
||||
private IntegrationTools(
|
||||
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
|
||||
final VerificationSessionManager verificationSessionManager,
|
||||
final PhoneNumberIdentifiers phoneNumberIdentifiers) {
|
||||
this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager;
|
||||
this.verificationSessionManager = verificationSessionManager;
|
||||
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> populateRecoveryPassword(final String phoneNumber, final byte[] password) {
|
||||
return phoneNumberIdentifiers
|
||||
.getPhoneNumberIdentifier(phoneNumber)
|
||||
.thenCompose(pni -> registrationRecoveryPasswordsManager.store(pni, password));
|
||||
}
|
||||
|
||||
public CompletableFuture<Optional<String>> peekVerificationSessionPushChallenge(final String sessionId) {
|
||||
return verificationSessionManager.findForId(sessionId)
|
||||
.thenApply(maybeSession -> maybeSession.map(VerificationSession::pushChallenge));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.integration;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.google.common.io.Resources;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import io.dropwizard.configuration.ConfigurationValidationException;
|
||||
import io.dropwizard.jersey.validation.Validators;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import java.io.IOException;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.signal.integration.config.Config;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.libsignal.protocol.kem.KEMKeyPair;
|
||||
import org.signal.libsignal.protocol.kem.KEMKeyType;
|
||||
import org.signal.libsignal.protocol.kem.KEMPublicKey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceActivationRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.RegistrationRequest;
|
||||
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.HeaderUtils;
|
||||
import org.whispersystems.textsecuregcm.util.HttpUtils;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
public final class Operations {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
|
||||
|
||||
private static final Config CONFIG = loadConfigFromClasspath("config.yml");
|
||||
|
||||
private static final IntegrationTools INTEGRATION_TOOLS = IntegrationTools.create(CONFIG);
|
||||
|
||||
private static final String USER_AGENT = "integration-test";
|
||||
|
||||
private static final FaultTolerantHttpClient CLIENT = buildClient();
|
||||
|
||||
|
||||
private Operations() {
|
||||
// utility class
|
||||
}
|
||||
|
||||
public static TestUser newRegisteredUser(final String number) {
|
||||
final byte[] registrationPassword = populateRandomRecoveryPassword(number);
|
||||
final String accountPassword = Base64.getEncoder().encodeToString(randomBytes(32));
|
||||
|
||||
final TestUser user = TestUser.create(number, accountPassword, registrationPassword);
|
||||
final AccountAttributes accountAttributes = user.accountAttributes();
|
||||
|
||||
final ECKeyPair aciIdentityKeyPair = ECKeyPair.generate();
|
||||
final ECKeyPair pniIdentityKeyPair = ECKeyPair.generate();
|
||||
|
||||
// register account
|
||||
final RegistrationRequest registrationRequest = new RegistrationRequest(null,
|
||||
registrationPassword,
|
||||
accountAttributes,
|
||||
true,
|
||||
new IdentityKey(aciIdentityKeyPair.getPublicKey()),
|
||||
new IdentityKey(pniIdentityKeyPair.getPublicKey()),
|
||||
new DeviceActivationRequest(generateSignedECPreKey(1, aciIdentityKeyPair),
|
||||
generateSignedECPreKey(2, pniIdentityKeyPair),
|
||||
generateSignedKEMPreKey(3, aciIdentityKeyPair),
|
||||
generateSignedKEMPreKey(4, pniIdentityKeyPair),
|
||||
Optional.empty(),
|
||||
Optional.empty()));
|
||||
|
||||
final AccountIdentityResponse registrationResponse = apiPost("/v1/registration", registrationRequest)
|
||||
.authorized(number, accountPassword)
|
||||
.executeExpectSuccess(AccountIdentityResponse.class);
|
||||
|
||||
user.setAciUuid(registrationResponse.uuid());
|
||||
user.setPniUuid(registrationResponse.pni());
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
public record PrescribedVerificationNumber(String number, String verificationCode) {}
|
||||
|
||||
public static PrescribedVerificationNumber prescribedVerificationNumber() {
|
||||
return new PrescribedVerificationNumber(
|
||||
CONFIG.prescribedRegistrationNumber(),
|
||||
CONFIG.prescribedRegistrationCode());
|
||||
}
|
||||
|
||||
public static void deleteUser(final TestUser user) {
|
||||
apiDelete("/v1/accounts/me").authorized(user).executeExpectSuccess();
|
||||
}
|
||||
|
||||
public static String peekVerificationSessionPushChallenge(final String sessionId) {
|
||||
return INTEGRATION_TOOLS.peekVerificationSessionPushChallenge(sessionId).join()
|
||||
.orElseThrow(() -> new RuntimeException("push challenge not found for the verification session"));
|
||||
}
|
||||
|
||||
public static byte[] populateRandomRecoveryPassword(final String number) {
|
||||
final byte[] recoveryPassword = randomBytes(32);
|
||||
INTEGRATION_TOOLS.populateRecoveryPassword(number, recoveryPassword).join();
|
||||
|
||||
return recoveryPassword;
|
||||
}
|
||||
|
||||
public static <T> T sendEmptyRequestAuthenticated(
|
||||
final String endpoint,
|
||||
final String method,
|
||||
final String username,
|
||||
final String password,
|
||||
final Class<T> outputType) {
|
||||
try {
|
||||
final HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(serverUri(endpoint, Collections.emptyList()))
|
||||
.method(method, HttpRequest.BodyPublishers.noBody())
|
||||
.header(HttpHeaders.AUTHORIZATION, HeaderUtils.basicAuthHeader(username, password))
|
||||
.header(HttpHeaders.CONTENT_TYPE, "application/json")
|
||||
.build();
|
||||
return CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8))
|
||||
.whenComplete((response, error) -> {
|
||||
if (error != null) {
|
||||
logger.error("request error", error);
|
||||
error.printStackTrace();
|
||||
} else {
|
||||
logger.info("response: {}", response.statusCode());
|
||||
System.out.println("response: " + response.statusCode() + ", " + response.body());
|
||||
}
|
||||
})
|
||||
.thenApply(response -> {
|
||||
try {
|
||||
return outputType.equals(Void.class)
|
||||
? null
|
||||
: SystemMapper.jsonMapper().readValue(response.body(), outputType);
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.get();
|
||||
} catch (final Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] randomBytes(int numBytes) {
|
||||
final byte[] bytes = new byte[numBytes];
|
||||
new SecureRandom().nextBytes(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static RequestBuilder apiGet(final String endpoint) {
|
||||
return new RequestBuilder(HttpRequest.newBuilder().GET(), endpoint);
|
||||
}
|
||||
|
||||
public static RequestBuilder apiDelete(final String endpoint) {
|
||||
return new RequestBuilder(HttpRequest.newBuilder().DELETE(), endpoint);
|
||||
}
|
||||
|
||||
public static <R> RequestBuilder apiPost(final String endpoint, final R input) {
|
||||
return RequestBuilder.withJsonBody(endpoint, "POST", input);
|
||||
}
|
||||
|
||||
public static <R> RequestBuilder apiPut(final String endpoint, final R input) {
|
||||
return RequestBuilder.withJsonBody(endpoint, "PUT", input);
|
||||
}
|
||||
|
||||
public static <R> RequestBuilder apiPatch(final String endpoint, final R input) {
|
||||
return RequestBuilder.withJsonBody(endpoint, "PATCH", input);
|
||||
}
|
||||
|
||||
private static URI serverUri(final String endpoint, final List<String> queryParams) {
|
||||
final String query = queryParams.isEmpty()
|
||||
? StringUtils.EMPTY
|
||||
: "?" + String.join("&", queryParams);
|
||||
return URI.create("https://" + CONFIG.domain() + endpoint + query);
|
||||
}
|
||||
|
||||
public static class RequestBuilder {
|
||||
|
||||
private final HttpRequest.Builder builder;
|
||||
|
||||
private final String endpoint;
|
||||
|
||||
private final List<String> queryParams = new ArrayList<>();
|
||||
|
||||
|
||||
private RequestBuilder(final HttpRequest.Builder builder, final String endpoint) {
|
||||
this.builder = builder;
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
private static <R> RequestBuilder withJsonBody(final String endpoint, final String method, final R input) {
|
||||
try {
|
||||
final byte[] body = SystemMapper.jsonMapper().writeValueAsBytes(input);
|
||||
return new RequestBuilder(HttpRequest.newBuilder()
|
||||
.header(HttpHeaders.CONTENT_TYPE, "application/json")
|
||||
.method(method, HttpRequest.BodyPublishers.ofByteArray(body)), endpoint);
|
||||
} catch (final JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public RequestBuilder authorized(final TestUser user) {
|
||||
return authorized(user, Device.PRIMARY_ID);
|
||||
}
|
||||
|
||||
public RequestBuilder authorized(final TestUser user, final byte deviceId) {
|
||||
final String username = "%s.%d".formatted(user.aciUuid().toString(), deviceId);
|
||||
return authorized(username, user.accountPassword());
|
||||
}
|
||||
|
||||
public RequestBuilder authorized(final String username, final String password) {
|
||||
builder.header(HttpHeaders.AUTHORIZATION, HeaderUtils.basicAuthHeader(username, password));
|
||||
return this;
|
||||
}
|
||||
|
||||
public RequestBuilder queryParam(final String key, final String value) {
|
||||
queryParams.add("%s=%s".formatted(key, value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public RequestBuilder header(final String name, final String value) {
|
||||
builder.header(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Pair<Integer, Void> execute() {
|
||||
return execute(Void.class);
|
||||
}
|
||||
|
||||
public Pair<Integer, Void> executeExpectSuccess() {
|
||||
final Pair<Integer, Void> execute = execute();
|
||||
Validate.isTrue(
|
||||
HttpUtils.isSuccessfulResponse(execute.getLeft()),
|
||||
"Unexpected response code: %d",
|
||||
execute.getLeft());
|
||||
return execute;
|
||||
}
|
||||
|
||||
public <T> T executeExpectSuccess(final Class<T> expectedType) {
|
||||
final Pair<Integer, T> execute = execute(expectedType);
|
||||
Validate.isTrue(
|
||||
HttpUtils.isSuccessfulResponse(execute.getLeft()),
|
||||
"Unexpected response code: %d : %s",
|
||||
execute.getLeft(), execute.getRight());
|
||||
return requireNonNull(execute.getRight());
|
||||
}
|
||||
|
||||
public void executeExpectStatusCode(final int expectedStatusCode) {
|
||||
final Pair<Integer, Void> execute = execute(Void.class);
|
||||
Validate.isTrue(
|
||||
execute.getLeft() == expectedStatusCode,
|
||||
"Unexpected response code: %d",
|
||||
execute.getLeft()
|
||||
);
|
||||
}
|
||||
|
||||
public <T> Pair<Integer, T> execute(final Class<T> expectedType) {
|
||||
builder.uri(serverUri(endpoint, queryParams))
|
||||
.header(HttpHeaders.USER_AGENT, USER_AGENT);
|
||||
return CLIENT.sendAsync(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8))
|
||||
.whenComplete((response, error) -> {
|
||||
if (error != null) {
|
||||
logger.error("request error", error);
|
||||
error.printStackTrace();
|
||||
}
|
||||
})
|
||||
.thenApply(response -> {
|
||||
try {
|
||||
final T result = expectedType.equals(Void.class)
|
||||
? null
|
||||
: SystemMapper.jsonMapper().readValue(response.body(), expectedType);
|
||||
return Pair.of(response.statusCode(), result);
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.join();
|
||||
}
|
||||
}
|
||||
|
||||
private static FaultTolerantHttpClient buildClient() {
|
||||
try {
|
||||
return FaultTolerantHttpClient.newBuilder("integration-test", Executors.newFixedThreadPool(16))
|
||||
.withTrustedServerCertificates(CONFIG.rootCert())
|
||||
.build();
|
||||
} catch (final CertificateException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Config loadConfigFromClasspath(final String filename) {
|
||||
try {
|
||||
final URL configFileUrl = Resources.getResource(filename);
|
||||
final Config config = SystemMapper.yamlMapper().readValue(Resources.toByteArray(configFileUrl), Config.class);
|
||||
|
||||
final Set<ConstraintViolation<Config>> constraintViolations = Validators.newValidator().validate(config);
|
||||
|
||||
if (!constraintViolations.isEmpty()) {
|
||||
throw new ConfigurationValidationException(filename, constraintViolations);
|
||||
}
|
||||
|
||||
return config;
|
||||
} catch (final Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static ECSignedPreKey generateSignedECPreKey(final long id, final ECKeyPair identityKeyPair) {
|
||||
final ECPublicKey pubKey = ECKeyPair.generate().getPublicKey();
|
||||
final byte[] signature = identityKeyPair.getPrivateKey().calculateSignature(pubKey.serialize());
|
||||
return new ECSignedPreKey(id, pubKey, signature);
|
||||
}
|
||||
|
||||
public static KEMSignedPreKey generateSignedKEMPreKey(final long id, final ECKeyPair identityKeyPair) {
|
||||
final KEMPublicKey pubKey = KEMKeyPair.generate(KEMKeyType.KYBER_1024).getPublicKey();
|
||||
final byte[] signature = identityKeyPair.getPrivateKey().calculateSignature(pubKey.serialize());
|
||||
return new KEMSignedPreKey(id, pubKey, signature);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.integration;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||
|
||||
public class TestDevice {
|
||||
|
||||
private final byte deviceId;
|
||||
|
||||
private final Map<Integer, Pair<IdentityKeyPair, SignedPreKeyRecord>> signedPreKeys = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
public static TestDevice create(
|
||||
final byte deviceId,
|
||||
final IdentityKeyPair aciIdentityKeyPair,
|
||||
final IdentityKeyPair pniIdentityKeyPair) {
|
||||
final TestDevice device = new TestDevice(deviceId);
|
||||
device.addSignedPreKey(aciIdentityKeyPair);
|
||||
device.addSignedPreKey(pniIdentityKeyPair);
|
||||
return device;
|
||||
}
|
||||
|
||||
public TestDevice(final byte deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
public byte deviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public SignedPreKeyRecord latestSignedPreKey(final IdentityKeyPair identity) {
|
||||
final int id = signedPreKeys.entrySet()
|
||||
.stream()
|
||||
.filter(p -> p.getValue().getLeft().equals(identity))
|
||||
.mapToInt(Map.Entry::getKey)
|
||||
.max()
|
||||
.orElseThrow();
|
||||
return signedPreKeys.get(id).getRight();
|
||||
}
|
||||
|
||||
public SignedPreKeyRecord addSignedPreKey(final IdentityKeyPair identity) {
|
||||
final int nextId = signedPreKeys.keySet().stream().mapToInt(k -> k + 1).max().orElse(0);
|
||||
final ECKeyPair keyPair = ECKeyPair.generate();
|
||||
final byte[] signature = keyPair.getPrivateKey().calculateSignature(keyPair.getPublicKey().serialize());
|
||||
final SignedPreKeyRecord signedPreKeyRecord = new SignedPreKeyRecord(nextId, System.currentTimeMillis(), keyPair, signature);
|
||||
signedPreKeys.put(nextId, Pair.of(identity, signedPreKeyRecord));
|
||||
return signedPreKeyRecord;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.integration;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import java.security.SecureRandom;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||
import org.signal.libsignal.protocol.util.KeyHelper;
|
||||
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
public class TestUser {
|
||||
|
||||
private final int registrationId;
|
||||
|
||||
private final int pniRegistrationId;
|
||||
|
||||
private final IdentityKeyPair aciIdentityKey;
|
||||
|
||||
private final Map<Byte, TestDevice> devices = new ConcurrentHashMap<>();
|
||||
|
||||
private final byte[] unidentifiedAccessKey;
|
||||
|
||||
private String phoneNumber;
|
||||
|
||||
private IdentityKeyPair pniIdentityKey;
|
||||
|
||||
private String accountPassword;
|
||||
|
||||
private byte[] registrationPassword;
|
||||
|
||||
private UUID aciUuid;
|
||||
|
||||
private UUID pniUuid;
|
||||
|
||||
|
||||
public static TestUser create(final String phoneNumber, final String accountPassword, final byte[] registrationPassword) {
|
||||
// ACI identity key pair
|
||||
final IdentityKeyPair aciIdentityKey = IdentityKeyPair.generate();
|
||||
// PNI identity key pair
|
||||
final IdentityKeyPair pniIdentityKey = IdentityKeyPair.generate();
|
||||
// registration id
|
||||
final int registrationId = KeyHelper.generateRegistrationId(false);
|
||||
final int pniRegistrationId = KeyHelper.generateRegistrationId(false);
|
||||
// uak
|
||||
final byte[] unidentifiedAccessKey = new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH];
|
||||
new SecureRandom().nextBytes(unidentifiedAccessKey);
|
||||
|
||||
return new TestUser(
|
||||
registrationId,
|
||||
pniRegistrationId,
|
||||
aciIdentityKey,
|
||||
phoneNumber,
|
||||
pniIdentityKey,
|
||||
unidentifiedAccessKey,
|
||||
accountPassword,
|
||||
registrationPassword);
|
||||
}
|
||||
|
||||
public TestUser(
|
||||
final int registrationId,
|
||||
final int pniRegistrationId,
|
||||
final IdentityKeyPair aciIdentityKey,
|
||||
final String phoneNumber,
|
||||
final IdentityKeyPair pniIdentityKey,
|
||||
final byte[] unidentifiedAccessKey,
|
||||
final String accountPassword,
|
||||
final byte[] registrationPassword) {
|
||||
this.registrationId = registrationId;
|
||||
this.pniRegistrationId = pniRegistrationId;
|
||||
this.aciIdentityKey = aciIdentityKey;
|
||||
this.phoneNumber = phoneNumber;
|
||||
this.pniIdentityKey = pniIdentityKey;
|
||||
this.unidentifiedAccessKey = unidentifiedAccessKey;
|
||||
this.accountPassword = accountPassword;
|
||||
this.registrationPassword = registrationPassword;
|
||||
devices.put(Device.PRIMARY_ID, TestDevice.create(Device.PRIMARY_ID, aciIdentityKey, pniIdentityKey));
|
||||
}
|
||||
|
||||
public int registrationId() {
|
||||
return registrationId;
|
||||
}
|
||||
|
||||
public IdentityKeyPair aciIdentityKey() {
|
||||
return aciIdentityKey;
|
||||
}
|
||||
|
||||
public String phoneNumber() {
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
public IdentityKeyPair pniIdentityKey() {
|
||||
return pniIdentityKey;
|
||||
}
|
||||
|
||||
public String accountPassword() {
|
||||
return accountPassword;
|
||||
}
|
||||
|
||||
public byte[] registrationPassword() {
|
||||
return registrationPassword;
|
||||
}
|
||||
|
||||
public UUID aciUuid() {
|
||||
return aciUuid;
|
||||
}
|
||||
|
||||
public UUID pniUuid() {
|
||||
return pniUuid;
|
||||
}
|
||||
|
||||
public AccountAttributes accountAttributes() {
|
||||
return new AccountAttributes(true, registrationId, pniRegistrationId, "".getBytes(StandardCharsets.UTF_8), "", true, Set.of())
|
||||
.withUnidentifiedAccessKey(unidentifiedAccessKey)
|
||||
.withRecoveryPassword(registrationPassword);
|
||||
}
|
||||
|
||||
public void setAciUuid(final UUID aciUuid) {
|
||||
this.aciUuid = aciUuid;
|
||||
}
|
||||
|
||||
public void setPniUuid(final UUID pniUuid) {
|
||||
this.pniUuid = pniUuid;
|
||||
}
|
||||
|
||||
public void setPhoneNumber(final String phoneNumber) {
|
||||
this.phoneNumber = phoneNumber;
|
||||
}
|
||||
|
||||
public void setPniIdentityKey(final IdentityKeyPair pniIdentityKey) {
|
||||
this.pniIdentityKey = pniIdentityKey;
|
||||
}
|
||||
|
||||
public void setAccountPassword(final String accountPassword) {
|
||||
this.accountPassword = accountPassword;
|
||||
}
|
||||
|
||||
public void setRegistrationPassword(final byte[] registrationPassword) {
|
||||
this.registrationPassword = registrationPassword;
|
||||
}
|
||||
|
||||
public PreKeySetPublicView preKeys(final byte deviceId, final boolean pni) {
|
||||
final IdentityKeyPair identity = pni
|
||||
? pniIdentityKey
|
||||
: aciIdentityKey;
|
||||
final TestDevice device = requireNonNull(devices.get(deviceId));
|
||||
final SignedPreKeyRecord signedPreKeyRecord = device.latestSignedPreKey(identity);
|
||||
try {
|
||||
return new PreKeySetPublicView(
|
||||
Collections.emptyList(),
|
||||
identity.getPublicKey(),
|
||||
new SignedPreKeyPublicView(
|
||||
signedPreKeyRecord.getId(),
|
||||
signedPreKeyRecord.getKeyPair().getPublicKey(),
|
||||
signedPreKeyRecord.getSignature()
|
||||
)
|
||||
);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public record SignedPreKeyPublicView(
|
||||
int keyId,
|
||||
@JsonSerialize(using = Codecs.ECPublicKeySerializer.class)
|
||||
@JsonDeserialize(using = Codecs.ECPublicKeyDeserializer.class)
|
||||
ECPublicKey publicKey,
|
||||
@JsonSerialize(using = Codecs.ByteArraySerializer.class)
|
||||
@JsonDeserialize(using = Codecs.ByteArrayDeserializer.class)
|
||||
byte[] signature) {
|
||||
}
|
||||
|
||||
public record PreKeySetPublicView(
|
||||
List<String> preKeys,
|
||||
@JsonSerialize(using = Codecs.IdentityKeySerializer.class)
|
||||
@JsonDeserialize(using = Codecs.IdentityKeyDeserializer.class)
|
||||
IdentityKey identityKey,
|
||||
SignedPreKeyPublicView signedPreKey) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.integration.config;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.configuration.DynamoDbClientFactory;
|
||||
|
||||
public record Config(@NotBlank String domain,
|
||||
@NotBlank String rootCert,
|
||||
@NotNull @Valid DynamoDbClientFactory dynamoDbClient,
|
||||
@NotNull @Valid DynamoDbTables dynamoDbTables,
|
||||
@NotBlank String prescribedRegistrationNumber,
|
||||
@NotBlank String prescribedRegistrationCode) {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.integration.config;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record DynamoDbTables(@NotBlank String registrationRecovery,
|
||||
@NotBlank String verificationSessions,
|
||||
@NotBlank String phoneNumberIdentifiers) {
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.integration;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||
import org.signal.libsignal.usernames.Username;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountIdentifierResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.ChangeNumberRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.ConfirmUsernameHashRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.UsernameHashResponse;
|
||||
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
public class AccountTest {
|
||||
|
||||
@Test
|
||||
public void testCreateAccount() {
|
||||
final TestUser user = Operations.newRegisteredUser("+19995550101");
|
||||
try {
|
||||
final Pair<Integer, AccountIdentityResponse> execute = Operations.apiGet("/v1/accounts/whoami")
|
||||
.authorized(user)
|
||||
.execute(AccountIdentityResponse.class);
|
||||
assertEquals(HttpStatus.SC_OK, execute.getLeft());
|
||||
} finally {
|
||||
Operations.deleteUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateAccountAtomic() {
|
||||
final TestUser user = Operations.newRegisteredUser("+19995550201");
|
||||
try {
|
||||
final Pair<Integer, AccountIdentityResponse> execute = Operations.apiGet("/v1/accounts/whoami")
|
||||
.authorized(user)
|
||||
.execute(AccountIdentityResponse.class);
|
||||
assertEquals(HttpStatus.SC_OK, execute.getLeft());
|
||||
} finally {
|
||||
Operations.deleteUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changePhoneNumber() {
|
||||
final TestUser user = Operations.newRegisteredUser("+19995550301");
|
||||
final String targetNumber = "+19995550302";
|
||||
|
||||
final ECKeyPair pniIdentityKeyPair = ECKeyPair.generate();
|
||||
|
||||
final ChangeNumberRequest changeNumberRequest = new ChangeNumberRequest(null,
|
||||
Operations.populateRandomRecoveryPassword(targetNumber),
|
||||
targetNumber,
|
||||
null,
|
||||
new IdentityKey(pniIdentityKeyPair.getPublicKey()),
|
||||
Collections.emptyList(),
|
||||
Map.of(Device.PRIMARY_ID, Operations.generateSignedECPreKey(1, pniIdentityKeyPair)),
|
||||
Map.of(Device.PRIMARY_ID, Operations.generateSignedKEMPreKey(2, pniIdentityKeyPair)),
|
||||
Map.of(Device.PRIMARY_ID, 17));
|
||||
|
||||
final AccountIdentityResponse accountIdentityResponse =
|
||||
Operations.apiPut("/v2/accounts/number", changeNumberRequest)
|
||||
.authorized(user)
|
||||
.executeExpectSuccess(AccountIdentityResponse.class);
|
||||
|
||||
assertEquals(user.aciUuid(), accountIdentityResponse.uuid());
|
||||
assertNotEquals(user.pniUuid(), accountIdentityResponse.pni());
|
||||
assertEquals(targetNumber, accountIdentityResponse.number());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUsernameOperations() throws Exception {
|
||||
final TestUser user = Operations.newRegisteredUser("+19995550102");
|
||||
try {
|
||||
verifyFullUsernameLifecycle(user);
|
||||
// no do it again to check changing usernames
|
||||
verifyFullUsernameLifecycle(user);
|
||||
} finally {
|
||||
Operations.deleteUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
private static void verifyFullUsernameLifecycle(final TestUser user) throws BaseUsernameException {
|
||||
final String preferred = "test";
|
||||
final List<Username> candidates = Username.candidatesFrom(preferred, preferred.length(), preferred.length() + 1);
|
||||
|
||||
// reserve a username
|
||||
final ReserveUsernameHashRequest reserveUsernameHashRequest = new ReserveUsernameHashRequest(
|
||||
candidates.stream().map(Username::getHash).toList());
|
||||
// try unauthorized
|
||||
Operations
|
||||
.apiPut("/v1/accounts/username_hash/reserve", reserveUsernameHashRequest)
|
||||
.executeExpectStatusCode(HttpStatus.SC_UNAUTHORIZED);
|
||||
|
||||
final ReserveUsernameHashResponse reserveUsernameHashResponse = Operations
|
||||
.apiPut("/v1/accounts/username_hash/reserve", reserveUsernameHashRequest)
|
||||
.authorized(user)
|
||||
.executeExpectSuccess(ReserveUsernameHashResponse.class);
|
||||
|
||||
// find which one is the reserved username
|
||||
final byte[] reservedHash = reserveUsernameHashResponse.usernameHash();
|
||||
final Username reservedUsername = candidates.stream()
|
||||
.filter(u -> Arrays.equals(u.getHash(), reservedHash))
|
||||
.findAny()
|
||||
.orElseThrow();
|
||||
|
||||
// confirm a username
|
||||
final ConfirmUsernameHashRequest confirmUsernameHashRequest = new ConfirmUsernameHashRequest(
|
||||
reservedUsername.getHash(),
|
||||
reservedUsername.generateProof(),
|
||||
"cluck cluck i'm a parrot".getBytes()
|
||||
);
|
||||
// try unauthorized
|
||||
Operations
|
||||
.apiPut("/v1/accounts/username_hash/confirm", confirmUsernameHashRequest)
|
||||
.executeExpectStatusCode(HttpStatus.SC_UNAUTHORIZED);
|
||||
Operations
|
||||
.apiPut("/v1/accounts/username_hash/confirm", confirmUsernameHashRequest)
|
||||
.authorized(user)
|
||||
.executeExpectSuccess(UsernameHashResponse.class);
|
||||
|
||||
|
||||
// lookup username
|
||||
final AccountIdentifierResponse accountIdentifierResponse = Operations
|
||||
.apiGet("/v1/accounts/username_hash/" + Base64.getUrlEncoder().encodeToString(reservedHash))
|
||||
.executeExpectSuccess(AccountIdentifierResponse.class);
|
||||
assertEquals(new AciServiceIdentifier(user.aciUuid()), accountIdentifierResponse.uuid());
|
||||
// try authorized
|
||||
Operations
|
||||
.apiGet("/v1/accounts/username_hash/" + Base64.getUrlEncoder().encodeToString(reservedHash))
|
||||
.authorized(user)
|
||||
.executeExpectStatusCode(HttpStatus.SC_BAD_REQUEST);
|
||||
|
||||
// delete username
|
||||
Operations
|
||||
.apiDelete("/v1/accounts/username_hash")
|
||||
.authorized(user)
|
||||
.executeExpectSuccess();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.integration;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
|
||||
import org.whispersystems.textsecuregcm.entities.SendMessageResponse;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
public class MessagingTest {
|
||||
|
||||
@Test
|
||||
public void testSendMessageUnsealed() {
|
||||
final TestUser userA = Operations.newRegisteredUser("+19995550102");
|
||||
final TestUser userB = Operations.newRegisteredUser("+19995550103");
|
||||
|
||||
try {
|
||||
final byte[] expectedContent = "Hello, World!".getBytes(StandardCharsets.UTF_8);
|
||||
final IncomingMessage message = new IncomingMessage(1, Device.PRIMARY_ID, userB.registrationId(), expectedContent);
|
||||
final IncomingMessageList messages = new IncomingMessageList(List.of(message), false, true, System.currentTimeMillis());
|
||||
|
||||
Operations
|
||||
.apiPut("/v1/messages/%s".formatted(userB.aciUuid().toString()), messages)
|
||||
.authorized(userA)
|
||||
.execute(SendMessageResponse.class);
|
||||
|
||||
final Pair<Integer, OutgoingMessageEntityList> receiveMessages = Operations.apiGet("/v1/messages")
|
||||
.authorized(userB)
|
||||
.execute(OutgoingMessageEntityList.class);
|
||||
|
||||
final byte[] actualContent = receiveMessages.getRight().messages().getFirst().content();
|
||||
assertArrayEquals(expectedContent, actualContent);
|
||||
} finally {
|
||||
Operations.deleteUser(userA);
|
||||
Operations.deleteUser(userB);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.integration;
|
||||
|
||||
import io.micrometer.common.util.StringUtils;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.whispersystems.textsecuregcm.entities.CreateVerificationSessionRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.SubmitVerificationCodeRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.UpdateVerificationSessionRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.VerificationCodeRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.VerificationSessionResponse;
|
||||
|
||||
public class RegistrationTest {
|
||||
|
||||
@Test
|
||||
public void testRegistration() throws Exception {
|
||||
final UpdateVerificationSessionRequest originalRequest = new UpdateVerificationSessionRequest(
|
||||
"test", UpdateVerificationSessionRequest.PushTokenType.FCM, null, null, null, null);
|
||||
|
||||
final Operations.PrescribedVerificationNumber params = Operations.prescribedVerificationNumber();
|
||||
final CreateVerificationSessionRequest input = new CreateVerificationSessionRequest(params.number(),
|
||||
originalRequest);
|
||||
|
||||
final VerificationSessionResponse verificationSessionResponse = Operations
|
||||
.apiPost("/v1/verification/session", input)
|
||||
.executeExpectSuccess(VerificationSessionResponse.class);
|
||||
|
||||
final String sessionId = verificationSessionResponse.id();
|
||||
Assertions.assertTrue(StringUtils.isNotBlank(sessionId));
|
||||
|
||||
final String pushChallenge = Operations.peekVerificationSessionPushChallenge(sessionId);
|
||||
|
||||
// supply push challenge
|
||||
final UpdateVerificationSessionRequest updatedRequest = new UpdateVerificationSessionRequest(
|
||||
"test", UpdateVerificationSessionRequest.PushTokenType.FCM, pushChallenge, null, null, null);
|
||||
final VerificationSessionResponse pushChallengeSupplied = Operations
|
||||
.apiPatch("/v1/verification/session/%s".formatted(sessionId), updatedRequest)
|
||||
.executeExpectSuccess(VerificationSessionResponse.class);
|
||||
|
||||
Assertions.assertTrue(pushChallengeSupplied.allowedToRequestCode());
|
||||
|
||||
// request code
|
||||
final VerificationCodeRequest verificationCodeRequest = new VerificationCodeRequest(
|
||||
VerificationCodeRequest.Transport.SMS, "android-ng");
|
||||
|
||||
final VerificationSessionResponse codeRequested = Operations
|
||||
.apiPost("/v1/verification/session/%s/code".formatted(sessionId), verificationCodeRequest)
|
||||
.executeExpectSuccess(VerificationSessionResponse.class);
|
||||
|
||||
// verify code
|
||||
final SubmitVerificationCodeRequest submitVerificationCodeRequest = new SubmitVerificationCodeRequest(
|
||||
params.verificationCode());
|
||||
final VerificationSessionResponse codeVerified = Operations
|
||||
.apiPut("/v1/verification/session/%s/code".formatted(sessionId), submitVerificationCodeRequest)
|
||||
.executeExpectSuccess(VerificationSessionResponse.class);
|
||||
}
|
||||
}
|
||||
218
mvnw
vendored
218
mvnw
vendored
@@ -19,7 +19,7 @@
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Maven Start Up Batch script
|
||||
# Apache Maven Wrapper startup batch script, version 3.2.0
|
||||
#
|
||||
# Required ENV vars:
|
||||
# ------------------
|
||||
@@ -27,7 +27,6 @@
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# M2_HOME - location of maven2's installed home dir
|
||||
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
# e.g. to debug Maven itself, use
|
||||
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
@@ -54,7 +53,7 @@ fi
|
||||
cygwin=false;
|
||||
darwin=false;
|
||||
mingw=false
|
||||
case "`uname`" in
|
||||
case "$(uname)" in
|
||||
CYGWIN*) cygwin=true ;;
|
||||
MINGW*) mingw=true;;
|
||||
Darwin*) darwin=true
|
||||
@@ -62,9 +61,9 @@ case "`uname`" in
|
||||
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
if [ -x "/usr/libexec/java_home" ]; then
|
||||
export JAVA_HOME="`/usr/libexec/java_home`"
|
||||
JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
|
||||
else
|
||||
export JAVA_HOME="/Library/Java/Home"
|
||||
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
@@ -72,68 +71,38 @@ esac
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
if [ -r /etc/gentoo-release ] ; then
|
||||
JAVA_HOME=`java-config --jre-home`
|
||||
JAVA_HOME=$(java-config --jre-home)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$M2_HOME" ] ; then
|
||||
## resolve links - $0 may be a link to maven's home
|
||||
PRG="$0"
|
||||
|
||||
# need this for relative symlinks
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG="`dirname "$PRG"`/$link"
|
||||
fi
|
||||
done
|
||||
|
||||
saveddir=`pwd`
|
||||
|
||||
M2_HOME=`dirname "$PRG"`/..
|
||||
|
||||
# make it fully qualified
|
||||
M2_HOME=`cd "$M2_HOME" && pwd`
|
||||
|
||||
cd "$saveddir"
|
||||
# echo Using m2 at $M2_HOME
|
||||
fi
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched
|
||||
if $cygwin ; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME=`cygpath --unix "$M2_HOME"`
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
||||
JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
|
||||
CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
|
||||
fi
|
||||
|
||||
# For Mingw, ensure paths are in UNIX format before anything is touched
|
||||
if $mingw ; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME="`(cd "$M2_HOME"; pwd)`"
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
|
||||
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
|
||||
JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
javaExecutable="`which javac`"
|
||||
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
|
||||
javaExecutable="$(which javac)"
|
||||
if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
|
||||
# readlink(1) is not available as standard on Solaris 10.
|
||||
readLink=`which readlink`
|
||||
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
|
||||
readLink=$(which readlink)
|
||||
if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
|
||||
if $darwin ; then
|
||||
javaHome="`dirname \"$javaExecutable\"`"
|
||||
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
|
||||
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||
javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
|
||||
else
|
||||
javaExecutable="`readlink -f \"$javaExecutable\"`"
|
||||
javaExecutable="$(readlink -f "\"$javaExecutable\"")"
|
||||
fi
|
||||
javaHome="`dirname \"$javaExecutable\"`"
|
||||
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
|
||||
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
|
||||
JAVA_HOME="$javaHome"
|
||||
export JAVA_HOME
|
||||
fi
|
||||
@@ -149,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
else
|
||||
JAVACMD="`\\unset -f command; \\command -v java`"
|
||||
JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -163,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then
|
||||
echo "Warning: JAVA_HOME environment variable is not set."
|
||||
fi
|
||||
|
||||
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
|
||||
|
||||
# traverses directory structure from process work directory to filesystem root
|
||||
# first directory with .mvn subdirectory is considered project base directory
|
||||
find_maven_basedir() {
|
||||
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "Path not specified to find_maven_basedir"
|
||||
@@ -184,96 +150,99 @@ find_maven_basedir() {
|
||||
fi
|
||||
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
|
||||
if [ -d "${wdir}" ]; then
|
||||
wdir=`cd "$wdir/.."; pwd`
|
||||
wdir=$(cd "$wdir/.." || exit 1; pwd)
|
||||
fi
|
||||
# end of workaround
|
||||
done
|
||||
echo "${basedir}"
|
||||
printf '%s' "$(cd "$basedir" || exit 1; pwd)"
|
||||
}
|
||||
|
||||
# concatenates all lines of a file
|
||||
concat_lines() {
|
||||
if [ -f "$1" ]; then
|
||||
echo "$(tr -s '\n' ' ' < "$1")"
|
||||
# Remove \r in case we run on Windows within Git Bash
|
||||
# and check out the repository with auto CRLF management
|
||||
# enabled. Otherwise, we may read lines that are delimited with
|
||||
# \r\n and produce $'-Xarg\r' rather than -Xarg due to word
|
||||
# splitting rules.
|
||||
tr -s '\r\n' ' ' < "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
BASE_DIR=`find_maven_basedir "$(pwd)"`
|
||||
log() {
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
printf '%s\n' "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
|
||||
if [ -z "$BASE_DIR" ]; then
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
|
||||
log "$MAVEN_PROJECTBASEDIR"
|
||||
|
||||
##########################################################################################
|
||||
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
# This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
##########################################################################################
|
||||
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found .mvn/wrapper/maven-wrapper.jar"
|
||||
fi
|
||||
wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
|
||||
if [ -r "$wrapperJarPath" ]; then
|
||||
log "Found $wrapperJarPath"
|
||||
else
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
|
||||
fi
|
||||
log "Couldn't find $wrapperJarPath, downloading it ..."
|
||||
|
||||
if [ -n "$MVNW_REPOURL" ]; then
|
||||
jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
|
||||
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
else
|
||||
jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
|
||||
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
fi
|
||||
while IFS="=" read key value; do
|
||||
case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
|
||||
while IFS="=" read -r key value; do
|
||||
# Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
|
||||
safeValue=$(echo "$value" | tr -d '\r')
|
||||
case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
|
||||
esac
|
||||
done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Downloading from: $jarUrl"
|
||||
fi
|
||||
wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
|
||||
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
log "Downloading from: $wrapperUrl"
|
||||
|
||||
if $cygwin; then
|
||||
wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
|
||||
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
|
||||
fi
|
||||
|
||||
if command -v wget > /dev/null; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found wget ... using wget"
|
||||
fi
|
||||
log "Found wget ... using wget"
|
||||
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
else
|
||||
wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
elif command -v curl > /dev/null; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found curl ... using curl"
|
||||
fi
|
||||
log "Found curl ... using curl"
|
||||
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
curl -o "$wrapperJarPath" "$jarUrl" -f
|
||||
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||
else
|
||||
curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
|
||||
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
|
||||
else
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Falling back to using Java to download"
|
||||
fi
|
||||
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
||||
log "Falling back to using Java to download"
|
||||
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
||||
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
|
||||
# For Cygwin, switch paths to Windows format before running javac
|
||||
if $cygwin; then
|
||||
javaClass=`cygpath --path --windows "$javaClass"`
|
||||
javaSource=$(cygpath --path --windows "$javaSource")
|
||||
javaClass=$(cygpath --path --windows "$javaClass")
|
||||
fi
|
||||
if [ -e "$javaClass" ]; then
|
||||
if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo " - Compiling MavenWrapperDownloader.java ..."
|
||||
fi
|
||||
# Compiling the Java class
|
||||
("$JAVA_HOME/bin/javac" "$javaClass")
|
||||
if [ -e "$javaSource" ]; then
|
||||
if [ ! -e "$javaClass" ]; then
|
||||
log " - Compiling MavenWrapperDownloader.java ..."
|
||||
("$JAVA_HOME/bin/javac" "$javaSource")
|
||||
fi
|
||||
if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
|
||||
# Running the downloader
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo " - Running MavenWrapperDownloader.java ..."
|
||||
fi
|
||||
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
|
||||
if [ -e "$javaClass" ]; then
|
||||
log " - Running MavenWrapperDownloader.java ..."
|
||||
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -282,35 +251,58 @@ fi
|
||||
# End of extension
|
||||
##########################################################################################
|
||||
|
||||
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo $MAVEN_PROJECTBASEDIR
|
||||
# If specified, validate the SHA-256 sum of the Maven wrapper jar file
|
||||
wrapperSha256Sum=""
|
||||
while IFS="=" read -r key value; do
|
||||
case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
|
||||
esac
|
||||
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
if [ -n "$wrapperSha256Sum" ]; then
|
||||
wrapperSha256Result=false
|
||||
if command -v sha256sum > /dev/null; then
|
||||
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c - > /dev/null 2>&1; then
|
||||
wrapperSha256Result=true
|
||||
fi
|
||||
elif command -v shasum > /dev/null; then
|
||||
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
|
||||
wrapperSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
|
||||
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
|
||||
exit 1
|
||||
fi
|
||||
if [ $wrapperSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
|
||||
echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME=`cygpath --path --windows "$M2_HOME"`
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
|
||||
JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
|
||||
CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
|
||||
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
|
||||
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
|
||||
MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
|
||||
fi
|
||||
|
||||
# Provide a "standardized" way to retrieve the CLI args that will
|
||||
# work with both Windows and non-Windows executions.
|
||||
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
|
||||
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
|
||||
export MAVEN_CMD_LINE_ARGS
|
||||
|
||||
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
# shellcheck disable=SC2086 # safe args
|
||||
exec "$JAVACMD" \
|
||||
$MAVEN_OPTS \
|
||||
$MAVEN_DEBUG_OPTS \
|
||||
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
|
||||
"-Dmaven.home=${M2_HOME}" \
|
||||
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
|
||||
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
|
||||
|
||||
31
mvnw.cmd
vendored
31
mvnw.cmd
vendored
@@ -18,13 +18,12 @@
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Maven Start Up Batch script
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.2.0
|
||||
@REM
|
||||
@REM Required ENV vars:
|
||||
@REM JAVA_HOME - location of a JDK home dir
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM M2_HOME - location of maven2's installed home dir
|
||||
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
|
||||
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
|
||||
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
@@ -120,10 +119,10 @@ SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
|
||||
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
|
||||
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
|
||||
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
|
||||
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
|
||||
)
|
||||
|
||||
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
@@ -134,11 +133,11 @@ if exist %WRAPPER_JAR% (
|
||||
)
|
||||
) else (
|
||||
if not "%MVNW_REPOURL%" == "" (
|
||||
SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
|
||||
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||
)
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Couldn't find %WRAPPER_JAR%, downloading it ...
|
||||
echo Downloading from: %DOWNLOAD_URL%
|
||||
echo Downloading from: %WRAPPER_URL%
|
||||
)
|
||||
|
||||
powershell -Command "&{"^
|
||||
@@ -146,7 +145,7 @@ if exist %WRAPPER_JAR% (
|
||||
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
|
||||
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
|
||||
"}"^
|
||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
|
||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
|
||||
"}"
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Finished downloading %WRAPPER_JAR%
|
||||
@@ -154,6 +153,24 @@ if exist %WRAPPER_JAR% (
|
||||
)
|
||||
@REM End of extension
|
||||
|
||||
@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
|
||||
SET WRAPPER_SHA_256_SUM=""
|
||||
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
|
||||
)
|
||||
IF NOT %WRAPPER_SHA_256_SUM%=="" (
|
||||
powershell -Command "&{"^
|
||||
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
|
||||
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
|
||||
" Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
|
||||
" Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
|
||||
" Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
|
||||
" exit 1;"^
|
||||
"}"^
|
||||
"}"
|
||||
if ERRORLEVEL 1 goto error
|
||||
)
|
||||
|
||||
@REM Provide a "standardized" way to retrieve the CLI args that will
|
||||
@REM work with both Windows and non-Windows executions.
|
||||
set MAVEN_CMD_LINE_ARGS=%*
|
||||
|
||||
367
pom.xml
367
pom.xml
@@ -14,14 +14,6 @@
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>dynamodb-local-oregon</id>
|
||||
<name>DynamoDB Local Release Repository</name>
|
||||
<url>https://s3-us-west-2.amazonaws.com/dynamodb-local/release</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<pluginRepositories>
|
||||
@@ -38,42 +30,67 @@
|
||||
</pluginRepositories>
|
||||
|
||||
<modules>
|
||||
<module>event-logger</module>
|
||||
<module>redis-dispatch</module>
|
||||
<module>websocket-resources</module>
|
||||
<module>api-doc</module>
|
||||
<module>integration-tests</module>
|
||||
<module>service</module>
|
||||
<module>websocket-resources</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<aws.sdk.version>1.12.376</aws.sdk.version>
|
||||
<aws.sdk2.version>2.19.8</aws.sdk2.version>
|
||||
<braintree.version>3.19.0</braintree.version>
|
||||
<commons-csv.version>1.9.0</commons-csv.version>
|
||||
<commons-io.version>2.9.0</commons-io.version>
|
||||
<dropwizard.version>2.0.34</dropwizard.version>
|
||||
<dropwizard-metrics-datadog.version>1.1.13</dropwizard-metrics-datadog.version>
|
||||
<google-cloud-libraries.version>26.1.3</google-cloud-libraries.version>
|
||||
<grpc.version>1.51.1</grpc.version> <!-- this should be kept in sync with the value from Google’s libraries-bom -->
|
||||
<gson.version>2.9.0</gson.version>
|
||||
<jackson.version>2.13.4</jackson.version>
|
||||
<jaxb.version>2.3.1</jaxb.version>
|
||||
<jedis.version>2.9.0</jedis.version>
|
||||
<kotlin.version>1.8.0</kotlin.version>
|
||||
<kotlinx-serialization.version>1.4.1</kotlinx-serialization.version>
|
||||
<lettuce.version>6.2.1.RELEASE</lettuce.version>
|
||||
<libphonenumber.version>8.12.54</libphonenumber.version>
|
||||
<logstash.logback.version>7.2</logstash.logback.version>
|
||||
<micrometer.version>1.10.3</micrometer.version>
|
||||
<mockito.version>4.11.0</mockito.version>
|
||||
<netty.version>4.1.82.Final</netty.version>
|
||||
<opentest4j.version>1.2.0</opentest4j.version>
|
||||
<protobuf.version>3.21.7</protobuf.version>
|
||||
<pushy.version>0.15.2</pushy.version>
|
||||
<resilience4j.version>1.7.0</resilience4j.version>
|
||||
<aws.sdk2.version>2.33.8</aws.sdk2.version>
|
||||
<braintree.version>3.44.0</braintree.version>
|
||||
<commons-csv.version>1.14.1</commons-csv.version>
|
||||
<commons-io.version>2.20.0</commons-io.version>
|
||||
<dropwizard.version>4.0.16</dropwizard.version>
|
||||
<!-- Note: when updating FoundationDB, also include a copy of `libfdb_c.so` from the FoundationDB release at
|
||||
src/main/jib/usr/lib/libfdb_c.so. We use x86_64 builds without AVX instructions enabled (i.e. FoundationDB versions
|
||||
with even-numbered patch versions). Also when updating FoundationDB, make sure to update the version of FoundationDB
|
||||
used by GitHub Actions. -->
|
||||
<foundationdb.version>7.3.62</foundationdb.version>
|
||||
<foundationdb.api-version>730</foundationdb.api-version>
|
||||
<foundationdb.client-library-sha256>bfed237b787fae3cde1222676e6bfbb0d218fc27bf9e903397a7a7aa96fb2d33</foundationdb.client-library-sha256>
|
||||
<google-cloud-libraries.version>26.67.0</google-cloud-libraries.version>
|
||||
<grpc.version>1.73.0</grpc.version> <!-- should be kept in sync with the value from Google libraries-bom -->
|
||||
<gson.version>2.13.2</gson.version>
|
||||
<!-- several libraries (AWS, Google Cloud) use Apache http components transitively, and we need to align them -->
|
||||
<httpcore.version>4.4.16</httpcore.version>
|
||||
<httpclient.version>4.5.14</httpclient.version>
|
||||
<jackson.version>2.20.0</jackson.version>
|
||||
<junit-pioneer.version>2.3.0</junit-pioneer.version>
|
||||
<jsr305.version>3.0.2</jsr305.version>
|
||||
<kotlin.version>2.2.20</kotlin.version>
|
||||
<!-- Logback 1.5.14+ has a null pointer bug: https://github.com/qos-ch/logback/issues/929. -->
|
||||
<logback.version>1.5.13</logback.version>
|
||||
<logback-access-common.version>2.0.6</logback-access-common.version>
|
||||
<lettuce.version>6.8.1.RELEASE</lettuce.version>
|
||||
<libphonenumber.version>9.0.13</libphonenumber.version>
|
||||
<logstash.logback.version>8.1</logstash.logback.version>
|
||||
<log4j-bom.version>2.25.1</log4j-bom.version>
|
||||
<luajava.version>3.5.0</luajava.version>
|
||||
<micrometer.version>1.15.4</micrometer.version>
|
||||
<netty.version>4.1.127.Final</netty.version>
|
||||
<!-- Must be less than or equal to the value from Google libraries-bom which controls the protobuf runtime version.
|
||||
See https://protobuf.dev/support/cross-version-runtime-guarantee/. -->
|
||||
<protoc.version>4.29.4</protoc.version>
|
||||
<pushy.version>0.15.4</pushy.version>
|
||||
<reactive.grpc.version>1.2.4</reactive.grpc.version>
|
||||
<reactor-bom.version>2024.0.10</reactor-bom.version> <!-- 3.7.11, see https://github.com/reactor/reactor#bom-versioning-scheme -->
|
||||
<resilience4j.version>2.3.0</resilience4j.version>
|
||||
<semver4j.version>3.1.0</semver4j.version>
|
||||
<slf4j.version>1.7.30</slf4j.version>
|
||||
<stripe.version>21.2.0</stripe.version>
|
||||
<vavr.version>0.10.4</vavr.version>
|
||||
<simple-grpc.version>0.1.0</simple-grpc.version>
|
||||
<slf4j.version>2.0.17</slf4j.version>
|
||||
<stripe.version>30.2.0</stripe.version>
|
||||
<swagger.version>2.2.36</swagger.version>
|
||||
<testcontainers.version>1.21.3</testcontainers.version>
|
||||
|
||||
<!-- images to use in tests via testcontainers -->
|
||||
<dynamodb.image>amazon/dynamodb-local:3.0.0@sha256:2fed5e3a965a4ba5aa6ac82baec57058b5a3848e959d705518f3fd579a77e76b</dynamodb.image>
|
||||
<localstack.image>localstack/localstack:4@sha256:5a97e0f9917a3f0d9630bb13b9d8ccf10cbe52f33252807d3b4e21418cc21348</localstack.image>
|
||||
<redis.image>redis:7.4-alpine@sha256:af1d0fc3f63b02b13ff7906c9baf7c5b390b8881ca08119cd570677fe2f60b55</redis.image>
|
||||
<redis-cluster.image>docker.io/bitnamilegacy/redis-cluster:7.4.3@sha256:a53d023fdfaf8a8d7ddc58da040d3494e4cb45772644618ffa44c42dcd32b9af</redis-cluster.image>
|
||||
|
||||
<!-- eclipse-temurin:24.0.2_12-jre-noble (note: always use the multi-arch manifest *LIST* here) -->
|
||||
<docker.image.sha256>85ecfc9bbb42af046d2bacbf1219d2005be4840cbfa16c2e6fd910d9ccfec95b</docker.image.sha256>
|
||||
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
@@ -112,13 +129,6 @@
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-bom</artifactId>
|
||||
<version>${aws.sdk.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>bom</artifactId>
|
||||
@@ -133,6 +143,11 @@
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.salesforce.servicelibs</groupId>
|
||||
<artifactId>reactor-grpc-stub</artifactId>
|
||||
<version>${reactive.grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-bom</artifactId>
|
||||
@@ -147,10 +162,24 @@
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.opentelemetry</groupId>
|
||||
<artifactId>opentelemetry-bom</artifactId>
|
||||
<version>1.54.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.opentelemetry.instrumentation</groupId>
|
||||
<artifactId>opentelemetry-instrumentation-bom</artifactId>
|
||||
<version>2.20.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-bom</artifactId>
|
||||
<version>2020.0.24</version> <!-- 3.4.x, see https://github.com/reactor/reactor#bom-versioning-scheme -->
|
||||
<version>${reactor-bom.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
@@ -171,11 +200,6 @@
|
||||
<artifactId>pushy-dropwizard-metrics-listener</artifactId>
|
||||
<version>${pushy.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
<version>${protobuf.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.googlecode.libphonenumber</groupId>
|
||||
<artifactId>libphonenumber</artifactId>
|
||||
@@ -196,16 +220,6 @@
|
||||
<artifactId>lettuce-core</artifactId>
|
||||
<version>${lettuce.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vavr</groupId>
|
||||
<artifactId>vavr</artifactId>
|
||||
<version>${vavr.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
<version>${jaxb.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.logstash.logback</groupId>
|
||||
<artifactId>logstash-logback-encoder</artifactId>
|
||||
@@ -217,33 +231,9 @@
|
||||
<version>${commons-csv.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.coursera</groupId>
|
||||
<artifactId>dropwizard-metrics-datadog</artifactId>
|
||||
<version>${dropwizard-metrics-datadog.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jaxb</groupId>
|
||||
<artifactId>jaxb-runtime</artifactId>
|
||||
<version>${jaxb.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-inline</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.opentest4j</groupId>
|
||||
<artifactId>opentest4j</artifactId>
|
||||
<version>${opentest4j.version}</version>
|
||||
<scope>test</scope>
|
||||
<groupId>org.foundationdb</groupId>
|
||||
<artifactId>fdb-java</artifactId>
|
||||
<version>${foundationdb.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
@@ -256,20 +246,15 @@
|
||||
<version>${slf4j.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
<version>${jedis.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
<version>1.2</version>
|
||||
<version>1.3.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.ow2.asm</groupId>
|
||||
<artifactId>asm</artifactId>
|
||||
<version>9.2</version>
|
||||
<version>9.8</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -282,29 +267,77 @@
|
||||
<artifactId>braintree-java</artifactId>
|
||||
<version>${braintree.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
<version>${jsr305.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>${gson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>embedded-redis</artifactId>
|
||||
<version>0.8.3</version>
|
||||
<groupId>com.redis</groupId>
|
||||
<artifactId>testcontainers-redis</artifactId>
|
||||
<version>2.2.4</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>libsignal-server</artifactId>
|
||||
<version>0.21.1</version>
|
||||
<version>0.80.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>simple-grpc-runtime</artifactId>
|
||||
<version>${simple-grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-bom</artifactId>
|
||||
<version>2.17.1</version>
|
||||
<version>${log4j-bom.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpcore</artifactId>
|
||||
<version>${httpcore.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>${httpclient.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-core</artifactId>
|
||||
<version>${logback.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>${logback.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback.access</groupId>
|
||||
<artifactId>logback-access-common</artifactId>
|
||||
<version>${logback-access-common.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers-bom</artifactId>
|
||||
<version>${testcontainers.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>earth.adi</groupId>
|
||||
<artifactId>testcontainers-foundationdb</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
@@ -316,25 +349,18 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.tomakehurst</groupId>
|
||||
<artifactId>wiremock-jre8</artifactId>
|
||||
<version>2.35.0</version>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>aws-crt-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.wiremock</groupId>
|
||||
<artifactId>wiremock</artifactId>
|
||||
<version>3.13.1</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.hamcrest</groupId>
|
||||
<artifactId>hamcrest-core</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -350,7 +376,7 @@
|
||||
<dependency>
|
||||
<groupId>org.junit-pioneer</groupId>
|
||||
<artifactId>junit-pioneer</artifactId>
|
||||
<version>1.9.1</version>
|
||||
<version>${junit-pioneer.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -387,6 +413,50 @@
|
||||
<version>1.7.0</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.google.cloud.tools</groupId>
|
||||
<artifactId>jib-maven-plugin</artifactId>
|
||||
<version>3.4.4</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.5.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>3.5.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.4.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>3.7.1</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>properties-maven-plugin</artifactId>
|
||||
<version>1.2.1</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
<plugins>
|
||||
|
||||
<plugin>
|
||||
@@ -395,9 +465,27 @@
|
||||
<version>0.6.1</version>
|
||||
<configuration>
|
||||
<checkStaleness>false</checkStaleness>
|
||||
<protocArtifact>com.google.protobuf:protoc:3.21.1:exe:${os.detected.classifier}</protocArtifact>
|
||||
<protocArtifact>com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}</protocArtifact>
|
||||
<pluginId>grpc-java</pluginId>
|
||||
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
|
||||
|
||||
<protocPlugins>
|
||||
<protocPlugin>
|
||||
<id>reactor-grpc</id>
|
||||
<groupId>com.salesforce.servicelibs</groupId>
|
||||
<artifactId>reactor-grpc</artifactId>
|
||||
<version>${reactive.grpc.version}</version>
|
||||
<mainClass>com.salesforce.reactorgrpc.ReactorGrpcGenerator</mainClass>
|
||||
</protocPlugin>
|
||||
|
||||
<protocPlugin>
|
||||
<id>simple</id>
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>simple-grpc-generator</artifactId>
|
||||
<version>${simple-grpc.version}</version>
|
||||
<mainClass>org.signal.grpc.simple.SimpleGrpcGenerator</mainClass>
|
||||
</protocPlugin>
|
||||
</protocPlugins>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
@@ -405,6 +493,7 @@
|
||||
<goal>compile</goal>
|
||||
<goal>compile-custom</goal>
|
||||
<goal>test-compile</goal>
|
||||
<goal>test-compile-custom</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
@@ -413,16 +502,16 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<release>17</release>
|
||||
<release>24</release>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.2.0</version>
|
||||
<version>3.4.2</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
@@ -435,41 +524,27 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<version>3.1.2</version>
|
||||
<version>3.8.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy</id>
|
||||
<phase>test-compile</phase>
|
||||
<!--
|
||||
Set dependencies as properties for use in argLine property for mockito jar.
|
||||
The property isn't needed until the test phase, and deferring it from the default
|
||||
`initialize` addresses issues running lifecycle phases that precede `test` in isolation.
|
||||
-->
|
||||
<phase>process-test-classes</phase>
|
||||
<goals>
|
||||
<goal>copy-dependencies</goal>
|
||||
<goal>properties</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<includeScope>test</includeScope>
|
||||
<includeTypes>so,dll,dylib</includeTypes>
|
||||
<outputDirectory>${project.build.directory}/lib</outputDirectory>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
<configuration>
|
||||
<systemProperties>
|
||||
<property>
|
||||
<name>sqlite4java.library.path</name>
|
||||
<value>${project.build.directory}/lib</value>
|
||||
</property>
|
||||
</systemProperties>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-enforcer-plugin</artifactId>
|
||||
<version>3.0.0-M3</version>
|
||||
<version>3.5.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
@@ -479,7 +554,7 @@
|
||||
<rules>
|
||||
<dependencyConvergence/>
|
||||
<requireMavenVersion>
|
||||
<version>3.8.6</version>
|
||||
<version>3.9.11</version>
|
||||
</requireMavenVersion>
|
||||
</rules>
|
||||
</configuration>
|
||||
@@ -490,7 +565,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-install-plugin</artifactId>
|
||||
<version>3.0.0-M1</version>
|
||||
<version>3.1.3</version>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
@@ -499,7 +574,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<version>3.0.0-M1</version>
|
||||
<version>3.1.3</version>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>TextSecureServer</artifactId>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<version>JGITVER</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>redis-dispatch</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -1,11 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.dispatch;
|
||||
|
||||
public interface DispatchChannel {
|
||||
void onDispatchMessage(String channel, byte[] message);
|
||||
void onDispatchSubscribed(String channel);
|
||||
void onDispatchUnsubscribed(String channel);
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.dispatch;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.dispatch.io.RedisPubSubConnectionFactory;
|
||||
import org.whispersystems.dispatch.redis.PubSubConnection;
|
||||
import org.whispersystems.dispatch.redis.PubSubReply;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class DispatchManager extends Thread {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DispatchManager.class);
|
||||
private final Executor executor = Executors.newCachedThreadPool();
|
||||
private final Map<String, DispatchChannel> subscriptions = new ConcurrentHashMap<>();
|
||||
|
||||
private final Optional<DispatchChannel> deadLetterChannel;
|
||||
private final RedisPubSubConnectionFactory redisPubSubConnectionFactory;
|
||||
|
||||
private PubSubConnection pubSubConnection;
|
||||
private volatile boolean running;
|
||||
|
||||
public DispatchManager(RedisPubSubConnectionFactory redisPubSubConnectionFactory,
|
||||
Optional<DispatchChannel> deadLetterChannel)
|
||||
{
|
||||
this.redisPubSubConnectionFactory = redisPubSubConnectionFactory;
|
||||
this.deadLetterChannel = deadLetterChannel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.pubSubConnection = redisPubSubConnectionFactory.connect();
|
||||
this.running = true;
|
||||
super.start();
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
this.running = false;
|
||||
this.pubSubConnection.close();
|
||||
}
|
||||
|
||||
public synchronized void subscribe(String name, DispatchChannel dispatchChannel) {
|
||||
Optional<DispatchChannel> previous = Optional.ofNullable(subscriptions.get(name));
|
||||
subscriptions.put(name, dispatchChannel);
|
||||
|
||||
try {
|
||||
pubSubConnection.subscribe(name);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Subscription error", e);
|
||||
}
|
||||
|
||||
previous.ifPresent(channel -> dispatchUnsubscription(name, channel));
|
||||
}
|
||||
|
||||
public synchronized void unsubscribe(String name, DispatchChannel channel) {
|
||||
Optional<DispatchChannel> subscription = Optional.ofNullable(subscriptions.get(name));
|
||||
|
||||
if (subscription.isPresent() && subscription.get() == channel) {
|
||||
subscriptions.remove(name);
|
||||
|
||||
try {
|
||||
pubSubConnection.unsubscribe(name);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Unsubscribe error", e);
|
||||
}
|
||||
|
||||
dispatchUnsubscription(name, subscription.get());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasSubscription(String name) {
|
||||
return subscriptions.containsKey(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (running) {
|
||||
try {
|
||||
PubSubReply reply = pubSubConnection.read();
|
||||
|
||||
switch (reply.getType()) {
|
||||
case UNSUBSCRIBE: break;
|
||||
case SUBSCRIBE: dispatchSubscribe(reply); break;
|
||||
case MESSAGE: dispatchMessage(reply); break;
|
||||
default: throw new AssertionError("Unknown pubsub reply type! " + reply.getType());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("***** PubSub Connection Error *****", e);
|
||||
if (running) {
|
||||
this.pubSubConnection.close();
|
||||
this.pubSubConnection = redisPubSubConnectionFactory.connect();
|
||||
resubscribeAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.warn("DispatchManager Shutting Down...");
|
||||
}
|
||||
|
||||
private void dispatchSubscribe(final PubSubReply reply) {
|
||||
Optional<DispatchChannel> subscription = Optional.ofNullable(subscriptions.get(reply.getChannel()));
|
||||
|
||||
if (subscription.isPresent()) {
|
||||
dispatchSubscription(reply.getChannel(), subscription.get());
|
||||
} else {
|
||||
logger.info("Received subscribe event for non-existing channel: " + reply.getChannel());
|
||||
}
|
||||
}
|
||||
|
||||
private void dispatchMessage(PubSubReply reply) {
|
||||
Optional<DispatchChannel> subscription = Optional.ofNullable(subscriptions.get(reply.getChannel()));
|
||||
|
||||
if (subscription.isPresent()) {
|
||||
dispatchMessage(reply.getChannel(), subscription.get(), reply.getContent().get());
|
||||
} else if (deadLetterChannel.isPresent()) {
|
||||
dispatchMessage(reply.getChannel(), deadLetterChannel.get(), reply.getContent().get());
|
||||
} else {
|
||||
logger.warn("Received message for non-existing channel, with no dead letter handler: " + reply.getChannel());
|
||||
}
|
||||
}
|
||||
|
||||
private void resubscribeAll() {
|
||||
new Thread(() -> {
|
||||
synchronized (DispatchManager.this) {
|
||||
try {
|
||||
for (String name : subscriptions.keySet()) {
|
||||
pubSubConnection.subscribe(name);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("***** RESUBSCRIPTION ERROR *****", e);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void dispatchMessage(final String name, final DispatchChannel channel, final byte[] message) {
|
||||
executor.execute(() -> channel.onDispatchMessage(name, message));
|
||||
}
|
||||
|
||||
private void dispatchSubscription(final String name, final DispatchChannel channel) {
|
||||
executor.execute(() -> channel.onDispatchSubscribed(name));
|
||||
}
|
||||
|
||||
private void dispatchUnsubscription(final String name, final DispatchChannel channel) {
|
||||
executor.execute(() -> channel.onDispatchUnsubscribed(name));
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.dispatch.io;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class RedisInputStream {
|
||||
|
||||
private static final byte CR = 0x0D;
|
||||
private static final byte LF = 0x0A;
|
||||
|
||||
private final InputStream inputStream;
|
||||
|
||||
public RedisInputStream(InputStream inputStream) {
|
||||
this.inputStream = inputStream;
|
||||
}
|
||||
|
||||
public String readLine() throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
boolean foundCr = false;
|
||||
|
||||
while (true) {
|
||||
int character = inputStream.read();
|
||||
|
||||
if (character == -1) {
|
||||
throw new IOException("Stream closed!");
|
||||
}
|
||||
|
||||
baos.write(character);
|
||||
|
||||
if (foundCr && character == LF) break;
|
||||
else if (character == CR) foundCr = true;
|
||||
else if (foundCr) foundCr = false;
|
||||
}
|
||||
|
||||
byte[] data = baos.toByteArray();
|
||||
return new String(data, 0, data.length-2);
|
||||
}
|
||||
|
||||
public byte[] readFully(int size) throws IOException {
|
||||
byte[] result = new byte[size];
|
||||
int offset = 0;
|
||||
int remaining = result.length;
|
||||
|
||||
while (remaining > 0) {
|
||||
int read = inputStream.read(result, offset, remaining);
|
||||
|
||||
if (read < 0) {
|
||||
throw new IOException("Stream closed!");
|
||||
}
|
||||
|
||||
offset += read;
|
||||
remaining -= read;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
inputStream.close();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.dispatch.io;
|
||||
|
||||
import org.whispersystems.dispatch.redis.PubSubConnection;
|
||||
|
||||
public interface RedisPubSubConnectionFactory {
|
||||
|
||||
PubSubConnection connect();
|
||||
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.dispatch.redis;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.dispatch.io.RedisInputStream;
|
||||
import org.whispersystems.dispatch.redis.protocol.ArrayReplyHeader;
|
||||
import org.whispersystems.dispatch.redis.protocol.IntReply;
|
||||
import org.whispersystems.dispatch.redis.protocol.StringReplyHeader;
|
||||
import org.whispersystems.dispatch.util.Util;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class PubSubConnection {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PubSubConnection.class);
|
||||
|
||||
private static final byte[] UNSUBSCRIBE_TYPE = {'u', 'n', 's', 'u', 'b', 's', 'c', 'r', 'i', 'b', 'e' };
|
||||
private static final byte[] SUBSCRIBE_TYPE = {'s', 'u', 'b', 's', 'c', 'r', 'i', 'b', 'e' };
|
||||
private static final byte[] MESSAGE_TYPE = {'m', 'e', 's', 's', 'a', 'g', 'e' };
|
||||
|
||||
private static final byte[] SUBSCRIBE_COMMAND = {'S', 'U', 'B', 'S', 'C', 'R', 'I', 'B', 'E', ' ' };
|
||||
private static final byte[] UNSUBSCRIBE_COMMAND = {'U', 'N', 'S', 'U', 'B', 'S', 'C', 'R', 'I', 'B', 'E', ' '};
|
||||
private static final byte[] CRLF = {'\r', '\n' };
|
||||
|
||||
private final OutputStream outputStream;
|
||||
private final RedisInputStream inputStream;
|
||||
private final Socket socket;
|
||||
private final AtomicBoolean closed;
|
||||
|
||||
public PubSubConnection(Socket socket) throws IOException {
|
||||
this.socket = socket;
|
||||
this.outputStream = socket.getOutputStream();
|
||||
this.inputStream = new RedisInputStream(new BufferedInputStream(socket.getInputStream()));
|
||||
this.closed = new AtomicBoolean(false);
|
||||
}
|
||||
|
||||
public void subscribe(String channelName) throws IOException {
|
||||
if (closed.get()) throw new IOException("Connection closed!");
|
||||
|
||||
byte[] command = Util.combine(SUBSCRIBE_COMMAND, channelName.getBytes(), CRLF);
|
||||
outputStream.write(command);
|
||||
}
|
||||
|
||||
public void unsubscribe(String channelName) throws IOException {
|
||||
if (closed.get()) throw new IOException("Connection closed!");
|
||||
|
||||
byte[] command = Util.combine(UNSUBSCRIBE_COMMAND, channelName.getBytes(), CRLF);
|
||||
outputStream.write(command);
|
||||
}
|
||||
|
||||
public PubSubReply read() throws IOException {
|
||||
if (closed.get()) throw new IOException("Connection closed!");
|
||||
|
||||
ArrayReplyHeader replyHeader = new ArrayReplyHeader(inputStream.readLine());
|
||||
|
||||
if (replyHeader.getElementCount() != 3) {
|
||||
throw new IOException("Received array reply header with strange count: " + replyHeader.getElementCount());
|
||||
}
|
||||
|
||||
StringReplyHeader replyTypeHeader = new StringReplyHeader(inputStream.readLine());
|
||||
byte[] replyType = inputStream.readFully(replyTypeHeader.getStringLength());
|
||||
inputStream.readLine();
|
||||
|
||||
if (Arrays.equals(SUBSCRIBE_TYPE, replyType)) return readSubscribeReply();
|
||||
else if (Arrays.equals(UNSUBSCRIBE_TYPE, replyType)) return readUnsubscribeReply();
|
||||
else if (Arrays.equals(MESSAGE_TYPE, replyType)) return readMessageReply();
|
||||
else throw new IOException("Unknown reply type: " + new String(replyType));
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
this.closed.set(true);
|
||||
this.inputStream.close();
|
||||
this.outputStream.close();
|
||||
this.socket.close();
|
||||
} catch (IOException e) {
|
||||
logger.warn("Exception while closing", e);
|
||||
}
|
||||
}
|
||||
|
||||
private PubSubReply readMessageReply() throws IOException {
|
||||
StringReplyHeader channelNameHeader = new StringReplyHeader(inputStream.readLine());
|
||||
byte[] channelName = inputStream.readFully(channelNameHeader.getStringLength());
|
||||
inputStream.readLine();
|
||||
|
||||
StringReplyHeader messageHeader = new StringReplyHeader(inputStream.readLine());
|
||||
byte[] message = inputStream.readFully(messageHeader.getStringLength());
|
||||
inputStream.readLine();
|
||||
|
||||
return new PubSubReply(PubSubReply.Type.MESSAGE, new String(channelName), Optional.of(message));
|
||||
}
|
||||
|
||||
private PubSubReply readUnsubscribeReply() throws IOException {
|
||||
String channelName = readSubscriptionReply();
|
||||
return new PubSubReply(PubSubReply.Type.UNSUBSCRIBE, channelName, Optional.empty());
|
||||
}
|
||||
|
||||
private PubSubReply readSubscribeReply() throws IOException {
|
||||
String channelName = readSubscriptionReply();
|
||||
return new PubSubReply(PubSubReply.Type.SUBSCRIBE, channelName, Optional.empty());
|
||||
}
|
||||
|
||||
private String readSubscriptionReply() throws IOException {
|
||||
StringReplyHeader channelNameHeader = new StringReplyHeader(inputStream.readLine());
|
||||
byte[] channelName = inputStream.readFully(channelNameHeader.getStringLength());
|
||||
inputStream.readLine();
|
||||
|
||||
new IntReply(inputStream.readLine());
|
||||
|
||||
return new String(channelName);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.dispatch.redis;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class PubSubReply {
|
||||
|
||||
public enum Type {
|
||||
MESSAGE,
|
||||
SUBSCRIBE,
|
||||
UNSUBSCRIBE
|
||||
}
|
||||
|
||||
private final Type type;
|
||||
private final String channel;
|
||||
private final Optional<byte[]> content;
|
||||
|
||||
public PubSubReply(Type type, String channel, Optional<byte[]> content) {
|
||||
this.type = type;
|
||||
this.channel = channel;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getChannel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.dispatch.redis.protocol;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class ArrayReplyHeader {
|
||||
|
||||
private final int elementCount;
|
||||
|
||||
public ArrayReplyHeader(String header) throws IOException {
|
||||
if (header == null || header.length() < 2 || header.charAt(0) != '*') {
|
||||
throw new IOException("Invalid array reply header: " + header);
|
||||
}
|
||||
|
||||
try {
|
||||
this.elementCount = Integer.parseInt(header.substring(1));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public int getElementCount() {
|
||||
return elementCount;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.dispatch.redis.protocol;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class IntReply {
|
||||
|
||||
private final int value;
|
||||
|
||||
public IntReply(String reply) throws IOException {
|
||||
if (reply == null || reply.length() < 2 || reply.charAt(0) != ':') {
|
||||
throw new IOException("Invalid int reply: " + reply);
|
||||
}
|
||||
|
||||
try {
|
||||
this.value = Integer.parseInt(reply.substring(1));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.dispatch.redis.protocol;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class StringReplyHeader {
|
||||
|
||||
private final int stringLength;
|
||||
|
||||
public StringReplyHeader(String header) throws IOException {
|
||||
if (header == null || header.length() < 2 || header.charAt(0) != '$') {
|
||||
throw new IOException("Invalid string reply header: " + header);
|
||||
}
|
||||
|
||||
try {
|
||||
this.stringLength = Integer.parseInt(header.substring(1));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public int getStringLength() {
|
||||
return stringLength;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.dispatch.util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class Util {
|
||||
|
||||
public static byte[] combine(byte[]... elements) {
|
||||
try {
|
||||
int sum = 0;
|
||||
|
||||
for (byte[] element : elements) {
|
||||
sum += element.length;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(sum);
|
||||
|
||||
for (byte[] element : elements) {
|
||||
baos.write(element);
|
||||
}
|
||||
|
||||
return baos.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void sleep(long millis) {
|
||||
try {
|
||||
Thread.sleep(millis);
|
||||
} catch (InterruptedException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.dispatch;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.mockito.Mockito.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.timeout;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.whispersystems.dispatch.io.RedisPubSubConnectionFactory;
|
||||
import org.whispersystems.dispatch.redis.PubSubConnection;
|
||||
import org.whispersystems.dispatch.redis.PubSubReply;
|
||||
|
||||
public class DispatchManagerTest {
|
||||
|
||||
private PubSubConnection pubSubConnection;
|
||||
private RedisPubSubConnectionFactory socketFactory;
|
||||
private DispatchManager dispatchManager;
|
||||
private PubSubReplyInputStream pubSubReplyInputStream;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
pubSubConnection = mock(PubSubConnection.class );
|
||||
socketFactory = mock(RedisPubSubConnectionFactory.class);
|
||||
pubSubReplyInputStream = new PubSubReplyInputStream();
|
||||
|
||||
when(socketFactory.connect()).thenReturn(pubSubConnection);
|
||||
when(pubSubConnection.read()).thenAnswer((Answer<PubSubReply>) invocationOnMock -> pubSubReplyInputStream.read());
|
||||
|
||||
dispatchManager = new DispatchManager(socketFactory, Optional.empty());
|
||||
dispatchManager.start();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
dispatchManager.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnect() {
|
||||
verify(socketFactory).connect();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubscribe() {
|
||||
DispatchChannel dispatchChannel = mock(DispatchChannel.class);
|
||||
dispatchManager.subscribe("foo", dispatchChannel);
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.empty()));
|
||||
|
||||
verify(dispatchChannel, timeout(1000)).onDispatchSubscribed(eq("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubscribeUnsubscribe() {
|
||||
DispatchChannel dispatchChannel = mock(DispatchChannel.class);
|
||||
dispatchManager.subscribe("foo", dispatchChannel);
|
||||
dispatchManager.unsubscribe("foo", dispatchChannel);
|
||||
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.empty()));
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.UNSUBSCRIBE, "foo", Optional.empty()));
|
||||
|
||||
verify(dispatchChannel, timeout(1000)).onDispatchUnsubscribed(eq("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMessages() {
|
||||
DispatchChannel fooChannel = mock(DispatchChannel.class);
|
||||
DispatchChannel barChannel = mock(DispatchChannel.class);
|
||||
|
||||
dispatchManager.subscribe("foo", fooChannel);
|
||||
dispatchManager.subscribe("bar", barChannel);
|
||||
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.empty()));
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "bar", Optional.empty()));
|
||||
|
||||
verify(fooChannel, timeout(1000)).onDispatchSubscribed(eq("foo"));
|
||||
verify(barChannel, timeout(1000)).onDispatchSubscribed(eq("bar"));
|
||||
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.MESSAGE, "foo", Optional.of("hello".getBytes())));
|
||||
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.MESSAGE, "bar", Optional.of("there".getBytes())));
|
||||
|
||||
ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
|
||||
verify(fooChannel, timeout(1000)).onDispatchMessage(eq("foo"), captor.capture());
|
||||
|
||||
assertArrayEquals("hello".getBytes(), captor.getValue());
|
||||
|
||||
verify(barChannel, timeout(1000)).onDispatchMessage(eq("bar"), captor.capture());
|
||||
|
||||
assertArrayEquals("there".getBytes(), captor.getValue());
|
||||
}
|
||||
|
||||
private static class PubSubReplyInputStream {
|
||||
|
||||
private final List<PubSubReply> pubSubReplyList = new LinkedList<>();
|
||||
|
||||
public synchronized PubSubReply read() {
|
||||
try {
|
||||
while (pubSubReplyList.isEmpty()) wait();
|
||||
return pubSubReplyList.remove(0);
|
||||
} catch (InterruptedException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void write(PubSubReply pubSubReply) {
|
||||
pubSubReplyList.add(pubSubReply);
|
||||
notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.dispatch.redis;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.security.SecureRandom;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
class PubSubConnectionTest {
|
||||
|
||||
private static final String REPLY = "*3\r\n" +
|
||||
"$9\r\n" +
|
||||
"subscribe\r\n" +
|
||||
"$5\r\n" +
|
||||
"abcde\r\n" +
|
||||
":1\r\n" +
|
||||
"*3\r\n" +
|
||||
"$9\r\n" +
|
||||
"subscribe\r\n" +
|
||||
"$5\r\n" +
|
||||
"fghij\r\n" +
|
||||
":2\r\n" +
|
||||
"*3\r\n" +
|
||||
"$9\r\n" +
|
||||
"subscribe\r\n" +
|
||||
"$5\r\n" +
|
||||
"klmno\r\n" +
|
||||
":2\r\n" +
|
||||
"*3\r\n" +
|
||||
"$7\r\n" +
|
||||
"message\r\n" +
|
||||
"$5\r\n" +
|
||||
"abcde\r\n" +
|
||||
"$10\r\n" +
|
||||
"1234567890\r\n" +
|
||||
"*3\r\n" +
|
||||
"$7\r\n" +
|
||||
"message\r\n" +
|
||||
"$5\r\n" +
|
||||
"klmno\r\n" +
|
||||
"$10\r\n" +
|
||||
"0987654321\r\n";
|
||||
|
||||
|
||||
@Test
|
||||
void testSubscribe() throws IOException {
|
||||
OutputStream outputStream = mock(OutputStream.class);
|
||||
Socket socket = mock(Socket.class );
|
||||
when(socket.getOutputStream()).thenReturn(outputStream);
|
||||
PubSubConnection connection = new PubSubConnection(socket);
|
||||
|
||||
connection.subscribe("foobar");
|
||||
|
||||
ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
|
||||
verify(outputStream).write(captor.capture());
|
||||
|
||||
assertArrayEquals(captor.getValue(), "SUBSCRIBE foobar\r\n".getBytes());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnsubscribe() throws IOException {
|
||||
OutputStream outputStream = mock(OutputStream.class);
|
||||
Socket socket = mock(Socket.class );
|
||||
when(socket.getOutputStream()).thenReturn(outputStream);
|
||||
PubSubConnection connection = new PubSubConnection(socket);
|
||||
|
||||
connection.unsubscribe("bazbar");
|
||||
|
||||
ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
|
||||
verify(outputStream).write(captor.capture());
|
||||
|
||||
assertArrayEquals(captor.getValue(), "UNSUBSCRIBE bazbar\r\n".getBytes());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTricklyResponse() throws Exception {
|
||||
InputStream inputStream = mockInputStreamFor(new TrickleInputStream(REPLY.getBytes()));
|
||||
OutputStream outputStream = mock(OutputStream.class);
|
||||
Socket socket = mock(Socket.class );
|
||||
when(socket.getOutputStream()).thenReturn(outputStream);
|
||||
when(socket.getInputStream()).thenReturn(inputStream);
|
||||
|
||||
PubSubConnection pubSubConnection = new PubSubConnection(socket);
|
||||
readResponses(pubSubConnection);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFullResponse() throws Exception {
|
||||
InputStream inputStream = mockInputStreamFor(new FullInputStream(REPLY.getBytes()));
|
||||
OutputStream outputStream = mock(OutputStream.class);
|
||||
Socket socket = mock(Socket.class );
|
||||
when(socket.getOutputStream()).thenReturn(outputStream);
|
||||
when(socket.getInputStream()).thenReturn(inputStream);
|
||||
|
||||
PubSubConnection pubSubConnection = new PubSubConnection(socket);
|
||||
readResponses(pubSubConnection);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRandomLengthResponse() throws Exception {
|
||||
InputStream inputStream = mockInputStreamFor(new RandomInputStream(REPLY.getBytes()));
|
||||
OutputStream outputStream = mock(OutputStream.class);
|
||||
Socket socket = mock(Socket.class );
|
||||
when(socket.getOutputStream()).thenReturn(outputStream);
|
||||
when(socket.getInputStream()).thenReturn(inputStream);
|
||||
|
||||
PubSubConnection pubSubConnection = new PubSubConnection(socket);
|
||||
readResponses(pubSubConnection);
|
||||
}
|
||||
|
||||
private InputStream mockInputStreamFor(final MockInputStream stub) throws IOException {
|
||||
InputStream result = mock(InputStream.class);
|
||||
|
||||
when(result.read()).thenAnswer(new Answer<Integer>() {
|
||||
@Override
|
||||
public Integer answer(InvocationOnMock invocationOnMock) throws Throwable {
|
||||
return stub.read();
|
||||
}
|
||||
});
|
||||
|
||||
when(result.read(any(byte[].class))).thenAnswer(new Answer<Integer>() {
|
||||
@Override
|
||||
public Integer answer(InvocationOnMock invocationOnMock) throws Throwable {
|
||||
byte[] buffer = (byte[])invocationOnMock.getArguments()[0];
|
||||
return stub.read(buffer, 0, buffer.length);
|
||||
}
|
||||
});
|
||||
|
||||
when(result.read(any(byte[].class), anyInt(), anyInt())).thenAnswer(new Answer<Integer>() {
|
||||
@Override
|
||||
public Integer answer(InvocationOnMock invocationOnMock) throws Throwable {
|
||||
byte[] buffer = (byte[]) invocationOnMock.getArguments()[0];
|
||||
int offset = (int) invocationOnMock.getArguments()[1];
|
||||
int length = (int) invocationOnMock.getArguments()[2];
|
||||
|
||||
return stub.read(buffer, offset, length);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void readResponses(PubSubConnection pubSubConnection) throws Exception {
|
||||
PubSubReply reply = pubSubConnection.read();
|
||||
|
||||
assertEquals(reply.getType(), PubSubReply.Type.SUBSCRIBE);
|
||||
assertEquals(reply.getChannel(), "abcde");
|
||||
assertFalse(reply.getContent().isPresent());
|
||||
|
||||
reply = pubSubConnection.read();
|
||||
|
||||
assertEquals(reply.getType(), PubSubReply.Type.SUBSCRIBE);
|
||||
assertEquals(reply.getChannel(), "fghij");
|
||||
assertFalse(reply.getContent().isPresent());
|
||||
|
||||
reply = pubSubConnection.read();
|
||||
|
||||
assertEquals(reply.getType(), PubSubReply.Type.SUBSCRIBE);
|
||||
assertEquals(reply.getChannel(), "klmno");
|
||||
assertFalse(reply.getContent().isPresent());
|
||||
|
||||
reply = pubSubConnection.read();
|
||||
|
||||
assertEquals(reply.getType(), PubSubReply.Type.MESSAGE);
|
||||
assertEquals(reply.getChannel(), "abcde");
|
||||
assertArrayEquals(reply.getContent().get(), "1234567890".getBytes());
|
||||
|
||||
reply = pubSubConnection.read();
|
||||
|
||||
assertEquals(reply.getType(), PubSubReply.Type.MESSAGE);
|
||||
assertEquals(reply.getChannel(), "klmno");
|
||||
assertArrayEquals(reply.getContent().get(), "0987654321".getBytes());
|
||||
}
|
||||
|
||||
private interface MockInputStream {
|
||||
public int read();
|
||||
public int read(byte[] input, int offset, int length);
|
||||
}
|
||||
|
||||
private static class TrickleInputStream implements MockInputStream {
|
||||
|
||||
private final byte[] data;
|
||||
private int index = 0;
|
||||
|
||||
private TrickleInputStream(byte[] data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public int read() {
|
||||
return data[index++];
|
||||
}
|
||||
|
||||
public int read(byte[] input, int offset, int length) {
|
||||
input[offset] = data[index++];
|
||||
return 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class FullInputStream implements MockInputStream {
|
||||
|
||||
private final byte[] data;
|
||||
private int index = 0;
|
||||
|
||||
private FullInputStream(byte[] data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public int read() {
|
||||
return data[index++];
|
||||
}
|
||||
|
||||
public int read(byte[] input, int offset, int length) {
|
||||
int amount = Math.min(data.length - index, length);
|
||||
System.arraycopy(data, index, input, offset, amount);
|
||||
index += length;
|
||||
|
||||
return amount;
|
||||
}
|
||||
}
|
||||
|
||||
private static class RandomInputStream implements MockInputStream {
|
||||
private final byte[] data;
|
||||
private int index = 0;
|
||||
|
||||
private RandomInputStream(byte[] data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public int read() {
|
||||
return data[index++];
|
||||
}
|
||||
|
||||
public int read(byte[] input, int offset, int length) {
|
||||
int maxCopy = Math.min(data.length - index, length);
|
||||
int randomCopy = new SecureRandom().nextInt(maxCopy) + 1;
|
||||
int copyAmount = Math.min(maxCopy, randomCopy);
|
||||
|
||||
System.arraycopy(data, index, input, offset, copyAmount);
|
||||
index += copyAmount;
|
||||
|
||||
return copyAmount;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.dispatch.redis.protocol;
|
||||
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.io.IOException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ArrayReplyHeaderTest {
|
||||
|
||||
@Test
|
||||
void testNull() {
|
||||
assertThrows(IOException.class, () -> new ArrayReplyHeader(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBadPrefix() {
|
||||
assertThrows(IOException.class, () -> new ArrayReplyHeader(":3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmpty() {
|
||||
assertThrows(IOException.class, () -> new ArrayReplyHeader(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTruncated() {
|
||||
assertThrows(IOException.class, () -> new ArrayReplyHeader("*"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBadNumber() {
|
||||
assertThrows(IOException.class, () -> new ArrayReplyHeader("*ABC"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValid() throws IOException {
|
||||
assertEquals(4, new ArrayReplyHeader("*4").getElementCount());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.dispatch.redis.protocol;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.io.IOException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class IntReplyHeaderTest {
|
||||
|
||||
@Test
|
||||
void testNull() {
|
||||
assertThrows(IOException.class, () -> new IntReply(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmpty() {
|
||||
assertThrows(IOException.class, () -> new IntReply(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBadNumber() {
|
||||
assertThrows(IOException.class, () -> new IntReply(":A"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBadFormat() {
|
||||
assertThrows(IOException.class, () -> new IntReply("*"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValid() throws IOException {
|
||||
assertEquals(23, new IntReply(":23").getValue());
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.dispatch.redis.protocol;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.io.IOException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class StringReplyHeaderTest {
|
||||
|
||||
@Test
|
||||
void testNull() {
|
||||
assertThrows(IOException.class, () -> new StringReplyHeader(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBadNumber() {
|
||||
assertThrows(IOException.class, () -> new StringReplyHeader("$100A"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBadPrefix() {
|
||||
assertThrows(IOException.class, () -> new StringReplyHeader("*"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValid() throws IOException {
|
||||
assertEquals(1000, new StringReplyHeader("$1000").getStringLength());
|
||||
}
|
||||
|
||||
}
|
||||
102
service/config/sample-secrets-bundle.yml
Normal file
102
service/config/sample-secrets-bundle.yml
Normal file
@@ -0,0 +1,102 @@
|
||||
stripe.apiKey: unset
|
||||
stripe.idempotencyKeyGenerator: abcdefg12345678= # base64 for creating request idempotency hash
|
||||
|
||||
braintree.publicKey: unset
|
||||
braintree.privateKey: unset
|
||||
|
||||
googlePlayBilling.credentialsJson: |
|
||||
{ "json": true }
|
||||
|
||||
appleAppStore.encodedKey: unset
|
||||
|
||||
directoryV2.client.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with CDS to generate auth tokens for Signal users
|
||||
directoryV2.client.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with CDS to generate auth identity tokens for Signal users
|
||||
|
||||
svr2.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR2 to generate auth tokens for Signal users
|
||||
svr2.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR2 to generate auth identity tokens for Signal users
|
||||
|
||||
svrb.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVRB to generate auth tokens for Signal users
|
||||
svrb.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVRB to generate auth identity tokens for Signal users
|
||||
|
||||
tus.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG=
|
||||
|
||||
gcpAttachments.rsaSigningKey: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAA
|
||||
-----END PRIVATE KEY-----
|
||||
|
||||
apn.teamId: team-id
|
||||
apn.keyId: key-id
|
||||
apn.signingKey: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAA
|
||||
-----END PRIVATE KEY-----
|
||||
|
||||
fcm.credentials: |
|
||||
{ "json": true }
|
||||
|
||||
cdn.accessKey: test # AWS Access Key ID
|
||||
cdn.accessSecret: test # AWS Access Secret
|
||||
|
||||
cdn3StorageManager.clientSecret: test
|
||||
|
||||
unidentifiedDelivery.privateKey: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789AAAAAAA
|
||||
|
||||
keyTransparencyService.clientPrivateKey: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAA
|
||||
-----END PRIVATE KEY-----
|
||||
|
||||
storageService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
|
||||
zkConfig-libsignal-0.42.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdef
|
||||
|
||||
genericZkConfig.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
|
||||
callingZkConfig.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
|
||||
backupsZkConfig.serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
|
||||
|
||||
paymentsService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= # base64-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users
|
||||
paymentsService.fixerApiKey: unset
|
||||
paymentsService.coinGeckoApiKey: unset
|
||||
|
||||
currentReportingKey.secret: AAAAAAAAAAA=
|
||||
currentReportingKey.salt: AAAAAAAAAAA=
|
||||
|
||||
registrationService.collationKeySalt: AAAAAAAAAAA=
|
||||
|
||||
turn.cloudflare.apiToken: ABCDEFGHIJKLM
|
||||
|
||||
linkDevice.secret: AAAAAAAAAAA=
|
||||
|
||||
tlsKeyStore.password: unset
|
||||
@@ -3,39 +3,77 @@
|
||||
# `unset` values will need to be set to work properly.
|
||||
# Most other values are technically valid for a local/demonstration environment, but are probably not production-ready.
|
||||
|
||||
adminEventLoggingConfiguration:
|
||||
credentials: |
|
||||
Some credentials text
|
||||
blah blah blah
|
||||
projectId: some-project-id
|
||||
logName: some-log-name
|
||||
logging:
|
||||
level: INFO
|
||||
appenders:
|
||||
- type: console
|
||||
threshold: ALL
|
||||
timeZone: UTC
|
||||
target: stdout
|
||||
- type: otlp
|
||||
|
||||
tlsKeyStore:
|
||||
password: secret://tlsKeyStore.password
|
||||
|
||||
stripe:
|
||||
apiKey: unset
|
||||
idempotencyKeyGenerator: abcdefg12345678= # base64 for creating request idempotency hash
|
||||
apiKey: secret://stripe.apiKey
|
||||
idempotencyKeyGenerator: secret://stripe.idempotencyKeyGenerator
|
||||
boostDescription: >
|
||||
Example
|
||||
supportedCurrencies:
|
||||
- xts
|
||||
# - ...
|
||||
# - Nth supported currency
|
||||
|
||||
supportedCurrenciesByPaymentMethod:
|
||||
CARD:
|
||||
- usd
|
||||
- eur
|
||||
SEPA_DEBIT:
|
||||
- eur
|
||||
|
||||
braintree:
|
||||
merchantId: unset
|
||||
publicKey: unset
|
||||
privateKey: unset
|
||||
publicKey: secret://braintree.publicKey
|
||||
privateKey: secret://braintree.privateKey
|
||||
environment: unset
|
||||
graphqlUrl: unset
|
||||
merchantAccounts:
|
||||
# ISO 4217 currency code and its corresponding sub-merchant account
|
||||
'xts': unset
|
||||
supportedCurrencies:
|
||||
- xts
|
||||
# - ...
|
||||
# - Nth supported currency
|
||||
supportedCurrenciesByPaymentMethod:
|
||||
PAYPAL:
|
||||
- usd
|
||||
pubSubPublisher:
|
||||
project: example-project
|
||||
topic: example-topic
|
||||
credentialConfiguration: |
|
||||
{
|
||||
"credential": "configuration"
|
||||
}
|
||||
|
||||
dynamoDbClientConfiguration:
|
||||
googlePlayBilling:
|
||||
credentialsJson: secret://googlePlayBilling.credentialsJson
|
||||
packageName: package.name
|
||||
applicationName: test
|
||||
productIdToLevel: {}
|
||||
|
||||
appleAppStore:
|
||||
env: SANDBOX
|
||||
bundleId: bundle.name
|
||||
appAppleId: 12345
|
||||
issuerId: abcdefg
|
||||
keyId: abcdefg
|
||||
encodedKey: secret://appleAppStore.encodedKey
|
||||
subscriptionGroupId: example_subscriptionGroupId
|
||||
productIdToLevel: {}
|
||||
appleRootCerts: []
|
||||
|
||||
appleDeviceCheck:
|
||||
production: false
|
||||
teamId: 0123456789
|
||||
bundleId: bundle.name
|
||||
|
||||
deviceCheck:
|
||||
backupRedemptionDuration: P30D
|
||||
backupRedemptionLevel: 201
|
||||
|
||||
dynamoDbClient:
|
||||
region: us-west-2 # AWS Region
|
||||
|
||||
dynamoDbTables:
|
||||
@@ -44,56 +82,81 @@ dynamoDbTables:
|
||||
phoneNumberTableName: Example_Accounts_PhoneNumbers
|
||||
phoneNumberIdentifierTableName: Example_Accounts_PhoneNumberIdentifiers
|
||||
usernamesTableName: Example_Accounts_Usernames
|
||||
scanPageSize: 100
|
||||
usedLinkDeviceTokensTableName: Example_Accounts_UsedLinkDeviceTokens
|
||||
appleDeviceChecks:
|
||||
tableName: Example_AppleDeviceChecks
|
||||
appleDeviceCheckPublicKeys:
|
||||
tableName: Example_AppleDeviceCheckPublicKeys
|
||||
backups:
|
||||
tableName: Example_Backups
|
||||
clientReleases:
|
||||
tableName: Example_ClientReleases
|
||||
deletedAccounts:
|
||||
tableName: Example_DeletedAccounts
|
||||
needsReconciliationIndexName: NeedsReconciliation
|
||||
deletedAccountsLock:
|
||||
tableName: Example_DeletedAccountsLock
|
||||
issuedReceipts:
|
||||
tableName: Example_IssuedReceipts
|
||||
expiration: P30D # Duration of time until rows expire
|
||||
generator: abcdefg12345678= # random base64-encoded binary sequence
|
||||
keys:
|
||||
maxIssuedReceiptsPerPaymentId:
|
||||
STRIPE: 1
|
||||
BRAINTREE: 1
|
||||
GOOGLE_PLAY_BILLING: 1
|
||||
APPLE_APP_STORE: 1
|
||||
ecKeys:
|
||||
tableName: Example_Keys
|
||||
ecSignedPreKeys:
|
||||
tableName: Example_EC_Signed_Pre_Keys
|
||||
pqKeys:
|
||||
tableName: Example_PQ_Keys
|
||||
pagedPqKeys:
|
||||
tableName: Example_PQ_Paged_Keys
|
||||
pqLastResortKeys:
|
||||
tableName: Example_PQ_Last_Resort_Keys
|
||||
messages:
|
||||
tableName: Example_Messages
|
||||
expiration: P30D # Duration of time until rows expire
|
||||
pendingAccounts:
|
||||
tableName: Example_PendingAccounts
|
||||
pendingDevices:
|
||||
tableName: Example_PendingDevices
|
||||
onetimeDonations:
|
||||
tableName: Example_OnetimeDonations
|
||||
expiration: P90D
|
||||
phoneNumberIdentifiers:
|
||||
tableName: Example_PhoneNumberIdentifiers
|
||||
profiles:
|
||||
tableName: Example_Profiles
|
||||
pushChallenge:
|
||||
tableName: Example_PushChallenge
|
||||
pushNotificationExperimentSamples:
|
||||
tableName: Example_PushNotificationExperimentSamples
|
||||
redeemedReceipts:
|
||||
tableName: Example_RedeemedReceipts
|
||||
expiration: P30D # Duration of time until rows expire
|
||||
registrationRecovery:
|
||||
tableName: Example_RegistrationRecovery
|
||||
expiration: P300D # Duration of time until rows expire
|
||||
remoteConfig:
|
||||
tableName: Example_RemoteConfig
|
||||
reportMessage:
|
||||
tableName: Example_ReportMessage
|
||||
reservedUsernames:
|
||||
tableName: Example_ReservedUsernames
|
||||
scheduledJobs:
|
||||
tableName: Example_ScheduledJobs
|
||||
expiration: P7D
|
||||
subscriptions:
|
||||
tableName: Example_Subscriptions
|
||||
registrationRecovery:
|
||||
tableName: Example_RegistrationRecovery
|
||||
expiration: P300D # Duration of time until rows expire
|
||||
clientPublicKeys:
|
||||
tableName: Example_ClientPublicKeys
|
||||
verificationSessions:
|
||||
tableName: Example_VerificationSessions
|
||||
|
||||
pagedSingleUseKEMPreKeyStore:
|
||||
bucket: preKeyBucket # S3 Bucket name
|
||||
region: us-west-2 # AWS region
|
||||
|
||||
cacheCluster: # Redis server configuration for cache cluster
|
||||
configurationUri: redis://redis.example.com:6379/
|
||||
|
||||
clientPresenceCluster: # Redis server configuration for client presence cluster
|
||||
configurationUri: redis://redis.example.com:6379/
|
||||
|
||||
pubsub: # Redis server configuration for pubsub cluster
|
||||
url: redis://redis.example.com:6379/
|
||||
replicaUrls:
|
||||
- redis://redis.example.com:6379/
|
||||
uri: redis://redis.example.com:6379/
|
||||
|
||||
pushSchedulerCluster: # Redis server configuration for push scheduler cluster
|
||||
configurationUri: redis://redis.example.com:6379/
|
||||
@@ -101,147 +164,124 @@ pushSchedulerCluster: # Redis server configuration for push scheduler cluster
|
||||
rateLimitersCluster: # Redis server configuration for rate limiters cluster
|
||||
configurationUri: redis://redis.example.com:6379/
|
||||
|
||||
directory:
|
||||
client: # Configuration for interfacing with Contact Discovery Service cluster
|
||||
userAuthenticationTokenSharedSecret: 00000f # hex-encoded secret shared with CDS used to generate auth tokens for Signal users
|
||||
userAuthenticationTokenUserIdSecret: 00000f # hex-encoded secret shared among Signal-Servers to obscure user phone numbers from CDS
|
||||
sqs:
|
||||
accessKey: test # AWS SQS accessKey
|
||||
accessSecret: test # AWS SQS accessSecret
|
||||
queueUrls: # AWS SQS queue urls
|
||||
- https://sqs.example.com/directory.fifo
|
||||
server: # One or more CDS servers
|
||||
- replicationName: example # CDS replication name
|
||||
replicationUrl: cds.example.com # CDS replication endpoint base url
|
||||
replicationPassword: example # CDS replication endpoint password
|
||||
replicationCaCertificates: # CDS replication endpoint TLS certificate trust root
|
||||
- |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
directoryV2:
|
||||
client: # Configuration for interfacing with Contact Discovery Service v2 cluster
|
||||
userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with CDS to generate auth tokens for Signal users
|
||||
userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with CDS to generate auth identity tokens for Signal users
|
||||
userAuthenticationTokenSharedSecret: secret://directoryV2.client.userAuthenticationTokenSharedSecret
|
||||
userIdTokenSharedSecret: secret://directoryV2.client.userIdTokenSharedSecret
|
||||
|
||||
svr2:
|
||||
userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR2 to generate auth tokens for Signal users
|
||||
userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR2 to generate auth identity tokens for Signal users
|
||||
uri: svr2.example.com
|
||||
userAuthenticationTokenSharedSecret: secret://svr2.userAuthenticationTokenSharedSecret
|
||||
userIdTokenSharedSecret: secret://svr2.userIdTokenSharedSecret
|
||||
svrCaCertificates:
|
||||
- |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
svrb:
|
||||
uri: svrb.example.com
|
||||
userAuthenticationTokenSharedSecret: secret://svrb.userAuthenticationTokenSharedSecret
|
||||
userIdTokenSharedSecret: secret://svrb.userIdTokenSharedSecret
|
||||
svrCaCertificates:
|
||||
- |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
messageCache: # Redis server configuration for message store cache
|
||||
persistDelayMinutes: 1
|
||||
cluster:
|
||||
configurationUri: redis://redis.example.com:6379/
|
||||
|
||||
metricsCluster:
|
||||
configurationUri: redis://redis.example.com:6379/
|
||||
|
||||
awsAttachments: # AWS S3 configuration
|
||||
accessKey: test
|
||||
accessSecret: test
|
||||
bucket: aws-attachments
|
||||
region: us-west-2
|
||||
|
||||
gcpAttachments: # GCP Storage configuration
|
||||
domain: example.com
|
||||
email: user@example.cocm
|
||||
maxSizeInBytes: 1024
|
||||
pathPrefix:
|
||||
rsaSigningKey: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAA
|
||||
-----END PRIVATE KEY-----
|
||||
rsaSigningKey: secret://gcpAttachments.rsaSigningKey
|
||||
|
||||
accountDatabaseCrawler:
|
||||
chunkSize: 10 # accounts per run
|
||||
chunkIntervalMs: 60000 # time per run
|
||||
tus:
|
||||
uploadUri: https://example.org/upload
|
||||
userAuthenticationTokenSharedSecret: secret://tus.userAuthenticationTokenSharedSecret
|
||||
|
||||
apn: # Apple Push Notifications configuration
|
||||
sandbox: true
|
||||
bundleId: com.example.textsecuregcm
|
||||
keyId: unset
|
||||
teamId: unset
|
||||
signingKey: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAA
|
||||
-----END PRIVATE KEY-----
|
||||
keyId: secret://apn.keyId
|
||||
teamId: secret://apn.teamId
|
||||
signingKey: secret://apn.signingKey
|
||||
|
||||
fcm: # FCM configuration
|
||||
credentials: |
|
||||
{ "json": true }
|
||||
credentials: secret://fcm.credentials
|
||||
|
||||
cdn:
|
||||
accessKey: test # AWS Access Key ID
|
||||
accessSecret: test # AWS Access Secret
|
||||
bucket: cdn # S3 Bucket name
|
||||
credentials:
|
||||
accessKeyId: secret://cdn.accessKey
|
||||
secretAccessKey: secret://cdn.accessSecret
|
||||
region: us-west-2 # AWS region
|
||||
|
||||
datadog:
|
||||
apiKey: unset
|
||||
cdn3StorageManager:
|
||||
baseUri: https://storage-manager.example.com
|
||||
clientId: example
|
||||
clientSecret: secret://cdn3StorageManager.clientSecret
|
||||
sourceSchemes:
|
||||
2: gcs
|
||||
3: r2
|
||||
|
||||
openTelemetry:
|
||||
enabled: true
|
||||
environment: dev
|
||||
url: http://127.0.0.1:4318/
|
||||
|
||||
unidentifiedDelivery:
|
||||
certificate: ABCD1234
|
||||
privateKey: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789AAAAAAA
|
||||
certificate: CgIIAQ==
|
||||
privateKey: secret://unidentifiedDelivery.privateKey
|
||||
expiresDays: 7
|
||||
|
||||
recaptcha:
|
||||
projectPath: projects/example
|
||||
credentialConfigurationJson: "{ }" # service account configuration for backend authentication
|
||||
|
||||
hCaptcha:
|
||||
apiKey: unset
|
||||
shortCode:
|
||||
baseUrl: https://example.com/shortcodes/
|
||||
|
||||
storageService:
|
||||
uri: storage.example.com
|
||||
userAuthenticationTokenSharedSecret: 00000f
|
||||
userAuthenticationTokenSharedSecret: secret://storageService.userAuthenticationTokenSharedSecret
|
||||
storageCaCertificates:
|
||||
- |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
@@ -266,64 +306,37 @@ storageService:
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
backupService:
|
||||
uri: backup.example.com
|
||||
userAuthenticationTokenSharedSecret: 00000f
|
||||
backupCaCertificates:
|
||||
- |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
zkConfig:
|
||||
serverPublic: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
serverSecret: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAA==
|
||||
serverPublic: ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyzAB==
|
||||
serverSecret: secret://zkConfig-libsignal-0.42.serverSecret
|
||||
|
||||
appConfig:
|
||||
application: example
|
||||
environment: example
|
||||
configuration: example
|
||||
callingZkConfig:
|
||||
serverSecret: secret://callingZkConfig.serverSecret
|
||||
|
||||
backupsZkConfig:
|
||||
serverSecret: secret://backupsZkConfig.serverSecret
|
||||
|
||||
dynamicConfig:
|
||||
s3Region: a-region
|
||||
s3Bucket: a-bucket
|
||||
objectKey: dynamic-config.yaml
|
||||
maxSize: 100000
|
||||
refreshInterval: PT10S
|
||||
|
||||
remoteConfig:
|
||||
authorizedTokens:
|
||||
- # 1st authorized token
|
||||
- # 2nd authorized token
|
||||
- # ...
|
||||
- # Nth authorized token
|
||||
globalConfig: # keys and values that are given to clients on GET /v1/config
|
||||
EXAMPLE_KEY: VALUE
|
||||
|
||||
paymentsService:
|
||||
userAuthenticationTokenSharedSecret: 0000000f0000000f0000000f0000000f0000000f0000000f0000000f0000000f # hex-encoded 32-byte secret shared with MobileCoin services used to generate auth tokens for Signal users
|
||||
fixerApiKey: unset
|
||||
coinMarketCapApiKey: unset
|
||||
coinMarketCapCurrencyIds:
|
||||
MOB: 7878
|
||||
userAuthenticationTokenSharedSecret: secret://paymentsService.userAuthenticationTokenSharedSecret
|
||||
paymentCurrencies:
|
||||
# list of symbols for supported currencies
|
||||
- MOB
|
||||
|
||||
artService:
|
||||
userAuthenticationTokenSharedSecret: 0000000f0000000f0000000f0000000f0000000f0000000f0000000f0000000f # hex-encoded 32-byte secret not shared with any external service, but used in ArtController
|
||||
userAuthenticationTokenUserIdSecret: 00000f # hex-encoded secret to obscure user phone numbers from Sticker Creator
|
||||
externalClients:
|
||||
fixerApiKey: secret://paymentsService.fixerApiKey
|
||||
coinGeckoApiKey: secret://paymentsService.coinGeckoApiKey
|
||||
coinGeckoCurrencyIds:
|
||||
MOB: mobilecoin
|
||||
|
||||
badges:
|
||||
badges:
|
||||
@@ -346,7 +359,11 @@ badges:
|
||||
'1': TEST
|
||||
|
||||
subscription: # configuration for Stripe subscriptions
|
||||
badgeExpiration: P30D
|
||||
badgeGracePeriod: P15D
|
||||
backupExpiration: P30D
|
||||
backupGracePeriod: P15D
|
||||
backupFreeTierMediaDuration: P30D
|
||||
levels:
|
||||
500:
|
||||
badge: EXAMPLE
|
||||
@@ -359,6 +376,7 @@ subscription: # configuration for Stripe subscriptions
|
||||
BRAINTREE: plan_example # braintree Plan ID
|
||||
|
||||
oneTimeDonations:
|
||||
sepaMaximumEuros: '10000'
|
||||
boost:
|
||||
level: 1
|
||||
expiration: P90D
|
||||
@@ -382,7 +400,13 @@ oneTimeDonations:
|
||||
|
||||
registrationService:
|
||||
host: registration.example.com
|
||||
apiKey: EXAMPLE
|
||||
port: 443
|
||||
credentialConfigurationJson: |
|
||||
{
|
||||
"example": "example"
|
||||
}
|
||||
identityTokenAudience: https://registration.example.com
|
||||
collationKeySalt: secret://registrationService.collationKeySalt
|
||||
registrationCaCertificate: | # Registration service TLS certificate trust root
|
||||
-----BEGIN CERTIFICATE-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
@@ -405,3 +429,100 @@ registrationService:
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
keyTransparencyService:
|
||||
host: kt.example.com
|
||||
port: 443
|
||||
tlsCertificate: |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
clientCertificate: |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
clientPrivateKey: secret://keyTransparencyService.clientPrivateKey
|
||||
|
||||
turn:
|
||||
cloudflare:
|
||||
apiToken: secret://turn.cloudflare.apiToken
|
||||
endpoint: https://rtc.live.cloudflare.com/v1/turn/keys/LMNOP/credentials/generate
|
||||
urls:
|
||||
- turn:turn.example.com:80
|
||||
urlsWithIps:
|
||||
- turn:%s
|
||||
- turn:%s:80?transport=tcp
|
||||
- turns:%s:443?transport=tcp
|
||||
requestedCredentialTtl: PT24H
|
||||
clientCredentialTtl: PT12H
|
||||
hostname: turn.cloudflare.example.com
|
||||
numHttpClients: 1
|
||||
|
||||
linkDevice:
|
||||
secret: secret://linkDevice.secret
|
||||
|
||||
externalRequestFilter:
|
||||
grpcMethods:
|
||||
- com.example.grpc.ExampleService/exampleMethod
|
||||
paths:
|
||||
- /example
|
||||
permittedInternalRanges:
|
||||
- 127.0.0.0/8
|
||||
|
||||
idlePrimaryDeviceReminder:
|
||||
minIdleDuration: P30D
|
||||
|
||||
grpc:
|
||||
port: 50051
|
||||
|
||||
asnTable:
|
||||
s3Region: a-region
|
||||
s3Bucket: a-bucket
|
||||
objectKey: asn.tsv
|
||||
maxSize: 100000
|
||||
refreshInterval: PT10S
|
||||
|
||||
callQualitySurvey:
|
||||
pubSubPublisher:
|
||||
project: example-project
|
||||
topic: example-topic
|
||||
credentialConfiguration: |
|
||||
{
|
||||
"credential": "configuration"
|
||||
}
|
||||
|
||||
464
service/pom.xml
464
service/pom.xml
@@ -10,7 +10,55 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>service</artifactId>
|
||||
|
||||
<properties>
|
||||
<firebase-admin.version>9.6.0</firebase-admin.version>
|
||||
<java-uuid-generator.version>5.1.0</java-uuid-generator.version>
|
||||
<google-androidpublisher.version>v3-rev20250904-2.0.0</google-androidpublisher.version>
|
||||
<storekit.version>3.6.0</storekit.version>
|
||||
<webauthn4j.version>0.29.6.RELEASE</webauthn4j.version>
|
||||
<java-jwt.version>4.5.0</java-jwt.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>java-jwt</artifactId>
|
||||
<version>${java-jwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.apis</groupId>
|
||||
<artifactId>google-api-services-androidpublisher</artifactId>
|
||||
<version>${google-androidpublisher.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.apple.itunes.storekit</groupId>
|
||||
<artifactId>app-store-server-library</artifactId>
|
||||
<version>${storekit.version}</version>
|
||||
<exclusions>
|
||||
<!-- conflicts with other users; resolved manually with explicit import -->
|
||||
<exclusion>
|
||||
<groupId>com.squareup.okio</groupId>
|
||||
<artifactId>okio-jvm</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.webauthn4j</groupId>
|
||||
<artifactId>webauthn4j-appattest</artifactId>
|
||||
<version>${webauthn4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-jaxrs2-jakarta</artifactId>
|
||||
<version>${swagger.version}</version>
|
||||
<exclusions>
|
||||
<!-- conflicts with jackson-dataformat-yaml -->
|
||||
<exclusion>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
@@ -24,16 +72,6 @@
|
||||
<artifactId>jakarta.ws.rs-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<artifactId>event-logger</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<artifactId>redis-dispatch</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.whispersystems.textsecure</groupId>
|
||||
<artifactId>websocket-resources</artifactId>
|
||||
@@ -44,6 +82,11 @@
|
||||
<artifactId>libsignal-server</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>simple-grpc-runtime</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-core</artifactId>
|
||||
@@ -58,7 +101,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-db</artifactId>
|
||||
<artifactId>dropwizard-http2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
@@ -107,8 +150,8 @@
|
||||
<artifactId>logback-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-access</artifactId>
|
||||
<groupId>ch.qos.logback.access</groupId>
|
||||
<artifactId>logback-access-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
@@ -143,31 +186,68 @@
|
||||
<groupId>org.glassfish.jersey.core</groupId>
|
||||
<artifactId>jersey-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jaxb</groupId>
|
||||
<artifactId>jaxb-runtime</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.dropwizard</groupId>
|
||||
<artifactId>dropwizard-testing</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.opentelemetry</groupId>
|
||||
<artifactId>opentelemetry-sdk</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.opentelemetry</groupId>
|
||||
<artifactId>opentelemetry-exporter-otlp</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.opentelemetry.instrumentation</groupId>
|
||||
<artifactId>opentelemetry-logback-appender-1.0</artifactId>
|
||||
<!-- *all* opentelemetry-logback-appender versions are "alpha" despite the advanced version number -->
|
||||
<version>2.19.0-alpha</version>
|
||||
<exclusions>
|
||||
<!-- incubator packages aren't included in the opentelemetry BOM, and we don't use them -->
|
||||
<exclusion>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<groupId>io.opentelemetry.instrumentation</groupId>
|
||||
<artifactId>opentelemetry-instrumentation-api-incubator</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>party.iroiro.luajava</groupId>
|
||||
<artifactId>luajava</artifactId>
|
||||
<version>${luajava.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>party.iroiro.luajava</groupId>
|
||||
<artifactId>lua51</artifactId>
|
||||
<version>${luajava.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>party.iroiro.luajava</groupId>
|
||||
<artifactId>lua51-platform</artifactId>
|
||||
<version>${luajava.version}</version>
|
||||
<classifier>natives-desktop</classifier>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||
<artifactId>websocket-api</artifactId>
|
||||
<artifactId>websocket-jetty-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-servlets</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||
<artifactId>websocket-jetty-client</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
@@ -177,11 +257,52 @@
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-csv</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-compress</artifactId>
|
||||
<version>1.28.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.cloud</groupId>
|
||||
<artifactId>google-cloud-pubsub</artifactId>
|
||||
<exclusions>
|
||||
<!-- our direct import of guava brings in a more recent version of failureaccess, so excluding it here -->
|
||||
<exclusion>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>failureaccess</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.firebase</groupId>
|
||||
<artifactId>firebase-admin</artifactId>
|
||||
<version>9.1.1</version>
|
||||
<version>${firebase-admin.version}</version>
|
||||
<exclusions>
|
||||
<!-- our direct import of guava brings in a more recent version of failureaccess, so excluding it here -->
|
||||
<exclusion>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>failureaccess</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.cloud</groupId>
|
||||
<artifactId>google-cloud-firestore</artifactId>
|
||||
<exclusions>
|
||||
<!-- incubator packages aren't included in the opentelemetry BOM, and we don't use them -->
|
||||
<exclusion>
|
||||
<groupId>io.opentelemetry.instrumentation</groupId>
|
||||
<artifactId>opentelemetry-instrumentation-api-incubator</artifactId>
|
||||
</exclusion>
|
||||
<!-- our direct import of guava brings in a more recent version of failureaccess, so excluding it here -->
|
||||
<exclusion>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>failureaccess</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -204,8 +325,7 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-netty-shaded</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<artifactId>grpc-netty</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
@@ -228,11 +348,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-registry-datadog</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.coursera</groupId>
|
||||
<artifactId>dropwizard-metrics-datadog</artifactId>
|
||||
<artifactId>micrometer-registry-otlp</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
@@ -259,6 +375,24 @@
|
||||
<artifactId>jackson-jaxrs-json-provider</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.salesforce.servicelibs</groupId>
|
||||
<artifactId>reactor-grpc-stub</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.foundationdb</groupId>
|
||||
<artifactId>fdb-java</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>apache-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>netty-nio-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>sts</artifactId>
|
||||
@@ -267,41 +401,14 @@
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>sqs</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>dynamodb</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>appconfig</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>appconfigdata</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>dynamodb-lock-client</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
<version>1.4.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -338,11 +445,30 @@
|
||||
<artifactId>libphonenumber</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Provides tools for mapping phone numbers to time zones, which is helpful for scheduling push notifications
|
||||
during waking hours -->
|
||||
<dependency>
|
||||
<groupId>com.googlecode.libphonenumber</groupId>
|
||||
<artifactId>geocoder</artifactId>
|
||||
<version>3.14</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.argparse4j</groupId>
|
||||
<artifactId>argparse4j</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-codec-haproxy</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-transport-native-epoll</artifactId>
|
||||
<classifier>linux-x86_64</classifier>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.test-framework</groupId>
|
||||
<artifactId>jersey-test-framework-core</artifactId>
|
||||
@@ -358,23 +484,6 @@
|
||||
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
|
||||
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.almworks.sqlite4java</groupId>
|
||||
<artifactId>sqlite4java</artifactId>
|
||||
<version>1.0.392</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -382,8 +491,8 @@
|
||||
<artifactId>reactor-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vavr</groupId>
|
||||
<artifactId>vavr</artifactId>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-core-micrometer</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -398,28 +507,39 @@
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>embedded-redis</artifactId>
|
||||
<groupId>com.redis</groupId>
|
||||
<artifactId>testcontainers-redis</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.uuid</groupId>
|
||||
<artifactId>java-uuid-generator</artifactId>
|
||||
<version>4.0.1</version>
|
||||
<version>${java-uuid-generator.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>DynamoDBLocal</artifactId>
|
||||
<version>1.20.0</version>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>localstack</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.cloud</groupId>
|
||||
<artifactId>google-cloud-recaptchaenterprise</artifactId>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>earth.adi</groupId>
|
||||
<artifactId>testcontainers-foundationdb</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.auth</groupId>
|
||||
<artifactId>google-auth-library-oauth2-http</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -435,7 +555,26 @@
|
||||
<dependency>
|
||||
<groupId>com.apollographql.apollo3</groupId>
|
||||
<artifactId>apollo-api-jvm</artifactId>
|
||||
<version>3.7.1</version>
|
||||
<version>3.8.5</version>
|
||||
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
</exclusion>
|
||||
<!-- conflicts with other users; resolved manually with explicit import -->
|
||||
<exclusion>
|
||||
<groupId>com.squareup.okio</groupId>
|
||||
<artifactId>okio-jvm</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- to resolve conflicting imports from other dependencies -->
|
||||
<dependency>
|
||||
<groupId>com.squareup.okio</groupId>
|
||||
<artifactId>okio-jvm</artifactId>
|
||||
<version>3.15.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
@@ -445,10 +584,31 @@
|
||||
<id>exclude-spam-filter</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.github.download-maven-plugin</groupId>
|
||||
<artifactId>download-maven-plugin</artifactId>
|
||||
<version>2.0.0</version>
|
||||
|
||||
<executions>
|
||||
<execution>
|
||||
<id>install-foundationdb-client-library</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>wget</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
|
||||
<configuration>
|
||||
<url>https://github.com/apple/foundationdb/releases/download/${foundationdb.version}/libfdb_c.x86_64.so</url>
|
||||
<outputDirectory>${project.build.directory}/jib-extra/usr/lib</outputDirectory>
|
||||
<sha256>${foundationdb.client-library-sha256}</sha256>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.2.4</version>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>true</createDependencyReducedPom>
|
||||
<filters>
|
||||
@@ -483,7 +643,6 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>assembly.xml</descriptor>
|
||||
@@ -503,7 +662,6 @@
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>properties-maven-plugin</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>read-deploy-configuration</id>
|
||||
@@ -519,24 +677,102 @@
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.signal</groupId>
|
||||
<artifactId>s3-upload-maven-plugin</artifactId>
|
||||
<version>1.6-SNAPSHOT</version>
|
||||
<configuration>
|
||||
<source>${project.build.directory}/${project.build.finalName}-bin.tar.gz</source>
|
||||
<bucketName>${deploy.bucketName}</bucketName>
|
||||
<region>${deploy.bucketRegion}</region>
|
||||
<destination>${project.build.finalName}-bin.tar.gz</destination>
|
||||
</configuration>
|
||||
<groupId>com.google.cloud.tools</groupId>
|
||||
<artifactId>jib-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>deploy-to-s3</id>
|
||||
<phase>deploy</phase>
|
||||
<goals>
|
||||
<goal>s3-upload</goal>
|
||||
<goal>build</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<from>
|
||||
<image>eclipse-temurin@sha256:${docker.image.sha256}</image>
|
||||
<platforms>
|
||||
<platform>
|
||||
<architecture>amd64</architecture>
|
||||
<os>linux</os>
|
||||
</platform>
|
||||
<platform>
|
||||
<architecture>arm64</architecture>
|
||||
<os>linux</os>
|
||||
</platform>
|
||||
</platforms>
|
||||
</from>
|
||||
<to>
|
||||
<image>${docker.repo}:${project.version}</image>
|
||||
</to>
|
||||
<container>
|
||||
<mainClass>org.whispersystems.textsecuregcm.WhisperServerService</mainClass>
|
||||
<jvmFlags>
|
||||
<jvmFlag>-server</jvmFlag>
|
||||
<jvmFlag>-Djava.awt.headless=true</jvmFlag>
|
||||
<jvmFlag>-Djdk.nio.maxCachedBufferSize=262144</jvmFlag>
|
||||
<jvmFlag>-Dlog4j2.formatMsgNoLookups=true</jvmFlag>
|
||||
<jvmFlag>-Djdk.tls.server.newSessionTicketCount=0</jvmFlag>
|
||||
<jvmFlag>-XX:MaxRAMPercentage=75</jvmFlag>
|
||||
<jvmFlag>-XX:+HeapDumpOnOutOfMemoryError</jvmFlag>
|
||||
<jvmFlag>-XX:HeapDumpPath=/tmp/heapdump.bin</jvmFlag>
|
||||
</jvmFlags>
|
||||
<ports>
|
||||
<port>8080</port>
|
||||
</ports>
|
||||
<creationTime>USE_CURRENT_TIMESTAMP</creationTime>
|
||||
</container>
|
||||
<extraDirectories>
|
||||
<paths>
|
||||
<path>
|
||||
<from>${project.basedir}/config</from>
|
||||
<includes>*.yml</includes>
|
||||
<into>/usr/share/signal/</into>
|
||||
</path>
|
||||
<path>
|
||||
<from>${project.build.directory}/jib-extra</from>
|
||||
</path>
|
||||
</paths>
|
||||
</extraDirectories>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>include-spam-filter</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.google.cloud.tools</groupId>
|
||||
<artifactId>jib-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<!-- we don't want jib to execute on this module -->
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>test-server</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>start-test-server</id>
|
||||
<phase>integration-test</phase>
|
||||
<goals>
|
||||
<goal>java</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<mainClass>org.whispersystems.textsecuregcm.LocalWhisperServerService</mainClass>
|
||||
<classpathScope>test</classpathScope>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
@@ -549,7 +785,7 @@
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>templating-maven-plugin</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>3.0.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>filter-src</id>
|
||||
@@ -557,9 +793,24 @@
|
||||
<goal>filter-sources</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>filter-test-src</id>
|
||||
<goals>
|
||||
<goal>filter-test-sources</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<!-- add-opens: work around PATCH not being a supported method on HttpUrlConnection -->
|
||||
<argLine>-javaagent:${org.mockito:mockito-core:jar} --add-opens=java.base/java.net=ALL-UNNAMED</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
@@ -575,7 +826,6 @@
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>check-all-service-config</id>
|
||||
@@ -583,15 +833,15 @@
|
||||
<goals>
|
||||
<goal>java</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<mainClass>org.whispersystems.textsecuregcm.CheckServiceConfigurations</mainClass>
|
||||
<classpathScope>test</classpathScope>
|
||||
<arguments>
|
||||
<argument>${project.basedir}/config</argument>
|
||||
</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<mainClass>org.whispersystems.textsecuregcm.CheckServiceConfigurations</mainClass>
|
||||
<classpathScope>test</classpathScope>
|
||||
<arguments>
|
||||
<argument>${project.basedir}/config</argument>
|
||||
</arguments>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
public class FoundationDbVersion {
|
||||
|
||||
private static final String VERSION = "${foundationdb.version}";
|
||||
private static final int API_VERSION = ${foundationdb.api-version};
|
||||
|
||||
public static String getFoundationDbVersion() {
|
||||
return VERSION;
|
||||
}
|
||||
|
||||
public static int getFoundationDbApiVersion() {
|
||||
return API_VERSION;
|
||||
}
|
||||
}
|
||||
@@ -5,49 +5,65 @@
|
||||
package org.whispersystems.textsecuregcm;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.dropwizard.Configuration;
|
||||
import io.dropwizard.core.Configuration;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.configuration.SpamFilterConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AccountDatabaseCrawlerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AdminEventLoggingConfiguration;
|
||||
import org.whispersystems.textsecuregcm.attachments.TusConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AppConfigConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AppleAppStoreConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AppleDeviceCheckConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.AwsCredentialsProviderFactory;
|
||||
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.BraintreeConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.CallQualitySurveyConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.Cdn3StorageManagerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ClientReleaseConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DefaultAwsCredentialsFactory;
|
||||
import org.whispersystems.textsecuregcm.configuration.DeviceCheckConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DirectoryV2Configuration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DynamoDbClientConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DynamoDbClientFactory;
|
||||
import org.whispersystems.textsecuregcm.configuration.DynamoDbTables;
|
||||
import org.whispersystems.textsecuregcm.configuration.ExternalRequestFilterConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.FaultTolerantRedisClientFactory;
|
||||
import org.whispersystems.textsecuregcm.configuration.FaultTolerantRedisClusterFactory;
|
||||
import org.whispersystems.textsecuregcm.configuration.FcmConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.GcpAttachmentsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.HCaptchaConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.GenericZkConfig;
|
||||
import org.whispersystems.textsecuregcm.configuration.GooglePlayBillingConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.GrpcConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.IdlePrimaryDeviceReminderConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.KeyTransparencyServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.LinkDeviceSecretConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.MaxDeviceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.MessageByteLimitCardinalityEstimatorConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.MessageCacheConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.OneTimeDonationConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.OpenTelemetryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.PagedSingleUseKEMPreKeyStoreConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.PaymentsServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ArtServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RecaptchaConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RedisClusterConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RegistrationServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RegistrationServiceClientFactory;
|
||||
import org.whispersystems.textsecuregcm.configuration.RemoteConfigConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ReportMessageConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureBackupServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.RetryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.S3ObjectMonitorFactory;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureValueRecoveryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ShortCodeExpanderConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SpamFilterConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.StripeConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.TlsKeyStoreConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.TurnConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.UnidentifiedDeliveryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.VirtualThreadConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ZkConfig;
|
||||
import org.whispersystems.websocket.configuration.WebSocketConfiguration;
|
||||
|
||||
@@ -57,7 +73,12 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private AdminEventLoggingConfiguration adminEventLoggingConfiguration;
|
||||
private TlsKeyStoreConfiguration tlsKeyStore;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
AwsCredentialsProviderFactory awsCredentialsProvider = new DefaultAwsCredentialsFactory();
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@@ -72,18 +93,33 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private DynamoDbClientConfiguration dynamoDbClientConfiguration;
|
||||
private GooglePlayBillingConfiguration googlePlayBilling;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private AppleAppStoreConfiguration appleAppStore;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private AppleDeviceCheckConfiguration appleDeviceCheck;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private DeviceCheckConfiguration deviceCheck;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private DynamoDbClientFactory dynamoDbClient;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private DynamoDbTables dynamoDbTables;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private AwsAttachmentsConfiguration awsAttachments;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
@@ -97,27 +133,22 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private DatadogConfiguration datadog;
|
||||
private Cdn3StorageManagerConfiguration cdn3StorageManager;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private RedisClusterConfiguration cacheCluster;
|
||||
private OpenTelemetryConfiguration openTelemetry;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private RedisConfiguration pubsub;
|
||||
private FaultTolerantRedisClusterFactory cacheCluster;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private RedisClusterConfiguration metricsCluster;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private DirectoryConfiguration directory;
|
||||
private FaultTolerantRedisClientFactory pubsub;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@@ -127,48 +158,33 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private SecureValueRecovery2Configuration svr2;
|
||||
private SecureValueRecoveryConfiguration svr2;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private AccountDatabaseCrawlerConfiguration accountDatabaseCrawler;
|
||||
private SecureValueRecoveryConfiguration svrb;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private RedisClusterConfiguration pushSchedulerCluster;
|
||||
private FaultTolerantRedisClusterFactory pushSchedulerCluster;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private RedisClusterConfiguration rateLimitersCluster;
|
||||
private FaultTolerantRedisClusterFactory rateLimitersCluster;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private MessageCacheConfiguration messageCache;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private RedisClusterConfiguration clientPresenceCluster;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private List<TestDeviceConfiguration> testDevices = new LinkedList<>();
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private List<MaxDeviceConfiguration> maxDevices = new LinkedList<>();
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private RateLimitsConfiguration limits = new RateLimitsConfiguration();
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
@@ -192,38 +208,33 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private RecaptchaConfiguration recaptcha;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private HCaptchaConfiguration hCaptcha;
|
||||
private ShortCodeExpanderConfiguration shortCode;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private SecureStorageServiceConfiguration storageService;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private SecureBackupServiceConfiguration backupService;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private PaymentsServiceConfiguration paymentsService;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private ArtServiceConfiguration artService;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private ZkConfig zkConfig;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private GenericZkConfig callingZkConfig;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private GenericZkConfig backupsZkConfig;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
@@ -232,7 +243,7 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private AppConfigConfiguration appConfig;
|
||||
private S3ObjectMonitorFactory dynamicConfig;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@@ -249,6 +260,11 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
@NotNull
|
||||
private OneTimeDonationConfiguration oneTimeDonations;
|
||||
|
||||
@Valid
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private PagedSingleUseKEMPreKeyStoreConfiguration pagedSingleUseKEMPreKeyStore;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
@@ -256,15 +272,92 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private SpamFilterConfiguration spamFilterConfiguration;
|
||||
private SpamFilterConfiguration spamFilter;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private RegistrationServiceConfiguration registrationService;
|
||||
private RegistrationServiceClientFactory registrationService;
|
||||
|
||||
public AdminEventLoggingConfiguration getAdminEventLoggingConfiguration() {
|
||||
return adminEventLoggingConfiguration;
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private TurnConfiguration turn;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private TusConfiguration tus;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private ClientReleaseConfiguration clientRelease = new ClientReleaseConfiguration(Duration.ofHours(4));
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private MessageByteLimitCardinalityEstimatorConfiguration messageByteLimitCardinalityEstimator = new MessageByteLimitCardinalityEstimatorConfiguration(Duration.ofDays(1));
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private LinkDeviceSecretConfiguration linkDevice;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private VirtualThreadConfiguration virtualThread = new VirtualThreadConfiguration();
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private ExternalRequestFilterConfiguration externalRequestFilter;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private KeyTransparencyServiceConfiguration keyTransparencyService;
|
||||
|
||||
@JsonProperty
|
||||
private boolean logMessageDeliveryLoops;
|
||||
|
||||
@JsonProperty
|
||||
private IdlePrimaryDeviceReminderConfiguration idlePrimaryDeviceReminder =
|
||||
new IdlePrimaryDeviceReminderConfiguration(Duration.ofDays(30));
|
||||
|
||||
@JsonProperty
|
||||
private Map<String, @Valid CircuitBreakerConfiguration> circuitBreakers = Collections.emptyMap();
|
||||
|
||||
@JsonProperty
|
||||
private Map<String, @Valid RetryConfiguration> retries = Collections.emptyMap();
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
@NotNull
|
||||
private RetryConfiguration generalRedisRetry = new RetryConfiguration();
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private GrpcConfiguration grpc;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private S3ObjectMonitorFactory asnTable;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private CallQualitySurveyConfiguration callQualitySurvey;
|
||||
|
||||
public TlsKeyStoreConfiguration getTlsKeyStoreConfiguration() {
|
||||
return tlsKeyStore;
|
||||
}
|
||||
|
||||
public AwsCredentialsProviderFactory getAwsCredentialsConfiguration() {
|
||||
return awsCredentialsProvider;
|
||||
}
|
||||
|
||||
public StripeConfiguration getStripe() {
|
||||
@@ -275,54 +368,58 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return braintree;
|
||||
}
|
||||
|
||||
public DynamoDbClientConfiguration getDynamoDbClientConfiguration() {
|
||||
return dynamoDbClientConfiguration;
|
||||
public GooglePlayBillingConfiguration getGooglePlayBilling() {
|
||||
return googlePlayBilling;
|
||||
}
|
||||
|
||||
public AppleAppStoreConfiguration getAppleAppStore() {
|
||||
return appleAppStore;
|
||||
}
|
||||
|
||||
public AppleDeviceCheckConfiguration getAppleDeviceCheck() {
|
||||
return appleDeviceCheck;
|
||||
}
|
||||
|
||||
public DeviceCheckConfiguration getDeviceCheck() {
|
||||
return deviceCheck;
|
||||
}
|
||||
|
||||
public DynamoDbClientFactory getDynamoDbClientConfiguration() {
|
||||
return dynamoDbClient;
|
||||
}
|
||||
|
||||
public DynamoDbTables getDynamoDbTables() {
|
||||
return dynamoDbTables;
|
||||
}
|
||||
|
||||
public RecaptchaConfiguration getRecaptchaConfiguration() {
|
||||
return recaptcha;
|
||||
}
|
||||
|
||||
public HCaptchaConfiguration getHCaptchaConfiguration() {
|
||||
return hCaptcha;
|
||||
public ShortCodeExpanderConfiguration getShortCodeRetrieverConfiguration() {
|
||||
return shortCode;
|
||||
}
|
||||
|
||||
public WebSocketConfiguration getWebSocketConfiguration() {
|
||||
return webSocket;
|
||||
}
|
||||
|
||||
public AwsAttachmentsConfiguration getAwsAttachmentsConfiguration() {
|
||||
return awsAttachments;
|
||||
}
|
||||
|
||||
public GcpAttachmentsConfiguration getGcpAttachmentsConfiguration() {
|
||||
return gcpAttachments;
|
||||
}
|
||||
|
||||
public RedisClusterConfiguration getCacheClusterConfiguration() {
|
||||
public FaultTolerantRedisClusterFactory getCacheClusterConfiguration() {
|
||||
return cacheCluster;
|
||||
}
|
||||
|
||||
public RedisConfiguration getPubsubCacheConfiguration() {
|
||||
public FaultTolerantRedisClientFactory getRedisPubSubConfiguration() {
|
||||
return pubsub;
|
||||
}
|
||||
|
||||
public RedisClusterConfiguration getMetricsClusterConfiguration() {
|
||||
return metricsCluster;
|
||||
}
|
||||
|
||||
public DirectoryConfiguration getDirectoryConfiguration() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
public SecureValueRecovery2Configuration getSvr2Configuration() {
|
||||
public SecureValueRecoveryConfiguration getSvr2Configuration() {
|
||||
return svr2;
|
||||
}
|
||||
|
||||
public SecureValueRecoveryConfiguration getSvrbConfiguration() {
|
||||
return svrb;
|
||||
}
|
||||
|
||||
public DirectoryV2Configuration getDirectoryV2Configuration() {
|
||||
return directoryV2;
|
||||
}
|
||||
@@ -331,30 +428,18 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return storageService;
|
||||
}
|
||||
|
||||
public AccountDatabaseCrawlerConfiguration getAccountDatabaseCrawlerConfiguration() {
|
||||
return accountDatabaseCrawler;
|
||||
}
|
||||
|
||||
public MessageCacheConfiguration getMessageCacheConfiguration() {
|
||||
return messageCache;
|
||||
}
|
||||
|
||||
public RedisClusterConfiguration getClientPresenceClusterConfiguration() {
|
||||
return clientPresenceCluster;
|
||||
}
|
||||
|
||||
public RedisClusterConfiguration getPushSchedulerCluster() {
|
||||
public FaultTolerantRedisClusterFactory getPushSchedulerCluster() {
|
||||
return pushSchedulerCluster;
|
||||
}
|
||||
|
||||
public RedisClusterConfiguration getRateLimitersCluster() {
|
||||
public FaultTolerantRedisClusterFactory getRateLimitersCluster() {
|
||||
return rateLimitersCluster;
|
||||
}
|
||||
|
||||
public RateLimitsConfiguration getLimitsConfiguration() {
|
||||
return limits;
|
||||
}
|
||||
|
||||
public FcmConfiguration getFcmConfiguration() {
|
||||
return fcm;
|
||||
}
|
||||
@@ -367,25 +452,18 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return cdn;
|
||||
}
|
||||
|
||||
public DatadogConfiguration getDatadogConfiguration() {
|
||||
return datadog;
|
||||
public Cdn3StorageManagerConfiguration getCdn3StorageManagerConfiguration() {
|
||||
return cdn3StorageManager;
|
||||
}
|
||||
|
||||
public OpenTelemetryConfiguration getOpenTelemetryConfiguration() {
|
||||
return openTelemetry;
|
||||
}
|
||||
|
||||
public UnidentifiedDeliveryConfiguration getDeliveryCertificate() {
|
||||
return unidentifiedDelivery;
|
||||
}
|
||||
|
||||
public Map<String, Integer> getTestDevices() {
|
||||
Map<String, Integer> results = new HashMap<>();
|
||||
|
||||
for (TestDeviceConfiguration testDeviceConfiguration : testDevices) {
|
||||
results.put(testDeviceConfiguration.getNumber(),
|
||||
testDeviceConfiguration.getCode());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public Map<String, Integer> getMaxDevices() {
|
||||
Map<String, Integer> results = new HashMap<>();
|
||||
|
||||
@@ -397,28 +475,28 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return results;
|
||||
}
|
||||
|
||||
public SecureBackupServiceConfiguration getSecureBackupServiceConfiguration() {
|
||||
return backupService;
|
||||
}
|
||||
|
||||
public PaymentsServiceConfiguration getPaymentsServiceConfiguration() {
|
||||
return paymentsService;
|
||||
}
|
||||
|
||||
public ArtServiceConfiguration getArtServiceConfiguration() {
|
||||
return artService;
|
||||
}
|
||||
|
||||
public ZkConfig getZkConfig() {
|
||||
return zkConfig;
|
||||
}
|
||||
|
||||
public GenericZkConfig getCallingZkConfig() {
|
||||
return callingZkConfig;
|
||||
}
|
||||
|
||||
public GenericZkConfig getBackupsZkConfig() {
|
||||
return backupsZkConfig;
|
||||
}
|
||||
|
||||
public RemoteConfigConfiguration getRemoteConfigConfiguration() {
|
||||
return remoteConfig;
|
||||
}
|
||||
|
||||
public AppConfigConfiguration getAppConfig() {
|
||||
return appConfig;
|
||||
public S3ObjectMonitorFactory getDynamicConfig() {
|
||||
return dynamicConfig;
|
||||
}
|
||||
|
||||
public BadgesConfiguration getBadges() {
|
||||
@@ -433,15 +511,83 @@ public class WhisperServerConfiguration extends Configuration {
|
||||
return oneTimeDonations;
|
||||
}
|
||||
|
||||
public PagedSingleUseKEMPreKeyStoreConfiguration getPagedSingleUseKEMPreKeyStore() {
|
||||
return pagedSingleUseKEMPreKeyStore;
|
||||
}
|
||||
|
||||
public ReportMessageConfiguration getReportMessageConfiguration() {
|
||||
return reportMessage;
|
||||
}
|
||||
|
||||
public SpamFilterConfiguration getSpamFilterConfiguration() {
|
||||
return spamFilterConfiguration;
|
||||
return spamFilter;
|
||||
}
|
||||
|
||||
public RegistrationServiceConfiguration getRegistrationServiceConfiguration() {
|
||||
public RegistrationServiceClientFactory getRegistrationServiceConfiguration() {
|
||||
return registrationService;
|
||||
}
|
||||
|
||||
public TurnConfiguration getTurnConfiguration() {
|
||||
return turn;
|
||||
}
|
||||
|
||||
public TusConfiguration getTus() {
|
||||
return tus;
|
||||
}
|
||||
|
||||
public ClientReleaseConfiguration getClientReleaseConfiguration() {
|
||||
return clientRelease;
|
||||
}
|
||||
|
||||
public MessageByteLimitCardinalityEstimatorConfiguration getMessageByteLimitCardinalityEstimator() {
|
||||
return messageByteLimitCardinalityEstimator;
|
||||
}
|
||||
|
||||
public LinkDeviceSecretConfiguration getLinkDeviceSecretConfiguration() {
|
||||
return linkDevice;
|
||||
}
|
||||
|
||||
public VirtualThreadConfiguration getVirtualThreadConfiguration() {
|
||||
return virtualThread;
|
||||
}
|
||||
|
||||
public ExternalRequestFilterConfiguration getExternalRequestFilterConfiguration() {
|
||||
return externalRequestFilter;
|
||||
}
|
||||
|
||||
public KeyTransparencyServiceConfiguration getKeyTransparencyServiceConfiguration() {
|
||||
return keyTransparencyService;
|
||||
}
|
||||
|
||||
public boolean logMessageDeliveryLoops() {
|
||||
return logMessageDeliveryLoops;
|
||||
}
|
||||
|
||||
public IdlePrimaryDeviceReminderConfiguration idlePrimaryDeviceReminderConfiguration() {
|
||||
return idlePrimaryDeviceReminder;
|
||||
}
|
||||
|
||||
public Map<String, CircuitBreakerConfiguration> getCircuitBreakerConfigurations() {
|
||||
return circuitBreakers;
|
||||
}
|
||||
|
||||
public Map<String, RetryConfiguration> getRetryConfigurations() {
|
||||
return retries;
|
||||
}
|
||||
|
||||
public RetryConfiguration getGeneralRedisRetryConfiguration() {
|
||||
return generalRedisRetry;
|
||||
}
|
||||
|
||||
public GrpcConfiguration getGrpc() {
|
||||
return grpc;
|
||||
}
|
||||
|
||||
public S3ObjectMonitorFactory getAsnTableConfiguration() {
|
||||
return asnTable;
|
||||
}
|
||||
|
||||
public CallQualitySurveyConfiguration getCallQualitySurveyConfiguration() {
|
||||
return callQualitySurvey;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.asn;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public record AsnInfo(long asn, @Nonnull String regionCode) {
|
||||
|
||||
public AsnInfo {
|
||||
requireNonNull(regionCode, "regionCode must not be null");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.asn;
|
||||
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public interface AsnInfoProvider {
|
||||
|
||||
/// Gets ASN information for an IP address.
|
||||
///
|
||||
/// @param ipString a string representation of an IP address
|
||||
///
|
||||
/// @return ASN information for the given IP address or empty if no ASN information was found for the given IP address
|
||||
Optional<AsnInfo> lookup(@Nonnull String ipString);
|
||||
|
||||
AsnInfoProvider EMPTY = _ -> Optional.empty();
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.asn;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.math.BigInteger;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.NavigableMap;
|
||||
import java.util.Optional;
|
||||
import java.util.TreeMap;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import javax.annotation.Nonnull;
|
||||
import org.apache.commons.csv.CSVFormat;
|
||||
import org.apache.commons.csv.CSVParser;
|
||||
import org.apache.commons.csv.CSVRecord;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* {@code AsnInfoProvider} implementation that supports both IPv4 and IPv6.
|
||||
*/
|
||||
public class AsnInfoProviderImpl implements AsnInfoProvider {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
|
||||
|
||||
@Nonnull
|
||||
private final NavigableMap<Long, AsnRange<Long>> asnBlocksByFirstIpv4;
|
||||
|
||||
@Nonnull
|
||||
private final NavigableMap<BigInteger, AsnRange<BigInteger>> asnBlocksByFirstIpv6;
|
||||
|
||||
|
||||
/**
|
||||
* Creates an instance of {@code AsnInfoProviderImpl} using data from <a href="https://iptoasn.com/">iptoasn.com</a>.
|
||||
* @param tsvGzInputStream gzip input stream representing the data.
|
||||
*/
|
||||
@Nonnull
|
||||
public static AsnInfoProviderImpl fromTsvGz(@Nonnull final InputStream tsvGzInputStream) {
|
||||
try (final GZIPInputStream inputStream = new GZIPInputStream(tsvGzInputStream)) {
|
||||
return fromTsv(inputStream);
|
||||
} catch (final IOException e) {
|
||||
log.error("failed to ungzip the input stream", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of {@code AsnInfoProviderImpl} using data from <a href="https://iptoasn.com/">iptoasn.com</a>.
|
||||
* @param tsvInputStream input stream representing the data.
|
||||
*/
|
||||
@Nonnull
|
||||
public static AsnInfoProviderImpl fromTsv(@Nonnull final InputStream tsvInputStream) {
|
||||
try (final InputStreamReader tsvReader = new InputStreamReader(tsvInputStream)) {
|
||||
final NavigableMap<Long, AsnRange<Long>> ip4asns = new TreeMap<>();
|
||||
final NavigableMap<BigInteger, AsnRange<BigInteger>> ip6asns = new TreeMap<>();
|
||||
final Map<Long, AsnInfo> asnInfoCache = new HashMap<>();
|
||||
|
||||
try (final CSVParser csvParser = CSVFormat.TDF.parse(tsvReader)) {
|
||||
for (final CSVRecord record : csvParser) {
|
||||
// format:
|
||||
// range_start_ip_string range_end_ip_string AS_number country_code AS_description
|
||||
final InetAddress startIp = InetAddress.getByName(record.get(0));
|
||||
final InetAddress endIp = InetAddress.getByName(record.get(1));
|
||||
final long asn = Long.parseLong(record.get(2));
|
||||
final String regionCode = record.get(3);
|
||||
// country code should be the same for any ASN, so we're caching AsnInfo objects
|
||||
// not to have multiple instances with the same values
|
||||
final AsnInfo asnInfo = asnInfoCache.computeIfAbsent(asn, k -> new AsnInfo(asn, regionCode));
|
||||
if (!regionCode.equals(asnInfo.regionCode())) {
|
||||
log.warn("ASN {} mapped to country codes {} and {}", asn, regionCode, asnInfo.regionCode());
|
||||
}
|
||||
|
||||
// IPv4
|
||||
if (startIp instanceof Inet4Address) {
|
||||
final AsnRange<Long> asnRange = new AsnRange<>(
|
||||
ip4BytesToLong((Inet4Address) startIp),
|
||||
ip4BytesToLong((Inet4Address) endIp),
|
||||
asnInfo
|
||||
);
|
||||
ip4asns.put(asnRange.from(), asnRange);
|
||||
}
|
||||
|
||||
// IPv6
|
||||
if (startIp instanceof Inet6Address) {
|
||||
final AsnRange<BigInteger> asnRange = new AsnRange<>(
|
||||
ip6BytesToBigInteger((Inet6Address) startIp),
|
||||
ip6BytesToBigInteger((Inet6Address) endIp),
|
||||
asnInfo
|
||||
);
|
||||
ip6asns.put(asnRange.from(), asnRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new AsnInfoProviderImpl(ip4asns, ip6asns);
|
||||
} catch (final Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public AsnInfoProviderImpl(
|
||||
@Nonnull final NavigableMap<Long, AsnRange<Long>> asnBlocksByFirstIpv4,
|
||||
@Nonnull final NavigableMap<BigInteger, AsnRange<BigInteger>> asnBlocksByFirstIpv6) {
|
||||
this.asnBlocksByFirstIpv4 = requireNonNull(asnBlocksByFirstIpv4);
|
||||
this.asnBlocksByFirstIpv6 = requireNonNull(asnBlocksByFirstIpv6);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Optional<AsnInfo> lookup(@Nonnull final String ipString) {
|
||||
try {
|
||||
final InetAddress address = InetAddress.getByName(ipString);
|
||||
if (address instanceof Inet4Address ip4) {
|
||||
final Long key = ip4BytesToLong(ip4);
|
||||
return lookupInMap(asnBlocksByFirstIpv4, key);
|
||||
}
|
||||
if (address instanceof Inet6Address ip6) {
|
||||
final BigInteger key = ip6BytesToBigInteger(ip6);
|
||||
return lookupInMap(asnBlocksByFirstIpv6, key);
|
||||
}
|
||||
// safety net, should never happen
|
||||
log.warn("Unknown InetAddress implementation: {}", address.getClass().getName());
|
||||
} catch (final Exception e) {
|
||||
log.error("Could not resolve ASN for IP string {}", ipString);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected static long ip4BytesToLong(@Nonnull final Inet4Address address) {
|
||||
final byte[] arr = address.getAddress();
|
||||
Validate.isTrue(arr.length == 4);
|
||||
return Integer.toUnsignedLong(ByteBuffer.wrap(arr).getInt());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected static BigInteger ip6BytesToBigInteger(@Nonnull final Inet6Address address) {
|
||||
final byte[] arr = address.getAddress();
|
||||
Validate.isTrue(arr.length == 16);
|
||||
return new BigInteger(1, arr);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static <T extends Comparable<T>> Optional<AsnInfo> lookupInMap(
|
||||
@Nonnull final NavigableMap<T, AsnRange<T>> map,
|
||||
@Nonnull final T key) {
|
||||
return Optional.ofNullable(map.floorEntry(key))
|
||||
.filter(e -> e.getValue().contains(key) && e.getValue().asnInfo().asn() != 0)
|
||||
.map(e -> e.getValue().asnInfo());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.asn;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
public record AsnRange<T extends Comparable<T>>(@Nonnull T from,
|
||||
@Nonnull T to,
|
||||
@Nonnull AsnInfo asnInfo) {
|
||||
public AsnRange {
|
||||
requireNonNull(from);
|
||||
requireNonNull(to);
|
||||
requireNonNull(asnInfo);
|
||||
Validate.isTrue(from.compareTo(to) <= 0);
|
||||
}
|
||||
|
||||
boolean contains(@Nonnull final T element) {
|
||||
requireNonNull(element);
|
||||
return from.compareTo(element) <= 0
|
||||
&& element.compareTo(to) <= 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.attachments;
|
||||
import java.util.Map;
|
||||
|
||||
public interface AttachmentGenerator {
|
||||
|
||||
record Descriptor(Map<String, String> headers, String signedUploadLocation) {}
|
||||
|
||||
Descriptor generateAttachment(final String key);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.attachments;
|
||||
|
||||
import org.whispersystems.textsecuregcm.gcp.CanonicalRequest;
|
||||
import org.whispersystems.textsecuregcm.gcp.CanonicalRequestGenerator;
|
||||
import org.whispersystems.textsecuregcm.gcp.CanonicalRequestSigner;
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
public class GcsAttachmentGenerator implements AttachmentGenerator {
|
||||
@Nonnull
|
||||
private final CanonicalRequestGenerator canonicalRequestGenerator;
|
||||
|
||||
@Nonnull
|
||||
private final CanonicalRequestSigner canonicalRequestSigner;
|
||||
|
||||
public GcsAttachmentGenerator(@Nonnull String domain, @Nonnull String email,
|
||||
int maxSizeInBytes, @Nonnull String pathPrefix, @Nonnull String rsaSigningKey)
|
||||
throws IOException, InvalidKeyException, InvalidKeySpecException {
|
||||
this.canonicalRequestGenerator = new CanonicalRequestGenerator(domain, email, maxSizeInBytes, pathPrefix);
|
||||
this.canonicalRequestSigner = new CanonicalRequestSigner(rsaSigningKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Descriptor generateAttachment(final String key) {
|
||||
final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
final CanonicalRequest canonicalRequest = canonicalRequestGenerator.createFor(key, now);
|
||||
return new Descriptor(getHeaderMap(canonicalRequest), getSignedUploadLocation(canonicalRequest));
|
||||
}
|
||||
|
||||
private String getSignedUploadLocation(@Nonnull CanonicalRequest canonicalRequest) {
|
||||
return "https://" + canonicalRequest.getDomain() + canonicalRequest.getResourcePath()
|
||||
+ '?' + canonicalRequest.getCanonicalQuery()
|
||||
+ "&X-Goog-Signature=" + canonicalRequestSigner.sign(canonicalRequest);
|
||||
}
|
||||
|
||||
private static Map<String, String> getHeaderMap(@Nonnull CanonicalRequest canonicalRequest) {
|
||||
return Map.of(
|
||||
"host", canonicalRequest.getDomain(),
|
||||
"x-goog-content-length-range", "1," + canonicalRequest.getMaxSizeInBytes(),
|
||||
"x-goog-resumable", "start");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.attachments;
|
||||
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.util.HeaderUtils;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Clock;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
|
||||
public class TusAttachmentGenerator implements AttachmentGenerator {
|
||||
|
||||
private static final String ATTACHMENTS = "attachments";
|
||||
|
||||
final ExternalServiceCredentialsGenerator credentialsGenerator;
|
||||
final String tusUri;
|
||||
|
||||
public TusAttachmentGenerator(final TusConfiguration cfg) {
|
||||
this.tusUri = cfg.uploadUri();
|
||||
this.credentialsGenerator = credentialsGenerator(Clock.systemUTC(), cfg);
|
||||
}
|
||||
|
||||
private static ExternalServiceCredentialsGenerator credentialsGenerator(final Clock clock, final TusConfiguration cfg) {
|
||||
return ExternalServiceCredentialsGenerator
|
||||
.builder(cfg.userAuthenticationTokenSharedSecret())
|
||||
.prependUsername(false)
|
||||
.withClock(clock)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Descriptor generateAttachment(final String key) {
|
||||
final ExternalServiceCredentials credentials = credentialsGenerator.generateFor(ATTACHMENTS + "/" + key);
|
||||
final String b64Key = Base64.getEncoder().encodeToString(key.getBytes(StandardCharsets.UTF_8));
|
||||
final Map<String, String> headers = Map.of(
|
||||
HttpHeaders.AUTHORIZATION, HeaderUtils.basicAuthHeader(credentials),
|
||||
"Upload-Metadata", String.format("filename %s", b64Key)
|
||||
);
|
||||
return new Descriptor(headers, tusUri + "/" + ATTACHMENTS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.attachments;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
|
||||
public record TusConfiguration(
|
||||
@ExactlySize(32) SecretBytes userAuthenticationTokenSharedSecret,
|
||||
@NotEmpty String uploadUri
|
||||
){}
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
public interface AccountAndAuthenticatedDeviceHolder {
|
||||
|
||||
Account getAccount();
|
||||
|
||||
Device getAuthenticatedDevice();
|
||||
}
|
||||
@@ -1,25 +1,161 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* Copyright 2013 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.dropwizard.auth.Authenticator;
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Optional;
|
||||
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
||||
import java.util.UUID;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
public class AccountAuthenticator extends BaseAccountAuthenticator implements
|
||||
Authenticator<BasicCredentials, AuthenticatedAccount> {
|
||||
public class AccountAuthenticator implements Authenticator<BasicCredentials, AuthenticatedDevice> {
|
||||
|
||||
private static final String AUTHENTICATION_COUNTER_NAME = name(AccountAuthenticator.class, "authentication");
|
||||
private static final String AUTHENTICATION_SUCCEEDED_TAG_NAME = "succeeded";
|
||||
private static final String AUTHENTICATION_FAILURE_REASON_TAG_NAME = "reason";
|
||||
|
||||
private static final String DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME = name(AccountAuthenticator.class, "daysSinceLastSeen");
|
||||
private static final String IS_PRIMARY_DEVICE_TAG = "isPrimary";
|
||||
|
||||
private static final Counter OLD_TOKEN_VERSION_COUNTER =
|
||||
Metrics.counter(name(AccountAuthenticator.class, "oldTokenVersionCounter"));
|
||||
|
||||
@VisibleForTesting
|
||||
static final char DEVICE_ID_SEPARATOR = '.';
|
||||
|
||||
private final AccountsManager accountsManager;
|
||||
private final Clock clock;
|
||||
|
||||
public AccountAuthenticator(AccountsManager accountsManager) {
|
||||
super(accountsManager);
|
||||
this(accountsManager, Clock.systemUTC());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public AccountAuthenticator(AccountsManager accountsManager, Clock clock) {
|
||||
this.accountsManager = accountsManager;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
static Pair<String, Byte> getIdentifierAndDeviceId(final String basicUsername) {
|
||||
final String identifier;
|
||||
final byte deviceId;
|
||||
|
||||
final int deviceIdSeparatorIndex = basicUsername.indexOf(DEVICE_ID_SEPARATOR);
|
||||
|
||||
if (deviceIdSeparatorIndex == -1) {
|
||||
identifier = basicUsername;
|
||||
deviceId = Device.PRIMARY_ID;
|
||||
} else {
|
||||
identifier = basicUsername.substring(0, deviceIdSeparatorIndex);
|
||||
deviceId = Byte.parseByte(basicUsername.substring(deviceIdSeparatorIndex + 1));
|
||||
}
|
||||
|
||||
return new Pair<>(identifier, deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AuthenticatedAccount> authenticate(BasicCredentials basicCredentials) {
|
||||
return super.authenticate(basicCredentials, true);
|
||||
public Optional<AuthenticatedDevice> authenticate(BasicCredentials basicCredentials) {
|
||||
boolean succeeded = false;
|
||||
String failureReason = null;
|
||||
|
||||
try {
|
||||
final UUID accountUuid;
|
||||
final byte deviceId;
|
||||
{
|
||||
final Pair<String, Byte> identifierAndDeviceId = getIdentifierAndDeviceId(basicCredentials.getUsername());
|
||||
|
||||
accountUuid = UUID.fromString(identifierAndDeviceId.first());
|
||||
deviceId = identifierAndDeviceId.second();
|
||||
}
|
||||
|
||||
Optional<Account> account = accountsManager.getByAccountIdentifier(accountUuid);
|
||||
|
||||
if (account.isEmpty()) {
|
||||
failureReason = "noSuchAccount";
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Optional<Device> device = account.get().getDevice(deviceId);
|
||||
|
||||
if (device.isEmpty()) {
|
||||
failureReason = "noSuchDevice";
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
SaltedTokenHash deviceSaltedTokenHash = device.get().getAuthTokenHash();
|
||||
if (deviceSaltedTokenHash.verify(basicCredentials.getPassword())) {
|
||||
succeeded = true;
|
||||
Account authenticatedAccount = updateLastSeen(account.get(), device.get());
|
||||
if (deviceSaltedTokenHash.getVersion() != SaltedTokenHash.CURRENT_VERSION) {
|
||||
OLD_TOKEN_VERSION_COUNTER.increment();
|
||||
authenticatedAccount = accountsManager.updateDeviceAuthentication(
|
||||
authenticatedAccount,
|
||||
device.get(),
|
||||
SaltedTokenHash.generateFor(basicCredentials.getPassword())); // new credentials have current version
|
||||
}
|
||||
return Optional.of(new AuthenticatedDevice(authenticatedAccount.getIdentifier(IdentityType.ACI),
|
||||
device.get().getId(),
|
||||
Instant.ofEpochMilli(authenticatedAccount.getPrimaryDevice().getLastSeen())));
|
||||
} else {
|
||||
failureReason = "incorrectPassword";
|
||||
return Optional.empty();
|
||||
}
|
||||
} catch (IllegalArgumentException | InvalidAuthorizationHeaderException iae) {
|
||||
failureReason = "invalidHeader";
|
||||
return Optional.empty();
|
||||
} finally {
|
||||
Tags tags = Tags.of(
|
||||
AUTHENTICATION_SUCCEEDED_TAG_NAME, String.valueOf(succeeded));
|
||||
|
||||
if (StringUtils.isNotBlank(failureReason)) {
|
||||
tags = tags.and(AUTHENTICATION_FAILURE_REASON_TAG_NAME, failureReason);
|
||||
}
|
||||
|
||||
Metrics.counter(AUTHENTICATION_COUNTER_NAME, tags).increment();
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public Account updateLastSeen(Account account, Device device) {
|
||||
// compute a non-negative integer between 0 and 86400.
|
||||
long n = Util.ensureNonNegativeLong(account.getUuid().getLeastSignificantBits());
|
||||
final long lastSeenOffsetSeconds = n % ChronoUnit.DAYS.getDuration().toSeconds();
|
||||
|
||||
// produce a truncated timestamp which is either today at UTC midnight
|
||||
// or yesterday at UTC midnight, based on per-user randomized offset used.
|
||||
final long todayInMillisWithOffset = Util.todayInMillisGivenOffsetFromNow(clock,
|
||||
Duration.ofSeconds(lastSeenOffsetSeconds).negated());
|
||||
|
||||
// only update the device's last seen time when it falls behind the truncated timestamp.
|
||||
// this ensures a few things:
|
||||
// (1) each account will only update last-seen at most once per day
|
||||
// (2) these updates will occur throughout the day rather than all occurring at UTC midnight.
|
||||
if (device.getLastSeen() < todayInMillisWithOffset) {
|
||||
Metrics.summary(DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME, IS_PRIMARY_DEVICE_TAG, String.valueOf(device.isPrimary()))
|
||||
.record(Duration.ofMillis(todayInMillisWithOffset - device.getLastSeen()).toDays());
|
||||
|
||||
return accountsManager.updateDeviceLastSeen(account, device, Util.todayInMillis(clock));
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.util.Base64;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
public class Anonymous {
|
||||
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.glassfish.jersey.server.ContainerRequest;
|
||||
import org.glassfish.jersey.server.monitoring.RequestEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
|
||||
/**
|
||||
* This {@link WebsocketRefreshRequirementProvider} observes intra-request changes in {@link Account#isEnabled()} and
|
||||
* {@link Device#isEnabled()}.
|
||||
* <p>
|
||||
* If a change in {@link Account#isEnabled()} or any associated {@link Device#isEnabled()} is observed, then any active
|
||||
* WebSocket connections for the account must be closed in order for clients to get a refreshed
|
||||
* {@link io.dropwizard.auth.Auth} object with a current device list.
|
||||
*
|
||||
* @see AuthenticatedAccount
|
||||
* @see DisabledPermittedAuthenticatedAccount
|
||||
*/
|
||||
public class AuthEnablementRefreshRequirementProvider implements WebsocketRefreshRequirementProvider {
|
||||
|
||||
private final AccountsManager accountsManager;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AuthEnablementRefreshRequirementProvider.class);
|
||||
|
||||
private static final String ACCOUNT_UUID = AuthEnablementRefreshRequirementProvider.class.getName() + ".accountUuid";
|
||||
private static final String DEVICES_ENABLED = AuthEnablementRefreshRequirementProvider.class.getName() + ".devicesEnabled";
|
||||
|
||||
public AuthEnablementRefreshRequirementProvider(final AccountsManager accountsManager) {
|
||||
this.accountsManager = accountsManager;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static Map<Long, Boolean> buildDevicesEnabledMap(final Account account) {
|
||||
return account.getDevices().stream().collect(Collectors.toMap(Device::getId, Device::isEnabled));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleRequestFiltered(final RequestEvent requestEvent) {
|
||||
if (requestEvent.getUriInfo().getMatchedResourceMethod().getInvocable().getHandlingMethod().getAnnotation(ChangesDeviceEnabledState.class) != null) {
|
||||
// The authenticated principal, if any, will be available after filters have run.
|
||||
// Now that the account is known, capture a snapshot of `isEnabled` for the account's devices before carrying out
|
||||
// the request’s business logic.
|
||||
ContainerRequestUtil.getAuthenticatedAccount(requestEvent.getContainerRequest()).ifPresent(account ->
|
||||
setAccount(requestEvent.getContainerRequest(), account));
|
||||
}
|
||||
}
|
||||
|
||||
public static void setAccount(final ContainerRequest containerRequest, final Account account) {
|
||||
containerRequest.setProperty(ACCOUNT_UUID, account.getUuid());
|
||||
containerRequest.setProperty(DEVICES_ENABLED, buildDevicesEnabledMap(account));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Pair<UUID, Long>> handleRequestFinished(final RequestEvent requestEvent) {
|
||||
// Now that the request is finished, check whether `isEnabled` changed for any of the devices. If the value did
|
||||
// change or if a devices was added or removed, all devices must disconnect and reauthenticate.
|
||||
if (requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED) != null) {
|
||||
|
||||
@SuppressWarnings("unchecked") final Map<Long, Boolean> initialDevicesEnabled =
|
||||
(Map<Long, Boolean>) requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED);
|
||||
|
||||
return accountsManager.getByAccountIdentifier((UUID) requestEvent.getContainerRequest().getProperty(ACCOUNT_UUID)).map(account -> {
|
||||
final Set<Long> deviceIdsToDisplace;
|
||||
final Map<Long, Boolean> currentDevicesEnabled = buildDevicesEnabledMap(account);
|
||||
|
||||
if (!initialDevicesEnabled.equals(currentDevicesEnabled)) {
|
||||
deviceIdsToDisplace = new HashSet<>(initialDevicesEnabled.keySet());
|
||||
deviceIdsToDisplace.addAll(currentDevicesEnabled.keySet());
|
||||
} else {
|
||||
deviceIdsToDisplace = Collections.emptySet();
|
||||
}
|
||||
|
||||
return deviceIdsToDisplace.stream()
|
||||
.map(deviceId -> new Pair<>(account.getUuid(), deviceId))
|
||||
.collect(Collectors.toList());
|
||||
}).orElseGet(() -> {
|
||||
logger.error("Request had account, but it is no longer present");
|
||||
return Collections.emptyList();
|
||||
});
|
||||
} else
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.function.Supplier;
|
||||
import javax.security.auth.Subject;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
|
||||
public class AuthenticatedAccount implements Principal, AccountAndAuthenticatedDeviceHolder {
|
||||
|
||||
private final Supplier<Pair<Account, Device>> accountAndDevice;
|
||||
|
||||
public AuthenticatedAccount(final Supplier<Pair<Account, Device>> accountAndDevice) {
|
||||
this.accountAndDevice = accountAndDevice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Account getAccount() {
|
||||
return accountAndDevice.get().first();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Device getAuthenticatedDevice() {
|
||||
return accountAndDevice.get().second();
|
||||
}
|
||||
|
||||
// Principal implementation
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean implies(final Subject subject) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.signal.libsignal.zkgroup.backups.BackupCredentialType;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupLevel;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public record AuthenticatedBackupUser(
|
||||
byte[] backupId,
|
||||
BackupCredentialType credentialType,
|
||||
BackupLevel backupLevel,
|
||||
String backupDir,
|
||||
String mediaDir,
|
||||
@Nullable UserAgent userAgent) {
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
import javax.security.auth.Subject;
|
||||
|
||||
public record AuthenticatedDevice(UUID accountIdentifier, byte deviceId, Instant primaryDeviceLastSeen)
|
||||
implements Principal {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean implies(final Subject subject) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.RefreshingAccountAndDeviceSupplier;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
public class BaseAccountAuthenticator {
|
||||
|
||||
private static final String AUTHENTICATION_COUNTER_NAME = name(BaseAccountAuthenticator.class, "authentication");
|
||||
private static final String ENABLED_NOT_REQUIRED_AUTHENTICATION_COUNTER_NAME = name(BaseAccountAuthenticator.class,
|
||||
"enabledNotRequiredAuthentication");
|
||||
private static final String AUTHENTICATION_SUCCEEDED_TAG_NAME = "succeeded";
|
||||
private static final String AUTHENTICATION_FAILURE_REASON_TAG_NAME = "reason";
|
||||
private static final String ENABLED_TAG_NAME = "enabled";
|
||||
private static final String AUTHENTICATION_HAS_STORY_CAPABILITY = "hasStoryCapability";
|
||||
|
||||
private static final String STORY_ADOPTION_COUNTER_NAME = name(BaseAccountAuthenticator.class, "storyAdoption");
|
||||
|
||||
private static final String DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME = name(BaseAccountAuthenticator.class, "daysSinceLastSeen");
|
||||
private static final String IS_PRIMARY_DEVICE_TAG = "isPrimary";
|
||||
|
||||
@VisibleForTesting
|
||||
static final char DEVICE_ID_SEPARATOR = '.';
|
||||
|
||||
private final AccountsManager accountsManager;
|
||||
private final Clock clock;
|
||||
|
||||
public BaseAccountAuthenticator(AccountsManager accountsManager) {
|
||||
this(accountsManager, Clock.systemUTC());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public BaseAccountAuthenticator(AccountsManager accountsManager, Clock clock) {
|
||||
this.accountsManager = accountsManager;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
static Pair<String, Long> getIdentifierAndDeviceId(final String basicUsername) {
|
||||
final String identifier;
|
||||
final long deviceId;
|
||||
|
||||
final int deviceIdSeparatorIndex = basicUsername.indexOf(DEVICE_ID_SEPARATOR);
|
||||
|
||||
if (deviceIdSeparatorIndex == -1) {
|
||||
identifier = basicUsername;
|
||||
deviceId = Device.MASTER_ID;
|
||||
} else {
|
||||
identifier = basicUsername.substring(0, deviceIdSeparatorIndex);
|
||||
deviceId = Long.parseLong(basicUsername.substring(deviceIdSeparatorIndex + 1));
|
||||
}
|
||||
|
||||
return new Pair<>(identifier, deviceId);
|
||||
}
|
||||
|
||||
public Optional<AuthenticatedAccount> authenticate(BasicCredentials basicCredentials, boolean enabledRequired) {
|
||||
boolean succeeded = false;
|
||||
String failureReason = null;
|
||||
boolean hasStoryCapability = false;
|
||||
|
||||
try {
|
||||
final UUID accountUuid;
|
||||
final long deviceId;
|
||||
{
|
||||
final Pair<String, Long> identifierAndDeviceId = getIdentifierAndDeviceId(basicCredentials.getUsername());
|
||||
|
||||
accountUuid = UUID.fromString(identifierAndDeviceId.first());
|
||||
deviceId = identifierAndDeviceId.second();
|
||||
}
|
||||
|
||||
Optional<Account> account = accountsManager.getByAccountIdentifier(accountUuid);
|
||||
|
||||
if (account.isEmpty()) {
|
||||
failureReason = "noSuchAccount";
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
hasStoryCapability = account.map(Account::isStoriesSupported).orElse(false);
|
||||
|
||||
Optional<Device> device = account.get().getDevice(deviceId);
|
||||
|
||||
if (device.isEmpty()) {
|
||||
failureReason = "noSuchDevice";
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (enabledRequired) {
|
||||
final boolean deviceDisabled = !device.get().isEnabled();
|
||||
if (deviceDisabled) {
|
||||
failureReason = "deviceDisabled";
|
||||
}
|
||||
|
||||
final boolean accountDisabled = !account.get().isEnabled();
|
||||
if (accountDisabled) {
|
||||
failureReason = "accountDisabled";
|
||||
}
|
||||
if (accountDisabled || deviceDisabled) {
|
||||
return Optional.empty();
|
||||
}
|
||||
} else {
|
||||
Metrics.counter(ENABLED_NOT_REQUIRED_AUTHENTICATION_COUNTER_NAME,
|
||||
ENABLED_TAG_NAME, String.valueOf(device.get().isEnabled() && account.get().isEnabled()),
|
||||
IS_PRIMARY_DEVICE_TAG, String.valueOf(device.get().isMaster()))
|
||||
.increment();
|
||||
}
|
||||
|
||||
SaltedTokenHash deviceSaltedTokenHash = device.get().getAuthTokenHash();
|
||||
if (deviceSaltedTokenHash.verify(basicCredentials.getPassword())) {
|
||||
succeeded = true;
|
||||
Account authenticatedAccount = updateLastSeen(account.get(), device.get());
|
||||
if (deviceSaltedTokenHash.getVersion() != SaltedTokenHash.CURRENT_VERSION) {
|
||||
authenticatedAccount = accountsManager.updateDeviceAuthentication(
|
||||
authenticatedAccount,
|
||||
device.get(),
|
||||
SaltedTokenHash.generateFor(basicCredentials.getPassword())); // new credentials have current version
|
||||
}
|
||||
return Optional.of(new AuthenticatedAccount(
|
||||
new RefreshingAccountAndDeviceSupplier(authenticatedAccount, device.get().getId(), accountsManager)));
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
} catch (IllegalArgumentException | InvalidAuthorizationHeaderException iae) {
|
||||
failureReason = "invalidHeader";
|
||||
return Optional.empty();
|
||||
} finally {
|
||||
Tags tags = Tags.of(
|
||||
AUTHENTICATION_SUCCEEDED_TAG_NAME, String.valueOf(succeeded));
|
||||
|
||||
if (StringUtils.isNotBlank(failureReason)) {
|
||||
tags = tags.and(AUTHENTICATION_FAILURE_REASON_TAG_NAME, failureReason);
|
||||
}
|
||||
|
||||
Metrics.counter(AUTHENTICATION_COUNTER_NAME, tags).increment();
|
||||
|
||||
Tags storyTags = Tags.of(AUTHENTICATION_HAS_STORY_CAPABILITY, String.valueOf(hasStoryCapability));
|
||||
Metrics.counter(STORY_ADOPTION_COUNTER_NAME, storyTags).increment();
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public Account updateLastSeen(Account account, Device device) {
|
||||
// compute a non-negative integer between 0 and 86400.
|
||||
long n = Util.ensureNonNegativeLong(account.getUuid().getLeastSignificantBits());
|
||||
final long lastSeenOffsetSeconds = n % ChronoUnit.DAYS.getDuration().toSeconds();
|
||||
|
||||
// produce a truncated timestamp which is either today at UTC midnight
|
||||
// or yesterday at UTC midnight, based on per-user randomized offset used.
|
||||
final long todayInMillisWithOffset = Util.todayInMillisGivenOffsetFromNow(clock, Duration.ofSeconds(lastSeenOffsetSeconds).negated());
|
||||
|
||||
// only update the device's last seen time when it falls behind the truncated timestamp.
|
||||
// this ensure a few things:
|
||||
// (1) each account will only update last-seen at most once per day
|
||||
// (2) these updates will occur throughout the day rather than all occurring at UTC midnight.
|
||||
if (device.getLastSeen() < todayInMillisWithOffset) {
|
||||
Metrics.summary(DAYS_SINCE_LAST_SEEN_DISTRIBUTION_NAME, IS_PRIMARY_DEVICE_TAG, String.valueOf(device.isMaster()))
|
||||
.record(Duration.ofMillis(todayInMillisWithOffset - device.getLastSeen()).toDays());
|
||||
|
||||
return accountsManager.updateDeviceLastSeen(account, device, Util.todayInMillis(clock));
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,10 @@ import org.whispersystems.textsecuregcm.util.Pair;
|
||||
public class BasicAuthorizationHeader {
|
||||
|
||||
private final String username;
|
||||
private final long deviceId;
|
||||
private final byte deviceId;
|
||||
private final String password;
|
||||
|
||||
private BasicAuthorizationHeader(final String username, final long deviceId, final String password) {
|
||||
private BasicAuthorizationHeader(final String username, final byte deviceId, final String password) {
|
||||
this.username = username;
|
||||
this.deviceId = deviceId;
|
||||
this.password = password;
|
||||
@@ -59,10 +59,10 @@ public class BasicAuthorizationHeader {
|
||||
final String usernameComponent = credentials.substring(0, credentialSeparatorIndex);
|
||||
|
||||
final String username;
|
||||
final long deviceId;
|
||||
final byte deviceId;
|
||||
{
|
||||
final Pair<String, Long> identifierAndDeviceId =
|
||||
BaseAccountAuthenticator.getIdentifierAndDeviceId(usernameComponent);
|
||||
final Pair<String, Byte> identifierAndDeviceId =
|
||||
AccountAuthenticator.getIdentifierAndDeviceId(usernameComponent);
|
||||
|
||||
username = identifierAndDeviceId.first();
|
||||
deviceId = identifierAndDeviceId.second();
|
||||
|
||||
@@ -8,12 +8,11 @@ package org.whispersystems.textsecuregcm.auth;
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECPrivateKey;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.SenderCertificate;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.ServerCertificate;
|
||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
@@ -31,13 +30,13 @@ public class CertificateGenerator {
|
||||
this.serverCertificate = ServerCertificate.parseFrom(serverCertificate);
|
||||
}
|
||||
|
||||
public byte[] createFor(Account account, Device device, boolean includeE164) throws InvalidKeyException {
|
||||
public byte[] createFor(final Account account, final byte deviceId, boolean includeE164) throws InvalidKeyException {
|
||||
SenderCertificate.Certificate.Builder builder = SenderCertificate.Certificate.newBuilder()
|
||||
.setSenderDevice(Math.toIntExact(device.getId()))
|
||||
.setExpires(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(expiresDays))
|
||||
.setIdentityKey(ByteString.copyFrom(Base64.getDecoder().decode(account.getIdentityKey())))
|
||||
.setSigner(serverCertificate)
|
||||
.setSenderUuid(account.getUuid().toString());
|
||||
.setSenderDevice(Math.toIntExact(deviceId))
|
||||
.setExpires(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(expiresDays))
|
||||
.setIdentityKey(ByteString.copyFrom(account.getIdentityKey(IdentityType.ACI).serialize()))
|
||||
.setSigner(serverCertificate)
|
||||
.setSenderUuid(account.getUuid().toString());
|
||||
|
||||
if (includeE164) {
|
||||
builder.setSender(account.getNumber());
|
||||
@@ -45,11 +44,7 @@ public class CertificateGenerator {
|
||||
|
||||
byte[] certificate = builder.build().toByteArray();
|
||||
byte[] signature;
|
||||
try {
|
||||
signature = Curve.calculateSignature(privateKey, certificate);
|
||||
} catch (org.signal.libsignal.protocol.InvalidKeyException e) {
|
||||
throw new InvalidKeyException(e);
|
||||
}
|
||||
signature = privateKey.calculateSignature(certificate);
|
||||
|
||||
return SenderCertificate.newBuilder()
|
||||
.setCertificate(ByteString.copyFrom(certificate))
|
||||
|
||||
@@ -16,5 +16,5 @@ import java.lang.annotation.Target;
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ChangesDeviceEnabledState {
|
||||
public @interface ChangesLinkedDevices {
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Indicates that an endpoint changes the phone number and PNI keys associated with an account, and that
|
||||
* any websockets associated with the account may need to be refreshed after a call to that endpoint.
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ChangesPhoneNumber {
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import io.netty.resolver.dns.DnsNameResolver;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import javax.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
|
||||
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
public class CloudflareTurnCredentialsManager {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CloudflareTurnCredentialsManager.class);
|
||||
|
||||
private final List<String> cloudflareTurnUrls;
|
||||
private final List<String> cloudflareTurnUrlsWithIps;
|
||||
private final String cloudflareTurnHostname;
|
||||
private final HttpRequest getCredentialsRequest;
|
||||
|
||||
private final FaultTolerantHttpClient cloudflareTurnClient;
|
||||
private final DnsNameResolver dnsNameResolver;
|
||||
|
||||
private final Duration clientCredentialTtl;
|
||||
|
||||
private record CredentialRequest(long ttl) {}
|
||||
|
||||
private record CloudflareTurnResponse(IceServer iceServers) {
|
||||
|
||||
private record IceServer(
|
||||
String username,
|
||||
String credential,
|
||||
List<String> urls) {
|
||||
}
|
||||
}
|
||||
|
||||
public CloudflareTurnCredentialsManager(final String cloudflareTurnApiToken,
|
||||
final String cloudflareTurnEndpoint,
|
||||
final Duration requestedCredentialTtl,
|
||||
final Duration clientCredentialTtl,
|
||||
final List<String> cloudflareTurnUrls,
|
||||
final List<String> cloudflareTurnUrlsWithIps,
|
||||
final String cloudflareTurnHostname,
|
||||
final int cloudflareTurnNumHttpClients,
|
||||
@Nullable final String circuitBreakerConfigurationName,
|
||||
final ExecutorService executor,
|
||||
@Nullable final String retryConfigurationName,
|
||||
final ScheduledExecutorService retryExecutor,
|
||||
final DnsNameResolver dnsNameResolver) {
|
||||
|
||||
this.cloudflareTurnClient = FaultTolerantHttpClient.newBuilder("cloudflare-turn", executor)
|
||||
.withCircuitBreaker(circuitBreakerConfigurationName)
|
||||
.withRetry(retryConfigurationName, retryExecutor)
|
||||
.withNumClients(cloudflareTurnNumHttpClients)
|
||||
.build();
|
||||
this.cloudflareTurnUrls = cloudflareTurnUrls;
|
||||
this.cloudflareTurnUrlsWithIps = cloudflareTurnUrlsWithIps;
|
||||
this.cloudflareTurnHostname = cloudflareTurnHostname;
|
||||
this.dnsNameResolver = dnsNameResolver;
|
||||
|
||||
final String credentialsRequestBody;
|
||||
|
||||
try {
|
||||
credentialsRequestBody =
|
||||
SystemMapper.jsonMapper().writeValueAsString(new CredentialRequest(requestedCredentialTtl.toSeconds()));
|
||||
} catch (final JsonProcessingException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
|
||||
// We repeat the same request to Cloudflare every time, so we can construct it once and re-use it
|
||||
this.getCredentialsRequest = HttpRequest.newBuilder()
|
||||
.uri(URI.create(cloudflareTurnEndpoint))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", String.format("Bearer %s", cloudflareTurnApiToken))
|
||||
.POST(HttpRequest.BodyPublishers.ofString(credentialsRequestBody))
|
||||
.build();
|
||||
|
||||
this.clientCredentialTtl = clientCredentialTtl;
|
||||
}
|
||||
|
||||
public TurnToken retrieveFromCloudflare() throws IOException {
|
||||
final List<String> cloudflareTurnComposedUrls;
|
||||
try {
|
||||
cloudflareTurnComposedUrls = dnsNameResolver.resolveAll(cloudflareTurnHostname).get().stream()
|
||||
.map(i -> switch (i) {
|
||||
case Inet6Address i6 -> "[" + i6.getHostAddress() + "]";
|
||||
default -> i.getHostAddress();
|
||||
})
|
||||
.flatMap(i -> cloudflareTurnUrlsWithIps.stream().map(u -> u.formatted(i)))
|
||||
.toList();
|
||||
} catch (Exception e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
final HttpResponse<String> response;
|
||||
try {
|
||||
response = cloudflareTurnClient.sendAsync(getCredentialsRequest, HttpResponse.BodyHandlers.ofString()).join();
|
||||
} catch (CompletionException e) {
|
||||
logger.warn("failed to make http request to Cloudflare Turn: {}", e.getMessage());
|
||||
throw new IOException(ExceptionUtils.unwrap(e));
|
||||
}
|
||||
|
||||
if (response.statusCode() != Response.Status.CREATED.getStatusCode()) {
|
||||
logger.warn("failure request credentials from Cloudflare Turn (code={}): {}", response.statusCode(), response);
|
||||
throw new IOException("Cloudflare Turn http failure : " + response.statusCode());
|
||||
}
|
||||
|
||||
final CloudflareTurnResponse cloudflareTurnResponse = SystemMapper.jsonMapper()
|
||||
.readValue(response.body(), CloudflareTurnResponse.class);
|
||||
|
||||
return new TurnToken(
|
||||
cloudflareTurnResponse.iceServers().username(),
|
||||
cloudflareTurnResponse.iceServers().credential(),
|
||||
clientCredentialTtl.toSeconds(),
|
||||
cloudflareTurnUrls == null ? Collections.emptyList() : cloudflareTurnUrls,
|
||||
cloudflareTurnComposedUrls,
|
||||
cloudflareTurnHostname
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,10 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import java.util.Base64;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
|
||||
public class CombinedUnidentifiedSenderAccessKeys {
|
||||
private final byte[] combinedUnidentifiedSenderAccessKeys;
|
||||
@@ -16,7 +16,7 @@ public class CombinedUnidentifiedSenderAccessKeys {
|
||||
public CombinedUnidentifiedSenderAccessKeys(String header) {
|
||||
try {
|
||||
this.combinedUnidentifiedSenderAccessKeys = Base64.getDecoder().decode(header);
|
||||
if (this.combinedUnidentifiedSenderAccessKeys == null || this.combinedUnidentifiedSenderAccessKeys.length != 16) {
|
||||
if (this.combinedUnidentifiedSenderAccessKeys == null || this.combinedUnidentifiedSenderAccessKeys.length != UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH) {
|
||||
throw new WebApplicationException("Invalid combined unidentified sender access keys", Status.UNAUTHORIZED);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.glassfish.jersey.server.ContainerRequest;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import javax.ws.rs.core.SecurityContext;
|
||||
import java.util.Optional;
|
||||
|
||||
class ContainerRequestUtil {
|
||||
|
||||
static Optional<Account> getAuthenticatedAccount(final ContainerRequest request) {
|
||||
return Optional.ofNullable(request.getSecurityContext())
|
||||
.map(SecurityContext::getUserPrincipal)
|
||||
.map(principal -> principal instanceof AccountAndAuthenticatedDeviceHolder
|
||||
? ((AccountAndAuthenticatedDeviceHolder) principal).getAccount() : null);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import io.dropwizard.auth.Authenticator;
|
||||
import io.dropwizard.auth.basic.BasicCredentials;
|
||||
import java.util.Optional;
|
||||
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
|
||||
public class DisabledPermittedAccountAuthenticator extends BaseAccountAuthenticator implements
|
||||
Authenticator<BasicCredentials, DisabledPermittedAuthenticatedAccount> {
|
||||
|
||||
public DisabledPermittedAccountAuthenticator(AccountsManager accountsManager) {
|
||||
super(accountsManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<DisabledPermittedAuthenticatedAccount> authenticate(BasicCredentials credentials) {
|
||||
Optional<AuthenticatedAccount> account = super.authenticate(credentials, false);
|
||||
return account.map(DisabledPermittedAuthenticatedAccount::new);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import java.security.Principal;
|
||||
import javax.security.auth.Subject;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
public class DisabledPermittedAuthenticatedAccount implements Principal, AccountAndAuthenticatedDeviceHolder {
|
||||
|
||||
private final AuthenticatedAccount authenticatedAccount;
|
||||
|
||||
public DisabledPermittedAuthenticatedAccount(final AuthenticatedAccount authenticatedAccount) {
|
||||
this.authenticatedAccount = authenticatedAccount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Account getAccount() {
|
||||
return authenticatedAccount.getAccount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Device getAuthenticatedDevice() {
|
||||
return authenticatedAccount.getAuthenticatedDevice();
|
||||
}
|
||||
|
||||
// Principal implementation
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean implies(Subject subject) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
/**
|
||||
* A disconnection request listener receives and handles a request to close an authenticated network connection for a
|
||||
* specific client.
|
||||
*/
|
||||
public interface DisconnectionRequestListener {
|
||||
|
||||
/**
|
||||
* Handles a request to close an authenticated network connection for a specific authenticated device. Requests are
|
||||
* dispatched on dedicated threads, and implementations may safely block.
|
||||
*/
|
||||
void handleDisconnectionRequest();
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import io.dropwizard.lifecycle.Managed;
|
||||
import io.lettuce.core.RedisCommandTimeoutException;
|
||||
import io.lettuce.core.pubsub.RedisPubSubAdapter;
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import javax.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantPubSubConnection;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisClient;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.ResilienceUtil;
|
||||
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
||||
|
||||
/**
|
||||
* A disconnection request manager broadcasts and dispatches requests for servers to close authenticated connections
|
||||
* from specific clients.
|
||||
*
|
||||
* @see DisconnectionRequestListener
|
||||
*/
|
||||
public class DisconnectionRequestManager extends RedisPubSubAdapter<byte[], byte[]> implements Managed {
|
||||
|
||||
private final FaultTolerantRedisClient pubSubClient;
|
||||
private final Executor listenerEventExecutor;
|
||||
private final ScheduledExecutorService retryExecutor;
|
||||
|
||||
private static final String RETRY_NAME = ResilienceUtil.name(DisconnectionRequestManager.class);
|
||||
|
||||
private static final Duration SUBSCRIBE_RETRY_DELAY = Duration.ofSeconds(5);
|
||||
|
||||
private final Map<AccountIdentifierAndDeviceId, List<DisconnectionRequestListener>> listeners =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
@Nullable
|
||||
private FaultTolerantPubSubConnection<byte[], byte[]> pubSubConnection;
|
||||
|
||||
private static final byte[] DISCONNECTION_REQUEST_CHANNEL = "disconnection_requests".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
private static final Counter DISCONNECTION_REQUESTS_SENT_COUNTER =
|
||||
Metrics.counter(MetricsUtil.name(DisconnectionRequestManager.class, "requestsSent"));
|
||||
|
||||
private static final Counter DISCONNECTION_REQUESTS_RECEIVED_COUNTER =
|
||||
Metrics.counter(MetricsUtil.name(DisconnectionRequestManager.class, "requestsReceived"));
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DisconnectionRequestManager.class);
|
||||
|
||||
private record AccountIdentifierAndDeviceId(UUID accountIdentifier, byte deviceId) {}
|
||||
|
||||
public DisconnectionRequestManager(final FaultTolerantRedisClient pubSubClient,
|
||||
final Executor listenerEventExecutor,
|
||||
final ScheduledExecutorService retryExecutor) {
|
||||
|
||||
this.pubSubClient = pubSubClient;
|
||||
this.listenerEventExecutor = listenerEventExecutor;
|
||||
this.retryExecutor = retryExecutor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void start() {
|
||||
this.pubSubConnection = pubSubClient.createBinaryPubSubConnection();
|
||||
this.pubSubConnection.usePubSubConnection(connection -> {
|
||||
connection.addListener(this);
|
||||
|
||||
boolean subscribed = false;
|
||||
|
||||
// Loop indefinitely until we establish a subscription. We don't want to fail immediately if there's a temporary
|
||||
// Redis connectivity issue, since that would derail the whole startup process and likely lead to unnecessary pod
|
||||
// churn, which might make things worse. If we never establish a connection, readiness probes will eventually fail
|
||||
// and terminate the pods.
|
||||
do {
|
||||
try {
|
||||
connection.sync().subscribe(DISCONNECTION_REQUEST_CHANNEL);
|
||||
subscribed = true;
|
||||
} catch (final RedisCommandTimeoutException e) {
|
||||
try {
|
||||
Thread.sleep(SUBSCRIBE_RETRY_DELAY);
|
||||
} catch (final InterruptedException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
} while (!subscribed);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void stop() {
|
||||
if (pubSubConnection != null) {
|
||||
pubSubConnection.usePubSubConnection(connection -> {
|
||||
connection.removeListener(this);
|
||||
connection.close();
|
||||
});
|
||||
}
|
||||
|
||||
pubSubConnection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener for disconnection requests for a specific authenticated device.
|
||||
*
|
||||
* @param accountIdentifier TODO
|
||||
* @param deviceId TODO
|
||||
* @param listener the listener to register
|
||||
*/
|
||||
public void addListener(final UUID accountIdentifier, final byte deviceId, final DisconnectionRequestListener listener) {
|
||||
listeners.compute(new AccountIdentifierAndDeviceId(accountIdentifier, deviceId), (_, existingListeners) -> {
|
||||
final List<DisconnectionRequestListener> listeners =
|
||||
existingListeners == null ? new ArrayList<>() : existingListeners;
|
||||
|
||||
listeners.add(listener);
|
||||
|
||||
return listeners;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a listener for disconnection requests for a specific authenticated device.
|
||||
*
|
||||
* @param accountIdentifier TODO
|
||||
* @param deviceId TODO
|
||||
* @param listener the listener to remove
|
||||
*/
|
||||
public void removeListener(final UUID accountIdentifier, final byte deviceId, final DisconnectionRequestListener listener) {
|
||||
listeners.computeIfPresent(new AccountIdentifierAndDeviceId(accountIdentifier, deviceId), (_, existingListeners) -> {
|
||||
existingListeners.remove(listener);
|
||||
|
||||
return existingListeners.isEmpty() ? null : existingListeners;
|
||||
});
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
List<DisconnectionRequestListener> getListeners(final UUID accountIdentifier, final byte deviceId) {
|
||||
return listeners.getOrDefault(new AccountIdentifierAndDeviceId(accountIdentifier, deviceId), Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a request to close all connections associated with the given account identifier to all servers.
|
||||
*
|
||||
* @param account the account for which to close connections
|
||||
*
|
||||
* @return a future that completes when the request has been broadcast
|
||||
*/
|
||||
public CompletionStage<Void> requestDisconnection(final Account account) {
|
||||
return requestDisconnection(account.getIdentifier(IdentityType.ACI),
|
||||
account.getDevices().stream().map(Device::getId).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a request to close connections associated with the given account identifier and device IDs to all
|
||||
* servers.
|
||||
*
|
||||
* @param accountIdentifier the account for which to close connections
|
||||
* @param deviceIds the device IDs for which to close connections
|
||||
*
|
||||
* @return a future that completes when the request has been broadcast
|
||||
*/
|
||||
public CompletionStage<Void> requestDisconnection(final UUID accountIdentifier, final Collection<Byte> deviceIds) {
|
||||
final DisconnectionRequest disconnectionRequest = DisconnectionRequest.newBuilder()
|
||||
.setAccountIdentifier(UUIDUtil.toByteString(accountIdentifier))
|
||||
.addAllDeviceIds(deviceIds.stream().mapToInt(Byte::intValue).boxed().toList())
|
||||
.build();
|
||||
|
||||
return ResilienceUtil.getGeneralRedisRetry(RETRY_NAME)
|
||||
.executeCompletionStage(retryExecutor, () -> pubSubClient.withBinaryConnection(connection ->
|
||||
connection.async().publish(DISCONNECTION_REQUEST_CHANNEL, disconnectionRequest.toByteArray()))
|
||||
.toCompletableFuture())
|
||||
.thenRun(DISCONNECTION_REQUESTS_SENT_COUNTER::increment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void message(final byte[] channel, final byte[] message) {
|
||||
final UUID accountIdentifier;
|
||||
final List<Byte> deviceIds;
|
||||
|
||||
try {
|
||||
final DisconnectionRequest disconnectionRequest = DisconnectionRequest.parseFrom(message);
|
||||
DISCONNECTION_REQUESTS_RECEIVED_COUNTER.increment();
|
||||
|
||||
accountIdentifier = UUIDUtil.fromByteString(disconnectionRequest.getAccountIdentifier());
|
||||
deviceIds = disconnectionRequest.getDeviceIdsList().stream()
|
||||
.map(deviceIdInt -> {
|
||||
if (deviceIdInt == null || deviceIdInt < Device.PRIMARY_ID || deviceIdInt > Byte.MAX_VALUE) {
|
||||
throw new IllegalArgumentException("Invalid device ID: " + deviceIdInt);
|
||||
}
|
||||
|
||||
return deviceIdInt.byteValue();
|
||||
})
|
||||
.toList();
|
||||
} catch (final InvalidProtocolBufferException e) {
|
||||
logger.error("Could not parse disconnection request protobuf", e);
|
||||
return;
|
||||
} catch (final IllegalArgumentException e) {
|
||||
logger.error("Could not parse part of disconnection request", e);
|
||||
return;
|
||||
}
|
||||
|
||||
deviceIds.forEach(deviceId -> {
|
||||
listeners.getOrDefault(new AccountIdentifierAndDeviceId(accountIdentifier, deviceId), Collections.emptyList())
|
||||
.forEach(listener -> listenerEventExecutor.execute(() -> {
|
||||
try {
|
||||
listener.handleDisconnectionRequest();
|
||||
} catch (final Exception e) {
|
||||
logger.warn("Listener failed to handle disconnection request", e);
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -10,18 +10,22 @@ import static org.whispersystems.textsecuregcm.util.HmacUtils.hmac256ToHexString
|
||||
import static org.whispersystems.textsecuregcm.util.HmacUtils.hmac256TruncatedToHexString;
|
||||
import static org.whispersystems.textsecuregcm.util.HmacUtils.hmacHexStringsEqual;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
|
||||
|
||||
public class ExternalServiceCredentialsGenerator {
|
||||
|
||||
private static final int TRUNCATE_LENGTH = 10;
|
||||
|
||||
private static final String DELIMITER = ":";
|
||||
|
||||
private static final int TRUNCATED_SIGNATURE_LENGTH = 10;
|
||||
|
||||
private final byte[] key;
|
||||
|
||||
private final byte[] userDerivationKey;
|
||||
@@ -30,9 +34,20 @@ public class ExternalServiceCredentialsGenerator {
|
||||
|
||||
private final boolean truncateSignature;
|
||||
|
||||
private final String usernameTimestampPrefix;
|
||||
|
||||
private final Function<Instant, Instant> usernameTimestampTruncator;
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
private final int derivedUsernameTruncateLength;
|
||||
|
||||
|
||||
public static ExternalServiceCredentialsGenerator.Builder builder(final SecretBytes key) {
|
||||
return builder(key.value());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static ExternalServiceCredentialsGenerator.Builder builder(final byte[] key) {
|
||||
return new Builder(key);
|
||||
}
|
||||
@@ -42,12 +57,22 @@ public class ExternalServiceCredentialsGenerator {
|
||||
final byte[] userDerivationKey,
|
||||
final boolean prependUsername,
|
||||
final boolean truncateSignature,
|
||||
final int derivedUsernameTruncateLength,
|
||||
final String usernameTimestampPrefix,
|
||||
final Function<Instant, Instant> usernameTimestampTruncator,
|
||||
final Clock clock) {
|
||||
this.key = requireNonNull(key);
|
||||
this.userDerivationKey = requireNonNull(userDerivationKey);
|
||||
this.prependUsername = prependUsername;
|
||||
this.truncateSignature = truncateSignature;
|
||||
this.usernameTimestampPrefix = usernameTimestampPrefix;
|
||||
this.usernameTimestampTruncator = usernameTimestampTruncator;
|
||||
this.clock = requireNonNull(clock);
|
||||
this.derivedUsernameTruncateLength = derivedUsernameTruncateLength;
|
||||
|
||||
if (hasUsernameTimestampPrefix() ^ hasUsernameTimestampTruncator()) {
|
||||
throw new RuntimeException("Configured to have only one of (usernameTimestampPrefix, usernameTimestampTruncator)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,16 +90,37 @@ public class ExternalServiceCredentialsGenerator {
|
||||
* @return an instance of {@link ExternalServiceCredentials}
|
||||
*/
|
||||
public ExternalServiceCredentials generateFor(final String identity) {
|
||||
if (usernameIsTimestamp()) {
|
||||
throw new RuntimeException("Configured to use timestamp as username");
|
||||
}
|
||||
|
||||
return generate(identity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates `ExternalServiceCredentials` using a prefix concatenated with a truncated timestamp as the username, following this generator's configuration.
|
||||
* @return an instance of {@link ExternalServiceCredentials}
|
||||
*/
|
||||
public ExternalServiceCredentials generateWithTimestampAsUsername() {
|
||||
if (!usernameIsTimestamp()) {
|
||||
throw new RuntimeException("Not configured to use timestamp as username");
|
||||
}
|
||||
|
||||
final String truncatedTimestampSeconds = String.valueOf(usernameTimestampTruncator.apply(clock.instant()).getEpochSecond());
|
||||
return generate(usernameTimestampPrefix + DELIMITER + truncatedTimestampSeconds);
|
||||
}
|
||||
|
||||
private ExternalServiceCredentials generate(final String identity) {
|
||||
final String username = shouldDeriveUsername()
|
||||
? hmac256TruncatedToHexString(userDerivationKey, identity, TRUNCATE_LENGTH)
|
||||
? hmac256TruncatedToHexString(userDerivationKey, identity, derivedUsernameTruncateLength)
|
||||
: identity;
|
||||
|
||||
final long currentTimeSeconds = currentTimeSeconds();
|
||||
|
||||
final String dataToSign = username + DELIMITER + currentTimeSeconds;
|
||||
final String dataToSign = usernameIsTimestamp() ? username : username + DELIMITER + currentTimeSeconds;
|
||||
|
||||
final String signature = truncateSignature
|
||||
? hmac256TruncatedToHexString(key, dataToSign, TRUNCATE_LENGTH)
|
||||
? hmac256TruncatedToHexString(key, dataToSign, TRUNCATED_SIGNATURE_LENGTH)
|
||||
: hmac256ToHexString(key, dataToSign);
|
||||
|
||||
final String token = (prependUsername ? dataToSign : currentTimeSeconds) + DELIMITER + signature;
|
||||
@@ -83,7 +129,7 @@ public class ExternalServiceCredentialsGenerator {
|
||||
}
|
||||
|
||||
/**
|
||||
* In certain cases, identity (as it was passed to `generateFor` method)
|
||||
* In certain cases, identity (as it was passed to `generate` method)
|
||||
* is a part of the signature (`password`, in terms of `ExternalServiceCredentials`) string itself.
|
||||
* For such cases, this method returns the value of the identity string.
|
||||
* @param password `password` part of `ExternalServiceCredentials`
|
||||
@@ -95,14 +141,20 @@ public class ExternalServiceCredentialsGenerator {
|
||||
return Optional.empty();
|
||||
}
|
||||
// checking for the case of unexpected format
|
||||
return StringUtils.countMatches(password, DELIMITER) == 2
|
||||
? Optional.of(password.substring(0, password.indexOf(DELIMITER)))
|
||||
: Optional.empty();
|
||||
if (StringUtils.countMatches(password, DELIMITER) == 2) {
|
||||
if (usernameIsTimestamp()) {
|
||||
final int indexOfSecondDelimiter = password.indexOf(DELIMITER, password.indexOf(DELIMITER) + 1);
|
||||
return Optional.of(password.substring(0, indexOfSecondDelimiter));
|
||||
} else {
|
||||
return Optional.of(password.substring(0, password.indexOf(DELIMITER)));
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an instance of {@link ExternalServiceCredentials} object, checks that the password
|
||||
* matches the username taking into accound this generator's configuration.
|
||||
* matches the username taking into account this generator's configuration.
|
||||
* @param credentials an instance of {@link ExternalServiceCredentials}
|
||||
* @return An optional with a timestamp (seconds) of when the credentials were generated,
|
||||
* or an empty optional if the password doesn't match the username for any reason (including malformed data)
|
||||
@@ -114,7 +166,7 @@ public class ExternalServiceCredentialsGenerator {
|
||||
|
||||
// making sure password format matches our expectations based on the generator configuration
|
||||
if (parts.length == 3 && prependUsername) {
|
||||
final String username = parts[0];
|
||||
final String username = usernameIsTimestamp() ? parts[0] + DELIMITER + parts[1] : parts[0];
|
||||
// username has to match the one from `credentials`
|
||||
if (!credentials.username().equals(username)) {
|
||||
return Optional.empty();
|
||||
@@ -129,9 +181,9 @@ public class ExternalServiceCredentialsGenerator {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final String signedData = credentials.username() + DELIMITER + timestampSeconds;
|
||||
final String signedData = usernameIsTimestamp() ? credentials.username() : credentials.username() + DELIMITER + timestampSeconds;
|
||||
final String expectedSignature = truncateSignature
|
||||
? hmac256TruncatedToHexString(key, signedData, TRUNCATE_LENGTH)
|
||||
? hmac256TruncatedToHexString(key, signedData, TRUNCATED_SIGNATURE_LENGTH)
|
||||
: hmac256ToHexString(key, signedData);
|
||||
|
||||
// if the signature is valid it's safe to parse the `timestampSeconds` string into Long
|
||||
@@ -157,6 +209,18 @@ public class ExternalServiceCredentialsGenerator {
|
||||
return userDerivationKey.length > 0;
|
||||
}
|
||||
|
||||
private boolean hasUsernameTimestampPrefix() {
|
||||
return usernameTimestampPrefix != null;
|
||||
}
|
||||
|
||||
private boolean hasUsernameTimestampTruncator() {
|
||||
return usernameTimestampTruncator != null;
|
||||
}
|
||||
|
||||
private boolean usernameIsTimestamp() {
|
||||
return hasUsernameTimestampPrefix() && hasUsernameTimestampTruncator();
|
||||
}
|
||||
|
||||
private long currentTimeSeconds() {
|
||||
return clock.instant().getEpochSecond();
|
||||
}
|
||||
@@ -171,6 +235,12 @@ public class ExternalServiceCredentialsGenerator {
|
||||
|
||||
private boolean truncateSignature = true;
|
||||
|
||||
private int derivedUsernameTruncateLength = 10;
|
||||
|
||||
private String usernameTimestampPrefix = null;
|
||||
|
||||
private Function<Instant, Instant> usernameTimestampTruncator = null;
|
||||
|
||||
private Clock clock = Clock.systemUTC();
|
||||
|
||||
|
||||
@@ -178,6 +248,10 @@ public class ExternalServiceCredentialsGenerator {
|
||||
this.key = requireNonNull(key);
|
||||
}
|
||||
|
||||
public Builder withUserDerivationKey(final SecretBytes userDerivationKey) {
|
||||
return withUserDerivationKey(userDerivationKey.value());
|
||||
}
|
||||
|
||||
public Builder withUserDerivationKey(final byte[] userDerivationKey) {
|
||||
Validate.isTrue(requireNonNull(userDerivationKey).length > 0, "userDerivationKey must not be empty");
|
||||
this.userDerivationKey = userDerivationKey;
|
||||
@@ -189,6 +263,12 @@ public class ExternalServiceCredentialsGenerator {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withDerivedUsernameTruncateLength(int truncateLength) {
|
||||
Validate.inclusiveBetween(10, 32, truncateLength);
|
||||
this.derivedUsernameTruncateLength = truncateLength;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder prependUsername(final boolean prependUsername) {
|
||||
this.prependUsername = prependUsername;
|
||||
return this;
|
||||
@@ -199,9 +279,15 @@ public class ExternalServiceCredentialsGenerator {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withUsernameTimestampTruncatorAndPrefix(final Function<Instant, Instant> truncator, final String prefix) {
|
||||
this.usernameTimestampTruncator = truncator;
|
||||
this.usernameTimestampPrefix = prefix;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ExternalServiceCredentialsGenerator build() {
|
||||
return new ExternalServiceCredentialsGenerator(
|
||||
key, userDerivationKey, prependUsername, truncateSignature, clock);
|
||||
key, userDerivationKey, prependUsername, truncateSignature, derivedUsernameTruncateLength, usernameTimestampPrefix, usernameTimestampTruncator, clock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public class ExternalServiceCredentialsSelector {
|
||||
|
||||
private ExternalServiceCredentialsSelector() {}
|
||||
|
||||
public record CredentialInfo(String token, boolean valid, ExternalServiceCredentials credentials, long timestamp) {
|
||||
/**
|
||||
* @return a copy of this record with valid=false
|
||||
*/
|
||||
private CredentialInfo invalidate() {
|
||||
return new CredentialInfo(token, false, credentials, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a list of username:password credentials.
|
||||
* A credential is valid if it passes validation by the provided credentialsGenerator AND it is the most recent
|
||||
* credential in the provided list for a username.
|
||||
*
|
||||
* @param tokens A list of credentials, potentially with different usernames
|
||||
* @param credentialsGenerator To validate these credentials
|
||||
* @param maxAgeSeconds The maximum allowable age of the credential
|
||||
* @return A {@link CredentialInfo} for each provided token
|
||||
*/
|
||||
public static List<CredentialInfo> check(
|
||||
final List<String> tokens,
|
||||
final ExternalServiceCredentialsGenerator credentialsGenerator,
|
||||
final long maxAgeSeconds) {
|
||||
|
||||
// the credential for the username with the latest timestamp (so far)
|
||||
final Map<String, CredentialInfo> bestForUsername = new HashMap<>();
|
||||
final List<CredentialInfo> results = new ArrayList<>();
|
||||
for (String token : tokens) {
|
||||
// each token is supposed to be in a "${username}:${password}" form,
|
||||
// (note that password part may also contain ':' characters)
|
||||
final String[] parts = token.split(":", 2);
|
||||
if (parts.length != 2) {
|
||||
results.add(new CredentialInfo(token, false, null, 0L));
|
||||
continue;
|
||||
}
|
||||
final ExternalServiceCredentials credentials = new ExternalServiceCredentials(parts[0], parts[1]);
|
||||
final Optional<Long> maybeTimestamp = credentialsGenerator.validateAndGetTimestamp(credentials, maxAgeSeconds);
|
||||
if (maybeTimestamp.isEmpty()) {
|
||||
results.add(new CredentialInfo(token, false, null, 0L));
|
||||
continue;
|
||||
}
|
||||
|
||||
// now that we validated signature and token age, we will also find the latest of the tokens
|
||||
// for each username
|
||||
final long timestamp = maybeTimestamp.get();
|
||||
final CredentialInfo best = bestForUsername.get(credentials.username());
|
||||
if (best == null) {
|
||||
bestForUsername.put(credentials.username(), new CredentialInfo(token, true, credentials, timestamp));
|
||||
continue;
|
||||
}
|
||||
if (best.timestamp() < timestamp) {
|
||||
// we found a better credential for the username
|
||||
bestForUsername.put(credentials.username(), new CredentialInfo(token, true, credentials, timestamp));
|
||||
// mark the previous best as an invalid credential, since we have a better credential now
|
||||
results.add(best.invalidate());
|
||||
} else {
|
||||
// the credential we already had was more recent, this one can be marked invalid
|
||||
results.add(new CredentialInfo(token, false, null, 0L));
|
||||
}
|
||||
}
|
||||
|
||||
// all invalid tokens should be in results, just add the valid ones
|
||||
results.addAll(bestForUsername.values());
|
||||
return results;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import java.util.Base64;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken;
|
||||
|
||||
public record GroupSendTokenHeader(GroupSendFullToken token) {
|
||||
|
||||
public static GroupSendTokenHeader valueOf(String header) {
|
||||
try {
|
||||
return new GroupSendTokenHeader(new GroupSendFullToken(Base64.getDecoder().decode(header)));
|
||||
} catch (InvalidInputException | IllegalArgumentException e) {
|
||||
// Base64 throws IllegalArgumentException; GroupSendFullToken ctor throws InvalidInputException
|
||||
throw new WebApplicationException(e, Status.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest;
|
||||
import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.websocket.auth.AuthenticatedWebSocketUpgradeFilter;
|
||||
|
||||
public class IdlePrimaryDeviceAuthenticatedWebSocketUpgradeFilter implements
|
||||
AuthenticatedWebSocketUpgradeFilter<AuthenticatedDevice> {
|
||||
|
||||
private final Duration minIdleDuration;
|
||||
private final Clock clock;
|
||||
|
||||
@VisibleForTesting
|
||||
static final String ALERT_HEADER = "X-Signal-Alert";
|
||||
|
||||
@VisibleForTesting
|
||||
static final String IDLE_PRIMARY_DEVICE_ALERT = "idle-primary-device";
|
||||
|
||||
private static final Counter IDLE_PRIMARY_WARNING_COUNTER = Metrics.counter(
|
||||
MetricsUtil.name(IdlePrimaryDeviceAuthenticatedWebSocketUpgradeFilter.class, "idlePrimaryDeviceWarning"),
|
||||
"critical", "false");
|
||||
|
||||
public IdlePrimaryDeviceAuthenticatedWebSocketUpgradeFilter(final Duration minIdleDuration, final Clock clock) {
|
||||
this.minIdleDuration = minIdleDuration;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleAuthentication(final Optional<AuthenticatedDevice> authenticated,
|
||||
final JettyServerUpgradeRequest request,
|
||||
final JettyServerUpgradeResponse response) {
|
||||
|
||||
// No action needed if the connection is unauthenticated (in which case we don't know when we've last seen the
|
||||
// primary device) or if the authenticated device IS the primary device
|
||||
authenticated
|
||||
.filter(authenticatedDevice -> authenticatedDevice.deviceId() != Device.PRIMARY_ID)
|
||||
.ifPresent(authenticatedDevice -> {
|
||||
if (authenticatedDevice.primaryDeviceLastSeen().isBefore(clock.instant().minus(minIdleDuration))) {
|
||||
response.addHeader(ALERT_HEADER, IDLE_PRIMARY_DEVICE_ALERT);
|
||||
IDLE_PRIMARY_WARNING_COUNTER.increment();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
|
||||
public class InvalidAuthorizationHeaderException extends WebApplicationException {
|
||||
public InvalidAuthorizationHeaderException(String s) {
|
||||
|
||||
@@ -5,35 +5,37 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
import javax.ws.rs.NotAuthorizedException;
|
||||
import javax.ws.rs.NotFoundException;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.NotAuthorizedException;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Optional;
|
||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class OptionalAccess {
|
||||
|
||||
public static final String UNIDENTIFIED = "Unidentified-Access-Key";
|
||||
public static String ALL_DEVICES_SELECTOR = "*";
|
||||
|
||||
public static void verify(Optional<Account> requestAccount,
|
||||
Optional<Anonymous> accessKey,
|
||||
Optional<Account> targetAccount,
|
||||
ServiceIdentifier targetIdentifier,
|
||||
String deviceSelector) {
|
||||
|
||||
public static void verify(Optional<Account> requestAccount,
|
||||
Optional<Anonymous> accessKey,
|
||||
Optional<Account> targetAccount,
|
||||
String deviceSelector)
|
||||
{
|
||||
try {
|
||||
verify(requestAccount, accessKey, targetAccount);
|
||||
verify(requestAccount, accessKey, targetAccount, targetIdentifier);
|
||||
|
||||
if (!deviceSelector.equals("*")) {
|
||||
long deviceId = Long.parseLong(deviceSelector);
|
||||
if (!ALL_DEVICES_SELECTOR.equals(deviceSelector)) {
|
||||
byte deviceId = Byte.parseByte(deviceSelector);
|
||||
|
||||
Optional<Device> targetDevice = targetAccount.get().getDevice(deviceId);
|
||||
|
||||
if (targetDevice.isPresent() && targetDevice.get().isEnabled()) {
|
||||
if (targetDevice.isPresent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -48,29 +50,50 @@ public class OptionalAccess {
|
||||
}
|
||||
}
|
||||
|
||||
public static void verify(Optional<Account> requestAccount,
|
||||
Optional<Anonymous> accessKey,
|
||||
Optional<Account> targetAccount)
|
||||
{
|
||||
if (requestAccount.isPresent() && targetAccount.isPresent() && targetAccount.get().isEnabled()) {
|
||||
public static void verify(Optional<Account> requestAccount,
|
||||
Optional<Anonymous> accessKey,
|
||||
Optional<Account> targetAccount,
|
||||
ServiceIdentifier targetIdentifier) {
|
||||
|
||||
if (requestAccount.isPresent()) {
|
||||
// Authenticated requests are never unauthorized; if the target exists, return OK, otherwise throw not-found.
|
||||
if (targetAccount.isPresent()) {
|
||||
return;
|
||||
} else {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
// Anything past this point can only be authenticated by an access key. Even when the target
|
||||
// has unrestricted unidentified access, callers need to supply a fake access key. Likewise, if
|
||||
// the target account does not exist, we *also* report unauthorized here (*not* not-found,
|
||||
// since that would provide a free exists check).
|
||||
if (accessKey.isEmpty() || targetAccount.isEmpty()) {
|
||||
throw new NotAuthorizedException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
// Unidentified access is only for ACI identities
|
||||
if (IdentityType.PNI.equals(targetIdentifier.identityType())) {
|
||||
throw new NotAuthorizedException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
// Unrestricted unidentified access does what it says on the tin: we don't check if the key the
|
||||
// caller provided is right or not.
|
||||
if (targetAccount.get().isUnrestrictedUnidentifiedAccess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
//noinspection ConstantConditions
|
||||
if (requestAccount.isPresent() && (targetAccount.isEmpty() || (targetAccount.isPresent() && !targetAccount.get().isEnabled()))) {
|
||||
throw new NotFoundException();
|
||||
if (!targetAccount.get().isIdentifiedBy(targetIdentifier)) {
|
||||
throw new IllegalArgumentException("Target account is not identified by the given identifier");
|
||||
}
|
||||
|
||||
if (accessKey.isPresent() && targetAccount.isPresent() && targetAccount.get().isEnabled() && targetAccount.get().isUnrestrictedUnidentifiedAccess()) {
|
||||
return;
|
||||
// At this point, any successful authentication requires a real access key on the target account
|
||||
if (targetAccount.get().getUnidentifiedAccessKey().isEmpty()) {
|
||||
throw new NotAuthorizedException(Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if (accessKey.isPresent() &&
|
||||
targetAccount.isPresent() &&
|
||||
targetAccount.get().getUnidentifiedAccessKey().isPresent() &&
|
||||
targetAccount.get().isEnabled() &&
|
||||
MessageDigest.isEqual(accessKey.get().getAccessKey(), targetAccount.get().getUnidentifiedAccessKey().get()))
|
||||
{
|
||||
// Otherwise, access is gated by the caller having the unidentified-access key matching the target account.
|
||||
if (MessageDigest.isEqual(accessKey.get().getAccessKey(), targetAccount.get().getUnidentifiedAccessKey().get())) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.glassfish.jersey.server.monitoring.RequestEvent;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
|
||||
public class PhoneNumberChangeRefreshRequirementProvider implements WebsocketRefreshRequirementProvider {
|
||||
|
||||
private static final String INITIAL_NUMBER_KEY =
|
||||
PhoneNumberChangeRefreshRequirementProvider.class.getName() + ".initialNumber";
|
||||
|
||||
@Override
|
||||
public void handleRequestFiltered(final RequestEvent requestEvent) {
|
||||
ContainerRequestUtil.getAuthenticatedAccount(requestEvent.getContainerRequest())
|
||||
.ifPresent(account -> requestEvent.getContainerRequest().setProperty(INITIAL_NUMBER_KEY, account.getNumber()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Pair<UUID, Long>> handleRequestFinished(final RequestEvent requestEvent) {
|
||||
final String initialNumber = (String) requestEvent.getContainerRequest().getProperty(INITIAL_NUMBER_KEY);
|
||||
|
||||
if (initialNumber != null) {
|
||||
final Optional<Account> maybeAuthenticatedAccount =
|
||||
ContainerRequestUtil.getAuthenticatedAccount(requestEvent.getContainerRequest());
|
||||
|
||||
return maybeAuthenticatedAccount
|
||||
.filter(account -> !initialNumber.equals(account.getNumber()))
|
||||
.map(account -> account.getDevices().stream()
|
||||
.map(device -> new Pair<>(account.getUuid(), device.getId()))
|
||||
.collect(Collectors.toList()))
|
||||
.orElse(Collections.emptyList());
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import io.grpc.Status;
|
||||
import io.grpc.StatusRuntimeException;
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.ForbiddenException;
|
||||
import jakarta.ws.rs.NotAuthorizedException;
|
||||
import jakarta.ws.rs.ServerErrorException;
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.PhoneVerificationRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession;
|
||||
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
|
||||
import org.whispersystems.textsecuregcm.spam.RegistrationRecoveryChecker;
|
||||
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
|
||||
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
|
||||
|
||||
public class PhoneVerificationTokenManager {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PhoneVerificationTokenManager.class);
|
||||
private static final Duration REGISTRATION_RPC_TIMEOUT = Duration.ofSeconds(15);
|
||||
private static final long VERIFICATION_TIMEOUT_SECONDS = REGISTRATION_RPC_TIMEOUT.plusSeconds(1).getSeconds();
|
||||
|
||||
private final PhoneNumberIdentifiers phoneNumberIdentifiers;
|
||||
|
||||
private final RegistrationServiceClient registrationServiceClient;
|
||||
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
|
||||
private final RegistrationRecoveryChecker registrationRecoveryChecker;
|
||||
|
||||
public PhoneVerificationTokenManager(final PhoneNumberIdentifiers phoneNumberIdentifiers,
|
||||
final RegistrationServiceClient registrationServiceClient,
|
||||
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
|
||||
final RegistrationRecoveryChecker registrationRecoveryChecker) {
|
||||
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
|
||||
this.registrationServiceClient = registrationServiceClient;
|
||||
this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager;
|
||||
this.registrationRecoveryChecker = registrationRecoveryChecker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a {@link PhoneVerificationRequest} has a token that verifies the caller has confirmed access to the e164
|
||||
* number
|
||||
*
|
||||
* @param requestContext the container request context
|
||||
* @param number the e164 presented for verification
|
||||
* @param request the request with exactly one verification token (RegistrationService sessionId or registration
|
||||
* recovery password)
|
||||
* @return if verification was successful, returns the verification type
|
||||
* @throws BadRequestException if the number does not match the sessionId’s number, or the remote service rejects
|
||||
* the session ID as invalid
|
||||
* @throws NotAuthorizedException if the session is not verified
|
||||
* @throws ForbiddenException if the recovery password is not valid
|
||||
* @throws InterruptedException if verification did not complete before a timeout
|
||||
*/
|
||||
public PhoneVerificationRequest.VerificationType verify(final ContainerRequestContext requestContext, final String number, final PhoneVerificationRequest request)
|
||||
throws InterruptedException {
|
||||
|
||||
final PhoneVerificationRequest.VerificationType verificationType = request.verificationType();
|
||||
switch (verificationType) {
|
||||
case SESSION -> verifyBySessionId(number, request.decodeSessionId());
|
||||
case RECOVERY_PASSWORD -> verifyByRecoveryPassword(requestContext, number, request.recoveryPassword());
|
||||
}
|
||||
|
||||
return verificationType;
|
||||
}
|
||||
|
||||
private void verifyBySessionId(final String number, final byte[] sessionId) throws InterruptedException {
|
||||
try {
|
||||
final RegistrationServiceSession session = registrationServiceClient
|
||||
.getSession(sessionId, REGISTRATION_RPC_TIMEOUT)
|
||||
.get(VERIFICATION_TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.orElseThrow(() -> new NotAuthorizedException("session not verified"));
|
||||
|
||||
if (!MessageDigest.isEqual(number.getBytes(), session.number().getBytes())) {
|
||||
throw new BadRequestException("number does not match session");
|
||||
}
|
||||
if (!session.verified()) {
|
||||
throw new NotAuthorizedException("session not verified");
|
||||
}
|
||||
} catch (final ExecutionException e) {
|
||||
|
||||
if (e.getCause() instanceof StatusRuntimeException grpcRuntimeException) {
|
||||
if (grpcRuntimeException.getStatus().getCode() == Status.Code.INVALID_ARGUMENT) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
}
|
||||
|
||||
logger.error("Registration service failure", e);
|
||||
throw new ServerErrorException(Response.Status.SERVICE_UNAVAILABLE);
|
||||
|
||||
} catch (final CancellationException | TimeoutException e) {
|
||||
|
||||
logger.error("Registration service failure", e);
|
||||
throw new ServerErrorException(Response.Status.SERVICE_UNAVAILABLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyByRecoveryPassword(final ContainerRequestContext requestContext, final String number, final byte[] recoveryPassword)
|
||||
throws InterruptedException {
|
||||
if (!registrationRecoveryChecker.checkRegistrationRecoveryAttempt(requestContext, number)) {
|
||||
throw new ForbiddenException("recoveryPassword couldn't be verified");
|
||||
}
|
||||
try {
|
||||
final boolean verified = registrationRecoveryPasswordsManager.verify(phoneNumberIdentifiers.getPhoneNumberIdentifier(number).join(), recoveryPassword)
|
||||
.get(VERIFICATION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
if (!verified) {
|
||||
throw new ForbiddenException("recoveryPassword couldn't be verified");
|
||||
}
|
||||
} catch (final ExecutionException | TimeoutException e) {
|
||||
throw new ServerErrorException(Response.Status.SERVICE_UNAVAILABLE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Iterator;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/// A validated range of days for which a credential may be issued.
|
||||
public class RedemptionRange implements Iterable<Instant> {
|
||||
|
||||
public static final Duration MAX_REDEMPTION_DURATION = Duration.ofDays(7);
|
||||
|
||||
/// The first day for which a credential should be issued
|
||||
private final LocalDate from;
|
||||
|
||||
/// The last day for which a credential should be issued
|
||||
private final LocalDate end;
|
||||
|
||||
private RedemptionRange(final LocalDate from, final LocalDate end) {
|
||||
this.from = from;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
/// Construct a {@link RedemptionRange} if the provided day bounds are valid.
|
||||
///
|
||||
/// The redemption bounds must satisfy:
|
||||
/// - `redemptionEnd` >= `redemptionStart`
|
||||
/// - `redemptionStart` and `redemptionEnd` are day-aligned
|
||||
/// - `redemptionStart` is yesterday or later
|
||||
/// - `redemptionEnd` is tomorrow + `MAX_REDEMPTION_DURATION` or earlier
|
||||
/// - The number of days requested is less than `MAX_REDEMPTION_DURATION`
|
||||
///
|
||||
/// @param clock Clock to use to get current day
|
||||
/// @param redemptionStart The first day included in the range
|
||||
/// @param redemptionEnd The last day included in the range
|
||||
/// @return A {@link RedemptionRange} that can be used to iterate each day between `redemptionStart` and
|
||||
/// `redemptionEnd`
|
||||
/// @throws IllegalArgumentException if the redemption bounds were not valid
|
||||
public static RedemptionRange inclusive(Clock clock, Instant redemptionStart, Instant redemptionEnd)
|
||||
throws IllegalArgumentException {
|
||||
final Instant today = clock.instant().truncatedTo(ChronoUnit.DAYS);
|
||||
final Instant yesterday = today.minus(Duration.ofDays(1));
|
||||
|
||||
if (redemptionStart.isAfter(redemptionEnd)) {
|
||||
throw new IllegalArgumentException("end of range must be after start of range");
|
||||
}
|
||||
|
||||
if (!redemptionStart.truncatedTo(ChronoUnit.DAYS).equals(redemptionStart)
|
||||
|| !redemptionEnd.truncatedTo(ChronoUnit.DAYS).equals(redemptionEnd)) {
|
||||
throw new IllegalArgumentException("timestamps must be day aligned");
|
||||
}
|
||||
|
||||
if (redemptionStart.isBefore(yesterday)) {
|
||||
throw new IllegalArgumentException("start of range too far in the past");
|
||||
}
|
||||
|
||||
if (redemptionEnd.isAfter(today.plus(MAX_REDEMPTION_DURATION).plus(Duration.ofDays(1)))) {
|
||||
throw new IllegalArgumentException("end of range too far in the future");
|
||||
}
|
||||
|
||||
if (redemptionEnd.isAfter(redemptionStart.plus(MAX_REDEMPTION_DURATION))) {
|
||||
throw new IllegalArgumentException("redemption window too large");
|
||||
}
|
||||
|
||||
return new RedemptionRange(
|
||||
LocalDate.ofInstant(redemptionStart, ZoneOffset.UTC),
|
||||
LocalDate.ofInstant(redemptionEnd, ZoneOffset.UTC));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Iterator<Instant> iterator() {
|
||||
final Instant fromInstant = from.atStartOfDay(ZoneOffset.UTC).toInstant();
|
||||
final Instant endInstant = end.atStartOfDay(ZoneOffset.UTC).toInstant();
|
||||
return Stream
|
||||
.iterate(fromInstant, redemptionTime -> redemptionTime.plus(Duration.ofDays(1)))
|
||||
.takeWhile(redemptionTime -> !redemptionTime.isAfter(endInstant))
|
||||
.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
RedemptionRange that = (RedemptionRange) o;
|
||||
return Objects.equals(from, that.from) && Objects.equals(end, that.end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(from, end);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.micrometer.core.instrument.DistributionSummary;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.entities.PhoneVerificationRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure;
|
||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
|
||||
|
||||
public class RegistrationLockVerificationManager {
|
||||
public enum Flow {
|
||||
REGISTRATION,
|
||||
CHANGE_NUMBER
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static final int FAILURE_HTTP_STATUS = 423;
|
||||
|
||||
private static final String EXPIRED_REGISTRATION_LOCK_COUNTER_NAME =
|
||||
name(RegistrationLockVerificationManager.class, "expiredRegistrationLock");
|
||||
private static final String REQUIRED_REGISTRATION_LOCK_COUNTER_NAME =
|
||||
name(RegistrationLockVerificationManager.class, "requiredRegistrationLock");
|
||||
private static final String CHALLENGED_DEVICE_NOT_PUSH_REGISTERED_COUNTER_NAME =
|
||||
name(RegistrationLockVerificationManager.class, "challengedDeviceNotPushRegistered");
|
||||
private static final String ALREADY_LOCKED_TAG_NAME = "alreadyLocked";
|
||||
private static final String REGISTRATION_LOCK_VERIFICATION_FLOW_TAG_NAME = "flow";
|
||||
private static final String REGISTRATION_LOCK_MATCHES_TAG_NAME = "registrationLockMatches";
|
||||
private static final String PHONE_VERIFICATION_TYPE_TAG_NAME = "phoneVerificationType";
|
||||
|
||||
private final AccountsManager accounts;
|
||||
private final DisconnectionRequestManager disconnectionRequestManager;
|
||||
private final ExternalServiceCredentialsGenerator svr2CredentialGenerator;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
|
||||
private final PushNotificationManager pushNotificationManager;
|
||||
|
||||
public RegistrationLockVerificationManager(
|
||||
final AccountsManager accounts,
|
||||
final DisconnectionRequestManager disconnectionRequestManager,
|
||||
final ExternalServiceCredentialsGenerator svr2CredentialGenerator,
|
||||
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
|
||||
final PushNotificationManager pushNotificationManager,
|
||||
final RateLimiters rateLimiters) {
|
||||
this.accounts = accounts;
|
||||
this.disconnectionRequestManager = disconnectionRequestManager;
|
||||
this.svr2CredentialGenerator = svr2CredentialGenerator;
|
||||
this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager;
|
||||
this.pushNotificationManager = pushNotificationManager;
|
||||
this.rateLimiters = rateLimiters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the given registration lock credentials against the account’s current registration lock, if any
|
||||
*
|
||||
* @param account
|
||||
* @param clientRegistrationLock
|
||||
* @throws RateLimitExceededException
|
||||
* @throws WebApplicationException
|
||||
*/
|
||||
public void verifyRegistrationLock(final Account account, @Nullable final String clientRegistrationLock,
|
||||
final String userAgent,
|
||||
final Flow flow,
|
||||
final PhoneVerificationRequest.VerificationType phoneVerificationType
|
||||
) throws RateLimitExceededException, WebApplicationException {
|
||||
|
||||
final Tags expiredTags = Tags.of(UserAgentTagUtil.getPlatformTag(userAgent),
|
||||
Tag.of(REGISTRATION_LOCK_VERIFICATION_FLOW_TAG_NAME, flow.name()),
|
||||
Tag.of(PHONE_VERIFICATION_TYPE_TAG_NAME, phoneVerificationType.name())
|
||||
);
|
||||
|
||||
final StoredRegistrationLock existingRegistrationLock = account.getRegistrationLock();
|
||||
|
||||
switch (existingRegistrationLock.getStatus()) {
|
||||
case EXPIRED:
|
||||
Metrics.counter(EXPIRED_REGISTRATION_LOCK_COUNTER_NAME, expiredTags).increment();
|
||||
return;
|
||||
case ABSENT:
|
||||
return;
|
||||
case REQUIRED:
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Unexpected status: " + existingRegistrationLock.getStatus());
|
||||
}
|
||||
|
||||
if (StringUtils.isNotEmpty(clientRegistrationLock)) {
|
||||
rateLimiters.getPinLimiter().validate(account.getNumber());
|
||||
}
|
||||
|
||||
final String phoneNumber = account.getNumber();
|
||||
final boolean registrationLockMatches = existingRegistrationLock.verify(clientRegistrationLock);
|
||||
final boolean alreadyLocked = account.hasLockedCredentials();
|
||||
|
||||
final Tags additionalTags = expiredTags.and(
|
||||
REGISTRATION_LOCK_MATCHES_TAG_NAME, Boolean.toString(registrationLockMatches),
|
||||
ALREADY_LOCKED_TAG_NAME, Boolean.toString(alreadyLocked)
|
||||
);
|
||||
|
||||
Metrics.counter(REQUIRED_REGISTRATION_LOCK_COUNTER_NAME, additionalTags).increment();
|
||||
|
||||
final DistributionSummary registrationLockIdleDays = DistributionSummary
|
||||
.builder(name(RegistrationLockVerificationManager.class, "registrationLockIdleDays"))
|
||||
.tags(additionalTags)
|
||||
.publishPercentiles(0.75, 0.95, 0.99, 0.999)
|
||||
.distributionStatisticExpiry(Duration.ofHours(2))
|
||||
.register(Metrics.globalRegistry);
|
||||
|
||||
final Instant accountLastSeen = Instant.ofEpochMilli(account.getLastSeen());
|
||||
final Duration timeSinceLastSeen = Duration.between(accountLastSeen, Instant.now());
|
||||
|
||||
registrationLockIdleDays.record(timeSinceLastSeen.toDays());
|
||||
|
||||
if (!registrationLockMatches) {
|
||||
// At this point, the client verified ownership of the phone number but doesn’t have the reglock PIN.
|
||||
// Freezing the existing account credentials will definitively start the reglock timeout.
|
||||
// Until the timeout, the current reglock can still be supplied,
|
||||
// along with phone number verification, to restore access.
|
||||
final Account updatedAccount;
|
||||
if (!alreadyLocked) {
|
||||
updatedAccount = accounts.update(account, Account::lockAuthTokenHash);
|
||||
} else {
|
||||
updatedAccount = account;
|
||||
}
|
||||
|
||||
// The client often sends an empty registration lock token on the first request
|
||||
// and sends an actual token if the server returns a 423 indicating that one is required.
|
||||
// This logic accounts for that behavior by not deleting the registration recovery password
|
||||
// if the user verified correctly via registration recovery password and sent an empty token.
|
||||
// This allows users to re-register via registration recovery password
|
||||
// instead of always being forced to fall back to SMS verification.
|
||||
if (!phoneVerificationType.equals(PhoneVerificationRequest.VerificationType.RECOVERY_PASSWORD) || clientRegistrationLock != null) {
|
||||
registrationRecoveryPasswordsManager.remove(updatedAccount.getIdentifier(IdentityType.PNI));
|
||||
}
|
||||
|
||||
final List<Byte> deviceIds = updatedAccount.getDevices().stream().map(Device::getId).toList();
|
||||
disconnectionRequestManager.requestDisconnection(updatedAccount.getUuid(), deviceIds);
|
||||
|
||||
try {
|
||||
// Send a push notification that prompts the client to attempt login and fail due to locked credentials
|
||||
pushNotificationManager.sendAttemptLoginNotification(updatedAccount, "failedRegistrationLock");
|
||||
} catch (final NotPushRegisteredException e) {
|
||||
Metrics.counter(CHALLENGED_DEVICE_NOT_PUSH_REGISTERED_COUNTER_NAME).increment();
|
||||
}
|
||||
|
||||
throw new WebApplicationException(Response.status(FAILURE_HTTP_STATUS)
|
||||
.entity(new RegistrationLockFailure(
|
||||
existingRegistrationLock.getTimeRemaining().toMillis(),
|
||||
svr2FailureCredentials(existingRegistrationLock, updatedAccount)))
|
||||
.build());
|
||||
}
|
||||
|
||||
rateLimiters.getPinLimiter().clear(phoneNumber);
|
||||
}
|
||||
|
||||
private @Nullable ExternalServiceCredentials svr2FailureCredentials(final StoredRegistrationLock existingRegistrationLock, final Account account) {
|
||||
if (!existingRegistrationLock.needsFailureCredentials()) {
|
||||
return null;
|
||||
}
|
||||
return svr2CredentialGenerator.generateForUuid(account.getUuid());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,25 +6,35 @@
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.annotation.Nullable;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class StoredRegistrationLock {
|
||||
public enum Status {
|
||||
REQUIRED,
|
||||
EXPIRED,
|
||||
ABSENT
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static final Duration REGISTRATION_LOCK_EXPIRATION_DAYS = Duration.ofDays(7);
|
||||
|
||||
private final Optional<String> registrationLock;
|
||||
|
||||
private final Optional<String> registrationLockSalt;
|
||||
|
||||
private final long lastSeen;
|
||||
private final Instant lastSeen;
|
||||
|
||||
/**
|
||||
* @return milliseconds since the last time the account was seen.
|
||||
*/
|
||||
private long timeSinceLastSeen() {
|
||||
return System.currentTimeMillis() - lastSeen;
|
||||
return System.currentTimeMillis() - lastSeen.toEpochMilli();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,36 +44,40 @@ public class StoredRegistrationLock {
|
||||
return registrationLock.isPresent() && registrationLockSalt.isPresent();
|
||||
}
|
||||
|
||||
public StoredRegistrationLock(Optional<String> registrationLock, Optional<String> registrationLockSalt, long lastSeen) {
|
||||
public boolean isPresent() {
|
||||
return hasLockAndSalt();
|
||||
}
|
||||
|
||||
public StoredRegistrationLock(Optional<String> registrationLock, Optional<String> registrationLockSalt, Instant lastSeen) {
|
||||
this.registrationLock = registrationLock;
|
||||
this.registrationLockSalt = registrationLockSalt;
|
||||
this.lastSeen = lastSeen;
|
||||
}
|
||||
|
||||
public boolean requiresClientRegistrationLock() {
|
||||
boolean hasTimeRemaining = getTimeRemaining() >= 0;
|
||||
return hasLockAndSalt() && hasTimeRemaining;
|
||||
public Status getStatus() {
|
||||
if (!isPresent()) {
|
||||
return Status.ABSENT;
|
||||
}
|
||||
if (getTimeRemaining().toMillis() > 0) {
|
||||
return Status.REQUIRED;
|
||||
}
|
||||
return Status.EXPIRED;
|
||||
}
|
||||
|
||||
public boolean needsFailureCredentials() {
|
||||
return hasLockAndSalt();
|
||||
}
|
||||
|
||||
public long getTimeRemaining() {
|
||||
return TimeUnit.DAYS.toMillis(7) - timeSinceLastSeen();
|
||||
public Duration getTimeRemaining() {
|
||||
return REGISTRATION_LOCK_EXPIRATION_DAYS.minus(timeSinceLastSeen(), ChronoUnit.MILLIS);
|
||||
}
|
||||
|
||||
public boolean verify(@Nullable String clientRegistrationLock) {
|
||||
if (hasLockAndSalt() && Util.nonEmpty(clientRegistrationLock)) {
|
||||
if (hasLockAndSalt() && StringUtils.isNotEmpty(clientRegistrationLock)) {
|
||||
SaltedTokenHash credentials = new SaltedTokenHash(registrationLock.get(), registrationLockSalt.get());
|
||||
return credentials.verify(clientRegistrationLock);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public StoredRegistrationLock forTime(long timestamp) {
|
||||
return new StoredRegistrationLock(registrationLock, registrationLockSalt, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Duration;
|
||||
import javax.annotation.Nullable;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
public record StoredVerificationCode(String code,
|
||||
long timestamp,
|
||||
String pushCode,
|
||||
@Nullable byte[] sessionId) {
|
||||
|
||||
public static final Duration EXPIRATION = Duration.ofMinutes(10);
|
||||
|
||||
public boolean isValid(String theirCodeString) {
|
||||
if (Util.isEmpty(code) || Util.isEmpty(theirCodeString)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] ourCode = code.getBytes();
|
||||
byte[] theirCode = theirCodeString.getBytes();
|
||||
|
||||
return MessageDigest.isEqual(ourCode, theirCode);
|
||||
}
|
||||
}
|
||||
@@ -6,29 +6,15 @@
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.List;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class TurnToken {
|
||||
|
||||
@JsonProperty
|
||||
private String username;
|
||||
|
||||
@JsonProperty
|
||||
private String password;
|
||||
|
||||
@JsonProperty
|
||||
private List<String> urls;
|
||||
|
||||
public TurnToken(String username, String password, List<String> urls) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.urls = urls;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
List<String> getUrls() {
|
||||
return urls;
|
||||
}
|
||||
public record TurnToken(
|
||||
String username,
|
||||
String password,
|
||||
@JsonProperty("ttl") long ttlSeconds,
|
||||
@Nonnull List<String> urls,
|
||||
@Nonnull List<String> urlsWithIps,
|
||||
@Nullable String hostname) {
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.auth;
|
||||
|
||||
import org.whispersystems.textsecuregcm.configuration.TurnUriConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicTurnConfiguration;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
import org.whispersystems.textsecuregcm.util.WeightedRandomSelect;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class TurnTokenGenerator {
|
||||
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfiguration;
|
||||
|
||||
public TurnTokenGenerator(final DynamicConfigurationManager<DynamicConfiguration> config) {
|
||||
this.dynamicConfiguration = config;
|
||||
}
|
||||
|
||||
public TurnToken generate(final String e164) {
|
||||
try {
|
||||
byte[] key = dynamicConfiguration.getConfiguration().getTurnConfiguration().getSecret().getBytes();
|
||||
List<String> urls = urls(e164);
|
||||
Mac mac = Mac.getInstance("HmacSHA1");
|
||||
long validUntilSeconds = (System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1)) / 1000;
|
||||
long user = Util.ensureNonNegativeInt(new SecureRandom().nextInt());
|
||||
String userTime = validUntilSeconds + ":" + user;
|
||||
|
||||
mac.init(new SecretKeySpec(key, "HmacSHA1"));
|
||||
String password = Base64.getEncoder().encodeToString(mac.doFinal(userTime.getBytes()));
|
||||
|
||||
return new TurnToken(userTime, password, urls);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> urls(final String e164) {
|
||||
final DynamicTurnConfiguration turnConfig = dynamicConfiguration.getConfiguration().getTurnConfiguration();
|
||||
|
||||
// Check if number is enrolled to test out specific turn servers
|
||||
final Optional<TurnUriConfiguration> enrolled = turnConfig.getUriConfigs().stream()
|
||||
.filter(config -> config.getEnrolledNumbers().contains(e164))
|
||||
.findFirst();
|
||||
if (enrolled.isPresent()) {
|
||||
return enrolled.get().getUris();
|
||||
}
|
||||
|
||||
// Otherwise, select from turn server sets by weighted choice
|
||||
return WeightedRandomSelect.select(turnConfig
|
||||
.getUriConfigs()
|
||||
.stream()
|
||||
.map(c -> new Pair<List<String>, Long>(c.getUris(), c.getWeight())).toList());
|
||||
}
|
||||
}
|
||||
@@ -9,23 +9,21 @@ import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.Optional;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class UnidentifiedAccessChecksum {
|
||||
|
||||
public static String generateFor(Optional<byte[]> unidentifiedAccessKey) {
|
||||
public static byte[] generateFor(byte[] unidentifiedAccessKey) {
|
||||
try {
|
||||
if (!unidentifiedAccessKey.isPresent()|| unidentifiedAccessKey.get().length != 16) return null;
|
||||
if (unidentifiedAccessKey.length != UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH) {
|
||||
throw new IllegalArgumentException("Invalid UAK length: " + unidentifiedAccessKey.length);
|
||||
}
|
||||
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(unidentifiedAccessKey.get(), "HmacSHA256"));
|
||||
mac.init(new SecretKeySpec(unidentifiedAccessKey, "HmacSHA256"));
|
||||
|
||||
return Base64.getEncoder().encodeToString(mac.doFinal(new byte[32]));
|
||||
return mac.doFinal(new byte[32]);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user