mirror of
https://github.com/signalapp/Signal-Server.git
synced 2025-12-17 02:10:36 +00:00
Compare commits
3329 Commits
v0.13
...
v11.73.0-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
358a286523 | ||
|
|
3bbab0027b | ||
|
|
8afe917a6c | ||
|
|
f5fec5e6bb | ||
|
|
0b81743683 | ||
|
|
9f715c3224 | ||
|
|
24f515ccb4 | ||
|
|
fd531242c9 | ||
|
|
3855bd257d | ||
|
|
c98b54ff15 | ||
|
|
d93d50d038 | ||
|
|
448365c7a0 | ||
|
|
515a863195 | ||
|
|
8d0e23bde1 | ||
|
|
dc8f62a4ad | ||
|
|
896e65545e | ||
|
|
cd4a4b1dcf | ||
|
|
38a0737afb | ||
|
|
4a2768b81d | ||
|
|
00e08b8402 | ||
|
|
48e8584e13 | ||
|
|
a89e30fe75 | ||
|
|
a01fcdad28 | ||
|
|
2a99529921 | ||
|
|
c934405a3e | ||
|
|
b8d922fcb7 | ||
|
|
eb499833c6 | ||
|
|
dd98f7f043 | ||
|
|
e8978ef91c | ||
|
|
669ff1cadf | ||
|
|
4ce85fdb19 | ||
|
|
035ddc4834 | ||
|
|
c2f40b8503 | ||
|
|
cf738a1c14 | ||
|
|
52d40c2321 | ||
|
|
cbf12d6b46 | ||
|
|
ab26a65b6a | ||
|
|
ee5aaf5383 | ||
|
|
1c1714b2c2 | ||
|
|
accb017ec5 | ||
|
|
304782d583 | ||
|
|
f361f436d8 | ||
|
|
a34b5a6122 | ||
|
|
f75ea18ccb | ||
|
|
9a06c40a28 | ||
|
|
e6ab97dc5a | ||
|
|
ba73f757e2 | ||
|
|
30f131096d | ||
|
|
b8ce922f92 | ||
|
|
11b62345e1 | ||
|
|
77289ecb51 | ||
|
|
dfb0b68997 | ||
|
|
d545f60fc4 | ||
|
|
5cda6e9d84 | ||
|
|
7caba89210 | ||
|
|
b8967b75c6 | ||
|
|
74d9849472 | ||
|
|
96b753cfd0 | ||
|
|
5a89e66fc0 | ||
|
|
b4a143b9de | ||
|
|
050035dd52 | ||
|
|
7018062606 | ||
|
|
9e1485de0a | ||
|
|
4e358b891f | ||
|
|
4044a9df30 | ||
|
|
5a7b675001 | ||
|
|
3be4e4bc57 | ||
|
|
5de51919bb | ||
|
|
b02b00818b | ||
|
|
010f88a2ad | ||
|
|
60edf4835f | ||
|
|
a60450d931 | ||
|
|
d138fa45df | ||
|
|
2c2c497c12 | ||
|
|
cb5d3840d9 | ||
|
|
9aceaa7a4d | ||
|
|
636c8ba384 | ||
|
|
ac78eb1425 | ||
|
|
65ad3fe623 | ||
|
|
dcec90fc52 | ||
|
|
24ac32e6e6 | ||
|
|
26f5ffdde3 | ||
|
|
a883426402 | ||
|
|
2f21e930e2 | ||
|
|
5fb158635c | ||
|
|
6f844f9ebb | ||
|
|
d88e358016 | ||
|
|
9cf2635528 | ||
|
|
d0e7579f13 | ||
|
|
cda82b0ea0 | ||
|
|
2ecbb18fe5 | ||
|
|
d40d2389a9 | ||
|
|
df8fb5cab7 | ||
|
|
99ad211c01 | ||
|
|
fb4ed20ff5 | ||
|
|
cb50b44d8f | ||
|
|
ae57853ec4 | ||
|
|
2881c0fd7e | ||
|
|
483fb0968b | ||
|
|
4d37418c15 | ||
|
|
e8ee4b50ff | ||
|
|
4f8aa2eee2 | ||
|
|
397d3cb45a | ||
|
|
e883d727fb | ||
|
|
986545a140 | ||
|
|
836307b0c7 | ||
|
|
b5a75d3079 | ||
|
|
c32067759c | ||
|
|
7fb7abb593 | ||
|
|
0d50b58c60 | ||
|
|
bdf4e24266 | ||
|
|
f41bdf1acb | ||
|
|
77d691df59 | ||
|
|
12300761ab | ||
|
|
25efcbda81 | ||
|
|
a01f96e0e4 | ||
|
|
1d1e3ba79d | ||
|
|
2c9c50711f | ||
|
|
d3f0ab8c6d | ||
|
|
80a3a8a43c | ||
|
|
e6e6eb323d | ||
|
|
681a5bafb4 | ||
|
|
5bec89ecc8 | ||
|
|
69ed0edb74 | ||
|
|
ad5925908e | ||
|
|
d186245c5c | ||
|
|
bbbab4b8a4 | ||
|
|
f83080eb8d | ||
|
|
e0178fa0ea | ||
|
|
c6a79ca176 | ||
|
|
6426e6cc49 | ||
|
|
b13cb098ce | ||
|
|
afda5ca98f | ||
|
|
eb57d87513 | ||
|
|
fbf6b9826e | ||
|
|
a01b29a6bd | ||
|
|
102992b095 | ||
|
|
bd69905f2e | ||
|
|
ce5a4bd94a | ||
|
|
f65a613815 | ||
|
|
d87c8468bd | ||
|
|
aa829af43b | ||
|
|
c10fda8363 | ||
|
|
4252284405 | ||
|
|
74d65b37a8 | ||
|
|
78f95e4859 | ||
|
|
91626dea45 | ||
|
|
5868d9969a | ||
|
|
90490c9c84 | ||
|
|
8ea794baef | ||
|
|
70a6c3e8e5 | ||
|
|
4813803c49 | ||
|
|
fe60cf003f | ||
|
|
0c357bc340 | ||
|
|
b711288faa | ||
|
|
44a5d86641 | ||
|
|
e7048aa9cf | ||
|
|
0120a85c39 | ||
|
|
a41d047f58 | ||
|
|
cccccb4dd6 | ||
|
|
0a64e31625 | ||
|
|
3c6c6c3706 | ||
|
|
8088b58b3b | ||
|
|
a7d5d51fb4 | ||
|
|
378d7987a8 | ||
|
|
3e0baf82a4 | ||
|
|
7a2683a06b | ||
|
|
17a3c90286 | ||
|
|
6341770768 | ||
|
|
308437ec93 | ||
|
|
d3d4916d6c | ||
|
|
d2fa00f0c6 | ||
|
|
d6c9652a70 | ||
|
|
0d20b73e76 | ||
|
|
3c655cdd5a | ||
|
|
fc5cd3a9ca | ||
|
|
83ab926f96 | ||
|
|
56e54e0724 | ||
|
|
544e4fb89a | ||
|
|
966c3a8f47 | ||
|
|
c2ab72c77e | ||
|
|
4468ee3142 | ||
|
|
c82c2c0ba4 | ||
|
|
6e595a0959 | ||
|
|
a79d709039 | ||
|
|
538a07542e | ||
|
|
07ed765250 | ||
|
|
2e497b5834 | ||
|
|
61b3cecd17 | ||
|
|
a4a666bb80 | ||
|
|
c14621a09f | ||
|
|
d0a8899daf | ||
|
|
65dbcb3e5f | ||
|
|
7f725b67c4 | ||
|
|
e25252dc69 | ||
|
|
8b65c11e1e | ||
|
|
320c5eac53 | ||
|
|
8199e0d2d5 | ||
|
|
53387f5a0c | ||
|
|
7d171a79d7 | ||
|
|
3b99bb9e78 | ||
|
|
132f026c75 | ||
|
|
abd0f9630c | ||
|
|
a4508ec84f | ||
|
|
6119b6ab89 | ||
|
|
307ac47ce0 | ||
|
|
4032ddd4fd | ||
|
|
98c8dc05f1 | ||
|
|
4c677ec2da | ||
|
|
c05692e417 | ||
|
|
1e7aa89664 | ||
|
|
ae1edf3c5c | ||
|
|
b17f41c3e8 | ||
|
|
08db4ba54b | ||
|
|
cb6cc39679 | ||
|
|
b6bf6c994c | ||
|
|
3bb4709563 | ||
|
|
b280c768a4 | ||
|
|
d23e89fb9c | ||
|
|
3a27bd0318 | ||
|
|
616513edaf | ||
|
|
09a51020e9 | ||
|
|
cb8cb94d1a | ||
|
|
2440dc0089 | ||
|
|
2336eef333 | ||
|
|
a0e948627c | ||
|
|
88159af588 | ||
|
|
38b77bb550 | ||
|
|
e72d1d0b6f | ||
|
|
1891622e69 | ||
|
|
628a112b38 | ||
|
|
50f5d760c9 | ||
|
|
7292a88ea3 | ||
|
|
07cb3ab576 | ||
|
|
27b749abbd | ||
|
|
27f67a077c | ||
|
|
393e15815b | ||
|
|
a7f1cd25b9 | ||
|
|
953cd2ae0c | ||
|
|
a84a7dbc3d | ||
|
|
24d01f1ab2 | ||
|
|
06eb890761 | ||
|
|
6d0345d327 | ||
|
|
1c67233eb0 | ||
|
|
b4281c5a70 | ||
|
|
5f6b66dad6 | ||
|
|
c2be0af9d9 | ||
|
|
c111e9a35a | ||
|
|
a53a85d788 | ||
|
|
a44c18e9b7 | ||
|
|
4d78437fe4 | ||
|
|
2bfe2c8ff8 | ||
|
|
65da844d70 | ||
|
|
5275c27ee1 | ||
|
|
390580a19d | ||
|
|
147917454f | ||
|
|
39562775d9 | ||
|
|
4a0ef1f834 | ||
|
|
85b16b674d | ||
|
|
ab5d8ba120 | ||
|
|
28076335e0 | ||
|
|
9e9333424f | ||
|
|
6f0faae4ce | ||
|
|
0d24828539 | ||
|
|
0a6d724f2c | ||
|
|
8956e1e0cf | ||
|
|
c9ae991aa3 | ||
|
|
421d594507 | ||
|
|
9c03f2e468 | ||
|
|
1175ff5867 | ||
|
|
55df593561 | ||
|
|
a06a663b94 | ||
|
|
3d2f7e731f | ||
|
|
2575372639 | ||
|
|
faa6e8324a | ||
|
|
d0e3fb1901 | ||
|
|
04287c5073 | ||
|
|
0c76fdd36c | ||
|
|
d582942244 | ||
|
|
3636626e09 | ||
|
|
3e0919106d | ||
|
|
d385838dc1 | ||
|
|
e28f1e8ceb | ||
|
|
3d875f1ce5 | ||
|
|
c4c5397b44 | ||
|
|
6b6f9b2405 | ||
|
|
7d4a8d03a4 | ||
|
|
e9119da040 | ||
|
|
aa36dc95ef | ||
|
|
a6f9409a39 | ||
|
|
41a113e22c | ||
|
|
4cfcdb0c96 | ||
|
|
36050f580e | ||
|
|
98760b631b | ||
|
|
d00aa1e77a | ||
|
|
dce391a248 | ||
|
|
c252118cfc | ||
|
|
e9fd32de79 | ||
|
|
788246a56f | ||
|
|
bc02fe3831 | ||
|
|
d290aad27b | ||
|
|
6754ec5e10 | ||
|
|
1ba00a66eb | ||
|
|
1dd7d33e23 | ||
|
|
e200548e35 | ||
|
|
fdf7b69996 | ||
|
|
92d36b725f | ||
|
|
4e131858ca | ||
|
|
a45d95905e | ||
|
|
0fdfdabf2a | ||
|
|
a25e967978 | ||
|
|
38e30c7513 | ||
|
|
e38e5fa17d | ||
|
|
c1f9bedf2f | ||
|
|
dd5d0ea2b3 | ||
|
|
42fd29d38b | ||
|
|
bf6d3aa324 | ||
|
|
023ccc6563 | ||
|
|
da49db5b9e | ||
|
|
cc8dda28cc | ||
|
|
47300c1d44 | ||
|
|
d31550d444 | ||
|
|
51f37350eb | ||
|
|
ecfa161da8 | ||
|
|
e3778c17ea | ||
|
|
cbc95415b7 | ||
|
|
776c0aa488 | ||
|
|
327eb0219d | ||
|
|
8507b6a1f0 | ||
|
|
a853748303 | ||
|
|
192e884e4a | ||
|
|
7001ad1445 | ||
|
|
5cfb133f79 | ||
|
|
5df24edebf | ||
|
|
95d0293a96 | ||
|
|
f5a2efb57c | ||
|
|
e4b9ae4eee | ||
|
|
bc1ac5a37f | ||
|
|
96ac56faac | ||
|
|
f0bc444388 | ||
|
|
8584f47d95 | ||
|
|
f6235b8c08 | ||
|
|
d452e90470 | ||
|
|
418a869451 | ||
|
|
cf89e2215c | ||
|
|
a4ca1ef1a8 | ||
|
|
c38572307d | ||
|
|
20902df122 | ||
|
|
d31ddb72f3 | ||
|
|
d5f2d86bd2 | ||
|
|
2ce8bcd565 | ||
|
|
75c92eaa93 | ||
|
|
0445adcac3 | ||
|
|
c45ff61954 | ||
|
|
06dd4c5026 | ||
|
|
058caadf4f | ||
|
|
7b7d309105 | ||
|
|
63be7b93ce | ||
|
|
578ea12b59 | ||
|
|
364e59be57 | ||
|
|
fece4dac9e | ||
|
|
ce85c1aabc | ||
|
|
0ac2ce5e72 | ||
|
|
391c800bf5 | ||
|
|
9c27b58194 | ||
|
|
f6471cf8f9 | ||
|
|
f21e9bcc4d | ||
|
|
1eaff753a6 | ||
|
|
9b3a8897cd | ||
|
|
40f8cddfb2 | ||
|
|
c29d5de1eb | ||
|
|
d94c171d63 | ||
|
|
2717967d61 | ||
|
|
53203dbcef | ||
|
|
9e66f8ac11 | ||
|
|
796fb3b4cd | ||
|
|
473ecbdf2d | ||
|
|
7b3703506b | ||
|
|
5816f76bbe | ||
|
|
355996bafc | ||
|
|
c2bb46f41d | ||
|
|
12f76c24b1 | ||
|
|
4b8ebc9a17 | ||
|
|
42a109e593 | ||
|
|
8064e68873 | ||
|
|
3dc0d0bb92 | ||
|
|
2bb8f92af1 | ||
|
|
5b7d5d2b93 | ||
|
|
2b27db18d8 | ||
|
|
f3c811cc03 | ||
|
|
df415208a4 | ||
|
|
77fd01bd9f | ||
|
|
fa3a9570d6 | ||
|
|
c06a5ac96c | ||
|
|
33467b42da | ||
|
|
13fb641113 | ||
|
|
53f17c2baa | ||
|
|
06a57ef811 | ||
|
|
86a09b16ff | ||
|
|
c70d7535b9 | ||
|
|
8541360bf3 | ||
|
|
2a832d36d7 | ||
|
|
1578c89475 | ||
|
|
5c13e54149 | ||
|
|
8e74cf6633 | ||
|
|
bab6b36e4d | ||
|
|
f75e616397 | ||
|
|
941a9c3b39 | ||
|
|
7ba0f604e6 | ||
|
|
cf8a4cc939 | ||
|
|
ee78daeeef | ||
|
|
2f6b0b1a55 | ||
|
|
c048074c31 | ||
|
|
5ca89709e3 | ||
|
|
5a88ff0811 | ||
|
|
de68c251f8 | ||
|
|
7c9ae3561d | ||
|
|
b608ece57e | ||
|
|
8dfffebaf1 | ||
|
|
109a3bb2b9 | ||
|
|
fef37f739b | ||
|
|
7a5615182a | ||
|
|
02a7003ffe | ||
|
|
1571f14815 | ||
|
|
9cb098ad8a | ||
|
|
6283f5952d | ||
|
|
9b9edbae0e | ||
|
|
491155d1cf | ||
|
|
54207254f1 | ||
|
|
1395dcc0be | ||
|
|
2a68d9095d | ||
|
|
a984b3640e | ||
|
|
f9c1e411aa | ||
|
|
f6cbc32ee7 | ||
|
|
602614acf6 | ||
|
|
3854b7d472 | ||
|
|
5e25481088 | ||
|
|
fe86e15d80 | ||
|
|
179b4a69eb | ||
|
|
eee6307789 | ||
|
|
9fc5002619 | ||
|
|
faa6ae284a | ||
|
|
8b4355b21d | ||
|
|
e8835da740 | ||
|
|
75854e104e | ||
|
|
93d06e3f4d | ||
|
|
c560b9229c | ||
|
|
935e268dec | ||
|
|
3a1c716c73 | ||
|
|
f3457502a6 | ||
|
|
7ded802df4 | ||
|
|
d3cd1d1b15 | ||
|
|
f5a75c6319 | ||
|
|
ae3a5c5f5e | ||
|
|
43792e2426 | ||
|
|
551d639951 | ||
|
|
c367a71223 | ||
|
|
d259ef0348 | ||
|
|
288cbf4a80 | ||
|
|
ba5e5a780f | ||
|
|
73fa3c3fe4 | ||
|
|
579eb85175 | ||
|
|
b2b20072ae | ||
|
|
a2c4d3fe95 | ||
|
|
31e2be2e4d | ||
|
|
9f5d97e1c6 | ||
|
|
baaae6cd9f | ||
|
|
ed398aa7b9 | ||
|
|
6e2ae42dab | ||
|
|
7f832ad783 | ||
|
|
2ce6f8cb6c | ||
|
|
2574125199 | ||
|
|
41bf2b2c42 | ||
|
|
51bac394ec | ||
|
|
b696649c9d | ||
|
|
b4828ad8de | ||
|
|
639d634426 | ||
|
|
5358fc4f43 | ||
|
|
6a654ab90b | ||
|
|
99eda80a78 | ||
|
|
a6182acc9c | ||
|
|
2241e4d8ea | ||
|
|
cbbdea1ba4 | ||
|
|
05e7c98620 | ||
|
|
1f1d618dea | ||
|
|
b18117ef89 | ||
|
|
44cb796574 | ||
|
|
ccf60ffc4b | ||
|
|
f69db11f42 | ||
|
|
96a680dcf0 | ||
|
|
c8367c9b7a | ||
|
|
c612663490 | ||
|
|
de5d967d18 | ||
|
|
7fc63f7847 | ||
|
|
49009cbcad | ||
|
|
b5fbeffb86 | ||
|
|
146655e997 | ||
|
|
87d66f04d8 | ||
|
|
bb27dd0c3b | ||
|
|
f45a1c232f | ||
|
|
d7a3c12bbe | ||
|
|
a1e84f5a88 | ||
|
|
b758737907 | ||
|
|
c488c14d25 | ||
|
|
5e0cca0702 | ||
|
|
8559e46e4a | ||
|
|
4bc00e00e3 | ||
|
|
3e777df86c | ||
|
|
278b4e810d | ||
|
|
346c7cd743 | ||
|
|
867bf97d8f | ||
|
|
8a67949168 | ||
|
|
5baa51d547 | ||
|
|
616db337e1 | ||
|
|
3895871462 | ||
|
|
a87b84fbe2 | ||
|
|
b2f0ace9db | ||
|
|
20c95e2606 | ||
|
|
22dccaeddb | ||
|
|
e611a70ba4 | ||
|
|
66845d7080 | ||
|
|
4ea7278c6f | ||
|
|
2b2e26f14b | ||
|
|
b496ef8d6f | ||
|
|
7f5e83141d | ||
|
|
2d1ca98605 | ||
|
|
eaa4c318e3 | ||
|
|
31373fd1ba | ||
|
|
9086246947 | ||
|
|
7855b70682 | ||
|
|
0ce87153e5 | ||
|
|
dba1711e8d | ||
|
|
a70b057e1c | ||
|
|
9a5ffea0ad | ||
|
|
96f4b771ea | ||
|
|
3df143dd3d | ||
|
|
d78d7c726e | ||
|
|
d0ad580c7d | ||
|
|
4a8a2a70b5 | ||
|
|
20a71b7df2 | ||
|
|
68412b3901 | ||
|
|
93a7c60a15 | ||
|
|
31e5058b15 | ||
|
|
14cff958e9 | ||
|
|
9628f147f1 | ||
|
|
13e346d4eb | ||
|
|
e507ce2f26 | ||
|
|
9c62622733 | ||
|
|
62aa0cef39 | ||
|
|
401953313a | ||
|
|
4d2403d619 | ||
|
|
c5f261305d | ||
|
|
394f58f6cc | ||
|
|
674bf1b0e0 | ||
|
|
606ddd8a9b | ||
|
|
e23a1fac50 | ||
|
|
342323a7e6 | ||
|
|
efb410444b | ||
|
|
17c9b4c5d3 | ||
|
|
706de8e2f1 | ||
|
|
23bc11f3b6 | ||
|
|
4eb7dde1c8 | ||
|
|
064861b930 | ||
|
|
afa910bbd7 | ||
|
|
6aceb24fd2 | ||
|
|
d94e86781f | ||
|
|
0d4a3b1ad4 | ||
|
|
acfcb18f29 | ||
|
|
f7ff8e3837 | ||
|
|
048e17c62b | ||
|
|
d89b4f7e95 | ||
|
|
795b226b90 | ||
|
|
e485c380e0 | ||
|
|
bb4f4bc441 | ||
|
|
65b49b2d9c | ||
|
|
9e7010f185 | ||
|
|
3bb8e5bb00 | ||
|
|
2a4d1da2ca | ||
|
|
6b71b66bd2 | ||
|
|
ebf24fb125 | ||
|
|
46d64b949e | ||
|
|
6919354520 | ||
|
|
a42fe9bfb0 | ||
|
|
ee1f8b34ea | ||
|
|
c910fa406d | ||
|
|
559205e33f | ||
|
|
c0756e9c60 | ||
|
|
bf1190696e | ||
|
|
71dd0890de | ||
|
|
e5acdf1402 | ||
|
|
0f08b6bb59 | ||
|
|
6198a7b69a | ||
|
|
067aee6664 | ||
|
|
138a2ebbd0 | ||
|
|
296f6a7a88 | ||
|
|
069ffa9921 | ||
|
|
f42fd8a840 | ||
|
|
10f27af6f2 | ||
|
|
0bbd34d060 | ||
|
|
282daeb0dc | ||
|
|
d33b313c11 | ||
|
|
fb7316c9ae | ||
|
|
279b0a51d9 | ||
|
|
6547d5ebf3 | ||
|
|
4f1ef9a039 | ||
|
|
4c80714d19 | ||
|
|
077ead71a5 | ||
|
|
caba110266 | ||
|
|
0fdb23c1e9 | ||
|
|
13a84f0c72 | ||
|
|
669bd58e33 | ||
|
|
6e82740a9b | ||
|
|
7ea43a728d | ||
|
|
71b38356b1 | ||
|
|
5a99708f56 | ||
|
|
24191d9599 | ||
|
|
482ea8eb40 | ||
|
|
1dae05651f | ||
|
|
5164e92538 | ||
|
|
f89a20dbc7 | ||
|
|
3a4c5a2bfb | ||
|
|
5e1334e8de | ||
|
|
fa6e3d3690 | ||
|
|
9383e7716b | ||
|
|
cfe34fbf0f | ||
|
|
9fe110625c | ||
|
|
975f753c2b | ||
|
|
e6237480f8 | ||
|
|
966d4e29d4 | ||
|
|
26f876a2cb | ||
|
|
ab9e6ac48a | ||
|
|
c1d6c04ab2 | ||
|
|
888cec3d56 | ||
|
|
1461bcc2c2 | ||
|
|
11f1cf80bd | ||
|
|
c675cc8b26 | ||
|
|
0011b8925b | ||
|
|
73ea6e4251 | ||
|
|
e4441dddbb | ||
|
|
8d1d56f694 | ||
|
|
2015ba77ca | ||
|
|
7033a0f68f | ||
|
|
6ada76da7f | ||
|
|
cbdec0cb22 | ||
|
|
de6e9d31c9 | ||
|
|
f0a6be32fc | ||
|
|
5c4855cca6 | ||
|
|
2e1e380418 | ||
|
|
d07f0b4f71 | ||
|
|
aaa2a6eef1 | ||
|
|
b1f56c3324 | ||
|
|
da5c0ae4b6 | ||
|
|
1e1394560d | ||
|
|
bae0196bcf | ||
|
|
3398955c1a | ||
|
|
a1b925d1e0 | ||
|
|
31c0c3275f | ||
|
|
0a4392f700 | ||
|
|
eb86986cf4 | ||
|
|
1053a47e42 | ||
|
|
99b1f48e0e | ||
|
|
c21eb6aa50 | ||
|
|
6dddf54222 | ||
|
|
9e3eb2319e | ||
|
|
1d8dcda815 | ||
|
|
ee52a84262 | ||
|
|
eb51e81faa | ||
|
|
d41ef1df18 | ||
|
|
66d47aff2c | ||
|
|
c931103712 | ||
|
|
ad1aeea74b | ||
|
|
ae7f8af03e | ||
|
|
a52c91a665 | ||
|
|
94bf3a3902 | ||
|
|
f5a539e128 | ||
|
|
24480b2090 | ||
|
|
a124b3abe9 | ||
|
|
090d722b61 | ||
|
|
d27ec6fe8d | ||
|
|
8d34f3447b | ||
|
|
72b52965b9 | ||
|
|
ae7077c643 | ||
|
|
11598e855f | ||
|
|
534c577f59 | ||
|
|
7762afc497 | ||
|
|
a3fe4b9980 | ||
|
|
598599cd14 | ||
|
|
07cd69ab34 | ||
|
|
3b764bed7a | ||
|
|
c91d5c2fdb | ||
|
|
40f7e6e994 | ||
|
|
ee9aa9ce12 | ||
|
|
08304bf375 | ||
|
|
8b8c6237be | ||
|
|
c0837104cd | ||
|
|
fe21d014f7 | ||
|
|
75c5032cd3 | ||
|
|
f84e7aebd0 | ||
|
|
c379a3d297 | ||
|
|
eedeaaecee | ||
|
|
64eeb1e361 | ||
|
|
e07597eba7 | ||
|
|
5f2656710c | ||
|
|
1af53f2612 | ||
|
|
c89cfa4927 | ||
|
|
bbde93a3c7 | ||
|
|
b01b76d78f | ||
|
|
75c22038eb | ||
|
|
3c1705994d | ||
|
|
439d2f5df8 | ||
|
|
d2bc3c7360 | ||
|
|
9734433f00 | ||
|
|
5bd08800bb | ||
|
|
3032415141 | ||
|
|
ba58a95a0f | ||
|
|
aa4bd92fee | ||
|
|
f37c76dab1 | ||
|
|
863969c77c | ||
|
|
2383aaaa3d | ||
|
|
715d1157ad | ||
|
|
4aaae3f445 | ||
|
|
8359ef73f4 | ||
|
|
c6bb649adb | ||
|
|
e333cbd94d | ||
|
|
cc9a825279 | ||
|
|
5189cbe5c7 | ||
|
|
d1d6e5c652 | ||
|
|
3e5087e60b | ||
|
|
93c3cea912 | ||
|
|
e824b861d4 | ||
|
|
e8dd1e0bf2 | ||
|
|
533afa4c6e | ||
|
|
559026933d | ||
|
|
7864405efd | ||
|
|
a5575902de | ||
|
|
5b9bce59e1 | ||
|
|
041aed2d72 | ||
|
|
02a296e500 | ||
|
|
98e41f9a37 | ||
|
|
6a71d369e2 | ||
|
|
75661fa800 | ||
|
|
df5498e1c0 | ||
|
|
c0af911197 | ||
|
|
44bc90e5ab | ||
|
|
5c1cde1b28 | ||
|
|
3172b571c6 | ||
|
|
17e8b77e88 | ||
|
|
8011935a3b | ||
|
|
3f3052c23c | ||
|
|
8f17f45339 | ||
|
|
009e2eeb97 | ||
|
|
c70fa48835 | ||
|
|
bd5f5c407b | ||
|
|
2bc573a53d | ||
|
|
537d61d5bd | ||
|
|
09519ae942 | ||
|
|
2a67b2e610 | ||
|
|
8161f55a82 | ||
|
|
ecee189ad8 | ||
|
|
ef0900f3ac | ||
|
|
383d744bd8 | ||
|
|
c2ba8ab562 | ||
|
|
cd49ea43c0 | ||
|
|
53aa45a2bb | ||
|
|
83e0a19561 | ||
|
|
6a5d475198 | ||
|
|
49ccbba2e3 | ||
|
|
41735ed40e | ||
|
|
2d11a433c9 | ||
|
|
e79ab2521f | ||
|
|
fb1f99da87 | ||
|
|
08c6a8c2e5 | ||
|
|
ce3835e176 | ||
|
|
39f6eadbb9 | ||
|
|
16dba09b61 | ||
|
|
d5ebf2f2ed | ||
|
|
8a8e6e7b49 | ||
|
|
34e21b9f7b | ||
|
|
98a31d1474 | ||
|
|
72a0c1be0f | ||
|
|
5b25e38e41 | ||
|
|
2fb400280b | ||
|
|
79ad09524e | ||
|
|
5f8accb492 | ||
|
|
6fcadc2297 | ||
|
|
3f4e1522eb | ||
|
|
6304c84cdb | ||
|
|
894297efa9 | ||
|
|
a51a7a0901 | ||
|
|
372e131e25 | ||
|
|
6c6e6a4975 | ||
|
|
cd66a1ceb7 | ||
|
|
feb59deb28 | ||
|
|
489519a982 | ||
|
|
a96865d0f5 | ||
|
|
12e11609a9 | ||
|
|
5b404095b0 | ||
|
|
6a6555e2d5 | ||
|
|
49489a6021 | ||
|
|
8cd93d68e4 | ||
|
|
f3b9a8d97f | ||
|
|
b91a69d8b3 | ||
|
|
624e40e3b7 | ||
|
|
23a076a204 | ||
|
|
016141a05d | ||
|
|
a064b25a14 | ||
|
|
bd40e32f3b | ||
|
|
81a21c0d5f | ||
|
|
6478210330 | ||
|
|
aa1c37fe26 | ||
|
|
6ee23b0186 | ||
|
|
40eb445592 | ||
|
|
ce7d687205 | ||
|
|
758900b7a8 | ||
|
|
539b62a829 | ||
|
|
2866f1b213 | ||
|
|
fc1465c05d | ||
|
|
bc887ec6fa | ||
|
|
84b3d324bb | ||
|
|
fc10108788 | ||
|
|
fbbc1bec58 | ||
|
|
2059bb5ef8 | ||
|
|
b080a5db4d | ||
|
|
b4aabd799b | ||
|
|
92f035bc2a | ||
|
|
18a6df34bd | ||
|
|
b1274125c9 | ||
|
|
dceebc1c8d | ||
|
|
6aadb4b458 | ||
|
|
703405b874 | ||
|
|
d1735c7e57 | ||
|
|
1f815b49dd | ||
|
|
a9339b7037 | ||
|
|
f2c6ca182d | ||
|
|
b946c27a20 | ||
|
|
9fd6358518 | ||
|
|
8a8a848fac | ||
|
|
aeb9f67266 | ||
|
|
e08c5a412e | ||
|
|
a7443a9ece | ||
|
|
54fe3b9a43 | ||
|
|
ba522b1691 | ||
|
|
739c5bf22c | ||
|
|
7cdadeb791 | ||
|
|
dadf43b93e | ||
|
|
bd820e6d2e | ||
|
|
19f7b207b7 | ||
|
|
a398e2269c | ||
|
|
2e28fb97a4 | ||
|
|
5c68d83a93 | ||
|
|
0b7c3ad745 | ||
|
|
0cde06557d | ||
|
|
27844fe692 | ||
|
|
779051ef9f | ||
|
|
d13741fbd5 | ||
|
|
f7f870fe62 | ||
|
|
de59aa099d | ||
|
|
57a478b898 | ||
|
|
3e8d79e147 | ||
|
|
a46045d987 | ||
|
|
662c905b80 | ||
|
|
31022aeb79 | ||
|
|
b3e6a50dee | ||
|
|
d29764d11f | ||
|
|
f8e4f6727a | ||
|
|
63d05df8a3 | ||
|
|
52d13d1d62 | ||
|
|
f58a320223 | ||
|
|
3e01bc1174 | ||
|
|
d1ada7f998 | ||
|
|
095fc8140e | ||
|
|
73c368ea86 | ||
|
|
ce5edbb7fc | ||
|
|
a680639718 | ||
|
|
becf6afbdd | ||
|
|
1dda015c6a | ||
|
|
a0427ecf8c | ||
|
|
cfd31e98ff | ||
|
|
bcb89924b4 | ||
|
|
23f9199439 | ||
|
|
1f6318a919 | ||
|
|
b0667b258b | ||
|
|
4c3a48f5be | ||
|
|
33fb7a72de | ||
|
|
2c808e369c | ||
|
|
906d0be382 | ||
|
|
1c9a3c6105 | ||
|
|
2aaddd721f | ||
|
|
4e2284b83f | ||
|
|
d5d9978e48 | ||
|
|
d45659ac76 | ||
|
|
13a07dc6cd | ||
|
|
51b7a8d868 | ||
|
|
df9c0051c9 | ||
|
|
331ff83cd5 | ||
|
|
44838d6238 | ||
|
|
5400abb065 | ||
|
|
f47fefb73e | ||
|
|
cdef745a7a | ||
|
|
1a1eab4ec0 | ||
|
|
3a966ef345 | ||
|
|
be20c04cd8 | ||
|
|
d09dcc90fe | ||
|
|
1fd1207bf6 | ||
|
|
0117fc12c7 | ||
|
|
ef9a7fda9a | ||
|
|
13447df1e0 | ||
|
|
3608c5bfb0 | ||
|
|
34dbff6786 | ||
|
|
a6066bfc2f | ||
|
|
8579190cdf | ||
|
|
917f667229 | ||
|
|
317a551bdb | ||
|
|
27e9271473 | ||
|
|
11dff6c546 | ||
|
|
e6712937ca | ||
|
|
cf8887bb5a | ||
|
|
696340f780 | ||
|
|
86ddcbaa08 | ||
|
|
2144d2a8d8 | ||
|
|
f7af861b31 | ||
|
|
208a09b3ae | ||
|
|
831023e41d | ||
|
|
ff627793d6 | ||
|
|
f971c76a99 | ||
|
|
8f41176c76 | ||
|
|
31bbbbb5e0 | ||
|
|
effcd6038d | ||
|
|
12be7d49c2 | ||
|
|
14863b575e | ||
|
|
32a95f96ff | ||
|
|
b757d4b334 | ||
|
|
bd03d910fe | ||
|
|
01ef855157 | ||
|
|
817866caf3 | ||
|
|
158d65c6a7 | ||
|
|
62022c7de1 | ||
|
|
a824b5575d | ||
|
|
78819d5382 | ||
|
|
d128bc782a | ||
|
|
530b2a310f | ||
|
|
d5b0d99a54 | ||
|
|
43be72d076 | ||
|
|
9558944e22 | ||
|
|
0f6c866c8d | ||
|
|
bac78e9291 | ||
|
|
c22ea78672 | ||
|
|
a85afe827d | ||
|
|
abaed821ec | ||
|
|
6fa9dcd954 | ||
|
|
819d59cd79 | ||
|
|
2f88f0eedb | ||
|
|
74ff491671 | ||
|
|
eac48a6617 | ||
|
|
19617c14f8 | ||
|
|
fc7291c3e8 | ||
|
|
88db808298 | ||
|
|
5193abdab3 | ||
|
|
a315c9be92 | ||
|
|
fc1541591a | ||
|
|
ae97c4db9f | ||
|
|
26bc5973b5 | ||
|
|
e52b8c8423 | ||
|
|
7395489bac | ||
|
|
b384ed7f5c | ||
|
|
e3afcae7d3 | ||
|
|
9faeed7b20 | ||
|
|
49adcca80e | ||
|
|
49c43a6816 | ||
|
|
84f85ae098 | ||
|
|
3d581941ab | ||
|
|
d2d39baede | ||
|
|
111f5ba024 | ||
|
|
ce3fb7fa99 | ||
|
|
fc421d3f21 | ||
|
|
71bea759c6 | ||
|
|
bf1dd791a5 | ||
|
|
4c99577c08 | ||
|
|
5d5c63e6d4 | ||
|
|
42ff3f8432 | ||
|
|
be6ef76486 | ||
|
|
bc297e6d34 | ||
|
|
3a526dcbd7 | ||
|
|
7883352b74 | ||
|
|
982d122d18 | ||
|
|
d8d94407c6 | ||
|
|
28cfc54170 | ||
|
|
2ee7279743 | ||
|
|
eb1b073385 | ||
|
|
c634185b6f | ||
|
|
827a3af419 | ||
|
|
2c33d22a30 | ||
|
|
b41ed9d810 | ||
|
|
58d3a12eff | ||
|
|
88c4b2be97 | ||
|
|
6cbd57f19f | ||
|
|
5522376584 | ||
|
|
5089c37d28 | ||
|
|
1ccf24e68c | ||
|
|
411f7298f2 | ||
|
|
5b0214c6f2 | ||
|
|
735573e61b | ||
|
|
c545cff1b3 | ||
|
|
cbd9681e3e | ||
|
|
ca876e40ca | ||
|
|
76f5a71727 | ||
|
|
117de2382d | ||
|
|
25e7036451 | ||
|
|
3131bd3dd9 | ||
|
|
1cf9397bbd | ||
|
|
c97be15e79 | ||
|
|
164fc40990 | ||
|
|
6456af6284 | ||
|
|
81212cc13a | ||
|
|
6f0750790c | ||
|
|
3e61b5c49d | ||
|
|
50c4df4f45 | ||
|
|
1eb946f5fe | ||
|
|
7bd402b48d | ||
|
|
90444d5b91 | ||
|
|
5ee093f87c | ||
|
|
623743286c | ||
|
|
67067f1d2d | ||
|
|
07f9bb112e | ||
|
|
417d48c452 | ||
|
|
358412c78a | ||
|
|
215621a9b0 | ||
|
|
c3f53c4dd9 | ||
|
|
01514f83a0 | ||
|
|
c10b64c367 | ||
|
|
722055c8b5 | ||
|
|
680e501f83 | ||
|
|
f13f7a5ff4 | ||
|
|
5290656c3b | ||
|
|
93fbb87741 | ||
|
|
ce76c5c117 | ||
|
|
e663e1b0a6 | ||
|
|
20cdd09171 | ||
|
|
f98dd80941 | ||
|
|
f84736bd32 | ||
|
|
9995f271c8 | ||
|
|
cf59d849b0 | ||
|
|
ee3b91e4fb | ||
|
|
77f134ddca | ||
|
|
8913192b7e | ||
|
|
94ac3f6cc8 | ||
|
|
b89de860d3 | ||
|
|
f8c623074b | ||
|
|
1160af9522 | ||
|
|
3056ea8cbc | ||
|
|
28e3b23e8c | ||
|
|
fbaf4a09e2 | ||
|
|
cfa8cbedc1 | ||
|
|
be4c46e674 | ||
|
|
bacf524ae6 | ||
|
|
aa65d34c36 | ||
|
|
0cd3640f13 | ||
|
|
c595d9415c | ||
|
|
1a604d8c79 | ||
|
|
f76e6705c0 | ||
|
|
10cd60738a | ||
|
|
89470ff536 | ||
|
|
79b8202452 | ||
|
|
d252e579f4 | ||
|
|
30b2c2b5ad | ||
|
|
282f39141e | ||
|
|
85e4de6933 | ||
|
|
0b993098a8 | ||
|
|
1880773fb9 | ||
|
|
00c9023e74 | ||
|
|
d59eabd9d7 | ||
|
|
2a3ea13c9e | ||
|
|
6906336dfb | ||
|
|
514b94a5cb | ||
|
|
df01be2dca | ||
|
|
10c6f885fd | ||
|
|
224e6dac31 | ||
|
|
3b1eb3a9db | ||
|
|
e320626c6e | ||
|
|
03dac2bf7e | ||
|
|
730303567f | ||
|
|
57ff9f86f5 | ||
|
|
bfd2c32d4e | ||
|
|
e9a3d52d7f | ||
|
|
ac7eb88194 | ||
|
|
d45154f2aa | ||
|
|
760462f8fb | ||
|
|
1999bd2bcb | ||
|
|
46110d4d65 | ||
|
|
5752853bba | ||
|
|
02d06af3fc | ||
|
|
09e0934eaf | ||
|
|
b100f09205 | ||
|
|
670b69df24 | ||
|
|
03a531e1b0 | ||
|
|
13ecbe7e53 | ||
|
|
17047513c3 | ||
|
|
7bd7d0e84e | ||
|
|
4571042814 | ||
|
|
9cb89b42bf | ||
|
|
9a4453c414 | ||
|
|
5fa22bc073 | ||
|
|
bf32b766a5 | ||
|
|
f0a8b5a54a | ||
|
|
8e68e0e037 | ||
|
|
b81b811400 | ||
|
|
b41f97233e | ||
|
|
350de1c759 | ||
|
|
055e8d80a1 | ||
|
|
dfb8a419e7 | ||
|
|
030a791d69 | ||
|
|
cf495ef7cf | ||
|
|
8fdbcbef44 | ||
|
|
30c9968928 | ||
|
|
f357ad098f | ||
|
|
1a8c40c02a | ||
|
|
20677d4be1 | ||
|
|
c448c37cc9 | ||
|
|
f117d9ff4d | ||
|
|
2dbd7ffc75 | ||
|
|
fac4538f6f | ||
|
|
01e526af25 | ||
|
|
7e805d1592 | ||
|
|
c63bebb3e7 | ||
|
|
0e6cfb460d | ||
|
|
cd6b2512e1 | ||
|
|
4f6b132449 | ||
|
|
b7c611a466 | ||
|
|
0163242c8a | ||
|
|
7fa17e33e9 | ||
|
|
e4dbb8efe7 | ||
|
|
89256fb5b3 | ||
|
|
59e401f41e | ||
|
|
4b42dd1db3 | ||
|
|
6196856a7c | ||
|
|
0e8d4f9a61 | ||
|
|
97d2d97ee7 | ||
|
|
62315f423c | ||
|
|
5ee56b022c | ||
|
|
6c37b658ac | ||
|
|
1f53900345 | ||
|
|
deece33a0d | ||
|
|
13053da97f | ||
|
|
4c019aef15 | ||
|
|
bab5e5769b | ||
|
|
f68390e96f | ||
|
|
76cbf734ad | ||
|
|
17ba630014 | ||
|
|
7057476048 | ||
|
|
3121867f72 | ||
|
|
435410b004 | ||
|
|
f190462879 | ||
|
|
7c0ff67625 | ||
|
|
ac72c8b2de | ||
|
|
20208ae528 | ||
|
|
6c6f073bc2 | ||
|
|
0663fe30df | ||
|
|
2c0a75586b | ||
|
|
b6cb23cbb5 | ||
|
|
ee555285ed | ||
|
|
b75456acf3 | ||
|
|
be6d6351b9 | ||
|
|
abafa2ccac | ||
|
|
53e6f419b6 | ||
|
|
b75dec40ac | ||
|
|
0f4f775ee2 | ||
|
|
5974328d9c | ||
|
|
a472774734 | ||
|
|
166d203e8e | ||
|
|
3b3764535c | ||
|
|
f2a1a65a45 | ||
|
|
b7c56108ca | ||
|
|
52478e7de0 | ||
|
|
ae9fd090de | ||
|
|
59bbd0c43c | ||
|
|
f6c9b2b6e7 | ||
|
|
0c0e33bc0e | ||
|
|
4d33ba48cc | ||
|
|
18fb23f27c | ||
|
|
92c25a8373 | ||
|
|
14f5271c20 | ||
|
|
37bda0b035 | ||
|
|
675785a4fd | ||
|
|
0572951c8a | ||
|
|
7d766ee39e | ||
|
|
1f24c913a6 | ||
|
|
2a8806ec2e | ||
|
|
ffcabe6fc4 | ||
|
|
365ad3a4f8 | ||
|
|
2cb788ceb7 | ||
|
|
257fef9734 | ||
|
|
37e0730d2a | ||
|
|
dea359ef91 | ||
|
|
64c9648dd8 | ||
|
|
6dfd13118d | ||
|
|
2f6105f9bc | ||
|
|
5c23f62cec | ||
|
|
ab4e94edab | ||
|
|
9589b7758c | ||
|
|
681cdf8eff | ||
|
|
ad6c271f9d | ||
|
|
c8414a63fb | ||
|
|
c10d9603ad | ||
|
|
91bd061110 | ||
|
|
83aa59f4dd | ||
|
|
3745a0b81d | ||
|
|
e2b093abce | ||
|
|
7e29ed1cc7 | ||
|
|
5965f0fd22 | ||
|
|
c3c46f2f74 | ||
|
|
a816aa0186 | ||
|
|
a7bad20eae | ||
|
|
089b6b1644 | ||
|
|
7509520883 | ||
|
|
9778775046 | ||
|
|
e5ae0572c5 | ||
|
|
63dac3bd9f | ||
|
|
19295eef46 | ||
|
|
0bc1369e04 | ||
|
|
ca2f7d2eed | ||
|
|
3ea535a412 | ||
|
|
a288b9df8e | ||
|
|
8b6012f8a8 | ||
|
|
1e5d7582da | ||
|
|
ad838b4827 | ||
|
|
25f603efc9 | ||
|
|
152c927929 | ||
|
|
b5bd16c6a9 | ||
|
|
14bfa83bb8 | ||
|
|
5dc8086968 | ||
|
|
7118340f12 | ||
|
|
efe7f2e4c1 | ||
|
|
fb2fc2335a | ||
|
|
345e116699 | ||
|
|
e50a1c0646 | ||
|
|
a6fd1aa06c | ||
|
|
3cdc58200a | ||
|
|
933dd81d82 | ||
|
|
a1434524a4 | ||
|
|
cdc6afefe2 | ||
|
|
738ec2a38e | ||
|
|
07886a9722 | ||
|
|
fde1b49729 | ||
|
|
58210141f4 | ||
|
|
e1f35102aa | ||
|
|
af2a8548c3 | ||
|
|
1faedd3870 | ||
|
|
f57a4171ba | ||
|
|
df9dc82de5 | ||
|
|
0573f09285 | ||
|
|
eb6fe11da1 | ||
|
|
823025f3b3 | ||
|
|
0ee3f0a5b5 | ||
|
|
6bff564129 | ||
|
|
7dabc92447 | ||
|
|
78bbe8855b | ||
|
|
5354104128 | ||
|
|
a5118e4daa | ||
|
|
b5ade5dc12 | ||
|
|
fff8c72f42 | ||
|
|
06ca5f14fc | ||
|
|
8c9d871268 | ||
|
|
5951ead1b6 | ||
|
|
4a0a0e10d2 | ||
|
|
7266eeee7a | ||
|
|
5839ce3e1a | ||
|
|
f85c6bf828 | ||
|
|
9af9e21e05 | ||
|
|
6d16ad2763 | ||
|
|
447fba1594 | ||
|
|
93f845610d | ||
|
|
aa8525385a | ||
|
|
ec783133c1 | ||
|
|
f630bddb19 | ||
|
|
71f0aab2c6 | ||
|
|
ae8de67271 | ||
|
|
6142998b87 | ||
|
|
142376f360 | ||
|
|
ae329e735f | ||
|
|
2dbab70c8c | ||
|
|
47916ecb0f | ||
|
|
635f669a32 | ||
|
|
5f49772ca6 | ||
|
|
6332552346 | ||
|
|
4fb7afcf7b | ||
|
|
ff448950ed | ||
|
|
d6319aeb92 | ||
|
|
8fc6f9c442 | ||
|
|
fdcf317963 | ||
|
|
e9ea79cc8e | ||
|
|
ad32555cc9 | ||
|
|
be8a1acca9 | ||
|
|
477615fc66 | ||
|
|
e0ed8fa0b8 | ||
|
|
dcbf285fae | ||
|
|
ceda459942 | ||
|
|
28fe44aea4 | ||
|
|
71510a8199 | ||
|
|
9cd121c8f6 | ||
|
|
03f14475ff | ||
|
|
2f105ed0a4 | ||
|
|
b4350ec77b | ||
|
|
0fa6eb4e31 | ||
|
|
704d54dd01 | ||
|
|
bee9b61831 | ||
|
|
9c6ce08db0 | ||
|
|
6c0de89de8 | ||
|
|
aa99e202b4 | ||
|
|
04728ea4bc | ||
|
|
6865cdfce3 | ||
|
|
d09b36b1d5 | ||
|
|
a5dd4f5fac | ||
|
|
9936b2967e | ||
|
|
0971613ac0 | ||
|
|
98f9bc3fc1 | ||
|
|
f5f2da11d1 | ||
|
|
f7d855c59e | ||
|
|
b6dba2cbe9 | ||
|
|
2fe743649d | ||
|
|
a015237fd2 | ||
|
|
e1f4deaacc | ||
|
|
1dceee3fa0 | ||
|
|
3a17a7c98f | ||
|
|
3298db8683 | ||
|
|
d4d9403829 | ||
|
|
426e6923ac | ||
|
|
b413f665d8 | ||
|
|
5e1a572bd8 | ||
|
|
3036a149bb | ||
|
|
0dcb4b645c | ||
|
|
d71082b491 | ||
|
|
fc4c8d6054 | ||
|
|
1a27c7eabc | ||
|
|
b2e9602aba | ||
|
|
408b959441 | ||
|
|
35fc98a188 | ||
|
|
92f6a79e1f | ||
|
|
5a9c8e304c | ||
|
|
8f94ed68a3 | ||
|
|
a4cd30451c | ||
|
|
ce1a4b94cb | ||
|
|
92a0deffcf | ||
|
|
97b6f6028b | ||
|
|
99e300a640 | ||
|
|
611e8c39ee | ||
|
|
af55287dee | ||
|
|
01f1c263a6 | ||
|
|
24ea6a9f1d | ||
|
|
46c800b8b7 | ||
|
|
f10be893ce | ||
|
|
c606c1664f | ||
|
|
90a938fe2b | ||
|
|
225932b4c9 | ||
|
|
6b850b9894 | ||
|
|
d8ef796a46 | ||
|
|
943a5d1036 | ||
|
|
e600e9c583 | ||
|
|
b25da8ceaa | ||
|
|
f7388f6492 | ||
|
|
10cdb7387d | ||
|
|
dd436dd1dd | ||
|
|
13b84635b5 | ||
|
|
144d1ea280 | ||
|
|
27534d408f | ||
|
|
b80a2921aa | ||
|
|
0a23ce870a | ||
|
|
ba1e100b42 | ||
|
|
2bc237468d | ||
|
|
c355ef8d53 | ||
|
|
3052d88164 | ||
|
|
1feb23ba99 | ||
|
|
767f650e6f | ||
|
|
59a0fd0799 | ||
|
|
00b5cfcf17 | ||
|
|
f7217944e7 | ||
|
|
9e342f253d | ||
|
|
20c48b6bb2 | ||
|
|
572004d37a | ||
|
|
4f9e7bb572 | ||
|
|
df9b692a32 | ||
|
|
0a322d5a9f | ||
|
|
59eb6d10c1 | ||
|
|
affb219d72 | ||
|
|
a57ce1dd17 | ||
|
|
4e7ace3b48 | ||
|
|
b100b3c36b | ||
|
|
b64b27e5ea | ||
|
|
81c1ba6eef | ||
|
|
46b981bb2f | ||
|
|
93ae4d1ee6 | ||
|
|
9c53d818f4 | ||
|
|
efb2a1d913 | ||
|
|
e5a2c1ab10 | ||
|
|
550c0c7625 | ||
|
|
0abc269a3e | ||
|
|
6b3cbe7882 | ||
|
|
67ed035b36 | ||
|
|
ca25105f13 | ||
|
|
ad30786f4a | ||
|
|
ff0bdcd0c2 | ||
|
|
2e01da5ec1 | ||
|
|
8fb37a0024 | ||
|
|
9412a7424c | ||
|
|
e440eb1733 | ||
|
|
f8cbb4f386 | ||
|
|
86ccaa52a5 | ||
|
|
db14d15953 | ||
|
|
cc3e5d23e4 | ||
|
|
b70d076324 | ||
|
|
cac86d1f77 | ||
|
|
22f7bb822f | ||
|
|
1b53f10091 | ||
|
|
2d697ac8db | ||
|
|
bac268a21c | ||
|
|
321e6e6679 | ||
|
|
e028700175 | ||
|
|
63a673cf1d | ||
|
|
22ef058cb6 | ||
|
|
adcdb19c88 | ||
|
|
d35fa8e8e1 | ||
|
|
9ee6419bc0 | ||
|
|
6af7bfb536 | ||
|
|
3bf0188e7f | ||
|
|
91fc0fd623 | ||
|
|
f936ec0236 | ||
|
|
d2fcf68381 | ||
|
|
a4d0c17efd | ||
|
|
ff1a721d5b | ||
|
|
c870a1bbd5 | ||
|
|
ebf332a8c9 | ||
|
|
b2d335e0da | ||
|
|
85d1fff18f | ||
|
|
2839a95198 | ||
|
|
6bb106c2cb | ||
|
|
e551fd2c1b | ||
|
|
34a11c2338 | ||
|
|
0de3a400eb | ||
|
|
e524ff965d | ||
|
|
7ba689aaeb | ||
|
|
c228e125c3 | ||
|
|
92fde83b3a | ||
|
|
3a268aef50 | ||
|
|
9486dcf6b0 | ||
|
|
f673bd8d7b | ||
|
|
299b680013 | ||
|
|
b1160af896 | ||
|
|
81e8352391 | ||
|
|
1a627d6a87 | ||
|
|
d5f00db9ea | ||
|
|
00a3e562dc | ||
|
|
36aca49fc3 | ||
|
|
0628c9161c | ||
|
|
9b28672e19 | ||
|
|
903ffef42c | ||
|
|
d764058a04 | ||
|
|
0aafe38496 | ||
|
|
d86d565b3f | ||
|
|
e7745db36e | ||
|
|
66d3e1b551 | ||
|
|
474b879b16 | ||
|
|
2f5d6e16a6 | ||
|
|
0a23b57ff8 | ||
|
|
251e1b51c5 | ||
|
|
68150b640e | ||
|
|
217d270457 | ||
|
|
143b6f0df1 | ||
|
|
81684b921e | ||
|
|
2cc6c959a5 | ||
|
|
fb9aa672c9 | ||
|
|
325e65db7f | ||
|
|
603e2b173d | ||
|
|
103b49ec45 | ||
|
|
6c78d7544f | ||
|
|
4d5fbec5a5 | ||
|
|
7cf50a15d0 | ||
|
|
adbc4e9fec | ||
|
|
4815434dd7 | ||
|
|
44f20e7ad6 | ||
|
|
b25e50bdae | ||
|
|
4aab388eff | ||
|
|
604287244f | ||
|
|
47646a4aa0 | ||
|
|
70ca5e2aef | ||
|
|
4a4a721e90 | ||
|
|
52078f7762 | ||
|
|
36377e59cb | ||
|
|
a4062b338e | ||
|
|
ec223ac2ed | ||
|
|
5587b7d469 | ||
|
|
b00577fda4 | ||
|
|
26870d134f | ||
|
|
fb2baad7cc | ||
|
|
0431a2abb1 | ||
|
|
4bae8d4cfb | ||
|
|
c2db2d3cbd | ||
|
|
05d9ec673e | ||
|
|
ae566dca98 | ||
|
|
1732cf9243 | ||
|
|
ab62c19de9 | ||
|
|
96d3a69479 | ||
|
|
2f7bb3499d | ||
|
|
8523bb1ad8 | ||
|
|
e266e1ce40 | ||
|
|
169c3d5a0f | ||
|
|
9cffbe3d49 | ||
|
|
6090439289 | ||
|
|
e6da54d9b8 | ||
|
|
0a843dc086 | ||
|
|
7b3ed2dcbf | ||
|
|
42ed6c3ded | ||
|
|
23ca011ac1 | ||
|
|
d82b3dc429 | ||
|
|
e391793c58 | ||
|
|
236cef4b56 | ||
|
|
45687513bf | ||
|
|
019ffdaf12 | ||
|
|
1a57d4fe11 | ||
|
|
3081f22e70 | ||
|
|
df847431eb | ||
|
|
5b2f1eee65 | ||
|
|
99f488d48f | ||
|
|
05929871c9 | ||
|
|
74b3daa70a | ||
|
|
5e30b0499a | ||
|
|
32bf742709 | ||
|
|
9535f399f2 | ||
|
|
85c7347899 | ||
|
|
4579d26a53 | ||
|
|
128605ab33 | ||
|
|
4dbc908619 | ||
|
|
3a84775912 | ||
|
|
290a82e61c | ||
|
|
adac7d7fb2 | ||
|
|
679fd9d60f | ||
|
|
52320ebb91 | ||
|
|
b4aa17bfbe | ||
|
|
3f8b7ec327 | ||
|
|
eab1f503a5 | ||
|
|
2bcc90a9eb | ||
|
|
a9d0aa136d | ||
|
|
bc7f2677b1 | ||
|
|
691ab3080d | ||
|
|
c5147e0c68 | ||
|
|
e9b0829860 | ||
|
|
95428ab8b0 | ||
|
|
8a595ed77a | ||
|
|
f0ce003765 | ||
|
|
775d56fe52 | ||
|
|
ac2ff29288 | ||
|
|
81cfa5891c | ||
|
|
8e1975efe4 | ||
|
|
39c09733d3 | ||
|
|
da16dfd528 | ||
|
|
e1c397993d | ||
|
|
96cbdd5c37 | ||
|
|
58ca4baf71 | ||
|
|
5245b68689 | ||
|
|
2b6811cb1b | ||
|
|
f0a8aa06bc | ||
|
|
c82496b972 | ||
|
|
c31348ea9a | ||
|
|
75d903b164 | ||
|
|
c885540749 | ||
|
|
7dd40fd2d4 | ||
|
|
bb087caddc | ||
|
|
899b54c082 | ||
|
|
5e3f8b9c2e | ||
|
|
1ccfe928f7 | ||
|
|
3016269268 | ||
|
|
952cfae4e6 | ||
|
|
df7f209ebc | ||
|
|
b09eb63e1e | ||
|
|
d464721397 | ||
|
|
551a85c1e6 | ||
|
|
f3f4bd33e5 | ||
|
|
2686761608 | ||
|
|
02a2c3224f | ||
|
|
8ec1dda9ba | ||
|
|
0308532523 | ||
|
|
10b3af2947 | ||
|
|
1f34569ddc | ||
|
|
158bfe4816 | ||
|
|
fb0941bbe9 | ||
|
|
16eefe333f | ||
|
|
65e585e122 | ||
|
|
2ba36ee04c | ||
|
|
fc05529574 | ||
|
|
010770904f | ||
|
|
07d24f487a | ||
|
|
0960e4caa4 | ||
|
|
811acdb7f5 | ||
|
|
2fce5c4d5d | ||
|
|
a7266364d1 | ||
|
|
e83b41dc01 | ||
|
|
76665dd56e | ||
|
|
2d42b478ba | ||
|
|
5797e8aeec | ||
|
|
885fa6beae | ||
|
|
65cdd5fcbe | ||
|
|
73da4844ee | ||
|
|
4302e19aba | ||
|
|
0c6f05f34a | ||
|
|
385123fd40 | ||
|
|
8040c285cd | ||
|
|
bf1ee61bf0 | ||
|
|
ada454f56f | ||
|
|
57d2ef8740 | ||
|
|
a97e0982e3 | ||
|
|
eaa2060d84 | ||
|
|
3e02c574e7 | ||
|
|
e873d55cd3 | ||
|
|
c7230ccbb0 | ||
|
|
fc71ced660 | ||
|
|
6041a9d094 | ||
|
|
599cd766e1 | ||
|
|
84e02099a2 | ||
|
|
e64c8007c0 | ||
|
|
9339823e84 | ||
|
|
1ab52cfce3 | ||
|
|
e6d4620af1 | ||
|
|
656e6db846 | ||
|
|
9ed16478f4 | ||
|
|
30474e3a2b | ||
|
|
460bd98f1b | ||
|
|
a553eba574 | ||
|
|
61f515670c | ||
|
|
789af0f8a6 | ||
|
|
86fae58c96 | ||
|
|
03ae741505 | ||
|
|
c54d3abe47 | ||
|
|
906cd975d1 | ||
|
|
6fe511eb50 | ||
|
|
839f34ec4e | ||
|
|
17d18b22c7 | ||
|
|
66a04ed730 | ||
|
|
7e14a0bc30 | ||
|
|
4f2e06407b | ||
|
|
77de0f86dc | ||
|
|
f79c998f95 | ||
|
|
3b4bc9163a | ||
|
|
e146135bd1 | ||
|
|
e9e18afb4a | ||
|
|
62c31eb202 | ||
|
|
8016e84bc7 | ||
|
|
1eacee85ae | ||
|
|
5986145282 | ||
|
|
5756be7d36 | ||
|
|
b134a69a28 | ||
|
|
83f9eacac4 | ||
|
|
baab6b951b | ||
|
|
b041fbe3ec | ||
|
|
1b7b6d4b7e | ||
|
|
903a1bec91 | ||
|
|
15c7d9b0f1 | ||
|
|
ebc3a251b7 | ||
|
|
a567f4a6de | ||
|
|
4dc49604b6 | ||
|
|
c660daf4c2 | ||
|
|
fadcf62166 | ||
|
|
c02b255766 | ||
|
|
89788fa665 | ||
|
|
a052e2ee8f | ||
|
|
158e5004b7 | ||
|
|
6f9ff3be37 | ||
|
|
f766c57743 | ||
|
|
8f53152c3e | ||
|
|
7bbc88d716 | ||
|
|
68256d2343 | ||
|
|
1a0c70acc2 | ||
|
|
f88c440c48 | ||
|
|
cfa56ba6d4 | ||
|
|
37e6297fb2 | ||
|
|
2c6b646d87 | ||
|
|
e7572094b5 | ||
|
|
ddd5e0e889 | ||
|
|
5e34823a49 | ||
|
|
fdef21a871 | ||
|
|
d40cff8a99 | ||
|
|
8927e45ded | ||
|
|
a602f73ed0 | ||
|
|
1a93df92d4 | ||
|
|
12fe28d8ab | ||
|
|
06754d6158 | ||
|
|
8f9ec07ac3 | ||
|
|
1d5087374e | ||
|
|
8356264fe0 | ||
|
|
18ecd748dd | ||
|
|
6061d0603a | ||
|
|
e324f27655 | ||
|
|
afd645fb11 | ||
|
|
e48d37ccab | ||
|
|
5b42593fbb | ||
|
|
25f3c6a548 | ||
|
|
5c04f2634a | ||
|
|
ad01610d1e | ||
|
|
697c380cd1 | ||
|
|
ce89bf3c77 | ||
|
|
81e8143a43 | ||
|
|
39c4117409 | ||
|
|
8409986ef5 | ||
|
|
2b50367d7f | ||
|
|
cd4b85b0b5 | ||
|
|
dcec02412d | ||
|
|
1dcc491fec | ||
|
|
c6419a9c61 | ||
|
|
d715f86713 | ||
|
|
5221828705 | ||
|
|
6aa4acd3db | ||
|
|
15936c29c1 | ||
|
|
41689a2d82 | ||
|
|
1f71d19004 | ||
|
|
8b70c69a0d | ||
|
|
dfe80a30dc | ||
|
|
82a7f2dc2d | ||
|
|
ce026e7ad0 | ||
|
|
58e3122dab | ||
|
|
3b55b2d1b2 | ||
|
|
e3a7164fe1 | ||
|
|
2326e61de5 | ||
|
|
32b18c9509 | ||
|
|
acf52ad8a3 | ||
|
|
08dd493f98 | ||
|
|
4188cc2949 | ||
|
|
07bbe7dfb2 | ||
|
|
0aa1b80e3e | ||
|
|
5ac390281e | ||
|
|
ac465c5a18 | ||
|
|
745cd9f501 | ||
|
|
1ef3546822 | ||
|
|
b9df028bfb | ||
|
|
e74ad2b555 | ||
|
|
71c0056c66 | ||
|
|
56b27ea785 | ||
|
|
7e8974683c | ||
|
|
2d75f59d33 | ||
|
|
a709a3bcc0 | ||
|
|
34bf5112e0 | ||
|
|
bfe18d1d28 | ||
|
|
6a76afc20d | ||
|
|
9c469c2f96 | ||
|
|
e68a1dee33 | ||
|
|
2ab42f3dd6 | ||
|
|
af34b43a8d | ||
|
|
0f71cc7864 | ||
|
|
115ca7b789 | ||
|
|
df90de3a5f | ||
|
|
42ea7a9814 | ||
|
|
809750b995 | ||
|
|
c683cbdb2d | ||
|
|
d243b73678 | ||
|
|
b9abd2f9a5 | ||
|
|
dc28d063aa | ||
|
|
34cb661c35 | ||
|
|
bb6045c1d0 | ||
|
|
f1a74b5939 | ||
|
|
6fb9038af1 | ||
|
|
27f721a1f5 | ||
|
|
5717dc294e | ||
|
|
ae0f8df11b | ||
|
|
20bbdf22c7 | ||
|
|
77460ba502 | ||
|
|
72c6a4289e | ||
|
|
f8235da4d8 | ||
|
|
29973d7a72 | ||
|
|
8d3316ccd6 | ||
|
|
2c29f831e8 | ||
|
|
9457325119 | ||
|
|
189f8afcc9 | ||
|
|
f3a34990ab | ||
|
|
9699b67510 | ||
|
|
d60633a46c | ||
|
|
ae2df33ce6 | ||
|
|
0fcf28e7e7 | ||
|
|
5fad8f74b1 | ||
|
|
e35e34d2e0 | ||
|
|
8943144b2b | ||
|
|
aa6acc6673 | ||
|
|
31a215d4d6 | ||
|
|
9be6af8481 | ||
|
|
30948de13d | ||
|
|
b97158bf7b | ||
|
|
b14a8ff2fd | ||
|
|
6646be8d94 | ||
|
|
647a2aea64 | ||
|
|
035693aa30 | ||
|
|
58e58ce51c | ||
|
|
7aff72fc7c | ||
|
|
4b7e48d3ec | ||
|
|
91086d004c | ||
|
|
0e074d3a5a | ||
|
|
5d86b8893c | ||
|
|
ea00224e7f | ||
|
|
38293efe75 | ||
|
|
3286c5e174 | ||
|
|
e0f8a28f38 | ||
|
|
bf1b00b163 | ||
|
|
2678b9003a | ||
|
|
4fa3a136ad | ||
|
|
178a6bd66e | ||
|
|
e7d3ee3bc8 | ||
|
|
57e1339230 | ||
|
|
97c9a9b0b0 | ||
|
|
4144423227 | ||
|
|
4d03514142 | ||
|
|
0bc5566976 | ||
|
|
99550b79ab | ||
|
|
925567add5 | ||
|
|
ad97731d46 | ||
|
|
40684a93a2 | ||
|
|
f3b644ceb8 | ||
|
|
901ba6e87f | ||
|
|
6e9b70a8d6 | ||
|
|
76389bd584 | ||
|
|
7bf8650d59 | ||
|
|
7cb24dd96d | ||
|
|
dee040318a | ||
|
|
b93c5a9daa | ||
|
|
baf563e46d | ||
|
|
42910ebe14 | ||
|
|
e10246f10b | ||
|
|
f524219d68 | ||
|
|
a9dfd88671 | ||
|
|
61b338f464 | ||
|
|
beac73b6c8 | ||
|
|
f9f93c77e2 | ||
|
|
6fc1b4c6c0 | ||
|
|
639898ec07 | ||
|
|
3d3790fdbc | ||
|
|
69c8968cb0 | ||
|
|
229caea5fd | ||
|
|
aa25fc7901 | ||
|
|
4aba493ee2 | ||
|
|
dd7a080e2d | ||
|
|
b9cfac5934 | ||
|
|
f8e97fcc32 | ||
|
|
d2a26d6d48 | ||
|
|
7f8f2641f6 | ||
|
|
022dbb606f | ||
|
|
fea72b190d | ||
|
|
bce4351b4f | ||
|
|
eea073f882 | ||
|
|
0352d413e3 | ||
|
|
56bf98d68a | ||
|
|
fc1d88f5bb | ||
|
|
2b109db1b1 | ||
|
|
acbe410e0b | ||
|
|
89bafea61f | ||
|
|
07b7e05caa | ||
|
|
ec072fd639 | ||
|
|
b874c1a8a8 | ||
|
|
67b03076d7 | ||
|
|
33a0c4a9ae | ||
|
|
e25914c3d3 | ||
|
|
4cc5999f05 | ||
|
|
403aa5fd3e | ||
|
|
08f203c0c2 | ||
|
|
0fbf31ec98 | ||
|
|
db9b7ca447 | ||
|
|
fc9fa2614d | ||
|
|
eecc71c77f | ||
|
|
d5f69aec10 | ||
|
|
5f898a9071 | ||
|
|
a08f21336a | ||
|
|
215125de26 | ||
|
|
dfa94eac41 | ||
|
|
5de72a74f5 | ||
|
|
a44f0a719e | ||
|
|
247d869a5c | ||
|
|
b9b6e1818f | ||
|
|
a7968ccc3c | ||
|
|
b7e0e5a356 | ||
|
|
e3aecb2aa9 | ||
|
|
2b879ab471 | ||
|
|
116ab83b95 | ||
|
|
f24ae0fc2c | ||
|
|
c5d0d4acd0 | ||
|
|
062bf737c2 | ||
|
|
06190286ec | ||
|
|
3bca856e87 | ||
|
|
b4437d9cfd | ||
|
|
b3a778b89a | ||
|
|
dcb11f7606 | ||
|
|
933ce42d5a | ||
|
|
6c1ba957bd | ||
|
|
e021286eee | ||
|
|
0ee7a66033 | ||
|
|
42c797ee97 | ||
|
|
c03699fc5b | ||
|
|
b585c6676d | ||
|
|
f5ddb0f1f8 | ||
|
|
ef97f9e738 | ||
|
|
26a03b55de | ||
|
|
e9b0100b06 | ||
|
|
b93a16abae | ||
|
|
ff2783d434 | ||
|
|
664df55525 | ||
|
|
66f93148a7 | ||
|
|
25a5a8db68 | ||
|
|
a68d91b54c | ||
|
|
e48afc9fdf | ||
|
|
acdefb394c | ||
|
|
88ec3a5751 | ||
|
|
e3af0a13da | ||
|
|
734dc2e37a | ||
|
|
06b97b91e0 | ||
|
|
6aecd8d44a | ||
|
|
8a4ac3ea10 | ||
|
|
0ca123f4bc | ||
|
|
bbf5e1fa78 | ||
|
|
7454e55693 | ||
|
|
c745fe7778 | ||
|
|
6adcebb247 | ||
|
|
4c1844e46a | ||
|
|
38f9b8f3dd | ||
|
|
c2e72c7641 | ||
|
|
7faf143a97 | ||
|
|
e53a7f65b8 | ||
|
|
21eb9df85f | ||
|
|
17cfd4924c | ||
|
|
a0bebca1e6 | ||
|
|
75cbfa2898 | ||
|
|
58a8ed1588 | ||
|
|
e032f8df59 | ||
|
|
b16e37d80a | ||
|
|
c17cc07b73 | ||
|
|
6f767a72a7 | ||
|
|
11196436e9 | ||
|
|
9afc433db4 | ||
|
|
f701e3d834 | ||
|
|
4c623ca3c5 | ||
|
|
0671f05c05 | ||
|
|
0713da7393 | ||
|
|
d980b8cfdc | ||
|
|
05955d0483 | ||
|
|
28c765bd9a | ||
|
|
8287317be7 | ||
|
|
08cc67d7c5 | ||
|
|
ec858b2d4c | ||
|
|
47ece983d2 | ||
|
|
52310b5dd9 | ||
|
|
c2a4a2778e | ||
|
|
1db5977e80 | ||
|
|
1b5dc0e434 | ||
|
|
251364d8be | ||
|
|
f07f02d866 | ||
|
|
1388103919 | ||
|
|
fe1054d58a | ||
|
|
ba6ac778fc | ||
|
|
228ffcbfce | ||
|
|
f18ab9e5cc | ||
|
|
06c82ee87d | ||
|
|
9ba5ee8043 | ||
|
|
eede4e50ca | ||
|
|
1e7b6f78ca | ||
|
|
aa10f63d9f | ||
|
|
5b984d924f | ||
|
|
a25af36e32 | ||
|
|
eb8b5e5c01 | ||
|
|
817f057927 | ||
|
|
a13c44d81a | ||
|
|
45ad8f8ffb | ||
|
|
7da9e88c0b | ||
|
|
674e63cd3e | ||
|
|
1c73c91133 | ||
|
|
b1d11d4f69 | ||
|
|
001a9310c3 | ||
|
|
4cea9023f2 | ||
|
|
8ffadfa1f1 | ||
|
|
39e0b8e40e | ||
|
|
50d7929e76 | ||
|
|
10840b22c5 | ||
|
|
acfbab5915 | ||
|
|
9b00f65798 | ||
|
|
48c324fe86 | ||
|
|
0c495e7e72 | ||
|
|
50ccfee201 | ||
|
|
f39a5f6e68 | ||
|
|
f1f2efc4f8 | ||
|
|
fa739c9594 | ||
|
|
95f0ce1816 | ||
|
|
3432529f9c | ||
|
|
a32c8fabed | ||
|
|
6a11501184 | ||
|
|
7ca228d466 | ||
|
|
09a00f7d42 | ||
|
|
eac4cd15e3 | ||
|
|
c03fd4645d | ||
|
|
911ddbe1c8 | ||
|
|
b76c7a4824 | ||
|
|
1408ac77f9 | ||
|
|
c641abc7cd | ||
|
|
7e97d10ae1 | ||
|
|
56b134facd | ||
|
|
41286650cc | ||
|
|
2aca007a59 | ||
|
|
39f5c00f7e | ||
|
|
7eab431e5d | ||
|
|
678b15e759 | ||
|
|
3c8e7c6c10 | ||
|
|
bb7433ab40 | ||
|
|
4f64513c83 | ||
|
|
4ee47b6b1b | ||
|
|
350f5ccb3c | ||
|
|
0d3f94860b | ||
|
|
3d7489563d | ||
|
|
ac1153c7cf | ||
|
|
d4c4220299 | ||
|
|
3b1672a4a7 | ||
|
|
009f81a9a6 | ||
|
|
69285f28ad | ||
|
|
3593df0e73 | ||
|
|
8b10b1dc62 | ||
|
|
0db2a81e4e | ||
|
|
9fe64008c2 | ||
|
|
077c259d5b | ||
|
|
879bd62468 | ||
|
|
e5746c19cf | ||
|
|
29814d7458 | ||
|
|
e399f9e851 | ||
|
|
e4e20c2d25 | ||
|
|
08a70664f4 | ||
|
|
9d77f8dcd2 | ||
|
|
1d76c644cb | ||
|
|
75fc35ee4b | ||
|
|
ba3102d667 | ||
|
|
a94fc22659 | ||
|
|
8a9fed64f2 | ||
|
|
4468b5a2e4 | ||
|
|
71c7e30548 | ||
|
|
940bd55079 | ||
|
|
f2aa40c772 | ||
|
|
886db1a2c3 | ||
|
|
c8979940a8 | ||
|
|
b4c06db031 | ||
|
|
82486a873a | ||
|
|
99760ba6a0 | ||
|
|
2b987e6e93 | ||
|
|
523134f24b | ||
|
|
99c228dd6d | ||
|
|
10f80f9a4f | ||
|
|
44d38a00d4 | ||
|
|
c623f70caa | ||
|
|
62a10047ca | ||
|
|
f16b783378 | ||
|
|
95c55a8ab3 | ||
|
|
a8c932ffe4 | ||
|
|
be4b75932b | ||
|
|
04d7f3a5dc | ||
|
|
eddfacd0f4 | ||
|
|
cba3c20d5c | ||
|
|
507783ed8d | ||
|
|
06c98ed229 | ||
|
|
69742839c0 | ||
|
|
bb52049bf4 | ||
|
|
20b5f0e681 | ||
|
|
3803b8f284 | ||
|
|
e3daf743f2 | ||
|
|
ae5da74bb1 | ||
|
|
05f37ec2bc | ||
|
|
cf78047830 | ||
|
|
284428a45a | ||
|
|
79f2efdfd9 | ||
|
|
07822b371f | ||
|
|
7a3a385569 | ||
|
|
0f8cb7ea6d | ||
|
|
fa7ae376e0 | ||
|
|
8c223056fe | ||
|
|
e57f78cf90 | ||
|
|
ebd79d388b | ||
|
|
10724fee04 | ||
|
|
0d46f85ead | ||
|
|
4727ba3b51 | ||
|
|
eb15a3d849 | ||
|
|
4d09bae09b | ||
|
|
56ad177d4a | ||
|
|
11902dec3c | ||
|
|
4fdbe9b9ff | ||
|
|
18037bb484 | ||
|
|
a6e7e30177 | ||
|
|
288285f22b | ||
|
|
5b69ff7e94 | ||
|
|
fa2d838e60 | ||
|
|
bc0c6be4c5 | ||
|
|
ef767728ac | ||
|
|
f56d219882 | ||
|
|
32afccb16d | ||
|
|
b4f528039f | ||
|
|
d6b470ffbe | ||
|
|
3c6b418ca8 | ||
|
|
105a38a7db | ||
|
|
e6f25b9c5e | ||
|
|
6e0b956e61 | ||
|
|
86af14ad71 | ||
|
|
a029768d24 | ||
|
|
e0b85131bd | ||
|
|
4d9c9206cf | ||
|
|
92ca8862e1 | ||
|
|
b77bc28a79 | ||
|
|
b5eca401c6 | ||
|
|
a88e6ec534 | ||
|
|
35116f9229 | ||
|
|
fe66a59618 | ||
|
|
a1f90cd39b | ||
|
|
743975db52 | ||
|
|
45dc7459b8 | ||
|
|
ff3056332e | ||
|
|
6877b663f1 | ||
|
|
3c69f81a10 | ||
|
|
d316d57e5d | ||
|
|
92eddf8eb6 | ||
|
|
109a5b4748 | ||
|
|
0c81556b90 | ||
|
|
7e4b572699 | ||
|
|
d72828b3f4 | ||
|
|
9220f4d829 | ||
|
|
66917cd2c0 | ||
|
|
d0d375aeb7 | ||
|
|
b41dde777e | ||
|
|
d3dcd39f61 | ||
|
|
4121cae1d6 | ||
|
|
341138b731 | ||
|
|
527c3996ae | ||
|
|
305b4148bd | ||
|
|
07c22ed5bc | ||
|
|
c2fba6b1cf | ||
|
|
da87059041 | ||
|
|
0e300df68c | ||
|
|
6a9c4cf8cc | ||
|
|
1bebd5488a | ||
|
|
bb354e4941 | ||
|
|
0b5053d49a | ||
|
|
b3a1e6d0a5 | ||
|
|
457459671c | ||
|
|
20f09e6c6e | ||
|
|
944e1d9698 | ||
|
|
e8f795763b | ||
|
|
1d11683ce8 | ||
|
|
cca4258887 | ||
|
|
9999321400 | ||
|
|
3de3fc00ce | ||
|
|
2debb32098 | ||
|
|
e49d7b4ec2 | ||
|
|
c009a56825 | ||
|
|
c75dada340 | ||
|
|
890b0ac301 | ||
|
|
77142eb2df | ||
|
|
1185fad75c | ||
|
|
7463652345 | ||
|
|
d63309ae51 | ||
|
|
40bac000ab | ||
|
|
dac9cee7ca | ||
|
|
ef4c0c529a | ||
|
|
9a4986f189 | ||
|
|
afa674e2ea | ||
|
|
8277c74c5b | ||
|
|
b3c615576e | ||
|
|
6610a29422 | ||
|
|
67b0f14be6 | ||
|
|
15bc5b5b5d | ||
|
|
57d594acb0 | ||
|
|
e4aa761098 | ||
|
|
40aa685aba | ||
|
|
ed766484d2 | ||
|
|
a5f844bc7d | ||
|
|
97bd9b6381 | ||
|
|
ca8f2f5734 | ||
|
|
bcaaf2bb13 | ||
|
|
23128b2e53 | ||
|
|
5fc0e4a071 | ||
|
|
d7d143b97f | ||
|
|
4fe25da30b | ||
|
|
74c8a199f7 | ||
|
|
5f4a2ec4e7 | ||
|
|
56f451b30f | ||
|
|
1683c8e963 | ||
|
|
3091a93a52 | ||
|
|
f18d310348 | ||
|
|
228bdf74a4 | ||
|
|
51a1977243 | ||
|
|
e201344ccd | ||
|
|
f630ccb134 | ||
|
|
3a100702e8 | ||
|
|
fa0745e226 | ||
|
|
e5a89946f6 | ||
|
|
276ba2cd8e | ||
|
|
05a55f4a43 | ||
|
|
37a4e8a4aa | ||
|
|
3776292278 | ||
|
|
a9bba9be2b | ||
|
|
92ee0a5227 | ||
|
|
052fd35c72 | ||
|
|
5e3357d062 | ||
|
|
9e2a55edc2 | ||
|
|
29d8efd26e | ||
|
|
4b8608906a | ||
|
|
dbfe4fd5ac | ||
|
|
266f1c3a49 | ||
|
|
6ce686ab9c | ||
|
|
ea38645493 | ||
|
|
a929aaca04 | ||
|
|
5090c07846 | ||
|
|
8eb6fc8343 | ||
|
|
7da7bec241 | ||
|
|
a69789d572 | ||
|
|
172bc81dd2 | ||
|
|
65234a5a9a | ||
|
|
495481725a | ||
|
|
88353e8748 | ||
|
|
939c46fafd | ||
|
|
3145be12c0 | ||
|
|
4cfb599165 | ||
|
|
30e834744d | ||
|
|
c95a0c86b3 | ||
|
|
2daabd000f | ||
|
|
b97fd17146 | ||
|
|
5987330e59 | ||
|
|
5903475f4a | ||
|
|
0c3dc3dea2 | ||
|
|
c2f2146872 | ||
|
|
05087a833c | ||
|
|
c6eb306691 | ||
|
|
a3545ce551 | ||
|
|
5a6aef7a1d | ||
|
|
585bbf3987 | ||
|
|
df9bd21f55 | ||
|
|
feb7cd7bbf | ||
|
|
79c05c37dd | ||
|
|
1dd3766c5f | ||
|
|
54a41b4f0a | ||
|
|
dc691daf54 | ||
|
|
768b52e517 | ||
|
|
bb0d26e116 | ||
|
|
deef167cb2 | ||
|
|
c5767a280e | ||
|
|
ce5f73a5a6 | ||
|
|
fedfc66403 | ||
|
|
27042dae4d | ||
|
|
0877c4cb29 | ||
|
|
098ea0f405 | ||
|
|
de86376724 | ||
|
|
7fc4d8a172 | ||
|
|
ab276a6a61 | ||
|
|
7e026a7072 | ||
|
|
8513b6fbd5 | ||
|
|
ee6785eff9 | ||
|
|
a341a20e2c | ||
|
|
fefadaebfa | ||
|
|
777d77db53 | ||
|
|
8d72515a30 | ||
|
|
7a262eac12 | ||
|
|
1cd0abf415 | ||
|
|
10575d80ad | ||
|
|
15cf010e44 | ||
|
|
e26e383bd7 | ||
|
|
6652f96349 | ||
|
|
3b2eacfc8e | ||
|
|
3a4cdfd7ca | ||
|
|
2a665f9d92 | ||
|
|
cdea1d5545 | ||
|
|
a577c2d859 | ||
|
|
8f74e83d83 | ||
|
|
4e9bcd0d1f | ||
|
|
bc29495dd0 | ||
|
|
5df598fd56 | ||
|
|
8ddc688c13 | ||
|
|
49dad3099a | ||
|
|
90ecc5c13b | ||
|
|
9b31e4f385 | ||
|
|
9923a07c25 | ||
|
|
35d6bfb3a8 | ||
|
|
aa4a567160 | ||
|
|
dc3ca6db4f | ||
|
|
86389a5fb3 | ||
|
|
d7140eac35 | ||
|
|
d2b81cd359 | ||
|
|
18bab4aa7d | ||
|
|
c0bbebd532 | ||
|
|
acbc2fd490 | ||
|
|
d765d11c3e | ||
|
|
79ab85c632 | ||
|
|
dea68f3cf5 | ||
|
|
50ea267664 | ||
|
|
789f11a5c4 | ||
|
|
322548f078 | ||
|
|
8ea805e4e3 | ||
|
|
3d7e4766f7 | ||
|
|
5c2166a019 | ||
|
|
f5aec1c894 | ||
|
|
b8fb8a52f1 | ||
|
|
9d32300612 | ||
|
|
51c3257df9 | ||
|
|
35180b41bc | ||
|
|
3200ba0ed0 | ||
|
|
8e742ceb91 | ||
|
|
02deea85e6 | ||
|
|
13ea678e5e | ||
|
|
2e98c16f05 | ||
|
|
ca6aa5213c | ||
|
|
54f25358eb | ||
|
|
6fce69bbac | ||
|
|
5ceb18414a | ||
|
|
716150cfd2 | ||
|
|
e10baa915d | ||
|
|
ef6ff68b0b | ||
|
|
b87a6a9fec | ||
|
|
2efe8ae0cf | ||
|
|
8b2f46f0ba | ||
|
|
3c41d4b3a4 | ||
|
|
189d95f4fa | ||
|
|
28939e7405 | ||
|
|
84be8cc045 | ||
|
|
2a7e2be675 | ||
|
|
a1057ef764 | ||
|
|
f79a0a8603 | ||
|
|
6b84f54611 | ||
|
|
1bd66297e2 | ||
|
|
ea08f39f6e | ||
|
|
818c5a9cf5 | ||
|
|
074fd14849 | ||
|
|
a783859ab2 | ||
|
|
4b84a5ec15 | ||
|
|
905db1e8ff | ||
|
|
934d7e0f02 | ||
|
|
3b9a76c1f2 | ||
|
|
dabd294eaf | ||
|
|
3d2f8a7ddb | ||
|
|
507d457900 | ||
|
|
875be1f028 | ||
|
|
71267ec333 | ||
|
|
3aed470a87 | ||
|
|
356b0ae659 | ||
|
|
d8e142d454 | ||
|
|
dd6c5292fd | ||
|
|
571c7a8069 | ||
|
|
5dbde869df | ||
|
|
432943d6ee | ||
|
|
014a821d05 | ||
|
|
d8d98e289a | ||
|
|
53a65ea810 | ||
|
|
fc0ac45f21 | ||
|
|
c2d8c9a662 | ||
|
|
2ad2a95cc6 | ||
|
|
47a8329cd0 | ||
|
|
0087f328d6 | ||
|
|
9250d90e57 | ||
|
|
2dfe9eea94 | ||
|
|
5b28594189 | ||
|
|
33c88ec9e4 | ||
|
|
6d4bb5dcbc | ||
|
|
f2d0f1e51e | ||
|
|
b6d3e76568 | ||
|
|
1515793109 | ||
|
|
26bd15ec28 | ||
|
|
b78dd69fd6 | ||
|
|
7277e30443 | ||
|
|
683c37aca1 | ||
|
|
856e5eca4c | ||
|
|
3c9963065d | ||
|
|
ad9886284b | ||
|
|
7b18ce41a1 | ||
|
|
0fb46ed60b | ||
|
|
a9875dff13 | ||
|
|
9e6427d406 | ||
|
|
92322288ca | ||
|
|
19a4c7253a | ||
|
|
8eed2329bc | ||
|
|
e07c521288 | ||
|
|
917eaa50fb | ||
|
|
9b274cb243 | ||
|
|
93cbdadff3 | ||
|
|
9f5e213402 | ||
|
|
7b60ae26fc | ||
|
|
26b552a12e | ||
|
|
4e8ca603fe | ||
|
|
8cbeecd347 | ||
|
|
ef25503d58 | ||
|
|
5c4c00bd88 | ||
|
|
52d1a103aa | ||
|
|
48888be408 | ||
|
|
8b5106adc7 | ||
|
|
6a80ce878f | ||
|
|
13a75adba9 | ||
|
|
804d4320d7 | ||
|
|
76bf89dda3 | ||
|
|
950bc05d62 | ||
|
|
f9acd6a66b | ||
|
|
d3023a0068 | ||
|
|
d7df99e960 | ||
|
|
6b8478dbe9 | ||
|
|
a297d03db5 | ||
|
|
9d3d9d1390 | ||
|
|
761ac95085 | ||
|
|
d95ca5f9e4 | ||
|
|
c410348278 | ||
|
|
d8a758211f | ||
|
|
33e60f2527 | ||
|
|
fb705eee23 | ||
|
|
635e16e934 | ||
|
|
1deb3ae67f | ||
|
|
16ff40f420 | ||
|
|
a8b5cb23fe | ||
|
|
82f88d04ad | ||
|
|
0e1091e0ea | ||
|
|
7b48f10cc9 | ||
|
|
0be34b1135 | ||
|
|
fb5e0242d0 | ||
|
|
d376035557 | ||
|
|
747b2dc7c5 | ||
|
|
fac2f1bee3 | ||
|
|
a211f6aed9 | ||
|
|
0bc494245d | ||
|
|
b31a88043e | ||
|
|
85509c6d8b | ||
|
|
2dd131cf79 | ||
|
|
51990d0b33 | ||
|
|
00a49afc30 | ||
|
|
faa0630851 | ||
|
|
aac3fc68fc | ||
|
|
9c08b96b50 | ||
|
|
15ddde1df4 | ||
|
|
f2a9de3ba8 | ||
|
|
fd725206e2 | ||
|
|
6368b9383a | ||
|
|
2b8a11b001 | ||
|
|
c9e0339a30 | ||
|
|
8d11595290 | ||
|
|
2fe9f3effa | ||
|
|
ae122ff8a2 | ||
|
|
8b941ddd33 | ||
|
|
2902ea6689 | ||
|
|
5ccbf355bd | ||
|
|
62d8f635b0 | ||
|
|
4c3aae63d3 | ||
|
|
8f94aa0c0d | ||
|
|
0370306bb6 | ||
|
|
c9176efe6f | ||
|
|
a3fd08b7ef | ||
|
|
83a9e36ef1 | ||
|
|
9668decc84 | ||
|
|
328bb47d44 | ||
|
|
c74e0b9eab | ||
|
|
20dc32413f | ||
|
|
d4e618893c | ||
|
|
8d0d934249 | ||
|
|
ef2441ad82 | ||
|
|
bb7859138c | ||
|
|
ebc4570941 | ||
|
|
d04baed38b | ||
|
|
001c81f797 | ||
|
|
3327bf4788 | ||
|
|
e0b480e232 | ||
|
|
b328d85230 | ||
|
|
3afaa5c1e6 | ||
|
|
f2c8699823 | ||
|
|
4c11315a3c | ||
|
|
0e3a347d6b | ||
|
|
dc723fadaa | ||
|
|
6396958a31 | ||
|
|
1fe57e4841 | ||
|
|
3885ae6337 | ||
|
|
39e3366b3b | ||
|
|
a5ffd47935 | ||
|
|
18a96a445b | ||
|
|
de366b976e | ||
|
|
8f6aff3a7e | ||
|
|
fb411b20cc | ||
|
|
52ce7d6935 | ||
|
|
75ee398633 | ||
|
|
53bdd946d6 | ||
|
|
79f36664ef | ||
|
|
83078a48ab | ||
|
|
6f67a812dc | ||
|
|
6ad705b40e | ||
|
|
4cb43415a1 | ||
|
|
bbb09b558c | ||
|
|
6363be81e0 | ||
|
|
c6810d7460 | ||
|
|
4c1e7e7c2f | ||
|
|
931081752a | ||
|
|
424e98e67e | ||
|
|
7cfa93f5f8 | ||
|
|
fd8e8d1475 | ||
|
|
7ed5eb22ec | ||
|
|
fa1c275904 | ||
|
|
558c72bbb7 | ||
|
|
37976455bc | ||
|
|
db6ee8f687 | ||
|
|
53e7ffa311 | ||
|
|
e0f7ff325a | ||
|
|
1fcd1e33c5 | ||
|
|
843b16c1f0 | ||
|
|
a58f3f0fe3 | ||
|
|
e69e395b25 | ||
|
|
456164fc24 | ||
|
|
9b7f61a09d | ||
|
|
2de9adb7ae | ||
|
|
c7e0cc1158 | ||
|
|
e79861c30a | ||
|
|
407f596b61 | ||
|
|
41d30fc8dc | ||
|
|
2e429c5b35 | ||
|
|
28afe3470b | ||
|
|
f623b24196 | ||
|
|
2016c17894 | ||
|
|
2d28077010 | ||
|
|
7011f3c3c7 | ||
|
|
1403dbd5dd | ||
|
|
6ef3845a34 | ||
|
|
de2f0914f0 | ||
|
|
080ae0985f | ||
|
|
887f49760f | ||
|
|
be77f2291b | ||
|
|
b585b849a1 | ||
|
|
0c94e3d994 | ||
|
|
4a93658d0f | ||
|
|
6da19c6254 | ||
|
|
289058be81 | ||
|
|
864675ecde | ||
|
|
c79d7e3e30 | ||
|
|
549cc6f492 | ||
|
|
aa84ab66af | ||
|
|
1fef812c67 | ||
|
|
9170f74887 | ||
|
|
c9bd700d31 | ||
|
|
0928e4c035 | ||
|
|
75aec0a8d4 | ||
|
|
1f5ee36a6b | ||
|
|
45a0b74b89 | ||
|
|
f7132bdbbc | ||
|
|
d2dbff173a | ||
|
|
79f83babb3 | ||
|
|
715181f830 | ||
|
|
5c1c80dad3 | ||
|
|
32c0712715 | ||
|
|
fa4e492d1c | ||
|
|
4711fa2a9a | ||
|
|
08291502eb | ||
|
|
1f0acd0622 | ||
|
|
e88b732715 | ||
|
|
dafda85c36 | ||
|
|
8441fa9687 | ||
|
|
77800dfb01 | ||
|
|
41d15b738b | ||
|
|
aa2a5ff929 | ||
|
|
56d3c1e73f | ||
|
|
f401f9a674 | ||
|
|
30933d792b | ||
|
|
905717977e | ||
|
|
b802994809 | ||
|
|
ac96f906b3 | ||
|
|
cc395e914f | ||
|
|
f8063f8faf | ||
|
|
958ada9110 | ||
|
|
3452ea29b8 | ||
|
|
675b6f4b5e | ||
|
|
4fab67b0f5 | ||
|
|
8a2131416d | ||
|
|
2525304215 | ||
|
|
fdb35d4f77 | ||
|
|
222c7ea641 | ||
|
|
8f2722263f | ||
|
|
fd662e3401 | ||
|
|
bc65461ecb | ||
|
|
30017371df | ||
|
|
b944b86bf8 | ||
|
|
6ba8352fa6 | ||
|
|
aadf76692e | ||
|
|
c9a1386a55 | ||
|
|
4eb88a3e02 | ||
|
|
160c0bfe14 | ||
|
|
4cd098af1d | ||
|
|
362abd618f | ||
|
|
69de9f6684 | ||
|
|
2aa379bf21 | ||
|
|
820a2f1a63 | ||
|
|
6fac7614f5 | ||
|
|
b724ea8d3b | ||
|
|
06f80c320d | ||
|
|
d9de015eab | ||
|
|
dd36c861ba | ||
|
|
b34e46af93 | ||
|
|
405802c492 | ||
|
|
e15f3c9d2b | ||
|
|
885af064c9 | ||
|
|
40529dc41f | ||
|
|
2452f6ef8a | ||
|
|
4c543e6f06 | ||
|
|
bc5fd5d441 | ||
|
|
7a33cef27e | ||
|
|
b433b9c879 |
1396
.editorconfig
Normal file
1396
.editorconfig
Normal file
File diff suppressed because it is too large
Load Diff
4
.github/FUNDING.yml
vendored
Normal file
4
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Copyright 2021 Signal Messenger, LLC
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
custom: https://signal.org/donate/
|
||||||
0
.github/stale.yml
vendored
Normal file
0
.github/stale.yml
vendored
Normal file
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@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||||
|
- uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '21'
|
||||||
|
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
|
||||||
34
.github/workflows/integration-tests.yml
vendored
Normal file
34
.github/workflows/integration-tests.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: Integration Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '30 19 * * MON-FRI'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
if: ${{ vars.INTEGRATION_TESTS_BUCKET != '' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||||
|
- uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '21'
|
||||||
|
cache: 'maven'
|
||||||
|
- uses: aws-actions/configure-aws-credentials@v2
|
||||||
|
name: Configure AWS credentials from Test account
|
||||||
|
with:
|
||||||
|
role-to-assume: ${{ vars.AWS_ROLE }}
|
||||||
|
aws-region: ${{ vars.AWS_REGION }}
|
||||||
|
- name: Fetch integration utils library
|
||||||
|
run: |
|
||||||
|
mkdir -p integration-tests/.libs
|
||||||
|
mkdir -p integration-tests/src/main/resources
|
||||||
|
wget -O integration-tests/.libs/software.amazon.awssdk-sso.jar https://repo1.maven.org/maven2/software/amazon/awssdk/sso/2.19.8/sso-2.19.8.jar
|
||||||
|
aws s3 cp "s3://${{ vars.INTEGRATION_TESTS_BUCKET }}/config-latest.yml" integration-tests/src/main/resources/config.yml
|
||||||
|
- name: Run and verify integration tests
|
||||||
|
run: ./mvnw clean compile test-compile failsafe:integration-test failsafe:verify
|
||||||
26
.github/workflows/test.yml
vendored
Normal file
26
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: Service CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- gh-pages
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: ubuntu:22.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||||
|
- name: Set up JDK 21
|
||||||
|
uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3.11.0
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: 21
|
||||||
|
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: Build with Maven
|
||||||
|
run: ./mvnw -e -B verify
|
||||||
20
.gitignore
vendored
20
.gitignore
vendored
@@ -8,4 +8,24 @@ local.yml
|
|||||||
config/production.yml
|
config/production.yml
|
||||||
config/federated.yml
|
config/federated.yml
|
||||||
config/staging.yml
|
config/staging.yml
|
||||||
|
config/testing.yml
|
||||||
|
config/deploy.properties
|
||||||
|
/service/config/production.yml
|
||||||
|
/service/config/federated.yml
|
||||||
|
/service/config/staging.yml
|
||||||
|
/service/config/testing.yml
|
||||||
|
/service/config/deploy.properties
|
||||||
|
/service/dependency-reduced-pom.xml
|
||||||
|
.java-version
|
||||||
.opsmanage
|
.opsmanage
|
||||||
|
put.sh
|
||||||
|
deployer-staging.properties
|
||||||
|
deployer-production.properties
|
||||||
|
deployer.log
|
||||||
|
/service/src/main/resources/org/signal/badges/Badges_*.properties
|
||||||
|
!/service/src/main/resources/org/signal/badges/Badges_en.properties
|
||||||
|
/service/src/main/resources/org/signal/subscriptions/Subscriptions_*.properties
|
||||||
|
!/service/src/main/resources/org/signal/subscriptions/Subscriptions_en.properties
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.settings
|
||||||
|
|||||||
11
.gitmodules
vendored
Normal file
11
.gitmodules
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Note that the implementation of the spam filter is private; internal
|
||||||
|
# developers will need to override this URL with:
|
||||||
|
#
|
||||||
|
# ```
|
||||||
|
# git config submodule.spam-filter.url PRIVATE_URL
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# External developers may safely ignore this submodule.
|
||||||
|
[submodule "spam-filter"]
|
||||||
|
path = spam-filter
|
||||||
|
url = REDACTED
|
||||||
Binary file not shown.
Binary file not shown.
9
.mvn/extensions.xml
Normal file
9
.mvn/extensions.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/EXTENSIONS/1.0.0 http://maven.apache.org/xsd/core-extensions-1.0.0.xsd">
|
||||||
|
<extension>
|
||||||
|
<groupId>fr.brouillard.oss</groupId>
|
||||||
|
<artifactId>jgitver-maven-plugin</artifactId>
|
||||||
|
<version>1.9.0</version>
|
||||||
|
</extension>
|
||||||
|
</extensions>
|
||||||
14
.mvn/jgitver.config.xml
Normal file
14
.mvn/jgitver.config.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<configuration xmlns="http://jgitver.github.io/maven/configuration/1.1.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://jgitver.github.io/maven/configuration/1.1.0 https://jgitver.github.io/maven/configuration/jgitver-configuration-v1_1_0.xsd">
|
||||||
|
<useDirty>true</useDirty>
|
||||||
|
<useDefaultBranchingPolicy>false</useDefaultBranchingPolicy>
|
||||||
|
<branchPolicies>
|
||||||
|
<branchPolicy>
|
||||||
|
<pattern>(.*)</pattern>
|
||||||
|
<transformations>
|
||||||
|
<transformation>IGNORE</transformation>
|
||||||
|
</transformations>
|
||||||
|
</branchPolicy>
|
||||||
|
</branchPolicies>
|
||||||
|
</configuration>
|
||||||
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
Binary file not shown.
20
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
20
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
# or more contributor license agreements. See the NOTICE file
|
||||||
|
# distributed with this work for additional information
|
||||||
|
# regarding copyright ownership. The ASF licenses this file
|
||||||
|
# to you under the Apache License, Version 2.0 (the
|
||||||
|
# "License"); you may not use this file except in compliance
|
||||||
|
# with the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing,
|
||||||
|
# software distributed under the License is distributed on an
|
||||||
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
# 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.9.4/apache-maven-3.9.4-bin.zip
|
||||||
|
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
|
||||||
|
distributionSha256Sum=e896b60329a71b719d77bb4388b251a50aebcd73c62f69d510c858ce360afe0f
|
||||||
|
wrapperSha256Sum=e63a53cfb9c4d291ebe3c2b0edacb7622bbc480326beaa5a0456e412f52f066a
|
||||||
661
LICENSE
Normal file
661
LICENSE
Normal file
@@ -0,0 +1,661 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
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 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
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
2
Procfile
2
Procfile
@@ -1,2 +0,0 @@
|
|||||||
web: java $JAVA_OPTS -Ddw.http.port=$PORT -Ddw.http.adminPort=$PORT -Ddw.federation.name=$FEDERATION_NAME -Ddw.federation.herokuPeers="$FEDERATED_PEERS" -Ddw.twilio.accountId=$TWILIO_ACCOUNT_SID -Ddw.twilio.accountToken=$TWILIO_ACCOUNT_TOKEN -Ddw.twilio.number=$TWILIO_NUMBER -Ddw.nexmo.apiKey=$NEXMO_KEY -Ddw.nexmo.apiSecret=$NEXMO_SECRET -Ddw.nexmo.number=$NEXMO_NUMBER -Ddw.gcm.apiKey=$GCM_KEY -Ddw.apn.certificate="$APN_CERTIFICATE" -Ddw.apn.key="$APN_KEY" -Ddw.s3.accessKey=$AWS_ACCESS_KEY -Ddw.s3.accessSecret=$AWS_SECRET_KEY -Ddw.s3.attachmentsBucket=$AWS_ATTACHMENTS_BUCKET -Ddw.memcache.servers=$MEMCACHIER_SERVERS -Ddw.memcache.user=$MEMCACHIER_USERNAME -Ddw.memcache.password=$MEMCACHIER_PASSWORD -Ddw.redis.url=$REDIS_URL -Ddw.database.driverClass=org.postgresql.Driver -Ddw.database.user=`echo $DATABASE_URL | awk -F'://' {'print $2'} | awk -F':' {'print $1'}` -Ddw.database.password=`echo $DATABASE_URL | awk -F'://' {'print $2'} | awk -F':' {'print $2'} | awk -F'@' {'print $1'}` -Ddw.database.url=jdbc:postgresql://`echo $DATABASE_URL | awk -F'@' {'print $2'}` -jar target/TextSecure-MGCM-1.0-SNAPSHOT.jar server
|
|
||||||
dir: java $JAVA_OPTS -Ddw.http.port=$PORT -Ddw.http.adminPort=$PORT -Ddw.federation.name=$FEDERATION_NAME -Ddw.federation.herokuPeers="$FEDERATED_PEERS" -Ddw.twilio.accountId=$TWILIO_ACCOUNT_SID -Ddw.twilio.accountToken=$TWILIO_ACCOUNT_TOKEN -Ddw.twilio.number=$TWILIO_NUMBER -Ddw.nexmo.apiKey=$NEXMO_KEY -Ddw.nexmo.apiSecret=$NEXMO_SECRET -Ddw.nexmo.number=$NEXMO_NUMBER -Ddw.gcm.apiKey=$GCM_KEY -Ddw.apn.certificate="$APN_CERTIFICATE" -Ddw.apn.key="$APN_KEY" -Ddw.s3.accessKey=$AWS_ACCESS_KEY -Ddw.s3.accessSecret=$AWS_SECRET_KEY -Ddw.s3.attachmentsBucket=$AWS_ATTACHMENTS_BUCKET -Ddw.memcache.servers=$MEMCACHIER_SERVERS -Ddw.memcache.user=$MEMCACHIER_USERNAME -Ddw.memcache.password=$MEMCACHIER_PASSWORD -Ddw.redis.url=$REDIS_URL -Ddw.database.driverClass=org.postgresql.Driver -Ddw.database.user=`echo $DATABASE_URL | awk -F'://' {'print $2'} | awk -F':' {'print $1'}` -Ddw.database.password=`echo $DATABASE_URL | awk -F'://' {'print $2'} | awk -F':' {'print $2'} | awk -F'@' {'print $1'}` -Ddw.database.url=jdbc:postgresql://`echo $DATABASE_URL | awk -F'@' {'print $2'}` -jar target/TextSecure-MGCM-1.0-SNAPSHOT.jar directory
|
|
||||||
37
README.md
37
README.md
@@ -1,46 +1,19 @@
|
|||||||
TextSecure-Server
|
Signal-Server
|
||||||
=================
|
=================
|
||||||
|
|
||||||
The server that handles message routing for the
|
|
||||||
[TextSecure](https://github.com/whispersystems/TextSecure/) data channel. Communication
|
|
||||||
is handled by a REST API and Push messaging (both GCM and APN).
|
|
||||||
|
|
||||||
Documentation
|
Documentation
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
Looking for protocol documentation? Check out the wiki!
|
Looking for protocol documentation? Check out the website!
|
||||||
|
|
||||||
https://github.com/WhisperSystems/TextSecure-Server/wiki/API-Protocol
|
|
||||||
|
|
||||||
|
|
||||||
Bug tracker
|
|
||||||
-----------
|
|
||||||
|
|
||||||
Have a bug? Please create an issue here on GitHub!
|
|
||||||
|
|
||||||
https://github.com/WhisperSystems/TextSecure-Server/issues
|
|
||||||
|
|
||||||
|
|
||||||
Mailing list
|
|
||||||
------------
|
|
||||||
|
|
||||||
Have a question? Ask on our mailing list!
|
|
||||||
|
|
||||||
whispersystems@lists.riseup.net
|
|
||||||
|
|
||||||
https://lists.riseup.net/www/info/whispersystems
|
|
||||||
|
|
||||||
Current BitHub Payment Per Commit:
|
|
||||||
=================
|
|
||||||

|
|
||||||
|
|
||||||
|
https://signal.org/docs/
|
||||||
|
|
||||||
Cryptography Notice
|
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.
|
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.
|
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 <http://www.wassenaar.org/> for more information.
|
See <https://www.wassenaar.org/> for more information.
|
||||||
|
|
||||||
The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms.
|
The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms.
|
||||||
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.
|
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.
|
||||||
@@ -48,6 +21,6 @@ The form and manner of this distribution makes it eligible for export under the
|
|||||||
License
|
License
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
Copyright 2013 Open Whisper Systems
|
Copyright 2013-2023 Signal Messenger, LLC
|
||||||
|
|
||||||
Licensed under the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html
|
Licensed under the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html
|
||||||
|
|||||||
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</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 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 javax.ws.rs.Consumes;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(AuthenticatedAccount.class)) {
|
||||||
|
return AUTHENTICATED_ACCOUNT;
|
||||||
|
}
|
||||||
|
if (type instanceof SimpleType simpleType
|
||||||
|
&& isOptionalOfType(simpleType, AuthenticatedAccount.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 java.lang.annotation.Annotation;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.ws.rs.Consumes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,98 +0,0 @@
|
|||||||
twilio:
|
|
||||||
accountId:
|
|
||||||
accountToken:
|
|
||||||
number:
|
|
||||||
localDomain: # The domain Twilio can call back to.
|
|
||||||
international: # Boolean specifying Twilio for international delivery
|
|
||||||
|
|
||||||
# Optional. If specified, Nexmo will be used for non-US SMS and
|
|
||||||
# voice verification if twilio.international is false. Otherwise,
|
|
||||||
# Nexmo, if specified, Nexmo will only be used as a fallback
|
|
||||||
# for failed Twilio deliveries.
|
|
||||||
nexmo:
|
|
||||||
apiKey:
|
|
||||||
apiSecret:
|
|
||||||
number:
|
|
||||||
|
|
||||||
gcm:
|
|
||||||
apiKey:
|
|
||||||
|
|
||||||
# Optional. Only if iOS clients are supported.
|
|
||||||
apn:
|
|
||||||
# In PEM format.
|
|
||||||
certificate:
|
|
||||||
|
|
||||||
# In PEM format.
|
|
||||||
key:
|
|
||||||
|
|
||||||
s3:
|
|
||||||
accessKey:
|
|
||||||
accessSecret:
|
|
||||||
|
|
||||||
# Name of the S3 bucket (needs to have been created)
|
|
||||||
# for attachments to go. Should be configured with
|
|
||||||
# correct permissions.
|
|
||||||
attachmentsBucket:
|
|
||||||
|
|
||||||
memcache:
|
|
||||||
servers:
|
|
||||||
user:
|
|
||||||
password:
|
|
||||||
|
|
||||||
redis:
|
|
||||||
url:
|
|
||||||
|
|
||||||
federation:
|
|
||||||
name:
|
|
||||||
peers:
|
|
||||||
-
|
|
||||||
name: somepeer
|
|
||||||
url: https://foo.com
|
|
||||||
authenticationToken: foo
|
|
||||||
certificate: in pem format
|
|
||||||
|
|
||||||
# Optional address of graphite server to report metrics
|
|
||||||
graphite:
|
|
||||||
host:
|
|
||||||
port:
|
|
||||||
|
|
||||||
http:
|
|
||||||
shutdownGracePeriod: 0s
|
|
||||||
|
|
||||||
database:
|
|
||||||
# the name of your JDBC driver
|
|
||||||
driverClass: org.postgresql.Driver
|
|
||||||
|
|
||||||
# the username
|
|
||||||
user:
|
|
||||||
|
|
||||||
# the password
|
|
||||||
password:
|
|
||||||
|
|
||||||
# the JDBC URL
|
|
||||||
url: jdbc:postgresql://somehost:somport/somedb
|
|
||||||
|
|
||||||
# any properties specific to your JDBC driver:
|
|
||||||
properties:
|
|
||||||
charSet: UTF-8
|
|
||||||
|
|
||||||
# the maximum amount of time to wait on an empty pool before throwing an exception
|
|
||||||
maxWaitForConnection: 1s
|
|
||||||
|
|
||||||
# the SQL query to run when validating a connection's liveness
|
|
||||||
validationQuery: "/* MyService Health Check */ SELECT 1"
|
|
||||||
|
|
||||||
# the minimum number of connections to keep open
|
|
||||||
minSize: 8
|
|
||||||
|
|
||||||
# the maximum number of connections to keep open
|
|
||||||
maxSize: 32
|
|
||||||
|
|
||||||
# whether or not idle connections should be validated
|
|
||||||
checkConnectionWhileIdle: false
|
|
||||||
|
|
||||||
# how long a connection must be held before it can be validated
|
|
||||||
checkConnectionHealthWhenIdleFor: 10s
|
|
||||||
|
|
||||||
# the maximum lifetime of an idle connection
|
|
||||||
closeConnectionIfIdleFor: 1 minute
|
|
||||||
2
integration-tests/.gitignore
vendored
Normal file
2
integration-tests/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.libs
|
||||||
|
src/main/resources/config.yml
|
||||||
62
integration-tests/pom.xml
Normal file
62
integration-tests/pom.xml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?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>
|
||||||
|
<version>3.1.2</version>
|
||||||
|
<configuration>
|
||||||
|
<excludes>
|
||||||
|
<exclude>**</exclude>
|
||||||
|
</excludes>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-failsafe-plugin</artifactId>
|
||||||
|
<version>3.1.2</version>
|
||||||
|
<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>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* 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.Curve;
|
||||||
|
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(bytes -> Curve.decodePoint(bytes, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,69 @@
|
|||||||
|
/*
|
||||||
|
* 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.concurrent.CompletableFuture;
|
||||||
|
import org.signal.integration.config.Config;
|
||||||
|
import org.whispersystems.textsecuregcm.registration.VerificationSession;
|
||||||
|
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 org.whispersystems.textsecuregcm.util.DynamoDbFromConfig;
|
||||||
|
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
|
||||||
|
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
|
||||||
|
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||||
|
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
|
||||||
|
|
||||||
|
public class IntegrationTools {
|
||||||
|
|
||||||
|
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
|
||||||
|
|
||||||
|
private final VerificationSessionManager verificationSessionManager;
|
||||||
|
|
||||||
|
|
||||||
|
public static IntegrationTools create(final Config config) {
|
||||||
|
final AwsCredentialsProvider credentialsProvider = DefaultCredentialsProvider.builder().build();
|
||||||
|
|
||||||
|
final DynamoDbAsyncClient dynamoDbAsyncClient = DynamoDbFromConfig.asyncClient(
|
||||||
|
config.dynamoDbClientConfiguration(),
|
||||||
|
credentialsProvider);
|
||||||
|
|
||||||
|
final DynamoDbClient dynamoDbClient = DynamoDbFromConfig.client(
|
||||||
|
config.dynamoDbClientConfiguration(),
|
||||||
|
credentialsProvider);
|
||||||
|
|
||||||
|
final RegistrationRecoveryPasswords registrationRecoveryPasswords = new RegistrationRecoveryPasswords(
|
||||||
|
config.dynamoDbTables().registrationRecovery(), Duration.ofDays(1), dynamoDbClient, dynamoDbAsyncClient);
|
||||||
|
|
||||||
|
final VerificationSessions verificationSessions = new VerificationSessions(
|
||||||
|
dynamoDbAsyncClient, config.dynamoDbTables().verificationSessions(), Clock.systemUTC());
|
||||||
|
|
||||||
|
return new IntegrationTools(
|
||||||
|
new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords),
|
||||||
|
new VerificationSessionManager(verificationSessions)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntegrationTools(
|
||||||
|
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
|
||||||
|
final VerificationSessionManager verificationSessionManager) {
|
||||||
|
this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager;
|
||||||
|
this.verificationSessionManager = verificationSessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Void> populateRecoveryPassword(final String e164, final byte[] password) {
|
||||||
|
return registrationRecoveryPasswordsManager.storeForCurrentNumber(e164, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Optional<String>> peekVerificationSessionPushChallenge(final String sessionId) {
|
||||||
|
return verificationSessionManager.findForId(sessionId)
|
||||||
|
.thenApply(maybeSession -> maybeSession.map(VerificationSession::pushChallenge));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,326 @@
|
|||||||
|
/*
|
||||||
|
* 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 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.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.Curve;
|
||||||
|
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.configuration.CircuitBreakerConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
|
||||||
|
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.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 = randomBytes(32);
|
||||||
|
final String accountPassword = Base64.getEncoder().encodeToString(randomBytes(32));
|
||||||
|
|
||||||
|
final TestUser user = TestUser.create(number, accountPassword, registrationPassword);
|
||||||
|
final AccountAttributes accountAttributes = user.accountAttributes();
|
||||||
|
|
||||||
|
INTEGRATION_TOOLS.populateRecoveryPassword(number, registrationPassword).join();
|
||||||
|
|
||||||
|
final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
|
||||||
|
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
||||||
|
|
||||||
|
// register account
|
||||||
|
final RegistrationRequest registrationRequest = new RegistrationRequest(null,
|
||||||
|
registrationPassword,
|
||||||
|
accountAttributes,
|
||||||
|
true,
|
||||||
|
new IdentityKey(aciIdentityKeyPair.getPublicKey()),
|
||||||
|
new IdentityKey(pniIdentityKeyPair.getPublicKey()),
|
||||||
|
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 <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(
|
||||||
|
execute.getLeft() >= 200 && execute.getLeft() < 300,
|
||||||
|
"Unexpected response code: %d",
|
||||||
|
execute.getLeft());
|
||||||
|
return execute;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T executeExpectSuccess(final Class<T> expectedType) {
|
||||||
|
final Pair<Integer, T> execute = execute(expectedType);
|
||||||
|
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()
|
||||||
|
.withName("integration-test")
|
||||||
|
.withExecutor(Executors.newFixedThreadPool(16))
|
||||||
|
.withRetryExecutor(Executors.newSingleThreadScheduledExecutor())
|
||||||
|
.withCircuitBreaker(new CircuitBreakerConfiguration())
|
||||||
|
.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);
|
||||||
|
return SystemMapper.yamlMapper().readValue(Resources.toByteArray(configFileUrl), Config.class);
|
||||||
|
} catch (final IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ECSignedPreKey generateSignedECPreKey(long id, final ECKeyPair identityKeyPair) {
|
||||||
|
final ECPublicKey pubKey = Curve.generateKeyPair().getPublicKey();
|
||||||
|
final byte[] sig = identityKeyPair.getPrivateKey().calculateSignature(pubKey.serialize());
|
||||||
|
return new ECSignedPreKey(id, pubKey, sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static KEMSignedPreKey generateSignedKEMPreKey(long id, final ECKeyPair identityKeyPair) {
|
||||||
|
final KEMPublicKey pubKey = KEMKeyPair.generate(KEMKeyType.KYBER_1024).getPublicKey();
|
||||||
|
final byte[] sig = identityKeyPair.getPrivateKey().calculateSignature(pubKey.serialize());
|
||||||
|
return new KEMSignedPreKey(id, pubKey, sig);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* 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.InvalidKeyException;
|
||||||
|
import org.signal.libsignal.protocol.ecc.Curve;
|
||||||
|
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) {
|
||||||
|
try {
|
||||||
|
final int nextId = signedPreKeys.keySet().stream().mapToInt(k -> k + 1).max().orElse(0);
|
||||||
|
final ECKeyPair keyPair = Curve.generateKeyPair();
|
||||||
|
final byte[] signature = Curve.calculateSignature(identity.getPrivateKey(), keyPair.getPublicKey().serialize());
|
||||||
|
final SignedPreKeyRecord signedPreKeyRecord = new SignedPreKeyRecord(nextId, System.currentTimeMillis(), keyPair, signature);
|
||||||
|
signedPreKeys.put(nextId, Pair.of(identity, signedPreKeyRecord));
|
||||||
|
return signedPreKeyRecord;
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
/*
|
||||||
|
* 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.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import org.signal.libsignal.protocol.IdentityKey;
|
||||||
|
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||||
|
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, new Device.DeviceCapabilities(false, false, false, false))
|
||||||
|
.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);
|
||||||
|
return new PreKeySetPublicView(
|
||||||
|
Collections.emptyList(),
|
||||||
|
identity.getPublicKey(),
|
||||||
|
new SignedPreKeyPublicView(
|
||||||
|
signedPreKeyRecord.getId(),
|
||||||
|
signedPreKeyRecord.getKeyPair().getPublicKey(),
|
||||||
|
signedPreKeyRecord.getSignature()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,16 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.integration.config;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.DynamoDbClientConfiguration;
|
||||||
|
|
||||||
|
public record Config(String domain,
|
||||||
|
String rootCert,
|
||||||
|
DynamoDbClientConfiguration dynamoDbClientConfiguration,
|
||||||
|
DynamoDbTables dynamoDbTables,
|
||||||
|
String prescribedRegistrationNumber,
|
||||||
|
String prescribedRegistrationCode) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.integration.config;
|
||||||
|
|
||||||
|
public record DynamoDbTables(String registrationRecovery,
|
||||||
|
String verificationSessions) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* 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 java.util.Arrays;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
import org.apache.http.HttpStatus;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
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.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;
|
||||||
|
|
||||||
|
public class AccountTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateAccount() throws Exception {
|
||||||
|
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() throws Exception {
|
||||||
|
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 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,50 @@
|
|||||||
|
/*
|
||||||
|
* 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.Base64;
|
||||||
|
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 String contentBase64 = Base64.getEncoder().encodeToString(expectedContent);
|
||||||
|
final IncomingMessage message = new IncomingMessage(1, Device.PRIMARY_ID, userB.registrationId(), contentBase64);
|
||||||
|
final IncomingMessageList messages = new IncomingMessageList(List.of(message), false, true, System.currentTimeMillis());
|
||||||
|
|
||||||
|
final Pair<Integer, SendMessageResponse> sendMessage = 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().get(0).content();
|
||||||
|
assertArrayEquals(expectedContent, actualContent);
|
||||||
|
} finally {
|
||||||
|
Operations.deleteUser(userA);
|
||||||
|
Operations.deleteUser(userB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.integration;
|
||||||
|
|
||||||
|
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();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
308
mvnw
vendored
Executable file
308
mvnw
vendored
Executable file
@@ -0,0 +1,308 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
# or more contributor license agreements. See the NOTICE file
|
||||||
|
# distributed with this work for additional information
|
||||||
|
# regarding copyright ownership. The ASF licenses this file
|
||||||
|
# to you under the Apache License, Version 2.0 (the
|
||||||
|
# "License"); you may not use this file except in compliance
|
||||||
|
# with the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing,
|
||||||
|
# software distributed under the License is distributed on an
|
||||||
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
# KIND, either express or implied. See the License for the
|
||||||
|
# specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Apache Maven Wrapper startup batch script, version 3.2.0
|
||||||
|
#
|
||||||
|
# Required ENV vars:
|
||||||
|
# ------------------
|
||||||
|
# JAVA_HOME - location of a JDK home dir
|
||||||
|
#
|
||||||
|
# Optional ENV vars
|
||||||
|
# -----------------
|
||||||
|
# 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
|
||||||
|
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [ -z "$MAVEN_SKIP_RC" ] ; then
|
||||||
|
|
||||||
|
if [ -f /usr/local/etc/mavenrc ] ; then
|
||||||
|
. /usr/local/etc/mavenrc
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f /etc/mavenrc ] ; then
|
||||||
|
. /etc/mavenrc
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$HOME/.mavenrc" ] ; then
|
||||||
|
. "$HOME/.mavenrc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
# OS specific support. $var _must_ be set to either true or false.
|
||||||
|
cygwin=false;
|
||||||
|
darwin=false;
|
||||||
|
mingw=false
|
||||||
|
case "$(uname)" in
|
||||||
|
CYGWIN*) cygwin=true ;;
|
||||||
|
MINGW*) mingw=true;;
|
||||||
|
Darwin*) darwin=true
|
||||||
|
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
|
||||||
|
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
|
||||||
|
if [ -z "$JAVA_HOME" ]; then
|
||||||
|
if [ -x "/usr/libexec/java_home" ]; then
|
||||||
|
JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
|
||||||
|
else
|
||||||
|
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -z "$JAVA_HOME" ] ; then
|
||||||
|
if [ -r /etc/gentoo-release ] ; then
|
||||||
|
JAVA_HOME=$(java-config --jre-home)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin, ensure paths are in UNIX format before anything is touched
|
||||||
|
if $cygwin ; then
|
||||||
|
[ -n "$JAVA_HOME" ] &&
|
||||||
|
JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
|
||||||
|
[ -n "$CLASSPATH" ] &&
|
||||||
|
CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Mingw, ensure paths are in UNIX format before anything is touched
|
||||||
|
if $mingw ; then
|
||||||
|
[ -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
|
||||||
|
# readlink(1) is not available as standard on Solaris 10.
|
||||||
|
readLink=$(which readlink)
|
||||||
|
if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
|
||||||
|
if $darwin ; then
|
||||||
|
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||||
|
javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
|
||||||
|
else
|
||||||
|
javaExecutable="$(readlink -f "\"$javaExecutable\"")"
|
||||||
|
fi
|
||||||
|
javaHome="$(dirname "\"$javaExecutable\"")"
|
||||||
|
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
|
||||||
|
JAVA_HOME="$javaHome"
|
||||||
|
export JAVA_HOME
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$JAVACMD" ] ; then
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
echo "Error: JAVA_HOME is not defined correctly." >&2
|
||||||
|
echo " We cannot execute $JAVACMD" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$JAVA_HOME" ] ; then
|
||||||
|
echo "Warning: JAVA_HOME environment variable is not set."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
basedir="$1"
|
||||||
|
wdir="$1"
|
||||||
|
while [ "$wdir" != '/' ] ; do
|
||||||
|
if [ -d "$wdir"/.mvn ] ; then
|
||||||
|
basedir=$wdir
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
|
||||||
|
if [ -d "${wdir}" ]; then
|
||||||
|
wdir=$(cd "$wdir/.." || exit 1; pwd)
|
||||||
|
fi
|
||||||
|
# end of workaround
|
||||||
|
done
|
||||||
|
printf '%s' "$(cd "$basedir" || exit 1; pwd)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# concatenates all lines of a file
|
||||||
|
concat_lines() {
|
||||||
|
if [ -f "$1" ]; then
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
##########################################################################################
|
||||||
|
wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
|
||||||
|
if [ -r "$wrapperJarPath" ]; then
|
||||||
|
log "Found $wrapperJarPath"
|
||||||
|
else
|
||||||
|
log "Couldn't find $wrapperJarPath, downloading it ..."
|
||||||
|
|
||||||
|
if [ -n "$MVNW_REPOURL" ]; then
|
||||||
|
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
|
||||||
|
else
|
||||||
|
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 -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 < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
log "Downloading from: $wrapperUrl"
|
||||||
|
|
||||||
|
if $cygwin; then
|
||||||
|
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v wget > /dev/null; then
|
||||||
|
log "Found wget ... using wget"
|
||||||
|
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
|
||||||
|
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||||
|
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||||
|
else
|
||||||
|
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
|
||||||
|
fi
|
||||||
|
elif command -v curl > /dev/null; then
|
||||||
|
log "Found curl ... using curl"
|
||||||
|
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
|
||||||
|
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||||
|
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||||
|
else
|
||||||
|
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
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
|
||||||
|
javaSource=$(cygpath --path --windows "$javaSource")
|
||||||
|
javaClass=$(cygpath --path --windows "$javaClass")
|
||||||
|
fi
|
||||||
|
if [ -e "$javaSource" ]; then
|
||||||
|
if [ ! -e "$javaClass" ]; then
|
||||||
|
log " - Compiling MavenWrapperDownloader.java ..."
|
||||||
|
("$JAVA_HOME/bin/javac" "$javaSource")
|
||||||
|
fi
|
||||||
|
if [ -e "$javaClass" ]; then
|
||||||
|
log " - Running MavenWrapperDownloader.java ..."
|
||||||
|
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
##########################################################################################
|
||||||
|
# End of extension
|
||||||
|
##########################################################################################
|
||||||
|
|
||||||
|
# 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 "$JAVA_HOME" ] &&
|
||||||
|
JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
|
||||||
|
[ -n "$CLASSPATH" ] &&
|
||||||
|
CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
|
||||||
|
[ -n "$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 $*"
|
||||||
|
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.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
|
||||||
|
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
|
||||||
205
mvnw.cmd
vendored
Normal file
205
mvnw.cmd
vendored
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
@REM or more contributor license agreements. See the NOTICE file
|
||||||
|
@REM distributed with this work for additional information
|
||||||
|
@REM regarding copyright ownership. The ASF licenses this file
|
||||||
|
@REM to you under the Apache License, Version 2.0 (the
|
||||||
|
@REM "License"); you may not use this file except in compliance
|
||||||
|
@REM with the License. You may obtain a copy of the License at
|
||||||
|
@REM
|
||||||
|
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@REM
|
||||||
|
@REM Unless required by applicable law or agreed to in writing,
|
||||||
|
@REM software distributed under the License is distributed on an
|
||||||
|
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
@REM KIND, either express or implied. See the License for the
|
||||||
|
@REM specific language governing permissions and limitations
|
||||||
|
@REM under the License.
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
@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 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
|
||||||
|
@REM e.g. to debug Maven itself, use
|
||||||
|
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||||
|
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
|
||||||
|
@echo off
|
||||||
|
@REM set title of command window
|
||||||
|
title %0
|
||||||
|
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
|
||||||
|
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
|
||||||
|
|
||||||
|
@REM set %HOME% to equivalent of $HOME
|
||||||
|
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
|
||||||
|
|
||||||
|
@REM Execute a user defined script before this one
|
||||||
|
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
|
||||||
|
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
|
||||||
|
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
|
||||||
|
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
|
||||||
|
:skipRcPre
|
||||||
|
|
||||||
|
@setlocal
|
||||||
|
|
||||||
|
set ERROR_CODE=0
|
||||||
|
|
||||||
|
@REM To isolate internal variables from possible post scripts, we use another setlocal
|
||||||
|
@setlocal
|
||||||
|
|
||||||
|
@REM ==== START VALIDATION ====
|
||||||
|
if not "%JAVA_HOME%" == "" goto OkJHome
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Error: JAVA_HOME not found in your environment. >&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||||
|
echo location of your Java installation. >&2
|
||||||
|
echo.
|
||||||
|
goto error
|
||||||
|
|
||||||
|
:OkJHome
|
||||||
|
if exist "%JAVA_HOME%\bin\java.exe" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Error: JAVA_HOME is set to an invalid directory. >&2
|
||||||
|
echo JAVA_HOME = "%JAVA_HOME%" >&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||||
|
echo location of your Java installation. >&2
|
||||||
|
echo.
|
||||||
|
goto error
|
||||||
|
|
||||||
|
@REM ==== END VALIDATION ====
|
||||||
|
|
||||||
|
:init
|
||||||
|
|
||||||
|
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
|
||||||
|
@REM Fallback to current working directory if not found.
|
||||||
|
|
||||||
|
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
|
||||||
|
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
|
||||||
|
|
||||||
|
set EXEC_DIR=%CD%
|
||||||
|
set WDIR=%EXEC_DIR%
|
||||||
|
:findBaseDir
|
||||||
|
IF EXIST "%WDIR%"\.mvn goto baseDirFound
|
||||||
|
cd ..
|
||||||
|
IF "%WDIR%"=="%CD%" goto baseDirNotFound
|
||||||
|
set WDIR=%CD%
|
||||||
|
goto findBaseDir
|
||||||
|
|
||||||
|
:baseDirFound
|
||||||
|
set MAVEN_PROJECTBASEDIR=%WDIR%
|
||||||
|
cd "%EXEC_DIR%"
|
||||||
|
goto endDetectBaseDir
|
||||||
|
|
||||||
|
:baseDirNotFound
|
||||||
|
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
|
||||||
|
cd "%EXEC_DIR%"
|
||||||
|
|
||||||
|
:endDetectBaseDir
|
||||||
|
|
||||||
|
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
|
||||||
|
|
||||||
|
@setlocal EnableExtensions EnableDelayedExpansion
|
||||||
|
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
|
||||||
|
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
|
||||||
|
|
||||||
|
:endReadAdditionalConfig
|
||||||
|
|
||||||
|
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 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 WRAPPER_URL=%%B
|
||||||
|
)
|
||||||
|
|
||||||
|
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||||
|
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||||
|
if exist %WRAPPER_JAR% (
|
||||||
|
if "%MVNW_VERBOSE%" == "true" (
|
||||||
|
echo Found %WRAPPER_JAR%
|
||||||
|
)
|
||||||
|
) else (
|
||||||
|
if not "%MVNW_REPOURL%" == "" (
|
||||||
|
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: %WRAPPER_URL%
|
||||||
|
)
|
||||||
|
|
||||||
|
powershell -Command "&{"^
|
||||||
|
"$webclient = new-object System.Net.WebClient;"^
|
||||||
|
"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('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
|
||||||
|
"}"
|
||||||
|
if "%MVNW_VERBOSE%" == "true" (
|
||||||
|
echo Finished downloading %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=%*
|
||||||
|
|
||||||
|
%MAVEN_JAVA_EXE% ^
|
||||||
|
%JVM_CONFIG_MAVEN_PROPS% ^
|
||||||
|
%MAVEN_OPTS% ^
|
||||||
|
%MAVEN_DEBUG_OPTS% ^
|
||||||
|
-classpath %WRAPPER_JAR% ^
|
||||||
|
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
|
||||||
|
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
|
||||||
|
if ERRORLEVEL 1 goto error
|
||||||
|
goto end
|
||||||
|
|
||||||
|
:error
|
||||||
|
set ERROR_CODE=1
|
||||||
|
|
||||||
|
:end
|
||||||
|
@endlocal & set ERROR_CODE=%ERROR_CODE%
|
||||||
|
|
||||||
|
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
|
||||||
|
@REM check for post script, once with legacy .bat ending and once with .cmd ending
|
||||||
|
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
|
||||||
|
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
|
||||||
|
:skipRcPost
|
||||||
|
|
||||||
|
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
|
||||||
|
if "%MAVEN_BATCH_PAUSE%"=="on" pause
|
||||||
|
|
||||||
|
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
|
||||||
|
|
||||||
|
cmd /C exit /B %ERROR_CODE%
|
||||||
729
pom.xml
729
pom.xml
@@ -1,224 +1,529 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
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">
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<prerequisites>
|
<packaging>pom</packaging>
|
||||||
<maven>3.0.0</maven>
|
|
||||||
</prerequisites>
|
|
||||||
|
|
||||||
<groupId>org.whispersystems.textsecure</groupId>
|
<repositories>
|
||||||
<artifactId>TextSecureServer</artifactId>
|
<repository>
|
||||||
<version>0.13</version>
|
<id>central</id>
|
||||||
|
<name>Central Repository</name>
|
||||||
|
<url>https://repo.maven.apache.org/maven2</url>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>false</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<pluginRepositories>
|
||||||
|
<pluginRepository>
|
||||||
|
<id>ossrh-snapshots</id>
|
||||||
|
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
|
||||||
|
<releases>
|
||||||
|
<enabled>false</enabled>
|
||||||
|
</releases>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</pluginRepository>
|
||||||
|
</pluginRepositories>
|
||||||
|
|
||||||
|
<modules>
|
||||||
|
<module>api-doc</module>
|
||||||
|
<module>integration-tests</module>
|
||||||
|
<module>service</module>
|
||||||
|
<module>websocket-resources</module>
|
||||||
|
</modules>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<aws.sdk2.version>2.21.5</aws.sdk2.version>
|
||||||
|
<braintree.version>3.27.0</braintree.version>
|
||||||
|
<commons-csv.version>1.10.0</commons-csv.version>
|
||||||
|
<commons-io.version>2.14.0</commons-io.version>
|
||||||
|
<dropwizard.version>3.0.4</dropwizard.version>
|
||||||
|
<dropwizard-metrics-datadog.version>1.1.13</dropwizard-metrics-datadog.version>
|
||||||
|
<google-cloud-libraries.version>26.25.0</google-cloud-libraries.version>
|
||||||
|
<grpc.version>1.58.0</grpc.version> <!-- should be kept in sync with the value from Google libraries-bom -->
|
||||||
|
<gson.version>2.10.1</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.16.0</jackson.version>
|
||||||
|
<jaxb.version>2.3.1</jaxb.version>
|
||||||
|
<junit-pioneer.version>2.1.0</junit-pioneer.version>
|
||||||
|
<jsr305.version>3.0.2</jsr305.version>
|
||||||
|
<kotlin.version>1.9.10</kotlin.version>
|
||||||
|
<kotlinx-serialization.version>1.5.1</kotlinx-serialization.version>
|
||||||
|
<lettuce.version>6.2.6.RELEASE</lettuce.version>
|
||||||
|
<libphonenumber.version>8.13.23</libphonenumber.version>
|
||||||
|
<logstash.logback.version>7.3</logstash.logback.version>
|
||||||
|
<log4j-bom.version>2.21.0</log4j-bom.version>
|
||||||
|
<luajava.version>3.4.0</luajava.version>
|
||||||
|
<micrometer.version>1.10.10</micrometer.version>
|
||||||
|
<netty.version>4.1.96.Final</netty.version>
|
||||||
|
<opentest4j.version>1.3.0</opentest4j.version>
|
||||||
|
<protobuf.version>3.24.3</protobuf.version> <!-- should be kept in sync with the value from Google libraries-bom -->
|
||||||
|
<pushy.version>0.15.2</pushy.version>
|
||||||
|
<reactive.grpc.version>1.2.4</reactive.grpc.version>
|
||||||
|
<reactor-bom.version>2022.0.12</reactor-bom.version> <!-- 3.5.x, see https://github.com/reactor/reactor#bom-versioning-scheme -->
|
||||||
|
<resilience4j.version>1.7.0</resilience4j.version>
|
||||||
|
<semver4j.version>3.1.0</semver4j.version>
|
||||||
|
<slf4j.version>2.0.9</slf4j.version>
|
||||||
|
<stripe.version>23.10.0</stripe.version>
|
||||||
|
<swagger.version>2.2.17</swagger.version>
|
||||||
|
<vavr.version>0.10.4</vavr.version>
|
||||||
|
|
||||||
|
<!-- 21.0.1_12-jre-jammy -->
|
||||||
|
<docker.image.sha256>2d00f6910282a7a20ae7747b8f5e2371f7d55f06daed6bf60a323fcc7eaa3da8</docker.image.sha256>
|
||||||
|
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<groupId>org.whispersystems.textsecure</groupId>
|
||||||
|
<artifactId>TextSecureServer</artifactId>
|
||||||
|
<version>JGITVER</version>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>bouncycastle</groupId>
|
<groupId>com.fasterxml.jackson</groupId>
|
||||||
<artifactId>bcprov-jdk16</artifactId>
|
<artifactId>jackson-bom</artifactId>
|
||||||
<version>140</version>
|
<version>${jackson.version}</version>
|
||||||
</dependency>
|
<type>pom</type>
|
||||||
<dependency>
|
<scope>import</scope>
|
||||||
<groupId>com.yammer.dropwizard</groupId>
|
</dependency>
|
||||||
<artifactId>dropwizard-core</artifactId>
|
<dependency>
|
||||||
<version>0.6.2</version>
|
<groupId>io.dropwizard</groupId>
|
||||||
</dependency>
|
<artifactId>dropwizard-dependencies</artifactId>
|
||||||
<dependency>
|
<version>${dropwizard.version}</version>
|
||||||
<groupId>com.yammer.metrics</groupId>
|
<type>pom</type>
|
||||||
<artifactId>metrics-graphite</artifactId>
|
<scope>import</scope>
|
||||||
<version>2.2.0</version>
|
</dependency>
|
||||||
</dependency>
|
<!-- Needed for gRPC with Java 9+ -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.android.gcm</groupId>
|
<groupId>org.apache.tomcat</groupId>
|
||||||
<artifactId>gcm-server</artifactId>
|
<artifactId>annotations-api</artifactId>
|
||||||
<version>1.0.2</version>
|
<version>6.0.53</version>
|
||||||
</dependency>
|
<scope>provided</scope>
|
||||||
<dependency>
|
</dependency>
|
||||||
<groupId>com.google.code.gson</groupId>
|
<dependency>
|
||||||
<artifactId>gson</artifactId>
|
<groupId>io.netty</groupId>
|
||||||
<version>2.2.2</version>
|
<artifactId>netty-bom</artifactId>
|
||||||
</dependency>
|
<version>${netty.version}</version>
|
||||||
<dependency>
|
<type>pom</type>
|
||||||
<groupId>net.spy</groupId>
|
<scope>import</scope>
|
||||||
<artifactId>spymemcached</artifactId>
|
</dependency>
|
||||||
<version>2.10.1</version>
|
<dependency>
|
||||||
</dependency>
|
<groupId>software.amazon.awssdk</groupId>
|
||||||
<dependency>
|
<artifactId>bom</artifactId>
|
||||||
<groupId>com.notnoop.apns</groupId>
|
<version>${aws.sdk2.version}</version>
|
||||||
<artifactId>apns</artifactId>
|
<type>pom</type>
|
||||||
<version>0.2.3</version>
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
<dependency>
|
<groupId>com.google.cloud</groupId>
|
||||||
<groupId>com.amazonaws</groupId>
|
<artifactId>libraries-bom</artifactId>
|
||||||
<artifactId>aws-java-sdk</artifactId>
|
<version>${google-cloud-libraries.version}</version>
|
||||||
<version>1.4.1</version>
|
<type>pom</type>
|
||||||
</dependency>
|
<scope>import</scope>
|
||||||
<dependency>
|
</dependency>
|
||||||
<groupId>com.google.protobuf</groupId>
|
<dependency>
|
||||||
<artifactId>protobuf-java</artifactId>
|
<groupId>com.salesforce.servicelibs</groupId>
|
||||||
<version>2.4.1</version>
|
<artifactId>reactor-grpc-stub</artifactId>
|
||||||
</dependency>
|
<version>${reactive.grpc.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>redis.clients</groupId>
|
<groupId>io.github.resilience4j</groupId>
|
||||||
<artifactId>jedis</artifactId>
|
<artifactId>resilience4j-bom</artifactId>
|
||||||
<version>2.2.1</version>
|
<version>${resilience4j.version}</version>
|
||||||
<type>jar</type>
|
<type>pom</type>
|
||||||
<scope>compile</scope>
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.yammer.dropwizard</groupId>
|
<groupId>io.micrometer</groupId>
|
||||||
<artifactId>dropwizard-jdbi</artifactId>
|
<artifactId>micrometer-bom</artifactId>
|
||||||
<version>0.6.2</version>
|
<version>${micrometer.version}</version>
|
||||||
</dependency>
|
<type>pom</type>
|
||||||
<dependency>
|
<scope>import</scope>
|
||||||
<groupId>com.yammer.dropwizard</groupId>
|
</dependency>
|
||||||
<artifactId>dropwizard-auth</artifactId>
|
<dependency>
|
||||||
<version>0.6.2</version>
|
<groupId>io.projectreactor</groupId>
|
||||||
</dependency>
|
<artifactId>reactor-bom</artifactId>
|
||||||
<dependency>
|
<version>${reactor-bom.version}</version>
|
||||||
<groupId>com.yammer.dropwizard</groupId>
|
<type>pom</type>
|
||||||
<artifactId>dropwizard-client</artifactId>
|
<scope>import</scope>
|
||||||
<version>0.6.2</version>
|
</dependency>
|
||||||
</dependency>
|
<dependency>
|
||||||
<dependency>
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
<groupId>com.yammer.dropwizard</groupId>
|
<artifactId>kotlin-bom</artifactId>
|
||||||
<artifactId>dropwizard-migrations</artifactId>
|
<version>${kotlin.version}</version>
|
||||||
<version>0.6.2</version>
|
<type>pom</type>
|
||||||
</dependency>
|
<scope>import</scope>
|
||||||
<dependency>
|
</dependency>
|
||||||
<groupId>com.yammer.dropwizard</groupId>
|
<dependency>
|
||||||
<artifactId>dropwizard-testing</artifactId>
|
<groupId>com.eatthepath</groupId>
|
||||||
<version>0.6.2</version>
|
<artifactId>pushy</artifactId>
|
||||||
</dependency>
|
<version>${pushy.version}</version>
|
||||||
<dependency>
|
</dependency>
|
||||||
<groupId>com.twilio.sdk</groupId>
|
<dependency>
|
||||||
<artifactId>twilio-java-sdk</artifactId>
|
<groupId>com.eatthepath</groupId>
|
||||||
<version>3.4.1</version>
|
<artifactId>pushy-dropwizard-metrics-listener</artifactId>
|
||||||
</dependency>
|
<version>${pushy.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>postgresql</groupId>
|
<groupId>com.google.protobuf</groupId>
|
||||||
<artifactId>postgresql</artifactId>
|
<artifactId>protobuf-java</artifactId>
|
||||||
<version>9.1-901.jdbc4</version>
|
<version>${protobuf.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
<dependency>
|
<groupId>com.googlecode.libphonenumber</groupId>
|
||||||
<groupId>com.sun.jersey</groupId>
|
<artifactId>libphonenumber</artifactId>
|
||||||
<artifactId>jersey-json</artifactId>
|
<version>${libphonenumber.version}</version>
|
||||||
<version>1.17.1</version>
|
</dependency>
|
||||||
</dependency>
|
<dependency>
|
||||||
|
<groupId>com.vdurmont</groupId>
|
||||||
<dependency>
|
<artifactId>semver4j</artifactId>
|
||||||
<groupId>org.eclipse.jetty</groupId>
|
<version>${semver4j.version}</version>
|
||||||
<artifactId>jetty-websocket</artifactId>
|
</dependency>
|
||||||
<version>8.1.14.v20131031</version>
|
<dependency>
|
||||||
</dependency>
|
<groupId>commons-io</groupId>
|
||||||
|
<artifactId>commons-io</artifactId>
|
||||||
<dependency>
|
<version>${commons-io.version}</version>
|
||||||
<groupId>org.coursera</groupId>
|
</dependency>
|
||||||
<artifactId>metrics-datadog</artifactId>
|
<dependency>
|
||||||
<version>0.1.5</version>
|
<groupId>io.lettuce</groupId>
|
||||||
</dependency>
|
<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>
|
||||||
|
<version>${logstash.logback.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-csv</artifactId>
|
||||||
|
<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.opentest4j</groupId>
|
||||||
|
<artifactId>opentest4j</artifactId>
|
||||||
|
<version>${opentest4j.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
<version>${slf4j.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-nop</artifactId>
|
||||||
|
<version>${slf4j.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-logging</groupId>
|
||||||
|
<artifactId>commons-logging</artifactId>
|
||||||
|
<version>1.2</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ow2.asm</groupId>
|
||||||
|
<artifactId>asm</artifactId>
|
||||||
|
<version>9.5</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.stripe</groupId>
|
||||||
|
<artifactId>stripe-java</artifactId>
|
||||||
|
<version>${stripe.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.braintreepayments.gateway</groupId>
|
||||||
|
<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>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.signal</groupId>
|
||||||
|
<artifactId>libsignal-server</artifactId>
|
||||||
|
<version>0.39.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.logging.log4j</groupId>
|
||||||
|
<artifactId>log4j-bom</artifactId>
|
||||||
|
<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>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
<build>
|
<dependencies>
|
||||||
<plugins>
|
<dependency>
|
||||||
<plugin>
|
<groupId>org.hamcrest</groupId>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<artifactId>hamcrest-all</artifactId>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<version>1.3</version>
|
||||||
<configuration>
|
<scope>test</scope>
|
||||||
<source>1.7</source>
|
</dependency>
|
||||||
<target>1.7</target>
|
<dependency>
|
||||||
</configuration>
|
<groupId>org.wiremock</groupId>
|
||||||
</plugin>
|
<!-- use standalone until Dropwizard 4 + jakarta.* -->
|
||||||
<plugin>
|
<artifactId>wiremock-standalone</artifactId>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<version>3.3.1</version>
|
||||||
<artifactId>maven-source-plugin</artifactId>
|
<scope>test</scope>
|
||||||
<version>2.2.1</version>
|
<exclusions>
|
||||||
<executions>
|
<exclusion>
|
||||||
<execution>
|
<groupId>org.hamcrest</groupId>
|
||||||
<id>attach-sources</id>
|
<artifactId>hamcrest-core</artifactId>
|
||||||
<goals>
|
</exclusion>
|
||||||
<goal>jar</goal>
|
<exclusion>
|
||||||
</goals>
|
<groupId>javax.xml.bind</groupId>
|
||||||
</execution>
|
<artifactId>jaxb-api</artifactId>
|
||||||
</executions>
|
</exclusion>
|
||||||
</plugin>
|
</exclusions>
|
||||||
<plugin>
|
</dependency>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<dependency>
|
||||||
<artifactId>maven-jar-plugin</artifactId>
|
<groupId>org.mockito</groupId>
|
||||||
<version>2.4</version>
|
<artifactId>mockito-core</artifactId>
|
||||||
<configuration>
|
<scope>test</scope>
|
||||||
<archive>
|
</dependency>
|
||||||
<manifest>
|
<dependency>
|
||||||
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
<groupId>org.assertj</groupId>
|
||||||
</manifest>
|
<artifactId>assertj-core</artifactId>
|
||||||
</archive>
|
<scope>test</scope>
|
||||||
</configuration>
|
</dependency>
|
||||||
</plugin>
|
<dependency>
|
||||||
<plugin>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
<scope>test</scope>
|
||||||
<version>1.6</version>
|
</dependency>
|
||||||
<configuration>
|
<dependency>
|
||||||
<createDependencyReducedPom>true</createDependencyReducedPom>
|
<groupId>org.junit-pioneer</groupId>
|
||||||
<filters>
|
<artifactId>junit-pioneer</artifactId>
|
||||||
<filter>
|
<version>${junit-pioneer.version}</version>
|
||||||
<artifact>*:*</artifact>
|
<scope>test</scope>
|
||||||
<excludes>
|
</dependency>
|
||||||
<exclude>META-INF/*.SF</exclude>
|
|
||||||
<exclude>META-INF/*.DSA</exclude>
|
|
||||||
<exclude>META-INF/*.RSA</exclude>
|
|
||||||
</excludes>
|
|
||||||
</filter>
|
|
||||||
</filters>
|
|
||||||
</configuration>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<phase>package</phase>
|
|
||||||
<goals>
|
|
||||||
<goal>shade</goal>
|
|
||||||
</goals>
|
|
||||||
<configuration>
|
|
||||||
<transformers>
|
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
|
||||||
<mainClass>org.whispersystems.textsecuregcm.WhisperServerService</mainClass>
|
|
||||||
</transformer>
|
|
||||||
</transformers>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
|
|
||||||
<plugin>
|
</dependencies>
|
||||||
<artifactId>maven-assembly-plugin</artifactId>
|
|
||||||
<version>2.4</version>
|
|
||||||
<configuration>
|
|
||||||
<descriptors>
|
|
||||||
<descriptor>assembly.xml</descriptor>
|
|
||||||
</descriptors>
|
|
||||||
</configuration>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<id>make-assembly</id> <!-- this is used for inheritance merges -->
|
|
||||||
<phase>package</phase> <!-- bind to the packaging phase -->
|
|
||||||
<goals>
|
|
||||||
<goal>single</goal>
|
|
||||||
</goals>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
|
|
||||||
</plugins>
|
<profiles>
|
||||||
</build>
|
<profile>
|
||||||
|
<id>include-spam-filter</id>
|
||||||
|
<activation>
|
||||||
|
<file>
|
||||||
|
<exists>spam-filter/pom.xml</exists>
|
||||||
|
</file>
|
||||||
|
</activation>
|
||||||
|
<modules>
|
||||||
|
<module>spam-filter</module>
|
||||||
|
</modules>
|
||||||
|
</profile>
|
||||||
|
|
||||||
<repositories>
|
<profile>
|
||||||
<repository>
|
<id>exclude-spam-filter</id>
|
||||||
<id>gcm-server-repository</id>
|
<activation>
|
||||||
<url>https://raw.github.com/whispersystems/maven/master/gcm-server/releases/</url>
|
<file>
|
||||||
</repository>
|
<missing>spam-filter/pom.xml</missing>
|
||||||
</repositories>
|
</file>
|
||||||
|
</activation>
|
||||||
|
</profile>
|
||||||
|
</profiles>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<extensions>
|
||||||
|
<extension>
|
||||||
|
<groupId>kr.motd.maven</groupId>
|
||||||
|
<artifactId>os-maven-plugin</artifactId>
|
||||||
|
<version>1.7.0</version>
|
||||||
|
</extension>
|
||||||
|
</extensions>
|
||||||
|
<pluginManagement>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>com.google.cloud.tools</groupId>
|
||||||
|
<artifactId>jib-maven-plugin</artifactId>
|
||||||
|
<version>3.4.0</version>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</pluginManagement>
|
||||||
|
<plugins>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.xolstice.maven.plugins</groupId>
|
||||||
|
<artifactId>protobuf-maven-plugin</artifactId>
|
||||||
|
<version>0.6.1</version>
|
||||||
|
<configuration>
|
||||||
|
<checkStaleness>false</checkStaleness>
|
||||||
|
<protocArtifact>com.google.protobuf:protoc:${protobuf.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>
|
||||||
|
</protocPlugins>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>compile</goal>
|
||||||
|
<goal>compile-custom</goal>
|
||||||
|
<goal>test-compile</goal>
|
||||||
|
<goal>test-compile-custom</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.11.0</version>
|
||||||
|
<configuration>
|
||||||
|
<release>21</release>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<version>3.3.0</version>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-dependency-plugin</artifactId>
|
||||||
|
<version>3.3.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>copy</id>
|
||||||
|
<phase>test-compile</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>copy-dependencies</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.1.2</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.3.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>enforce</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<rules>
|
||||||
|
<dependencyConvergence/>
|
||||||
|
<requireMavenVersion>
|
||||||
|
<version>3.8.6</version>
|
||||||
|
</requireMavenVersion>
|
||||||
|
</rules>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-install-plugin</artifactId>
|
||||||
|
<version>3.1.1</version>
|
||||||
|
<configuration>
|
||||||
|
<skip>true</skip>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-deploy-plugin</artifactId>
|
||||||
|
<version>3.1.1</version>
|
||||||
|
<configuration>
|
||||||
|
<skip>true</skip>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
all:
|
|
||||||
protoc --java_out=../src/main/java/ OutgoingMessageSignal.proto
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (C) 2013 Open WhisperSystems
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package textsecure;
|
|
||||||
|
|
||||||
option java_package = "org.whispersystems.textsecuregcm.entities";
|
|
||||||
option java_outer_classname = "MessageProtos";
|
|
||||||
|
|
||||||
message OutgoingMessageSignal {
|
|
||||||
optional uint32 type = 1;
|
|
||||||
optional string source = 2;
|
|
||||||
optional uint32 sourceDevice = 7;
|
|
||||||
optional string relay = 3;
|
|
||||||
// repeated string destinations = 4;
|
|
||||||
optional uint64 timestamp = 5;
|
|
||||||
optional bytes message = 6;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
|
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0"
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
|
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 http://maven.apache.org/xsd/assembly-2.1.0.xsd">
|
||||||
<id>bin</id>
|
<id>bin</id>
|
||||||
<includeBaseDirectory>false</includeBaseDirectory>
|
<includeBaseDirectory>false</includeBaseDirectory>
|
||||||
<formats>
|
<formats>
|
||||||
@@ -18,8 +18,8 @@
|
|||||||
<directory>${project.build.directory}</directory>
|
<directory>${project.build.directory}</directory>
|
||||||
<outputDirectory>/</outputDirectory>
|
<outputDirectory>/</outputDirectory>
|
||||||
<includes>
|
<includes>
|
||||||
<include>${project.name}-${project.version}.jar</include>
|
<include>${parent.artifactId}-${project.version}.jar</include>
|
||||||
</includes>
|
</includes>
|
||||||
</fileSet>
|
</fileSet>
|
||||||
</fileSets>
|
</fileSets>
|
||||||
</assembly>
|
</assembly>
|
||||||
99
service/config/sample-secrets-bundle.yml
Normal file
99
service/config/sample-secrets-bundle.yml
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
datadog.apiKey: unset
|
||||||
|
|
||||||
|
stripe.apiKey: unset
|
||||||
|
stripe.idempotencyKeyGenerator: abcdefg12345678= # base64 for creating request idempotency hash
|
||||||
|
|
||||||
|
braintree.privateKey: 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
|
||||||
|
|
||||||
|
svr3.userAuthenticationTokenSharedSecret: cbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR3 to generate auth tokens for Signal users
|
||||||
|
svr3.userIdTokenSharedSecret: dbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR3 to generate auth identity tokens for Signal users
|
||||||
|
|
||||||
|
tus.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG=
|
||||||
|
|
||||||
|
awsAttachments.accessKey: test
|
||||||
|
awsAttachments.accessSecret: test
|
||||||
|
|
||||||
|
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.certificate: ABCD1234
|
||||||
|
unidentifiedDelivery.privateKey: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789AAAAAAA
|
||||||
|
|
||||||
|
hCaptcha.apiKey: unset
|
||||||
|
|
||||||
|
storageService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||||
|
|
||||||
|
zkConfig.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==
|
||||||
|
zkConfig-libsignal-0.36.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==
|
||||||
|
zkConfig-libsignal-0.37.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==
|
||||||
|
|
||||||
|
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.coinMarketCapApiKey: unset
|
||||||
|
|
||||||
|
artService.userAuthenticationTokenSharedSecret: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= # base64-encoded 32-byte secret not shared with any external service, but used in ArtController
|
||||||
|
artService.userAuthenticationTokenUserIdSecret: AAAAAAAAAAA= # base64-encoded secret to obscure user phone numbers from Sticker Creator
|
||||||
|
|
||||||
|
currentReportingKey.secret: AAAAAAAAAAA=
|
||||||
|
currentReportingKey.salt: AAAAAAAAAAA=
|
||||||
|
|
||||||
|
turn.secret: AAAAAAAAAAA=
|
||||||
|
|
||||||
|
linkDevice.secret: AAAAAAAAAAA=
|
||||||
|
|
||||||
|
tlsKeyStore.password: unset
|
||||||
452
service/config/sample.yml
Normal file
452
service/config/sample.yml
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
# Example, relatively minimal, configuration that passes validation (see `io.dropwizard.cli.CheckCommand`)
|
||||||
|
#
|
||||||
|
# `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.
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level: INFO
|
||||||
|
appenders:
|
||||||
|
- type: console
|
||||||
|
threshold: ALL
|
||||||
|
timeZone: UTC
|
||||||
|
target: stdout
|
||||||
|
- type: logstashtcpsocket
|
||||||
|
destination: example.com:10516
|
||||||
|
apiKey: secret://datadog.apiKey
|
||||||
|
environment: staging
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
reporters:
|
||||||
|
- type: signal-datadog
|
||||||
|
frequency: 10 seconds
|
||||||
|
tags:
|
||||||
|
- "env:staging"
|
||||||
|
- "service:chat"
|
||||||
|
udpTransport:
|
||||||
|
statsdHost: localhost
|
||||||
|
port: 8125
|
||||||
|
excludesAttributes:
|
||||||
|
- m1_rate
|
||||||
|
- m5_rate
|
||||||
|
- m15_rate
|
||||||
|
- mean_rate
|
||||||
|
- stddev
|
||||||
|
useRegexFilters: true
|
||||||
|
excludes:
|
||||||
|
- ^.+\.total$
|
||||||
|
- ^.+\.request\.filtering$
|
||||||
|
- ^.+\.response\.filtering$
|
||||||
|
- ^executor\..+$
|
||||||
|
- ^lettuce\..+$
|
||||||
|
reportOnStop: true
|
||||||
|
|
||||||
|
grpcPort: 8080
|
||||||
|
|
||||||
|
tlsKeyStore:
|
||||||
|
password: secret://tlsKeyStore.password
|
||||||
|
|
||||||
|
stripe:
|
||||||
|
apiKey: secret://stripe.apiKey
|
||||||
|
idempotencyKeyGenerator: secret://stripe.idempotencyKeyGenerator
|
||||||
|
boostDescription: >
|
||||||
|
Example
|
||||||
|
supportedCurrenciesByPaymentMethod:
|
||||||
|
CARD:
|
||||||
|
- usd
|
||||||
|
- eur
|
||||||
|
SEPA_DEBIT:
|
||||||
|
- eur
|
||||||
|
|
||||||
|
braintree:
|
||||||
|
merchantId: unset
|
||||||
|
publicKey: unset
|
||||||
|
privateKey: secret://braintree.privateKey
|
||||||
|
environment: unset
|
||||||
|
graphqlUrl: unset
|
||||||
|
merchantAccounts:
|
||||||
|
# ISO 4217 currency code and its corresponding sub-merchant account
|
||||||
|
'xts': unset
|
||||||
|
supportedCurrenciesByPaymentMethod:
|
||||||
|
PAYPAL:
|
||||||
|
- usd
|
||||||
|
|
||||||
|
dynamoDbClientConfiguration:
|
||||||
|
region: us-west-2 # AWS Region
|
||||||
|
|
||||||
|
dynamoDbTables:
|
||||||
|
accounts:
|
||||||
|
tableName: Example_Accounts
|
||||||
|
phoneNumberTableName: Example_Accounts_PhoneNumbers
|
||||||
|
phoneNumberIdentifierTableName: Example_Accounts_PhoneNumberIdentifiers
|
||||||
|
usernamesTableName: Example_Accounts_Usernames
|
||||||
|
backups:
|
||||||
|
tableName: Example_Backups
|
||||||
|
clientReleases:
|
||||||
|
tableName: Example_ClientReleases
|
||||||
|
deletedAccounts:
|
||||||
|
tableName: Example_DeletedAccounts
|
||||||
|
deletedAccountsLock:
|
||||||
|
tableName: Example_DeletedAccountsLock
|
||||||
|
issuedReceipts:
|
||||||
|
tableName: Example_IssuedReceipts
|
||||||
|
expiration: P30D # Duration of time until rows expire
|
||||||
|
generator: abcdefg12345678= # random base64-encoded binary sequence
|
||||||
|
ecKeys:
|
||||||
|
tableName: Example_Keys
|
||||||
|
ecSignedPreKeys:
|
||||||
|
tableName: Example_EC_Signed_Pre_Keys
|
||||||
|
pqKeys:
|
||||||
|
tableName: Example_PQ_Keys
|
||||||
|
pqLastResortKeys:
|
||||||
|
tableName: Example_PQ_Last_Resort_Keys
|
||||||
|
messages:
|
||||||
|
tableName: Example_Messages
|
||||||
|
expiration: P30D # Duration of time until rows expire
|
||||||
|
onetimeDonations:
|
||||||
|
tableName: Example_OnetimeDonations
|
||||||
|
expiration: P90D
|
||||||
|
phoneNumberIdentifiers:
|
||||||
|
tableName: Example_PhoneNumberIdentifiers
|
||||||
|
profiles:
|
||||||
|
tableName: Example_Profiles
|
||||||
|
pushChallenge:
|
||||||
|
tableName: Example_PushChallenge
|
||||||
|
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
|
||||||
|
subscriptions:
|
||||||
|
tableName: Example_Subscriptions
|
||||||
|
clientPublicKeys:
|
||||||
|
tableName: Example_ClientPublicKeys
|
||||||
|
verificationSessions:
|
||||||
|
tableName: Example_VerificationSessions
|
||||||
|
|
||||||
|
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
|
||||||
|
uri: redis://redis.example.com:6379/
|
||||||
|
|
||||||
|
pushSchedulerCluster: # Redis server configuration for push scheduler cluster
|
||||||
|
configurationUri: redis://redis.example.com:6379/
|
||||||
|
|
||||||
|
rateLimitersCluster: # Redis server configuration for rate limiters cluster
|
||||||
|
configurationUri: redis://redis.example.com:6379/
|
||||||
|
|
||||||
|
directoryV2:
|
||||||
|
client: # Configuration for interfacing with Contact Discovery Service v2 cluster
|
||||||
|
userAuthenticationTokenSharedSecret: secret://directoryV2.client.userAuthenticationTokenSharedSecret
|
||||||
|
userIdTokenSharedSecret: secret://directoryV2.client.userIdTokenSharedSecret
|
||||||
|
|
||||||
|
svr2:
|
||||||
|
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-----
|
||||||
|
|
||||||
|
svr3:
|
||||||
|
uri: svr3.example.com
|
||||||
|
userAuthenticationTokenSharedSecret: secret://svr3.userAuthenticationTokenSharedSecret
|
||||||
|
userIdTokenSharedSecret: secret://svr3.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: secret://awsAttachments.accessKey
|
||||||
|
accessSecret: secret://awsAttachments.accessSecret
|
||||||
|
bucket: aws-attachments
|
||||||
|
region: us-west-2
|
||||||
|
|
||||||
|
gcpAttachments: # GCP Storage configuration
|
||||||
|
domain: example.com
|
||||||
|
email: user@example.cocm
|
||||||
|
maxSizeInBytes: 1024
|
||||||
|
pathPrefix:
|
||||||
|
rsaSigningKey: secret://gcpAttachments.rsaSigningKey
|
||||||
|
|
||||||
|
tus:
|
||||||
|
uploadUri: https://example.org/upload
|
||||||
|
userAuthenticationTokenSharedSecret: secret://tus.userAuthenticationTokenSharedSecret
|
||||||
|
|
||||||
|
apn: # Apple Push Notifications configuration
|
||||||
|
sandbox: true
|
||||||
|
bundleId: com.example.textsecuregcm
|
||||||
|
keyId: secret://apn.keyId
|
||||||
|
teamId: secret://apn.teamId
|
||||||
|
signingKey: secret://apn.signingKey
|
||||||
|
|
||||||
|
fcm: # FCM configuration
|
||||||
|
credentials: secret://fcm.credentials
|
||||||
|
|
||||||
|
cdn:
|
||||||
|
accessKey: secret://cdn.accessKey
|
||||||
|
accessSecret: secret://cdn.accessSecret
|
||||||
|
bucket: cdn # S3 Bucket name
|
||||||
|
region: us-west-2 # AWS region
|
||||||
|
|
||||||
|
clientCdn:
|
||||||
|
attachmentUrls:
|
||||||
|
2: https://cdn2.example.com/attachments/
|
||||||
|
caCertificates:
|
||||||
|
- |
|
||||||
|
-----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-----
|
||||||
|
|
||||||
|
cdn3StorageManager:
|
||||||
|
baseUri: https://storage-manager.example.com
|
||||||
|
clientId: example
|
||||||
|
clientSecret: secret://cdn3StorageManager.clientSecret
|
||||||
|
|
||||||
|
dogstatsd:
|
||||||
|
environment: dev
|
||||||
|
host: 127.0.0.1
|
||||||
|
|
||||||
|
unidentifiedDelivery:
|
||||||
|
certificate: secret://unidentifiedDelivery.certificate
|
||||||
|
privateKey: secret://unidentifiedDelivery.privateKey
|
||||||
|
expiresDays: 7
|
||||||
|
|
||||||
|
recaptcha:
|
||||||
|
projectPath: projects/example
|
||||||
|
credentialConfigurationJson: "{ }" # service account configuration for backend authentication
|
||||||
|
|
||||||
|
hCaptcha:
|
||||||
|
apiKey: secret://hCaptcha.apiKey
|
||||||
|
|
||||||
|
shortCode:
|
||||||
|
baseUrl: https://example.com/shortcodes/
|
||||||
|
|
||||||
|
storageService:
|
||||||
|
uri: storage.example.com
|
||||||
|
userAuthenticationTokenSharedSecret: secret://storageService.userAuthenticationTokenSharedSecret
|
||||||
|
storageCaCertificates:
|
||||||
|
- |
|
||||||
|
-----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: secret://zkConfig.serverSecret
|
||||||
|
|
||||||
|
callingZkConfig:
|
||||||
|
serverSecret: secret://callingZkConfig.serverSecret
|
||||||
|
|
||||||
|
backupsZkConfig:
|
||||||
|
serverSecret: secret://backupsZkConfig.serverSecret
|
||||||
|
|
||||||
|
appConfig:
|
||||||
|
application: example
|
||||||
|
environment: example
|
||||||
|
configuration: example
|
||||||
|
|
||||||
|
remoteConfig:
|
||||||
|
globalConfig: # keys and values that are given to clients on GET /v1/config
|
||||||
|
EXAMPLE_KEY: VALUE
|
||||||
|
|
||||||
|
paymentsService:
|
||||||
|
userAuthenticationTokenSharedSecret: secret://paymentsService.userAuthenticationTokenSharedSecret
|
||||||
|
fixerApiKey: secret://paymentsService.fixerApiKey
|
||||||
|
coinMarketCapApiKey: secret://paymentsService.coinMarketCapApiKey
|
||||||
|
coinMarketCapCurrencyIds:
|
||||||
|
MOB: 7878
|
||||||
|
paymentCurrencies:
|
||||||
|
# list of symbols for supported currencies
|
||||||
|
- MOB
|
||||||
|
|
||||||
|
artService:
|
||||||
|
userAuthenticationTokenSharedSecret: secret://artService.userAuthenticationTokenSharedSecret
|
||||||
|
userAuthenticationTokenUserIdSecret: secret://artService.userAuthenticationTokenUserIdSecret
|
||||||
|
|
||||||
|
badges:
|
||||||
|
badges:
|
||||||
|
- id: TEST
|
||||||
|
category: other
|
||||||
|
sprites: # exactly 6
|
||||||
|
- sprite-1.png
|
||||||
|
- sprite-2.png
|
||||||
|
- sprite-3.png
|
||||||
|
- sprite-4.png
|
||||||
|
- sprite-5.png
|
||||||
|
- sprite-6.png
|
||||||
|
svg: example.svg
|
||||||
|
svgs:
|
||||||
|
- light: example-light.svg
|
||||||
|
dark: example-dark.svg
|
||||||
|
badgeIdsEnabledForAll:
|
||||||
|
- TEST
|
||||||
|
receiptLevels:
|
||||||
|
'1': TEST
|
||||||
|
|
||||||
|
subscription: # configuration for Stripe subscriptions
|
||||||
|
badgeExpiration: P30D
|
||||||
|
badgeGracePeriod: P15D
|
||||||
|
levels:
|
||||||
|
500:
|
||||||
|
badge: EXAMPLE
|
||||||
|
prices:
|
||||||
|
# list of ISO 4217 currency codes and amounts for the given badge level
|
||||||
|
xts:
|
||||||
|
amount: '10'
|
||||||
|
processorIds:
|
||||||
|
STRIPE: price_example # stripe Price ID
|
||||||
|
BRAINTREE: plan_example # braintree Plan ID
|
||||||
|
|
||||||
|
oneTimeDonations:
|
||||||
|
sepaMaximumEuros: '10000'
|
||||||
|
boost:
|
||||||
|
level: 1
|
||||||
|
expiration: P90D
|
||||||
|
badge: EXAMPLE
|
||||||
|
gift:
|
||||||
|
level: 10
|
||||||
|
expiration: P90D
|
||||||
|
badge: EXAMPLE
|
||||||
|
currencies:
|
||||||
|
# ISO 4217 currency codes and amounts in those currencies
|
||||||
|
xts:
|
||||||
|
minimum: '0.5'
|
||||||
|
gift: '2'
|
||||||
|
boosts:
|
||||||
|
- '1'
|
||||||
|
- '2'
|
||||||
|
- '4'
|
||||||
|
- '8'
|
||||||
|
- '20'
|
||||||
|
- '40'
|
||||||
|
|
||||||
|
registrationService:
|
||||||
|
host: registration.example.com
|
||||||
|
port: 443
|
||||||
|
credentialConfigurationJson: |
|
||||||
|
{
|
||||||
|
"example": "example"
|
||||||
|
}
|
||||||
|
identityTokenAudience: https://registration.example.com
|
||||||
|
registrationCaCertificate: | # Registration service 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-----
|
||||||
|
|
||||||
|
turn:
|
||||||
|
secret: secret://turn.secret
|
||||||
|
|
||||||
|
linkDevice:
|
||||||
|
secret: secret://linkDevice.secret
|
||||||
708
service/pom.xml
Normal file
708
service/pom.xml
Normal file
@@ -0,0 +1,708 @@
|
|||||||
|
<?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>service</artifactId>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<firebase-admin.version>9.2.0</firebase-admin.version>
|
||||||
|
<java-uuid-generator.version>4.3.0</java-uuid-generator.version>
|
||||||
|
<sqlite4java.version>1.0.392</sqlite4java.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.swagger.core.v3</groupId>
|
||||||
|
<artifactId>swagger-jaxrs2</artifactId>
|
||||||
|
<version>${swagger.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<!-- org.yaml:snakeyaml is causing a dependency convergence error -->
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.yaml</groupId>
|
||||||
|
<artifactId>snakeyaml</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>jakarta.servlet</groupId>
|
||||||
|
<artifactId>jakarta.servlet-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>jakarta.validation</groupId>
|
||||||
|
<artifactId>jakarta.validation-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>jakarta.ws.rs</groupId>
|
||||||
|
<artifactId>jakarta.ws.rs-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.whispersystems.textsecure</groupId>
|
||||||
|
<artifactId>websocket-resources</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.signal</groupId>
|
||||||
|
<artifactId>libsignal-server</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-auth</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-client</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-db</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-logging</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-metrics</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-util</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-servlets</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-lifecycle</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-jersey</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-jetty</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard</groupId>
|
||||||
|
<artifactId>dropwizard-migrations</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>ch.qos.logback</groupId>
|
||||||
|
<artifactId>logback-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>ch.qos.logback</groupId>
|
||||||
|
<artifactId>logback-access</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>ch.qos.logback</groupId>
|
||||||
|
<artifactId>logback-classic</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.logstash.logback</groupId>
|
||||||
|
<artifactId>logstash-logback-encoder</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard.metrics</groupId>
|
||||||
|
<artifactId>metrics-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard.metrics</groupId>
|
||||||
|
<artifactId>metrics-healthchecks</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.dropwizard.metrics</groupId>
|
||||||
|
<artifactId>metrics-annotation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.glassfish.jersey.core</groupId>
|
||||||
|
<artifactId>jersey-common</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.glassfish.jersey.core</groupId>
|
||||||
|
<artifactId>jersey-server</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<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>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-jetty-api</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-servlets</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-lang3</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-csv</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.firebase</groupId>
|
||||||
|
<artifactId>firebase-admin</artifactId>
|
||||||
|
<version>${firebase-admin.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.code.findbugs</groupId>
|
||||||
|
<artifactId>jsr305</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.resilience4j</groupId>
|
||||||
|
<artifactId>resilience4j-circuitbreaker</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.resilience4j</groupId>
|
||||||
|
<artifactId>resilience4j-retry</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.resilience4j</groupId>
|
||||||
|
<artifactId>resilience4j-reactor</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.grpc</groupId>
|
||||||
|
<artifactId>grpc-netty-shaded</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.grpc</groupId>
|
||||||
|
<artifactId>grpc-protobuf</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.grpc</groupId>
|
||||||
|
<artifactId>grpc-stub</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- Needed for gRPC with Java 9+ -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.tomcat</groupId>
|
||||||
|
<artifactId>annotations-api</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.micrometer</groupId>
|
||||||
|
<artifactId>micrometer-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.micrometer</groupId>
|
||||||
|
<artifactId>micrometer-registry-statsd</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.coursera</groupId>
|
||||||
|
<artifactId>dropwizard-metrics-datadog</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-annotations</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||||
|
<artifactId>jackson-dataformat-yaml</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
|
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.jaxrs</groupId>
|
||||||
|
<artifactId>jackson-jaxrs-json-provider</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.salesforce.servicelibs</groupId>
|
||||||
|
<artifactId>reactor-grpc-stub</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>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>software.amazon.awssdk</groupId>
|
||||||
|
<artifactId>s3</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>dynamodb-lock-client</artifactId>
|
||||||
|
<version>1.2.0</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>commons-logging</groupId>
|
||||||
|
<artifactId>commons-logging</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.lettuce</groupId>
|
||||||
|
<artifactId>lettuce-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.eatthepath</groupId>
|
||||||
|
<artifactId>pushy</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.eatthepath</groupId>
|
||||||
|
<artifactId>pushy-dropwizard-metrics-listener</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.vdurmont</groupId>
|
||||||
|
<artifactId>semver4j</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.guava</groupId>
|
||||||
|
<artifactId>guava</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.protobuf</groupId>
|
||||||
|
<artifactId>protobuf-java</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.googlecode.libphonenumber</groupId>
|
||||||
|
<artifactId>libphonenumber</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.sourceforge.argparse4j</groupId>
|
||||||
|
<artifactId>argparse4j</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.glassfish.jersey.test-framework</groupId>
|
||||||
|
<artifactId>jersey-test-framework-core</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<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>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.almworks.sqlite4java</groupId>
|
||||||
|
<artifactId>sqlite4java</artifactId>
|
||||||
|
<version>${sqlite4java.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.projectreactor</groupId>
|
||||||
|
<artifactId>reactor-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.projectreactor</groupId>
|
||||||
|
<artifactId>reactor-core-micrometer</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.vavr</groupId>
|
||||||
|
<artifactId>vavr</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-params</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.projectreactor</groupId>
|
||||||
|
<artifactId>reactor-test</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.signal</groupId>
|
||||||
|
<artifactId>embedded-redis</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.uuid</groupId>
|
||||||
|
<artifactId>java-uuid-generator</artifactId>
|
||||||
|
<version>${java-uuid-generator.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.amazonaws</groupId>
|
||||||
|
<artifactId>DynamoDBLocal</artifactId>
|
||||||
|
<version>1.23.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.ganadist.sqlite4java</groupId>
|
||||||
|
<artifactId>libsqlite4java-osx-aarch64</artifactId>
|
||||||
|
<version>${sqlite4java.version}</version>
|
||||||
|
<type>dylib</type>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.auth</groupId>
|
||||||
|
<artifactId>google-auth-library-oauth2-http</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.cloud</groupId>
|
||||||
|
<artifactId>google-cloud-recaptchaenterprise</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.stripe</groupId>
|
||||||
|
<artifactId>stripe-java</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.braintreepayments.gateway</groupId>
|
||||||
|
<artifactId>braintree-java</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.apollographql.apollo3</groupId>
|
||||||
|
<artifactId>apollo-api-jvm</artifactId>
|
||||||
|
<version>3.8.2</version>
|
||||||
|
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.jetbrains</groupId>
|
||||||
|
<artifactId>annotations</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<profiles>
|
||||||
|
<profile>
|
||||||
|
<id>exclude-spam-filter</id>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.1</version>
|
||||||
|
<configuration>
|
||||||
|
<createDependencyReducedPom>true</createDependencyReducedPom>
|
||||||
|
<filters>
|
||||||
|
<filter>
|
||||||
|
<artifact>*:*</artifact>
|
||||||
|
<excludes>
|
||||||
|
<exclude>META-INF/*.SF</exclude>
|
||||||
|
<exclude>META-INF/*.DSA</exclude>
|
||||||
|
<exclude>META-INF/*.RSA</exclude>
|
||||||
|
</excludes>
|
||||||
|
</filter>
|
||||||
|
</filters>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<transformers>
|
||||||
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
||||||
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
|
<mainClass>org.whispersystems.textsecuregcm.WhisperServerService</mainClass>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-assembly-plugin</artifactId>
|
||||||
|
<version>3.6.0</version>
|
||||||
|
<configuration>
|
||||||
|
<descriptors>
|
||||||
|
<descriptor>assembly.xml</descriptor>
|
||||||
|
</descriptors>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>make-assembly</id> <!-- this is used for inheritance merges -->
|
||||||
|
<phase>package</phase> <!-- bind to the packaging phase -->
|
||||||
|
<goals>
|
||||||
|
<goal>single</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.codehaus.mojo</groupId>
|
||||||
|
<artifactId>properties-maven-plugin</artifactId>
|
||||||
|
<version>1.2.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>read-deploy-configuration</id>
|
||||||
|
<phase>deploy</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>read-project-properties</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<files>${project.basedir}/config/deploy.properties</files>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>com.google.cloud.tools</groupId>
|
||||||
|
<artifactId>jib-maven-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>deploy</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>build</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<from>
|
||||||
|
<image>eclipse-temurin@sha256:${docker.image.sha256}</image>
|
||||||
|
</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>-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>
|
||||||
|
</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>
|
||||||
|
</profiles>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<finalName>${project.parent.artifactId}-${project.version}</finalName>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.codehaus.mojo</groupId>
|
||||||
|
<artifactId>templating-maven-plugin</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>filter-src</id>
|
||||||
|
<goals>
|
||||||
|
<goal>filter-sources</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.1.2</version>
|
||||||
|
<configuration>
|
||||||
|
<!-- work around PATCH not being a supported method on HttpUrlConnection -->
|
||||||
|
<argLine>--add-opens=java.base/java.net=ALL-UNNAMED</argLine>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>test-jar</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.codehaus.mojo</groupId>
|
||||||
|
<artifactId>exec-maven-plugin</artifactId>
|
||||||
|
<version>3.1.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>check-all-service-config</id>
|
||||||
|
<phase>verify</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>java</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<mainClass>org.whispersystems.textsecuregcm.CheckServiceConfigurations</mainClass>
|
||||||
|
<classpathScope>test</classpathScope>
|
||||||
|
<arguments>
|
||||||
|
<argument>${project.basedir}/config</argument>
|
||||||
|
</arguments>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>com.github.aoudiamoncef</groupId>
|
||||||
|
<artifactId>apollo-client-maven-plugin</artifactId>
|
||||||
|
<version>5.0.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>generate</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<services>
|
||||||
|
<braintree>
|
||||||
|
<compilationUnit>
|
||||||
|
<name>braintree</name>
|
||||||
|
<compilerParams>
|
||||||
|
<schemaPackageName>com.braintree.graphql.client</schemaPackageName>
|
||||||
|
</compilerParams>
|
||||||
|
</compilationUnit>
|
||||||
|
</braintree>
|
||||||
|
</services>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# https://graphql.braintreepayments.com/reference/#Mutation--chargePaymentMethod
|
||||||
|
mutation ChargePayPalOneTimePayment($input: ChargePaymentMethodInput!) {
|
||||||
|
chargePaymentMethod(input: $input) {
|
||||||
|
transaction {
|
||||||
|
id,
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
mutation CreatePayPalBillingAgreement($input: CreatePayPalBillingAgreementInput!) {
|
||||||
|
createPayPalBillingAgreement(input: $input) {
|
||||||
|
approvalUrl,
|
||||||
|
billingAgreementToken
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# https://graphql.braintreepayments.com/reference/#Mutation--createPayPalOneTimePayment
|
||||||
|
mutation CreatePayPalOneTimePayment($input: CreatePayPalOneTimePaymentInput!) {
|
||||||
|
createPayPalOneTimePayment(input: $input) {
|
||||||
|
approvalUrl,
|
||||||
|
paymentId
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
mutation TokenizePayPalBillingAgreement($input: TokenizePayPalBillingAgreementInput!) {
|
||||||
|
tokenizePayPalBillingAgreement(input: $input) {
|
||||||
|
paymentMethod {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# https://graphql.braintreepayments.com/reference/#Mutation--tokenizePayPalOneTimePayment
|
||||||
|
mutation TokenizePayPalOneTimePayment($input: TokenizePayPalOneTimePaymentInput!) {
|
||||||
|
tokenizePayPalOneTimePayment(input: $input) {
|
||||||
|
paymentMethod {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
mutation VaultPaymentMethod($input: VaultPaymentMethodInput!) {
|
||||||
|
vaultPaymentMethod(input: $input) {
|
||||||
|
paymentMethod {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35093
service/src/main/graphql/braintree/schema.json
Normal file
35093
service/src/main/graphql/braintree/schema.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2021 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm;
|
||||||
|
|
||||||
|
public class WhisperServerVersion {
|
||||||
|
|
||||||
|
private static final String VERSION = "${project.version}";
|
||||||
|
|
||||||
|
public static String getServerVersion() {
|
||||||
|
return VERSION;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.i18n;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
import java.util.ResourceBundle.Control;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public class HeaderControlledResourceBundleLookup {
|
||||||
|
|
||||||
|
private static final int MAX_LOCALES = 15;
|
||||||
|
|
||||||
|
private final ResourceBundleFactory resourceBundleFactory;
|
||||||
|
|
||||||
|
public HeaderControlledResourceBundleLookup() {
|
||||||
|
this(ResourceBundle::getBundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public HeaderControlledResourceBundleLookup(
|
||||||
|
@Nonnull final ResourceBundleFactory resourceBundleFactory) {
|
||||||
|
this.resourceBundleFactory = Objects.requireNonNull(resourceBundleFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private List<Locale> getAcceptableLocales(final List<Locale> acceptableLanguages) {
|
||||||
|
return acceptableLanguages.stream().limit(MAX_LOCALES).distinct().collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public ResourceBundle getResourceBundle(final String baseName, final List<Locale> acceptableLocales) {
|
||||||
|
final List<Locale> deduplicatedLocales = getAcceptableLocales(acceptableLocales);
|
||||||
|
final Locale desiredLocale = deduplicatedLocales.isEmpty() ? Locale.getDefault() : deduplicatedLocales.get(0);
|
||||||
|
// define a control with a fallback order as specified in the header
|
||||||
|
Control control = new Control() {
|
||||||
|
@Override
|
||||||
|
public List<String> getFormats(final String baseName) {
|
||||||
|
Objects.requireNonNull(baseName);
|
||||||
|
return Control.FORMAT_PROPERTIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Locale getFallbackLocale(final String baseName, final Locale locale) {
|
||||||
|
Objects.requireNonNull(baseName);
|
||||||
|
if (locale.equals(Locale.getDefault())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final int localeIndex = deduplicatedLocales.indexOf(locale);
|
||||||
|
if (localeIndex < 0 || localeIndex >= deduplicatedLocales.size() - 1) {
|
||||||
|
return Locale.getDefault();
|
||||||
|
}
|
||||||
|
// [0, deduplicatedLocales.size() - 2] is now the possible range for localeIndex
|
||||||
|
return deduplicatedLocales.get(localeIndex + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return resourceBundleFactory.createBundle(baseName, desiredLocale, control);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.i18n;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
|
public interface ResourceBundleFactory {
|
||||||
|
ResourceBundle createBundle(String baseName, Locale locale, ResourceBundle.Control control);
|
||||||
|
}
|
||||||
@@ -0,0 +1,530 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
package org.whispersystems.textsecuregcm;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import io.dropwizard.core.Configuration;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import org.whispersystems.textsecuregcm.attachments.TusConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.AppConfigConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.ArtServiceConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.BraintreeConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.Cdn3StorageManagerConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.ClientCdnConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.ClientReleaseConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.DirectoryV2Configuration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.DogstatsdConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.DynamoDbClientConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.DynamoDbTables;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.FcmConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.GcpAttachmentsConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.GenericZkConfig;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.HCaptchaConfiguration;
|
||||||
|
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.PaymentsServiceConfiguration;
|
||||||
|
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.RemoteConfigConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.ReportMessageConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery3Configuration;
|
||||||
|
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.TlsKeyStoreConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.TurnSecretConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.UnidentifiedDeliveryConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.ZkConfig;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.RateLimiterConfig;
|
||||||
|
import org.whispersystems.websocket.configuration.WebSocketConfiguration;
|
||||||
|
|
||||||
|
/** @noinspection MismatchedQueryAndUpdateOfCollection, WeakerAccess */
|
||||||
|
public class WhisperServerConfiguration extends Configuration {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private TlsKeyStoreConfiguration tlsKeyStore;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private StripeConfiguration stripe;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private BraintreeConfiguration braintree;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private DynamoDbClientConfiguration dynamoDbClientConfiguration;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private DynamoDbTables dynamoDbTables;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private AwsAttachmentsConfiguration awsAttachments;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private GcpAttachmentsConfiguration gcpAttachments;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private CdnConfiguration cdn;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private ClientCdnConfiguration clientCdn;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private Cdn3StorageManagerConfiguration cdn3StorageManager;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private DogstatsdConfiguration dogstatsd = new DogstatsdConfiguration();
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private RedisClusterConfiguration cacheCluster;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private RedisConfiguration pubsub;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private RedisClusterConfiguration metricsCluster;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private DirectoryV2Configuration directoryV2;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private SecureValueRecovery2Configuration svr2;
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private SecureValueRecovery3Configuration svr3;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private RedisClusterConfiguration pushSchedulerCluster;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private RedisClusterConfiguration rateLimitersCluster;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private MessageCacheConfiguration messageCache;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private RedisClusterConfiguration clientPresenceCluster;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private Set<String> testDevices = new HashSet<>();
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private List<MaxDeviceConfiguration> maxDevices = new LinkedList<>();
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private Map<String, RateLimiterConfig> limits = new HashMap<>();
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private WebSocketConfiguration webSocket = new WebSocketConfiguration();
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private FcmConfiguration fcm;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private ApnConfiguration apn;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private UnidentifiedDeliveryConfiguration unidentifiedDelivery;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private RecaptchaConfiguration recaptcha;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private HCaptchaConfiguration hCaptcha;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private ShortCodeExpanderConfiguration shortCode;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private SecureStorageServiceConfiguration storageService;
|
||||||
|
|
||||||
|
@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
|
||||||
|
private RemoteConfigConfiguration remoteConfig;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private AppConfigConfiguration appConfig;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private BadgesConfiguration badges;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
@NotNull
|
||||||
|
private SubscriptionConfiguration subscription;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
@NotNull
|
||||||
|
private OneTimeDonationConfiguration oneTimeDonations;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private ReportMessageConfiguration reportMessage = new ReportMessageConfiguration();
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
private SpamFilterConfiguration spamFilterConfiguration;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private RegistrationServiceConfiguration registrationService;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private TurnSecretConfiguration turn;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private TusConfiguration tus;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private int grpcPort;
|
||||||
|
|
||||||
|
@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;
|
||||||
|
|
||||||
|
public TlsKeyStoreConfiguration getTlsKeyStoreConfiguration() {
|
||||||
|
return tlsKeyStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public StripeConfiguration getStripe() {
|
||||||
|
return stripe;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BraintreeConfiguration getBraintree() {
|
||||||
|
return braintree;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DynamoDbClientConfiguration getDynamoDbClientConfiguration() {
|
||||||
|
return dynamoDbClientConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
return cacheCluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RedisConfiguration getPubsubCacheConfiguration() {
|
||||||
|
return pubsub;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RedisClusterConfiguration getMetricsClusterConfiguration() {
|
||||||
|
return metricsCluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public SecureValueRecovery2Configuration getSvr2Configuration() {
|
||||||
|
return svr2;
|
||||||
|
}
|
||||||
|
public SecureValueRecovery3Configuration getSvr3Configuration() {
|
||||||
|
return svr3;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DirectoryV2Configuration getDirectoryV2Configuration() {
|
||||||
|
return directoryV2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SecureStorageServiceConfiguration getSecureStorageServiceConfiguration() {
|
||||||
|
return storageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessageCacheConfiguration getMessageCacheConfiguration() {
|
||||||
|
return messageCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RedisClusterConfiguration getClientPresenceClusterConfiguration() {
|
||||||
|
return clientPresenceCluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RedisClusterConfiguration getPushSchedulerCluster() {
|
||||||
|
return pushSchedulerCluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RedisClusterConfiguration getRateLimitersCluster() {
|
||||||
|
return rateLimitersCluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, RateLimiterConfig> getLimitsConfiguration() {
|
||||||
|
return limits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FcmConfiguration getFcmConfiguration() {
|
||||||
|
return fcm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApnConfiguration getApnConfiguration() {
|
||||||
|
return apn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CdnConfiguration getCdnConfiguration() {
|
||||||
|
return cdn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientCdnConfiguration getClientCdnConfiguration() {
|
||||||
|
return clientCdn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Cdn3StorageManagerConfiguration getCdn3StorageManagerConfiguration() {
|
||||||
|
return cdn3StorageManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DogstatsdConfiguration getDatadogConfiguration() {
|
||||||
|
return dogstatsd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnidentifiedDeliveryConfiguration getDeliveryCertificate() {
|
||||||
|
return unidentifiedDelivery;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getTestDevices() {
|
||||||
|
return testDevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Integer> getMaxDevices() {
|
||||||
|
Map<String, Integer> results = new HashMap<>();
|
||||||
|
|
||||||
|
for (MaxDeviceConfiguration maxDeviceConfiguration : maxDevices) {
|
||||||
|
results.put(maxDeviceConfiguration.getNumber(),
|
||||||
|
maxDeviceConfiguration.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 BadgesConfiguration getBadges() {
|
||||||
|
return badges;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SubscriptionConfiguration getSubscription() {
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OneTimeDonationConfiguration getOneTimeDonations() {
|
||||||
|
return oneTimeDonations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReportMessageConfiguration getReportMessageConfiguration() {
|
||||||
|
return reportMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpamFilterConfiguration getSpamFilterConfiguration() {
|
||||||
|
return spamFilterConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RegistrationServiceConfiguration getRegistrationServiceConfiguration() {
|
||||||
|
return registrationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TurnSecretConfiguration getTurnSecretConfiguration() {
|
||||||
|
return turn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TusConfiguration getTus() {
|
||||||
|
return tus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getGrpcPort() {
|
||||||
|
return grpcPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientReleaseConfiguration getClientReleaseConfiguration() {
|
||||||
|
return clientRelease;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessageByteLimitCardinalityEstimatorConfiguration getMessageByteLimitCardinalityEstimator() {
|
||||||
|
return messageByteLimitCardinalityEstimator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LinkDeviceSecretConfiguration getLinkDeviceSecretConfiguration() {
|
||||||
|
return linkDevice;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,951 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
package org.whispersystems.textsecuregcm;
|
||||||
|
|
||||||
|
import static com.codahale.metrics.MetricRegistry.name;
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import io.dropwizard.auth.AuthDynamicFeature;
|
||||||
|
import io.dropwizard.auth.AuthFilter;
|
||||||
|
import io.dropwizard.auth.AuthValueFactoryProvider;
|
||||||
|
import io.dropwizard.auth.basic.BasicCredentialAuthFilter;
|
||||||
|
import io.dropwizard.auth.basic.BasicCredentials;
|
||||||
|
import io.dropwizard.configuration.EnvironmentVariableSubstitutor;
|
||||||
|
import io.dropwizard.configuration.SubstitutingSourceProvider;
|
||||||
|
import io.dropwizard.core.Application;
|
||||||
|
import io.dropwizard.core.server.DefaultServerFactory;
|
||||||
|
import io.dropwizard.core.setup.Bootstrap;
|
||||||
|
import io.dropwizard.core.setup.Environment;
|
||||||
|
import io.dropwizard.jetty.HttpsConnectorFactory;
|
||||||
|
import io.grpc.ServerBuilder;
|
||||||
|
import io.grpc.ServerInterceptors;
|
||||||
|
import io.lettuce.core.metrics.MicrometerCommandLatencyRecorder;
|
||||||
|
import io.lettuce.core.metrics.MicrometerOptions;
|
||||||
|
import io.lettuce.core.resource.ClientResources;
|
||||||
|
import io.micrometer.core.instrument.Metrics;
|
||||||
|
import io.micrometer.core.instrument.binder.grpc.MetricCollectingServerInterceptor;
|
||||||
|
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.ServiceLoader;
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import javax.servlet.DispatcherType;
|
||||||
|
import javax.servlet.FilterRegistration;
|
||||||
|
import javax.servlet.ServletRegistration;
|
||||||
|
import org.eclipse.jetty.servlets.CrossOriginFilter;
|
||||||
|
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
|
||||||
|
import org.glassfish.jersey.server.ServerProperties;
|
||||||
|
import org.signal.i18n.HeaderControlledResourceBundleLookup;
|
||||||
|
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
|
||||||
|
import org.signal.libsignal.zkgroup.ServerSecretParams;
|
||||||
|
import org.signal.libsignal.zkgroup.auth.ServerZkAuthOperations;
|
||||||
|
import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations;
|
||||||
|
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation;
|
||||||
|
import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.attachments.GcsAttachmentGenerator;
|
||||||
|
import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.PhoneVerificationTokenManager;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.WebsocketRefreshApplicationEventListener;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.grpc.BasicCredentialAuthenticationInterceptor;
|
||||||
|
import org.whispersystems.textsecuregcm.backup.BackupAuthManager;
|
||||||
|
import org.whispersystems.textsecuregcm.backup.BackupManager;
|
||||||
|
import org.whispersystems.textsecuregcm.backup.BackupsDb;
|
||||||
|
import org.whispersystems.textsecuregcm.backup.Cdn3BackupCredentialGenerator;
|
||||||
|
import org.whispersystems.textsecuregcm.backup.Cdn3RemoteStorageManager;
|
||||||
|
import org.whispersystems.textsecuregcm.badges.ConfiguredProfileBadgeConverter;
|
||||||
|
import org.whispersystems.textsecuregcm.badges.ResourceBundleLevelTranslator;
|
||||||
|
import org.whispersystems.textsecuregcm.captcha.CaptchaChecker;
|
||||||
|
import org.whispersystems.textsecuregcm.captcha.HCaptchaClient;
|
||||||
|
import org.whispersystems.textsecuregcm.captcha.RecaptchaClient;
|
||||||
|
import org.whispersystems.textsecuregcm.captcha.RegistrationCaptchaManager;
|
||||||
|
import org.whispersystems.textsecuregcm.captcha.ShortCodeExpander;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.secrets.SecretStore;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.secrets.SecretsModule;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.AccountController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.AccountControllerV2;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.ArchiveController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.ArtController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV2;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV3;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV4;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.CallLinkController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.CertificateController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.ChallengeController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.DeviceController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.DirectoryV2Controller;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.DonationController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.KeepAliveController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.KeysController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.PaymentsController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.ProfileController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.ProvisioningController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.RegistrationController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.RemoteConfigController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.SecureStorageController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery2Controller;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery3Controller;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.StickerController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.SubscriptionController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.VerificationController;
|
||||||
|
import org.whispersystems.textsecuregcm.currency.CoinMarketCapClient;
|
||||||
|
import org.whispersystems.textsecuregcm.currency.CurrencyConversionManager;
|
||||||
|
import org.whispersystems.textsecuregcm.currency.FixerClient;
|
||||||
|
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
||||||
|
import org.whispersystems.textsecuregcm.filters.RemoteDeprecationFilter;
|
||||||
|
import org.whispersystems.textsecuregcm.filters.RequestStatisticsFilter;
|
||||||
|
import org.whispersystems.textsecuregcm.filters.TimestampResponseFilter;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.AcceptLanguageInterceptor;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.AccountsAnonymousGrpcService;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.AccountsGrpcService;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.ErrorMappingInterceptor;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.ExternalServiceCredentialsAnonymousGrpcService;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.ExternalServiceCredentialsGrpcService;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.GrpcServerManagedWrapper;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.KeysAnonymousGrpcService;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.KeysGrpcService;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.PaymentsGrpcService;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.ProfileAnonymousGrpcService;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.ProfileGrpcService;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.UserAgentInterceptor;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.CardinalityEstimator;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.PushChallengeManager;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
|
import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.mappers.GrpcStatusRuntimeExceptionMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.mappers.InvalidWebsocketAddressExceptionMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.mappers.JsonMappingExceptionMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.mappers.RegistrationServiceSenderExceptionMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.mappers.ServerRejectedExceptionMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.mappers.SubscriptionProcessorExceptionMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.metrics.MetricsApplicationEventListener;
|
||||||
|
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||||
|
import org.whispersystems.textsecuregcm.metrics.ReportedMessageMetricsListener;
|
||||||
|
import org.whispersystems.textsecuregcm.metrics.TrafficSource;
|
||||||
|
import org.whispersystems.textsecuregcm.providers.MultiRecipientMessageProvider;
|
||||||
|
import org.whispersystems.textsecuregcm.providers.RedisClusterHealthCheck;
|
||||||
|
import org.whispersystems.textsecuregcm.push.APNSender;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ApnPushNotificationScheduler;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
||||||
|
import org.whispersystems.textsecuregcm.push.FcmSender;
|
||||||
|
import org.whispersystems.textsecuregcm.push.MessageSender;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ProvisioningManager;
|
||||||
|
import org.whispersystems.textsecuregcm.push.PushLatencyManager;
|
||||||
|
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ReceiptSender;
|
||||||
|
import org.whispersystems.textsecuregcm.redis.ConnectionEventLogger;
|
||||||
|
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||||
|
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
|
||||||
|
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||||
|
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||||
|
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
|
||||||
|
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client;
|
||||||
|
import org.whispersystems.textsecuregcm.spam.FilterSpam;
|
||||||
|
import org.whispersystems.textsecuregcm.spam.PushChallengeConfigProvider;
|
||||||
|
import org.whispersystems.textsecuregcm.spam.RateLimitChallengeListener;
|
||||||
|
import org.whispersystems.textsecuregcm.spam.ReportSpamTokenProvider;
|
||||||
|
import org.whispersystems.textsecuregcm.spam.ScoreThresholdProvider;
|
||||||
|
import org.whispersystems.textsecuregcm.spam.SenderOverrideProvider;
|
||||||
|
import org.whispersystems.textsecuregcm.spam.SpamFilter;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountLockManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Accounts;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.ChangeNumberManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.ClientPublicKeys;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.ClientPublicKeysManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.ClientReleaseManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.ClientReleases;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.KeysManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.MessagesCache;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.OneTimeDonationsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Profiles;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.PushChallengeDynamoDb;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.RedeemedReceiptsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswords;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.RemoteConfigs;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.VerificationSessionManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.VerificationSessions;
|
||||||
|
import org.whispersystems.textsecuregcm.subscriptions.BankMandateTranslator;
|
||||||
|
import org.whispersystems.textsecuregcm.subscriptions.BraintreeManager;
|
||||||
|
import org.whispersystems.textsecuregcm.subscriptions.StripeManager;
|
||||||
|
import org.whispersystems.textsecuregcm.util.DynamoDbFromConfig;
|
||||||
|
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.util.UsernameHashZkProofVerifier;
|
||||||
|
import org.whispersystems.textsecuregcm.util.logging.LoggingUnhandledExceptionMapper;
|
||||||
|
import org.whispersystems.textsecuregcm.util.logging.UncaughtExceptionHandler;
|
||||||
|
import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener;
|
||||||
|
import org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener;
|
||||||
|
import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
|
||||||
|
import org.whispersystems.textsecuregcm.workers.AssignUsernameCommand;
|
||||||
|
import org.whispersystems.textsecuregcm.workers.CertificateCommand;
|
||||||
|
import org.whispersystems.textsecuregcm.workers.CheckDynamicConfigurationCommand;
|
||||||
|
import org.whispersystems.textsecuregcm.workers.DeleteUserCommand;
|
||||||
|
import org.whispersystems.textsecuregcm.workers.MessagePersisterServiceCommand;
|
||||||
|
import org.whispersystems.textsecuregcm.workers.ProcessPushNotificationFeedbackCommand;
|
||||||
|
import org.whispersystems.textsecuregcm.workers.RemoveExpiredAccountsCommand;
|
||||||
|
import org.whispersystems.textsecuregcm.workers.RemoveExpiredLinkedDevicesCommand;
|
||||||
|
import org.whispersystems.textsecuregcm.workers.ScheduledApnPushNotificationSenderServiceCommand;
|
||||||
|
import org.whispersystems.textsecuregcm.workers.ServerVersionCommand;
|
||||||
|
import org.whispersystems.textsecuregcm.workers.SetRequestLoggingEnabledTask;
|
||||||
|
import org.whispersystems.textsecuregcm.workers.SetUserDiscoverabilityCommand;
|
||||||
|
import org.whispersystems.textsecuregcm.workers.UnlinkDeviceCommand;
|
||||||
|
import org.whispersystems.textsecuregcm.workers.ZkParamsCommand;
|
||||||
|
import org.whispersystems.websocket.WebSocketResourceProviderFactory;
|
||||||
|
import org.whispersystems.websocket.setup.WebSocketEnvironment;
|
||||||
|
import reactor.core.scheduler.Scheduler;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||||
|
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||||
|
import software.amazon.awssdk.auth.credentials.WebIdentityTokenFileCredentialsProvider;
|
||||||
|
import software.amazon.awssdk.regions.Region;
|
||||||
|
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||||
|
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
|
||||||
|
import software.amazon.awssdk.services.s3.S3AsyncClient;
|
||||||
|
import software.amazon.awssdk.services.s3.S3Client;
|
||||||
|
|
||||||
|
public class WhisperServerService extends Application<WhisperServerConfiguration> {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(WhisperServerService.class);
|
||||||
|
|
||||||
|
public static final String SECRETS_BUNDLE_FILE_NAME_PROPERTY = "secrets.bundle.filename";
|
||||||
|
|
||||||
|
public static final software.amazon.awssdk.auth.credentials.AwsCredentialsProvider AWSSDK_CREDENTIALS_PROVIDER =
|
||||||
|
WebIdentityTokenFileCredentialsProvider.create();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize(final Bootstrap<WhisperServerConfiguration> bootstrap) {
|
||||||
|
// `SecretStore` needs to be initialized before Dropwizard reads the main application config file.
|
||||||
|
final String secretsBundleFileName = requireNonNull(
|
||||||
|
System.getProperty(SECRETS_BUNDLE_FILE_NAME_PROPERTY),
|
||||||
|
"Application requires property [%s] to be provided".formatted(SECRETS_BUNDLE_FILE_NAME_PROPERTY));
|
||||||
|
final SecretStore secretStore = SecretStore.fromYamlFileSecretsBundle(secretsBundleFileName);
|
||||||
|
SecretsModule.INSTANCE.setSecretStore(secretStore);
|
||||||
|
|
||||||
|
// Initializing SystemMapper here because parsing of the main application config happens before `run()` method is called.
|
||||||
|
SystemMapper.configureMapper(bootstrap.getObjectMapper());
|
||||||
|
|
||||||
|
// Enable variable substitution with environment variables
|
||||||
|
// https://www.dropwizard.io/en/stable/manual/core.html#environment-variables
|
||||||
|
final EnvironmentVariableSubstitutor substitutor = new EnvironmentVariableSubstitutor(true);
|
||||||
|
final SubstitutingSourceProvider provider =
|
||||||
|
new SubstitutingSourceProvider(bootstrap.getConfigurationSourceProvider(), substitutor);
|
||||||
|
bootstrap.setConfigurationSourceProvider(provider);
|
||||||
|
|
||||||
|
bootstrap.addCommand(new DeleteUserCommand());
|
||||||
|
bootstrap.addCommand(new CertificateCommand());
|
||||||
|
bootstrap.addCommand(new ZkParamsCommand());
|
||||||
|
bootstrap.addCommand(new ServerVersionCommand());
|
||||||
|
bootstrap.addCommand(new CheckDynamicConfigurationCommand());
|
||||||
|
bootstrap.addCommand(new SetUserDiscoverabilityCommand());
|
||||||
|
bootstrap.addCommand(new AssignUsernameCommand());
|
||||||
|
bootstrap.addCommand(new UnlinkDeviceCommand());
|
||||||
|
bootstrap.addCommand(new ScheduledApnPushNotificationSenderServiceCommand());
|
||||||
|
bootstrap.addCommand(new MessagePersisterServiceCommand());
|
||||||
|
bootstrap.addCommand(new RemoveExpiredAccountsCommand(Clock.systemUTC()));
|
||||||
|
bootstrap.addCommand(new ProcessPushNotificationFeedbackCommand(Clock.systemUTC()));
|
||||||
|
bootstrap.addCommand(new RemoveExpiredLinkedDevicesCommand());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "whisper-server";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(WhisperServerConfiguration config, Environment environment) throws Exception {
|
||||||
|
final Clock clock = Clock.systemUTC();
|
||||||
|
final int availableProcessors = Runtime.getRuntime().availableProcessors();
|
||||||
|
|
||||||
|
UncaughtExceptionHandler.register();
|
||||||
|
|
||||||
|
MetricsUtil.configureRegistries(config, environment);
|
||||||
|
|
||||||
|
final boolean useRemoteAddress = Optional.ofNullable(
|
||||||
|
System.getenv("SIGNAL_USE_REMOTE_ADDRESS"))
|
||||||
|
.isPresent();
|
||||||
|
|
||||||
|
if (config.getServerFactory() instanceof DefaultServerFactory defaultServerFactory) {
|
||||||
|
defaultServerFactory.getApplicationConnectors()
|
||||||
|
.forEach(connectorFactory -> {
|
||||||
|
if (connectorFactory instanceof HttpsConnectorFactory h) {
|
||||||
|
h.setKeyStorePassword(config.getTlsKeyStoreConfiguration().password().value());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
HeaderControlledResourceBundleLookup headerControlledResourceBundleLookup =
|
||||||
|
new HeaderControlledResourceBundleLookup();
|
||||||
|
ConfiguredProfileBadgeConverter profileBadgeConverter = new ConfiguredProfileBadgeConverter(
|
||||||
|
clock, config.getBadges(), headerControlledResourceBundleLookup);
|
||||||
|
ResourceBundleLevelTranslator resourceBundleLevelTranslator = new ResourceBundleLevelTranslator(
|
||||||
|
headerControlledResourceBundleLookup);
|
||||||
|
BankMandateTranslator bankMandateTranslator = new BankMandateTranslator(headerControlledResourceBundleLookup);
|
||||||
|
|
||||||
|
DynamoDbAsyncClient dynamoDbAsyncClient = DynamoDbFromConfig.asyncClient(config.getDynamoDbClientConfiguration(),
|
||||||
|
AWSSDK_CREDENTIALS_PROVIDER);
|
||||||
|
|
||||||
|
DynamoDbClient dynamoDbClient = DynamoDbFromConfig.client(config.getDynamoDbClientConfiguration(),
|
||||||
|
AWSSDK_CREDENTIALS_PROVIDER);
|
||||||
|
|
||||||
|
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager =
|
||||||
|
new DynamicConfigurationManager<>(config.getAppConfig().getApplication(),
|
||||||
|
config.getAppConfig().getEnvironment(),
|
||||||
|
config.getAppConfig().getConfigurationName(),
|
||||||
|
DynamicConfiguration.class);
|
||||||
|
|
||||||
|
BlockingQueue<Runnable> messageDeletionQueue = new LinkedBlockingQueue<>();
|
||||||
|
Metrics.gaugeCollectionSize(name(getClass(), "messageDeletionQueueSize"), Collections.emptyList(),
|
||||||
|
messageDeletionQueue);
|
||||||
|
ExecutorService messageDeletionAsyncExecutor = environment.lifecycle()
|
||||||
|
.executorService(name(getClass(), "messageDeletionAsyncExecutor-%d"))
|
||||||
|
.minThreads(2)
|
||||||
|
.maxThreads(2)
|
||||||
|
.allowCoreThreadTimeOut(true)
|
||||||
|
.workQueue(messageDeletionQueue).build();
|
||||||
|
|
||||||
|
Accounts accounts = new Accounts(
|
||||||
|
dynamoDbClient,
|
||||||
|
dynamoDbAsyncClient,
|
||||||
|
config.getDynamoDbTables().getAccounts().getTableName(),
|
||||||
|
config.getDynamoDbTables().getAccounts().getPhoneNumberTableName(),
|
||||||
|
config.getDynamoDbTables().getAccounts().getPhoneNumberIdentifierTableName(),
|
||||||
|
config.getDynamoDbTables().getAccounts().getUsernamesTableName(),
|
||||||
|
config.getDynamoDbTables().getDeletedAccounts().getTableName());
|
||||||
|
ClientReleases clientReleases = new ClientReleases(dynamoDbAsyncClient,
|
||||||
|
config.getDynamoDbTables().getClientReleases().getTableName());
|
||||||
|
PhoneNumberIdentifiers phoneNumberIdentifiers = new PhoneNumberIdentifiers(dynamoDbClient,
|
||||||
|
config.getDynamoDbTables().getPhoneNumberIdentifiers().getTableName());
|
||||||
|
Profiles profiles = new Profiles(dynamoDbClient, dynamoDbAsyncClient,
|
||||||
|
config.getDynamoDbTables().getProfiles().getTableName());
|
||||||
|
KeysManager keysManager = new KeysManager(
|
||||||
|
dynamoDbAsyncClient,
|
||||||
|
config.getDynamoDbTables().getEcKeys().getTableName(),
|
||||||
|
config.getDynamoDbTables().getKemKeys().getTableName(),
|
||||||
|
config.getDynamoDbTables().getEcSignedPreKeys().getTableName(),
|
||||||
|
config.getDynamoDbTables().getKemLastResortKeys().getTableName()
|
||||||
|
);
|
||||||
|
MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(dynamoDbClient, dynamoDbAsyncClient,
|
||||||
|
config.getDynamoDbTables().getMessages().getTableName(),
|
||||||
|
config.getDynamoDbTables().getMessages().getExpiration(),
|
||||||
|
messageDeletionAsyncExecutor);
|
||||||
|
RemoteConfigs remoteConfigs = new RemoteConfigs(dynamoDbClient,
|
||||||
|
config.getDynamoDbTables().getRemoteConfig().getTableName());
|
||||||
|
PushChallengeDynamoDb pushChallengeDynamoDb = new PushChallengeDynamoDb(dynamoDbClient,
|
||||||
|
config.getDynamoDbTables().getPushChallenge().getTableName());
|
||||||
|
ReportMessageDynamoDb reportMessageDynamoDb = new ReportMessageDynamoDb(dynamoDbClient,
|
||||||
|
config.getDynamoDbTables().getReportMessage().getTableName(),
|
||||||
|
config.getReportMessageConfiguration().getReportTtl());
|
||||||
|
RegistrationRecoveryPasswords registrationRecoveryPasswords = new RegistrationRecoveryPasswords(
|
||||||
|
config.getDynamoDbTables().getRegistrationRecovery().getTableName(),
|
||||||
|
config.getDynamoDbTables().getRegistrationRecovery().getExpiration(),
|
||||||
|
dynamoDbClient,
|
||||||
|
dynamoDbAsyncClient
|
||||||
|
);
|
||||||
|
ClientPublicKeys clientPublicKeys =
|
||||||
|
new ClientPublicKeys(dynamoDbAsyncClient, config.getDynamoDbTables().getClientPublicKeys().getTableName());
|
||||||
|
|
||||||
|
final VerificationSessions verificationSessions = new VerificationSessions(dynamoDbAsyncClient,
|
||||||
|
config.getDynamoDbTables().getVerificationSessions().getTableName(), clock);
|
||||||
|
|
||||||
|
final ClientResources redisClientResources = ClientResources.builder()
|
||||||
|
.commandLatencyRecorder(new MicrometerCommandLatencyRecorder(Metrics.globalRegistry, MicrometerOptions.builder().build()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ConnectionEventLogger.logConnectionEvents(redisClientResources);
|
||||||
|
|
||||||
|
FaultTolerantRedisCluster cacheCluster = new FaultTolerantRedisCluster("main_cache_cluster", config.getCacheClusterConfiguration(), redisClientResources);
|
||||||
|
FaultTolerantRedisCluster messagesCluster = new FaultTolerantRedisCluster("messages_cluster", config.getMessageCacheConfiguration().getRedisClusterConfiguration(), redisClientResources);
|
||||||
|
FaultTolerantRedisCluster clientPresenceCluster = new FaultTolerantRedisCluster("client_presence_cluster", config.getClientPresenceClusterConfiguration(), redisClientResources);
|
||||||
|
FaultTolerantRedisCluster metricsCluster = new FaultTolerantRedisCluster("metrics_cluster", config.getMetricsClusterConfiguration(), redisClientResources);
|
||||||
|
FaultTolerantRedisCluster pushSchedulerCluster = new FaultTolerantRedisCluster("push_scheduler", config.getPushSchedulerCluster(), redisClientResources);
|
||||||
|
FaultTolerantRedisCluster rateLimitersCluster = new FaultTolerantRedisCluster("rate_limiters", config.getRateLimitersCluster(), redisClientResources);
|
||||||
|
|
||||||
|
final BlockingQueue<Runnable> keyspaceNotificationDispatchQueue = new ArrayBlockingQueue<>(100_000);
|
||||||
|
Metrics.gaugeCollectionSize(name(getClass(), "keyspaceNotificationDispatchQueueSize"), Collections.emptyList(),
|
||||||
|
keyspaceNotificationDispatchQueue);
|
||||||
|
final BlockingQueue<Runnable> receiptSenderQueue = new LinkedBlockingQueue<>();
|
||||||
|
Metrics.gaugeCollectionSize(name(getClass(), "receiptSenderQueue"), Collections.emptyList(), receiptSenderQueue);
|
||||||
|
final BlockingQueue<Runnable> fcmSenderQueue = new LinkedBlockingQueue<>();
|
||||||
|
Metrics.gaugeCollectionSize(name(getClass(), "fcmSenderQueue"), Collections.emptyList(), fcmSenderQueue);
|
||||||
|
final BlockingQueue<Runnable> messageDeliveryQueue = new LinkedBlockingQueue<>();
|
||||||
|
Metrics.gaugeCollectionSize(MetricsUtil.name(getClass(), "messageDeliveryQueue"), Collections.emptyList(),
|
||||||
|
messageDeliveryQueue);
|
||||||
|
|
||||||
|
ScheduledExecutorService recurringJobExecutor = environment.lifecycle()
|
||||||
|
.scheduledExecutorService(name(getClass(), "recurringJob-%d")).threads(6).build();
|
||||||
|
ScheduledExecutorService websocketScheduledExecutor = environment.lifecycle()
|
||||||
|
.scheduledExecutorService(name(getClass(), "websocket-%d")).threads(8).build();
|
||||||
|
ExecutorService keyspaceNotificationDispatchExecutor = ExecutorServiceMetrics.monitor(Metrics.globalRegistry,
|
||||||
|
environment.lifecycle()
|
||||||
|
.executorService(name(getClass(), "keyspaceNotification-%d"))
|
||||||
|
.maxThreads(16)
|
||||||
|
.workQueue(keyspaceNotificationDispatchQueue)
|
||||||
|
.build(),
|
||||||
|
MetricsUtil.name(getClass(), "keyspaceNotificationExecutor"),
|
||||||
|
MetricsUtil.PREFIX);
|
||||||
|
ExecutorService apnSenderExecutor = environment.lifecycle().executorService(name(getClass(), "apnSender-%d"))
|
||||||
|
.maxThreads(1).minThreads(1).build();
|
||||||
|
ExecutorService fcmSenderExecutor = environment.lifecycle().executorService(name(getClass(), "fcmSender-%d"))
|
||||||
|
.maxThreads(32).minThreads(32).workQueue(fcmSenderQueue).build();
|
||||||
|
ExecutorService secureValueRecoveryServiceExecutor = environment.lifecycle()
|
||||||
|
.executorService(name(getClass(), "secureValueRecoveryService-%d")).maxThreads(1).minThreads(1).build();
|
||||||
|
ExecutorService storageServiceExecutor = environment.lifecycle()
|
||||||
|
.executorService(name(getClass(), "storageService-%d")).maxThreads(1).minThreads(1).build();
|
||||||
|
ScheduledExecutorService secureValueRecoveryServiceRetryExecutor = environment.lifecycle()
|
||||||
|
.scheduledExecutorService(name(getClass(), "secureValueRecoveryServiceRetry-%d")).threads(1).build();
|
||||||
|
ScheduledExecutorService storageServiceRetryExecutor = environment.lifecycle()
|
||||||
|
.scheduledExecutorService(name(getClass(), "storageServiceRetry-%d")).threads(1).build();
|
||||||
|
ScheduledExecutorService hcaptchaRetryExecutor = environment.lifecycle()
|
||||||
|
.scheduledExecutorService(name(getClass(), "hCaptchaRetry-%d")).threads(1).build();
|
||||||
|
ScheduledExecutorService remoteStorageExecutor = environment.lifecycle()
|
||||||
|
.scheduledExecutorService(name(getClass(), "remoteStorageRetry-%d")).threads(1).build();
|
||||||
|
ScheduledExecutorService registrationIdentityTokenRefreshExecutor = environment.lifecycle()
|
||||||
|
.scheduledExecutorService(name(getClass(), "registrationIdentityTokenRefresh-%d")).threads(1).build();
|
||||||
|
|
||||||
|
Scheduler messageDeliveryScheduler = Schedulers.fromExecutorService(
|
||||||
|
ExecutorServiceMetrics.monitor(Metrics.globalRegistry,
|
||||||
|
environment.lifecycle().executorService(name(getClass(), "messageDelivery-%d"))
|
||||||
|
.minThreads(20)
|
||||||
|
.maxThreads(20)
|
||||||
|
.workQueue(messageDeliveryQueue)
|
||||||
|
.build(),
|
||||||
|
MetricsUtil.name(getClass(), "messageDeliveryExecutor"), MetricsUtil.PREFIX),
|
||||||
|
"messageDelivery");
|
||||||
|
|
||||||
|
// TODO: generally speaking this is a DynamoDB I/O executor for the accounts table; we should eventually have a general executor for speaking to the accounts table, but most of the server is still synchronous so this isn't widely useful yet
|
||||||
|
ExecutorService batchIdentityCheckExecutor = environment.lifecycle().executorService(name(getClass(), "batchIdentityCheck-%d")).minThreads(32).maxThreads(32).build();
|
||||||
|
ExecutorService multiRecipientMessageExecutor = environment.lifecycle()
|
||||||
|
.executorService(name(getClass(), "multiRecipientMessage-%d")).minThreads(64).maxThreads(64).build();
|
||||||
|
ExecutorService subscriptionProcessorExecutor = environment.lifecycle()
|
||||||
|
.executorService(name(getClass(), "subscriptionProcessor-%d"))
|
||||||
|
.maxThreads(availableProcessors) // mostly this is IO bound so tying to number of processors is tenuous at best
|
||||||
|
.minThreads(availableProcessors) // mostly this is IO bound so tying to number of processors is tenuous at best
|
||||||
|
.allowCoreThreadTimeOut(true).
|
||||||
|
build();
|
||||||
|
ExecutorService receiptSenderExecutor = environment.lifecycle()
|
||||||
|
.executorService(name(getClass(), "receiptSender-%d"))
|
||||||
|
.maxThreads(2)
|
||||||
|
.minThreads(2)
|
||||||
|
.workQueue(receiptSenderQueue)
|
||||||
|
.rejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy())
|
||||||
|
.build();
|
||||||
|
ExecutorService registrationCallbackExecutor = environment.lifecycle()
|
||||||
|
.executorService(name(getClass(), "registration-%d"))
|
||||||
|
.maxThreads(2)
|
||||||
|
.minThreads(2)
|
||||||
|
.build();
|
||||||
|
ExecutorService accountLockExecutor = environment.lifecycle()
|
||||||
|
.executorService(name(getClass(), "accountLock-%d"))
|
||||||
|
.minThreads(8)
|
||||||
|
.maxThreads(8)
|
||||||
|
.build();
|
||||||
|
ExecutorService clientPresenceExecutor = environment.lifecycle()
|
||||||
|
.executorService(name(getClass(), "clientPresence-%d"))
|
||||||
|
.minThreads(8)
|
||||||
|
.maxThreads(8)
|
||||||
|
.build();
|
||||||
|
ScheduledExecutorService subscriptionProcessorRetryExecutor = environment.lifecycle()
|
||||||
|
.scheduledExecutorService(name(getClass(), "subscriptionProcessorRetry-%d")).threads(1).build();
|
||||||
|
|
||||||
|
StripeManager stripeManager = new StripeManager(config.getStripe().apiKey().value(), subscriptionProcessorExecutor,
|
||||||
|
config.getStripe().idempotencyKeyGenerator().value(), config.getStripe().boostDescription(), config.getStripe().supportedCurrenciesByPaymentMethod());
|
||||||
|
BraintreeManager braintreeManager = new BraintreeManager(config.getBraintree().merchantId(),
|
||||||
|
config.getBraintree().publicKey(), config.getBraintree().privateKey().value(),
|
||||||
|
config.getBraintree().environment(),
|
||||||
|
config.getBraintree().supportedCurrenciesByPaymentMethod(), config.getBraintree().merchantAccounts(),
|
||||||
|
config.getBraintree().graphqlUrl(), config.getBraintree().circuitBreaker(), subscriptionProcessorExecutor,
|
||||||
|
subscriptionProcessorRetryExecutor);
|
||||||
|
|
||||||
|
ExternalServiceCredentialsGenerator directoryV2CredentialsGenerator = DirectoryV2Controller.credentialsGenerator(
|
||||||
|
config.getDirectoryV2Configuration().getDirectoryV2ClientConfiguration());
|
||||||
|
ExternalServiceCredentialsGenerator storageCredentialsGenerator = SecureStorageController.credentialsGenerator(
|
||||||
|
config.getSecureStorageServiceConfiguration());
|
||||||
|
ExternalServiceCredentialsGenerator paymentsCredentialsGenerator = PaymentsController.credentialsGenerator(
|
||||||
|
config.getPaymentsServiceConfiguration());
|
||||||
|
ExternalServiceCredentialsGenerator artCredentialsGenerator = ArtController.credentialsGenerator(
|
||||||
|
config.getArtServiceConfiguration());
|
||||||
|
ExternalServiceCredentialsGenerator svr2CredentialsGenerator = SecureValueRecovery2Controller.credentialsGenerator(
|
||||||
|
config.getSvr2Configuration());
|
||||||
|
ExternalServiceCredentialsGenerator svr3CredentialsGenerator = SecureValueRecovery3Controller.credentialsGenerator(
|
||||||
|
config.getSvr3Configuration());
|
||||||
|
|
||||||
|
dynamicConfigurationManager.start();
|
||||||
|
|
||||||
|
ExperimentEnrollmentManager experimentEnrollmentManager = new ExperimentEnrollmentManager(
|
||||||
|
dynamicConfigurationManager);
|
||||||
|
RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = new RegistrationRecoveryPasswordsManager(
|
||||||
|
registrationRecoveryPasswords);
|
||||||
|
UsernameHashZkProofVerifier usernameHashZkProofVerifier = new UsernameHashZkProofVerifier();
|
||||||
|
|
||||||
|
RegistrationServiceClient registrationServiceClient = new RegistrationServiceClient(
|
||||||
|
config.getRegistrationServiceConfiguration().host(),
|
||||||
|
config.getRegistrationServiceConfiguration().port(),
|
||||||
|
config.getRegistrationServiceConfiguration().credentialConfigurationJson(),
|
||||||
|
config.getRegistrationServiceConfiguration().identityTokenAudience(),
|
||||||
|
config.getRegistrationServiceConfiguration().registrationCaCertificate(),
|
||||||
|
registrationCallbackExecutor,
|
||||||
|
registrationIdentityTokenRefreshExecutor);
|
||||||
|
SecureValueRecovery2Client secureValueRecovery2Client = new SecureValueRecovery2Client(svr2CredentialsGenerator,
|
||||||
|
secureValueRecoveryServiceExecutor, secureValueRecoveryServiceRetryExecutor, config.getSvr2Configuration());
|
||||||
|
SecureStorageClient secureStorageClient = new SecureStorageClient(storageCredentialsGenerator,
|
||||||
|
storageServiceExecutor, storageServiceRetryExecutor, config.getSecureStorageServiceConfiguration());
|
||||||
|
ClientPresenceManager clientPresenceManager = new ClientPresenceManager(clientPresenceCluster, recurringJobExecutor,
|
||||||
|
keyspaceNotificationDispatchExecutor);
|
||||||
|
ProfilesManager profilesManager = new ProfilesManager(profiles, cacheCluster);
|
||||||
|
MessagesCache messagesCache = new MessagesCache(messagesCluster, messagesCluster,
|
||||||
|
keyspaceNotificationDispatchExecutor, messageDeliveryScheduler, messageDeletionAsyncExecutor, clock);
|
||||||
|
ClientReleaseManager clientReleaseManager = new ClientReleaseManager(clientReleases,
|
||||||
|
recurringJobExecutor,
|
||||||
|
config.getClientReleaseConfiguration().refreshInterval(),
|
||||||
|
Clock.systemUTC());
|
||||||
|
PushLatencyManager pushLatencyManager = new PushLatencyManager(metricsCluster, clientReleaseManager);
|
||||||
|
ReportMessageManager reportMessageManager = new ReportMessageManager(reportMessageDynamoDb, rateLimitersCluster,
|
||||||
|
config.getReportMessageConfiguration().getCounterTtl());
|
||||||
|
MessagesManager messagesManager = new MessagesManager(messagesDynamoDb, messagesCache, reportMessageManager,
|
||||||
|
messageDeletionAsyncExecutor);
|
||||||
|
AccountLockManager accountLockManager = new AccountLockManager(dynamoDbClient,
|
||||||
|
config.getDynamoDbTables().getDeletedAccountsLock().getTableName());
|
||||||
|
ClientPublicKeysManager clientPublicKeysManager = new ClientPublicKeysManager(clientPublicKeys);
|
||||||
|
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
|
||||||
|
accountLockManager, keysManager, messagesManager, profilesManager,
|
||||||
|
secureStorageClient, secureValueRecovery2Client,
|
||||||
|
clientPresenceManager,
|
||||||
|
experimentEnrollmentManager, registrationRecoveryPasswordsManager, accountLockExecutor, clientPresenceExecutor,
|
||||||
|
clock);
|
||||||
|
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
|
||||||
|
APNSender apnSender = new APNSender(apnSenderExecutor, config.getApnConfiguration());
|
||||||
|
FcmSender fcmSender = new FcmSender(fcmSenderExecutor, config.getFcmConfiguration().credentials().value());
|
||||||
|
ApnPushNotificationScheduler apnPushNotificationScheduler = new ApnPushNotificationScheduler(pushSchedulerCluster,
|
||||||
|
apnSender, accountsManager, 0);
|
||||||
|
PushNotificationManager pushNotificationManager = new PushNotificationManager(accountsManager, apnSender, fcmSender,
|
||||||
|
apnPushNotificationScheduler, pushLatencyManager);
|
||||||
|
RateLimiters rateLimiters = RateLimiters.createAndValidate(config.getLimitsConfiguration(),
|
||||||
|
dynamicConfigurationManager, rateLimitersCluster);
|
||||||
|
ProvisioningManager provisioningManager = new ProvisioningManager(config.getPubsubCacheConfiguration().getUri(),
|
||||||
|
redisClientResources, config.getPubsubCacheConfiguration().getTimeout(),
|
||||||
|
config.getPubsubCacheConfiguration().getCircuitBreakerConfiguration());
|
||||||
|
IssuedReceiptsManager issuedReceiptsManager = new IssuedReceiptsManager(
|
||||||
|
config.getDynamoDbTables().getIssuedReceipts().getTableName(),
|
||||||
|
config.getDynamoDbTables().getIssuedReceipts().getExpiration(),
|
||||||
|
dynamoDbAsyncClient,
|
||||||
|
config.getDynamoDbTables().getIssuedReceipts().getGenerator());
|
||||||
|
OneTimeDonationsManager oneTimeDonationsManager = new OneTimeDonationsManager(
|
||||||
|
config.getDynamoDbTables().getOnetimeDonations().getTableName(), config.getDynamoDbTables().getOnetimeDonations().getExpiration(), dynamoDbAsyncClient);
|
||||||
|
RedeemedReceiptsManager redeemedReceiptsManager = new RedeemedReceiptsManager(clock,
|
||||||
|
config.getDynamoDbTables().getRedeemedReceipts().getTableName(),
|
||||||
|
dynamoDbAsyncClient,
|
||||||
|
config.getDynamoDbTables().getRedeemedReceipts().getExpiration());
|
||||||
|
SubscriptionManager subscriptionManager = new SubscriptionManager(
|
||||||
|
config.getDynamoDbTables().getSubscriptions().getTableName(), dynamoDbAsyncClient);
|
||||||
|
|
||||||
|
final RegistrationLockVerificationManager registrationLockVerificationManager = new RegistrationLockVerificationManager(
|
||||||
|
accountsManager, clientPresenceManager, svr2CredentialsGenerator, registrationRecoveryPasswordsManager, pushNotificationManager, rateLimiters);
|
||||||
|
final PhoneVerificationTokenManager phoneVerificationTokenManager = new PhoneVerificationTokenManager(
|
||||||
|
registrationServiceClient, registrationRecoveryPasswordsManager);
|
||||||
|
|
||||||
|
final ReportedMessageMetricsListener reportedMessageMetricsListener = new ReportedMessageMetricsListener(
|
||||||
|
accountsManager);
|
||||||
|
reportMessageManager.addListener(reportedMessageMetricsListener);
|
||||||
|
|
||||||
|
final AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager);
|
||||||
|
|
||||||
|
final MessageSender messageSender = new MessageSender(clientPresenceManager, messagesManager,
|
||||||
|
pushNotificationManager,
|
||||||
|
pushLatencyManager);
|
||||||
|
final ReceiptSender receiptSender = new ReceiptSender(accountsManager, messageSender, receiptSenderExecutor);
|
||||||
|
final TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(dynamicConfigurationManager,
|
||||||
|
config.getTurnSecretConfiguration().secret().value());
|
||||||
|
|
||||||
|
final CardinalityEstimator messageByteLimitCardinalityEstimator = new CardinalityEstimator(
|
||||||
|
rateLimitersCluster,
|
||||||
|
"message_byte_limit",
|
||||||
|
config.getMessageByteLimitCardinalityEstimator().period());
|
||||||
|
|
||||||
|
RecaptchaClient recaptchaClient = new RecaptchaClient(
|
||||||
|
config.getRecaptchaConfiguration().projectPath(),
|
||||||
|
config.getRecaptchaConfiguration().credentialConfigurationJson(),
|
||||||
|
dynamicConfigurationManager);
|
||||||
|
HCaptchaClient hCaptchaClient = new HCaptchaClient(
|
||||||
|
config.getHCaptchaConfiguration().getApiKey().value(),
|
||||||
|
hcaptchaRetryExecutor,
|
||||||
|
config.getHCaptchaConfiguration().getCircuitBreaker(),
|
||||||
|
config.getHCaptchaConfiguration().getRetry(),
|
||||||
|
dynamicConfigurationManager);
|
||||||
|
HttpClient shortCodeRetrieverHttpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2)
|
||||||
|
.connectTimeout(Duration.ofSeconds(10)).build();
|
||||||
|
ShortCodeExpander shortCodeRetriever = new ShortCodeExpander(shortCodeRetrieverHttpClient, config.getShortCodeRetrieverConfiguration().baseUrl());
|
||||||
|
CaptchaChecker captchaChecker = new CaptchaChecker(shortCodeRetriever, List.of(recaptchaClient, hCaptchaClient));
|
||||||
|
|
||||||
|
PushChallengeManager pushChallengeManager = new PushChallengeManager(pushNotificationManager,
|
||||||
|
pushChallengeDynamoDb);
|
||||||
|
|
||||||
|
ChangeNumberManager changeNumberManager = new ChangeNumberManager(messageSender, accountsManager);
|
||||||
|
|
||||||
|
HttpClient currencyClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).connectTimeout(Duration.ofSeconds(10)).build();
|
||||||
|
FixerClient fixerClient = new FixerClient(currencyClient, config.getPaymentsServiceConfiguration().fixerApiKey().value());
|
||||||
|
CoinMarketCapClient coinMarketCapClient = new CoinMarketCapClient(currencyClient, config.getPaymentsServiceConfiguration().coinMarketCapApiKey().value(), config.getPaymentsServiceConfiguration().coinMarketCapCurrencyIds());
|
||||||
|
CurrencyConversionManager currencyManager = new CurrencyConversionManager(fixerClient, coinMarketCapClient,
|
||||||
|
cacheCluster, config.getPaymentsServiceConfiguration().paymentCurrencies(), recurringJobExecutor, Clock.systemUTC());
|
||||||
|
|
||||||
|
environment.lifecycle().manage(apnSender);
|
||||||
|
environment.lifecycle().manage(apnPushNotificationScheduler);
|
||||||
|
environment.lifecycle().manage(provisioningManager);
|
||||||
|
environment.lifecycle().manage(messagesCache);
|
||||||
|
environment.lifecycle().manage(clientPresenceManager);
|
||||||
|
environment.lifecycle().manage(currencyManager);
|
||||||
|
environment.lifecycle().manage(registrationServiceClient);
|
||||||
|
environment.lifecycle().manage(clientReleaseManager);
|
||||||
|
|
||||||
|
final RegistrationCaptchaManager registrationCaptchaManager = new RegistrationCaptchaManager(captchaChecker,
|
||||||
|
rateLimiters, config.getTestDevices(), dynamicConfigurationManager);
|
||||||
|
|
||||||
|
StaticCredentialsProvider cdnCredentialsProvider = StaticCredentialsProvider
|
||||||
|
.create(AwsBasicCredentials.create(
|
||||||
|
config.getCdnConfiguration().accessKey().value(),
|
||||||
|
config.getCdnConfiguration().accessSecret().value()));
|
||||||
|
S3Client cdnS3Client = S3Client.builder()
|
||||||
|
.credentialsProvider(cdnCredentialsProvider)
|
||||||
|
.region(Region.of(config.getCdnConfiguration().region()))
|
||||||
|
.build();
|
||||||
|
S3AsyncClient asyncCdnS3Client = S3AsyncClient.builder()
|
||||||
|
.credentialsProvider(cdnCredentialsProvider)
|
||||||
|
.region(Region.of(config.getCdnConfiguration().region()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final GcsAttachmentGenerator gcsAttachmentGenerator = new GcsAttachmentGenerator(
|
||||||
|
config.getGcpAttachmentsConfiguration().domain(),
|
||||||
|
config.getGcpAttachmentsConfiguration().email(),
|
||||||
|
config.getGcpAttachmentsConfiguration().maxSizeInBytes(),
|
||||||
|
config.getGcpAttachmentsConfiguration().pathPrefix(),
|
||||||
|
config.getGcpAttachmentsConfiguration().rsaSigningKey().value());
|
||||||
|
|
||||||
|
PostPolicyGenerator profileCdnPolicyGenerator = new PostPolicyGenerator(config.getCdnConfiguration().region(),
|
||||||
|
config.getCdnConfiguration().bucket(), config.getCdnConfiguration().accessKey().value());
|
||||||
|
PolicySigner profileCdnPolicySigner = new PolicySigner(config.getCdnConfiguration().accessSecret().value(),
|
||||||
|
config.getCdnConfiguration().region());
|
||||||
|
|
||||||
|
ServerSecretParams zkSecretParams = new ServerSecretParams(config.getZkConfig().serverSecret().value());
|
||||||
|
GenericServerSecretParams callingGenericZkSecretParams = new GenericServerSecretParams(config.getCallingZkConfig().serverSecret().value());
|
||||||
|
GenericServerSecretParams backupsGenericZkSecretParams = new GenericServerSecretParams(config.getBackupsZkConfig().serverSecret().value());
|
||||||
|
ServerZkProfileOperations zkProfileOperations = new ServerZkProfileOperations(zkSecretParams);
|
||||||
|
ServerZkAuthOperations zkAuthOperations = new ServerZkAuthOperations(zkSecretParams);
|
||||||
|
ServerZkReceiptOperations zkReceiptOperations = new ServerZkReceiptOperations(zkSecretParams);
|
||||||
|
|
||||||
|
Cdn3BackupCredentialGenerator cdn3BackupCredentialGenerator = new Cdn3BackupCredentialGenerator(config.getTus());
|
||||||
|
BackupAuthManager backupAuthManager = new BackupAuthManager(dynamicConfigurationManager, rateLimiters, accountsManager, backupsGenericZkSecretParams, clock);
|
||||||
|
BackupsDb backupsDb = new BackupsDb(
|
||||||
|
dynamoDbAsyncClient,
|
||||||
|
config.getDynamoDbTables().getBackups().getTableName(),
|
||||||
|
clock);
|
||||||
|
BackupManager backupManager = new BackupManager(
|
||||||
|
backupsDb,
|
||||||
|
backupsGenericZkSecretParams,
|
||||||
|
cdn3BackupCredentialGenerator,
|
||||||
|
new Cdn3RemoteStorageManager(
|
||||||
|
remoteStorageExecutor,
|
||||||
|
config.getClientCdnConfiguration().getCircuitBreaker(),
|
||||||
|
config.getClientCdnConfiguration().getRetry(),
|
||||||
|
config.getClientCdnConfiguration().getCaCertificates(),
|
||||||
|
config.getCdn3StorageManagerConfiguration()),
|
||||||
|
config.getClientCdnConfiguration().getAttachmentUrls(),
|
||||||
|
clock);
|
||||||
|
|
||||||
|
final BasicCredentialAuthenticationInterceptor basicCredentialAuthenticationInterceptor =
|
||||||
|
new BasicCredentialAuthenticationInterceptor(new AccountAuthenticator(accountsManager));
|
||||||
|
|
||||||
|
final ServerBuilder<?> grpcServer = ServerBuilder.forPort(config.getGrpcPort())
|
||||||
|
.addService(ServerInterceptors.intercept(new AccountsGrpcService(accountsManager, rateLimiters, usernameHashZkProofVerifier, registrationRecoveryPasswordsManager), basicCredentialAuthenticationInterceptor))
|
||||||
|
.addService(new AccountsAnonymousGrpcService(accountsManager, rateLimiters))
|
||||||
|
.addService(ExternalServiceCredentialsGrpcService.createForAllExternalServices(config, rateLimiters))
|
||||||
|
.addService(ExternalServiceCredentialsAnonymousGrpcService.create(accountsManager, config))
|
||||||
|
.addService(ServerInterceptors.intercept(new KeysGrpcService(accountsManager, keysManager, rateLimiters), basicCredentialAuthenticationInterceptor))
|
||||||
|
.addService(new KeysAnonymousGrpcService(accountsManager, keysManager))
|
||||||
|
.addService(new PaymentsGrpcService(currencyManager))
|
||||||
|
.addService(ServerInterceptors.intercept(new ProfileGrpcService(clock, accountsManager, profilesManager, dynamicConfigurationManager,
|
||||||
|
config.getBadges(), asyncCdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, profileBadgeConverter, rateLimiters, zkProfileOperations, config.getCdnConfiguration().bucket()), basicCredentialAuthenticationInterceptor))
|
||||||
|
.addService(new ProfileAnonymousGrpcService(accountsManager, profilesManager, profileBadgeConverter, zkProfileOperations));
|
||||||
|
|
||||||
|
RemoteDeprecationFilter remoteDeprecationFilter = new RemoteDeprecationFilter(dynamicConfigurationManager);
|
||||||
|
environment.servlets()
|
||||||
|
.addFilter("RemoteDeprecationFilter", remoteDeprecationFilter)
|
||||||
|
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
|
||||||
|
|
||||||
|
// Note: interceptors run in the reverse order they are added; the remote deprecation filter
|
||||||
|
// depends on the user-agent context so it has to come first here!
|
||||||
|
// http://grpc.github.io/grpc-java/javadoc/io/grpc/ServerBuilder.html#intercept-io.grpc.ServerInterceptor-
|
||||||
|
grpcServer
|
||||||
|
// TODO: specialize metrics with user-agent platform
|
||||||
|
.intercept(new MetricCollectingServerInterceptor(Metrics.globalRegistry))
|
||||||
|
.intercept(new ErrorMappingInterceptor())
|
||||||
|
.intercept(new AcceptLanguageInterceptor())
|
||||||
|
.intercept(remoteDeprecationFilter)
|
||||||
|
.intercept(new UserAgentInterceptor());
|
||||||
|
|
||||||
|
environment.lifecycle().manage(new GrpcServerManagedWrapper(grpcServer.build()));
|
||||||
|
|
||||||
|
final AuthFilter<BasicCredentials, AuthenticatedAccount> accountAuthFilter =
|
||||||
|
new BasicCredentialAuthFilter.Builder<AuthenticatedAccount>()
|
||||||
|
.setAuthenticator(accountAuthenticator)
|
||||||
|
.buildAuthFilter();
|
||||||
|
|
||||||
|
environment.jersey().register(new RequestStatisticsFilter(TrafficSource.HTTP));
|
||||||
|
environment.jersey().register(MultiRecipientMessageProvider.class);
|
||||||
|
environment.jersey().register(new MetricsApplicationEventListener(TrafficSource.HTTP, clientReleaseManager));
|
||||||
|
environment.jersey().register(new AuthDynamicFeature(accountAuthFilter));
|
||||||
|
environment.jersey().register(new AuthValueFactoryProvider.Binder<>(AuthenticatedAccount.class));
|
||||||
|
environment.jersey().register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
|
||||||
|
environment.jersey().register(new TimestampResponseFilter());
|
||||||
|
|
||||||
|
///
|
||||||
|
WebSocketEnvironment<AuthenticatedAccount> webSocketEnvironment = new WebSocketEnvironment<>(environment,
|
||||||
|
config.getWebSocketConfiguration(), Duration.ofMillis(90000));
|
||||||
|
webSocketEnvironment.setAuthenticator(new WebSocketAccountAuthenticator(accountAuthenticator));
|
||||||
|
webSocketEnvironment.setConnectListener(
|
||||||
|
new AuthenticatedConnectListener(receiptSender, messagesManager, pushNotificationManager,
|
||||||
|
clientPresenceManager, websocketScheduledExecutor, messageDeliveryScheduler, clientReleaseManager));
|
||||||
|
webSocketEnvironment.jersey()
|
||||||
|
.register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
|
||||||
|
webSocketEnvironment.jersey().register(new RequestStatisticsFilter(TrafficSource.WEBSOCKET));
|
||||||
|
webSocketEnvironment.jersey().register(MultiRecipientMessageProvider.class);
|
||||||
|
webSocketEnvironment.jersey().register(new MetricsApplicationEventListener(TrafficSource.WEBSOCKET, clientReleaseManager));
|
||||||
|
webSocketEnvironment.jersey().register(new KeepAliveController(clientPresenceManager));
|
||||||
|
|
||||||
|
boolean registeredSpamFilter = false;
|
||||||
|
ReportSpamTokenProvider reportSpamTokenProvider = null;
|
||||||
|
|
||||||
|
List<RateLimitChallengeListener> rateLimitChallengeListeners = new ArrayList<>();
|
||||||
|
for (final SpamFilter filter : ServiceLoader.load(SpamFilter.class)) {
|
||||||
|
if (filter.getClass().isAnnotationPresent(FilterSpam.class)) {
|
||||||
|
try {
|
||||||
|
filter.configure(config.getSpamFilterConfiguration().getEnvironment());
|
||||||
|
|
||||||
|
ReportSpamTokenProvider thisProvider = filter.getReportSpamTokenProvider();
|
||||||
|
if (reportSpamTokenProvider == null) {
|
||||||
|
reportSpamTokenProvider = thisProvider;
|
||||||
|
} else if (thisProvider != null) {
|
||||||
|
log.info("Multiple spam report token providers found. Using the first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.getReportedMessageListeners().forEach(reportMessageManager::addListener);
|
||||||
|
|
||||||
|
environment.lifecycle().manage(filter);
|
||||||
|
environment.jersey().register(filter);
|
||||||
|
webSocketEnvironment.jersey().register(filter);
|
||||||
|
|
||||||
|
log.info("Registered spam filter: {}", filter.getClass().getName());
|
||||||
|
registeredSpamFilter = true;
|
||||||
|
} catch (final Exception e) {
|
||||||
|
log.warn("Failed to register spam filter: {}", filter.getClass().getName(), e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("Spam filter {} not annotated with @FilterSpam and will not be installed",
|
||||||
|
filter.getClass().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter instanceof RateLimitChallengeListener) {
|
||||||
|
log.info("Registered rate limit challenge listener: {}", filter.getClass().getName());
|
||||||
|
rateLimitChallengeListeners.add((RateLimitChallengeListener) filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RateLimitChallengeManager rateLimitChallengeManager = new RateLimitChallengeManager(pushChallengeManager,
|
||||||
|
captchaChecker, rateLimiters, rateLimitChallengeListeners);
|
||||||
|
|
||||||
|
|
||||||
|
if (!registeredSpamFilter) {
|
||||||
|
log.warn("No spam filters installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reportSpamTokenProvider == null) {
|
||||||
|
log.warn("No spam-reporting token providers found; using default (no-op) provider as a default");
|
||||||
|
reportSpamTokenProvider = ReportSpamTokenProvider.noop();
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<Object> commonControllers = Lists.newArrayList(
|
||||||
|
new AccountController(accountsManager, rateLimiters, turnTokenGenerator, registrationRecoveryPasswordsManager,
|
||||||
|
usernameHashZkProofVerifier),
|
||||||
|
new AccountControllerV2(accountsManager, changeNumberManager, phoneVerificationTokenManager,
|
||||||
|
registrationLockVerificationManager, rateLimiters),
|
||||||
|
new ArtController(rateLimiters, artCredentialsGenerator),
|
||||||
|
new AttachmentControllerV2(rateLimiters, config.getAwsAttachmentsConfiguration().accessKey().value(),
|
||||||
|
config.getAwsAttachmentsConfiguration().accessSecret().value(),
|
||||||
|
config.getAwsAttachmentsConfiguration().region(), config.getAwsAttachmentsConfiguration().bucket()),
|
||||||
|
new AttachmentControllerV3(rateLimiters, gcsAttachmentGenerator),
|
||||||
|
new AttachmentControllerV4(rateLimiters, gcsAttachmentGenerator, new TusAttachmentGenerator(config.getTus()),
|
||||||
|
experimentEnrollmentManager),
|
||||||
|
new ArchiveController(backupAuthManager, backupManager),
|
||||||
|
new CallLinkController(rateLimiters, callingGenericZkSecretParams),
|
||||||
|
new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().certificate().value(),
|
||||||
|
config.getDeliveryCertificate().ecPrivateKey(), config.getDeliveryCertificate().expiresDays()),
|
||||||
|
zkAuthOperations, callingGenericZkSecretParams, clock),
|
||||||
|
new ChallengeController(rateLimitChallengeManager, useRemoteAddress),
|
||||||
|
new DeviceController(config.getLinkDeviceSecretConfiguration().secret().value(), accountsManager,
|
||||||
|
rateLimiters, rateLimitersCluster, config.getMaxDevices(), clock),
|
||||||
|
new DirectoryV2Controller(directoryV2CredentialsGenerator),
|
||||||
|
new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
|
||||||
|
ReceiptCredentialPresentation::new),
|
||||||
|
new KeysController(rateLimiters, keysManager, accountsManager),
|
||||||
|
new MessageController(rateLimiters, messageByteLimitCardinalityEstimator, messageSender, receiptSender,
|
||||||
|
accountsManager, messagesManager, pushNotificationManager, reportMessageManager,
|
||||||
|
multiRecipientMessageExecutor, messageDeliveryScheduler, reportSpamTokenProvider, clientReleaseManager,
|
||||||
|
dynamicConfigurationManager, zkSecretParams),
|
||||||
|
new PaymentsController(currencyManager, paymentsCredentialsGenerator),
|
||||||
|
new ProfileController(clock, rateLimiters, accountsManager, profilesManager, dynamicConfigurationManager,
|
||||||
|
profileBadgeConverter, config.getBadges(), cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner,
|
||||||
|
config.getCdnConfiguration().bucket(), zkProfileOperations, batchIdentityCheckExecutor),
|
||||||
|
new ProvisioningController(rateLimiters, provisioningManager),
|
||||||
|
new RegistrationController(accountsManager, phoneVerificationTokenManager, registrationLockVerificationManager,
|
||||||
|
rateLimiters),
|
||||||
|
new RemoteConfigController(remoteConfigsManager, config.getRemoteConfigConfiguration().globalConfig(), clock),
|
||||||
|
new SecureStorageController(storageCredentialsGenerator),
|
||||||
|
new SecureValueRecovery2Controller(svr2CredentialsGenerator, accountsManager),
|
||||||
|
new SecureValueRecovery3Controller(svr3CredentialsGenerator, accountsManager),
|
||||||
|
new StickerController(rateLimiters, config.getCdnConfiguration().accessKey().value(),
|
||||||
|
config.getCdnConfiguration().accessSecret().value(), config.getCdnConfiguration().region(),
|
||||||
|
config.getCdnConfiguration().bucket()),
|
||||||
|
new VerificationController(registrationServiceClient, new VerificationSessionManager(verificationSessions),
|
||||||
|
pushNotificationManager, registrationCaptchaManager, registrationRecoveryPasswordsManager, rateLimiters,
|
||||||
|
accountsManager, useRemoteAddress, dynamicConfigurationManager, clock)
|
||||||
|
);
|
||||||
|
if (config.getSubscription() != null && config.getOneTimeDonations() != null) {
|
||||||
|
commonControllers.add(new SubscriptionController(clock, config.getSubscription(), config.getOneTimeDonations(),
|
||||||
|
subscriptionManager, stripeManager, braintreeManager, zkReceiptOperations, issuedReceiptsManager, oneTimeDonationsManager,
|
||||||
|
profileBadgeConverter, resourceBundleLevelTranslator, bankMandateTranslator));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Object controller : commonControllers) {
|
||||||
|
environment.jersey().register(controller);
|
||||||
|
webSocketEnvironment.jersey().register(controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketEnvironment<AuthenticatedAccount> provisioningEnvironment = new WebSocketEnvironment<>(environment,
|
||||||
|
webSocketEnvironment.getRequestLog(), Duration.ofMillis(60000));
|
||||||
|
provisioningEnvironment.jersey().register(new WebsocketRefreshApplicationEventListener(accountsManager, clientPresenceManager));
|
||||||
|
provisioningEnvironment.setConnectListener(new ProvisioningConnectListener(provisioningManager));
|
||||||
|
provisioningEnvironment.jersey().register(new MetricsApplicationEventListener(TrafficSource.WEBSOCKET, clientReleaseManager));
|
||||||
|
provisioningEnvironment.jersey().register(new KeepAliveController(clientPresenceManager));
|
||||||
|
|
||||||
|
registerCorsFilter(environment);
|
||||||
|
registerExceptionMappers(environment, webSocketEnvironment, provisioningEnvironment);
|
||||||
|
registerProviders(environment, webSocketEnvironment, provisioningEnvironment);
|
||||||
|
|
||||||
|
environment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
|
||||||
|
webSocketEnvironment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
|
||||||
|
provisioningEnvironment.jersey().property(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE);
|
||||||
|
|
||||||
|
JettyWebSocketServletContainerInitializer.configure(environment.getApplicationContext(), null);
|
||||||
|
|
||||||
|
WebSocketResourceProviderFactory<AuthenticatedAccount> webSocketServlet = new WebSocketResourceProviderFactory<>(
|
||||||
|
webSocketEnvironment, AuthenticatedAccount.class, config.getWebSocketConfiguration());
|
||||||
|
WebSocketResourceProviderFactory<AuthenticatedAccount> provisioningServlet = new WebSocketResourceProviderFactory<>(
|
||||||
|
provisioningEnvironment, AuthenticatedAccount.class, config.getWebSocketConfiguration());
|
||||||
|
|
||||||
|
ServletRegistration.Dynamic websocket = environment.servlets().addServlet("WebSocket", webSocketServlet);
|
||||||
|
ServletRegistration.Dynamic provisioning = environment.servlets().addServlet("Provisioning", provisioningServlet);
|
||||||
|
|
||||||
|
websocket.addMapping("/v1/websocket/");
|
||||||
|
websocket.setAsyncSupported(true);
|
||||||
|
|
||||||
|
provisioning.addMapping("/v1/websocket/provisioning/");
|
||||||
|
provisioning.setAsyncSupported(true);
|
||||||
|
|
||||||
|
environment.admin().addTask(new SetRequestLoggingEnabledTask());
|
||||||
|
|
||||||
|
environment.healthChecks().register("cacheCluster", new RedisClusterHealthCheck(cacheCluster));
|
||||||
|
|
||||||
|
MetricsUtil.registerSystemResourceMetrics(environment);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void registerProviders(Environment environment,
|
||||||
|
WebSocketEnvironment<AuthenticatedAccount> webSocketEnvironment,
|
||||||
|
WebSocketEnvironment<AuthenticatedAccount> provisioningEnvironment) {
|
||||||
|
List.of(
|
||||||
|
ScoreThresholdProvider.ScoreThresholdFeature.class,
|
||||||
|
SenderOverrideProvider.SenderOverrideFeature.class,
|
||||||
|
PushChallengeConfigProvider.PushChallengeConfigFeature.class)
|
||||||
|
.forEach(feature -> {
|
||||||
|
environment.jersey().register(feature);
|
||||||
|
webSocketEnvironment.jersey().register(feature);
|
||||||
|
provisioningEnvironment.jersey().register(feature);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerExceptionMappers(Environment environment,
|
||||||
|
WebSocketEnvironment<AuthenticatedAccount> webSocketEnvironment,
|
||||||
|
WebSocketEnvironment<AuthenticatedAccount> provisioningEnvironment) {
|
||||||
|
|
||||||
|
List.of(
|
||||||
|
new LoggingUnhandledExceptionMapper(),
|
||||||
|
new CompletionExceptionMapper(),
|
||||||
|
new GrpcStatusRuntimeExceptionMapper(),
|
||||||
|
new IOExceptionMapper(),
|
||||||
|
new RateLimitExceededExceptionMapper(),
|
||||||
|
new InvalidWebsocketAddressExceptionMapper(),
|
||||||
|
new DeviceLimitExceededExceptionMapper(),
|
||||||
|
new ServerRejectedExceptionMapper(),
|
||||||
|
new ImpossiblePhoneNumberExceptionMapper(),
|
||||||
|
new NonNormalizedPhoneNumberExceptionMapper(),
|
||||||
|
new RegistrationServiceSenderExceptionMapper(),
|
||||||
|
new SubscriptionProcessorExceptionMapper(),
|
||||||
|
new JsonMappingExceptionMapper()
|
||||||
|
).forEach(exceptionMapper -> {
|
||||||
|
environment.jersey().register(exceptionMapper);
|
||||||
|
webSocketEnvironment.jersey().register(exceptionMapper);
|
||||||
|
provisioningEnvironment.jersey().register(exceptionMapper);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerCorsFilter(Environment environment) {
|
||||||
|
FilterRegistration.Dynamic filter = environment.servlets().addFilter("CORS", CrossOriginFilter.class);
|
||||||
|
filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*");
|
||||||
|
filter.setInitParameter("allowedOrigins", "*");
|
||||||
|
filter.setInitParameter("allowedHeaders", "Content-Type,Authorization,X-Requested-With,Content-Length,Accept,Origin,X-Signal-Agent");
|
||||||
|
filter.setInitParameter("allowedMethods", "GET,PUT,POST,DELETE,OPTIONS");
|
||||||
|
filter.setInitParameter("preflightMaxAge", "5184000");
|
||||||
|
filter.setInitParameter("allowCredentials", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
new WhisperServerService().run(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||||
|
import javax.validation.constraints.NotEmpty;
|
||||||
|
|
||||||
|
public record TusConfiguration(
|
||||||
|
@ExactlySize(32) SecretBytes userAuthenticationTokenSharedSecret,
|
||||||
|
@NotEmpty String uploadUri
|
||||||
|
){}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
/*
|
||||||
|
* 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.Authenticator;
|
||||||
|
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 AccountAuthenticator implements Authenticator<BasicCredentials, AuthenticatedAccount> {
|
||||||
|
|
||||||
|
private static final String LEGACY_NAME_PREFIX = "org.whispersystems.textsecuregcm.auth.BaseAccountAuthenticator";
|
||||||
|
|
||||||
|
private static final String AUTHENTICATION_COUNTER_NAME = name(LEGACY_NAME_PREFIX, "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(LEGACY_NAME_PREFIX, "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 AccountAuthenticator(AccountsManager 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) {
|
||||||
|
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) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2020 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
import javax.ws.rs.WebApplicationException;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
public class Anonymous {
|
||||||
|
|
||||||
|
private final byte[] unidentifiedSenderAccessKey;
|
||||||
|
|
||||||
|
public Anonymous(String header) {
|
||||||
|
try {
|
||||||
|
this.unidentifiedSenderAccessKey = Base64.getDecoder().decode(header);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new WebApplicationException(e, Response.Status.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getAccessKey() {
|
||||||
|
return unidentifiedSenderAccessKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
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<Byte, 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, Byte>> 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<Byte, Boolean> initialDevicesEnabled =
|
||||||
|
(Map<Byte, Boolean>) requestEvent.getContainerRequest().getProperty(DEVICES_ENABLED);
|
||||||
|
|
||||||
|
return accountsManager.getByAccountIdentifier((UUID) requestEvent.getContainerRequest().getProperty(ACCOUNT_UUID)).map(account -> {
|
||||||
|
final Set<Byte> deviceIdsToDisplace;
|
||||||
|
final Map<Byte, 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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,10 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecuregcm.backup.BackupTier;
|
||||||
|
|
||||||
|
public record AuthenticatedBackupUser(byte[] backupId, BackupTier backupTier) {}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2021 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Pair;
|
||||||
|
|
||||||
|
public class BasicAuthorizationHeader {
|
||||||
|
|
||||||
|
private final String username;
|
||||||
|
private final byte deviceId;
|
||||||
|
private final String password;
|
||||||
|
|
||||||
|
private BasicAuthorizationHeader(final String username, final byte deviceId, final String password) {
|
||||||
|
this.username = username;
|
||||||
|
this.deviceId = deviceId;
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BasicAuthorizationHeader fromString(final String header) throws InvalidAuthorizationHeaderException {
|
||||||
|
try {
|
||||||
|
if (StringUtils.isBlank(header)) {
|
||||||
|
throw new InvalidAuthorizationHeaderException("Blank header");
|
||||||
|
}
|
||||||
|
|
||||||
|
final int spaceIndex = header.indexOf(' ');
|
||||||
|
|
||||||
|
if (spaceIndex == -1) {
|
||||||
|
throw new InvalidAuthorizationHeaderException("Invalid authorization header: " + header);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String authorizationType = header.substring(0, spaceIndex);
|
||||||
|
|
||||||
|
if (!"Basic".equals(authorizationType)) {
|
||||||
|
throw new InvalidAuthorizationHeaderException("Unsupported authorization method: " + authorizationType);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String credentials;
|
||||||
|
|
||||||
|
try {
|
||||||
|
credentials = new String(Base64.getDecoder().decode(header.substring(spaceIndex + 1)));
|
||||||
|
} catch (final IndexOutOfBoundsException e) {
|
||||||
|
throw new InvalidAuthorizationHeaderException("Missing credentials");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isEmpty(credentials)) {
|
||||||
|
throw new InvalidAuthorizationHeaderException("Bad decoded value: " + credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
final int credentialSeparatorIndex = credentials.indexOf(':');
|
||||||
|
|
||||||
|
if (credentialSeparatorIndex == -1) {
|
||||||
|
throw new InvalidAuthorizationHeaderException("Badly-formatted credentials: " + credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String usernameComponent = credentials.substring(0, credentialSeparatorIndex);
|
||||||
|
|
||||||
|
final String username;
|
||||||
|
final byte deviceId;
|
||||||
|
{
|
||||||
|
final Pair<String, Byte> identifierAndDeviceId =
|
||||||
|
AccountAuthenticator.getIdentifierAndDeviceId(usernameComponent);
|
||||||
|
|
||||||
|
username = identifierAndDeviceId.first();
|
||||||
|
deviceId = identifierAndDeviceId.second();
|
||||||
|
}
|
||||||
|
|
||||||
|
final String password = credentials.substring(credentialSeparatorIndex + 1);
|
||||||
|
|
||||||
|
if (StringUtils.isAnyBlank(username, password)) {
|
||||||
|
throw new InvalidAuthorizationHeaderException("Username or password were blank");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BasicAuthorizationHeader(username, deviceId, password);
|
||||||
|
} catch (final IllegalArgumentException | IndexOutOfBoundsException e) {
|
||||||
|
throw new InvalidAuthorizationHeaderException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getDeviceId() {
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2020 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public class CertificateGenerator {
|
||||||
|
|
||||||
|
private final ECPrivateKey privateKey;
|
||||||
|
private final int expiresDays;
|
||||||
|
private final ServerCertificate serverCertificate;
|
||||||
|
|
||||||
|
public CertificateGenerator(byte[] serverCertificate, ECPrivateKey privateKey, int expiresDays)
|
||||||
|
throws InvalidProtocolBufferException
|
||||||
|
{
|
||||||
|
this.privateKey = privateKey;
|
||||||
|
this.expiresDays = expiresDays;
|
||||||
|
this.serverCertificate = ServerCertificate.parseFrom(serverCertificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] createFor(Account account, Device device, 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(account.getIdentityKey(IdentityType.ACI).serialize()))
|
||||||
|
.setSigner(serverCertificate)
|
||||||
|
.setSenderUuid(account.getUuid().toString());
|
||||||
|
|
||||||
|
if (includeE164) {
|
||||||
|
builder.setSender(account.getNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] certificate = builder.build().toByteArray();
|
||||||
|
byte[] signature;
|
||||||
|
try {
|
||||||
|
signature = Curve.calculateSignature(privateKey, certificate);
|
||||||
|
} catch (org.signal.libsignal.protocol.InvalidKeyException e) {
|
||||||
|
throw new InvalidKeyException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SenderCertificate.newBuilder()
|
||||||
|
.setCertificate(ByteString.copyFrom(certificate))
|
||||||
|
.setSignature(ByteString.copyFrom(signature))
|
||||||
|
.build()
|
||||||
|
.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2021 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 may change the "enabled" state of one or more devices 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 ChangesDeviceEnabledState {
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
public CombinedUnidentifiedSenderAccessKeys(String header) {
|
||||||
|
try {
|
||||||
|
this.combinedUnidentifiedSenderAccessKeys = Base64.getDecoder().decode(header);
|
||||||
|
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) {
|
||||||
|
throw new WebApplicationException(e, Response.Status.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getAccessKeys() {
|
||||||
|
return combinedUnidentifiedSenderAccessKeys;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2020 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
|
||||||
|
public record ExternalServiceCredentials(String username, String password) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
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 String DELIMITER = ":";
|
||||||
|
|
||||||
|
private static final int TRUNCATED_SIGNATURE_LENGTH = 10;
|
||||||
|
|
||||||
|
private final byte[] key;
|
||||||
|
|
||||||
|
private final byte[] userDerivationKey;
|
||||||
|
|
||||||
|
private final boolean prependUsername;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExternalServiceCredentialsGenerator(
|
||||||
|
final byte[] key,
|
||||||
|
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)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A convenience method for the case of identity in the form of {@link UUID}.
|
||||||
|
* @param uuid identity to generate credentials for
|
||||||
|
* @return an instance of {@link ExternalServiceCredentials}
|
||||||
|
*/
|
||||||
|
public ExternalServiceCredentials generateForUuid(final UUID uuid) {
|
||||||
|
return generateFor(uuid.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates `ExternalServiceCredentials` for the given identity following this generator's configuration.
|
||||||
|
* @param identity identity string to generate credentials for
|
||||||
|
* @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, derivedUsernameTruncateLength)
|
||||||
|
: identity;
|
||||||
|
|
||||||
|
final long currentTimeSeconds = currentTimeSeconds();
|
||||||
|
|
||||||
|
final String dataToSign = usernameIsTimestamp() ? username : username + DELIMITER + currentTimeSeconds;
|
||||||
|
|
||||||
|
final String signature = truncateSignature
|
||||||
|
? hmac256TruncatedToHexString(key, dataToSign, TRUNCATED_SIGNATURE_LENGTH)
|
||||||
|
: hmac256ToHexString(key, dataToSign);
|
||||||
|
|
||||||
|
final String token = (prependUsername ? dataToSign : currentTimeSeconds) + DELIMITER + signature;
|
||||||
|
|
||||||
|
return new ExternalServiceCredentials(username, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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`
|
||||||
|
* @return non-empty optional with an identity string value, or empty if value can't be extracted.
|
||||||
|
*/
|
||||||
|
public Optional<String> identityFromSignature(final String password) {
|
||||||
|
// for some generators, identity in the clear is just not a part of the password
|
||||||
|
if (!prependUsername || shouldDeriveUsername() || StringUtils.isBlank(password)) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
// checking for the case of unexpected format
|
||||||
|
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 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)
|
||||||
|
*/
|
||||||
|
public Optional<Long> validateAndGetTimestamp(final ExternalServiceCredentials credentials) {
|
||||||
|
final String[] parts = requireNonNull(credentials).password().split(DELIMITER);
|
||||||
|
final String timestampSeconds;
|
||||||
|
final String actualSignature;
|
||||||
|
|
||||||
|
// making sure password format matches our expectations based on the generator configuration
|
||||||
|
if (parts.length == 3 && prependUsername) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
timestampSeconds = parts[1];
|
||||||
|
actualSignature = parts[2];
|
||||||
|
} else if (parts.length == 2 && !prependUsername) {
|
||||||
|
timestampSeconds = parts[0];
|
||||||
|
actualSignature = parts[1];
|
||||||
|
} else {
|
||||||
|
// unexpected password format
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
final String signedData = usernameIsTimestamp() ? credentials.username() : credentials.username() + DELIMITER + timestampSeconds;
|
||||||
|
final String expectedSignature = truncateSignature
|
||||||
|
? hmac256TruncatedToHexString(key, signedData, TRUNCATED_SIGNATURE_LENGTH)
|
||||||
|
: hmac256ToHexString(key, signedData);
|
||||||
|
|
||||||
|
// if the signature is valid it's safe to parse the `timestampSeconds` string into Long
|
||||||
|
return hmacHexStringsEqual(expectedSignature, actualSignature)
|
||||||
|
? Optional.of(Long.valueOf(timestampSeconds))
|
||||||
|
: Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an instance of {@link ExternalServiceCredentials} object and the max allowed age for those credentials,
|
||||||
|
* checks if credentials are valid and not expired.
|
||||||
|
* @param credentials an instance of {@link ExternalServiceCredentials}
|
||||||
|
* @param maxAgeSeconds age in seconds
|
||||||
|
* @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)
|
||||||
|
*/
|
||||||
|
public Optional<Long> validateAndGetTimestamp(final ExternalServiceCredentials credentials, final long maxAgeSeconds) {
|
||||||
|
return validateAndGetTimestamp(credentials)
|
||||||
|
.filter(ts -> currentTimeSeconds() - ts <= maxAgeSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldDeriveUsername() {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
|
||||||
|
private final byte[] key;
|
||||||
|
|
||||||
|
private byte[] userDerivationKey = new byte[0];
|
||||||
|
|
||||||
|
private boolean prependUsername = true;
|
||||||
|
|
||||||
|
private boolean truncateSignature = true;
|
||||||
|
|
||||||
|
private int derivedUsernameTruncateLength = 10;
|
||||||
|
|
||||||
|
private String usernameTimestampPrefix = null;
|
||||||
|
|
||||||
|
private Function<Instant, Instant> usernameTimestampTruncator = null;
|
||||||
|
|
||||||
|
private Clock clock = Clock.systemUTC();
|
||||||
|
|
||||||
|
|
||||||
|
private Builder(final byte[] key) {
|
||||||
|
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;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withClock(final Clock clock) {
|
||||||
|
this.clock = requireNonNull(clock);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder truncateSignature(final boolean truncateSignature) {
|
||||||
|
this.truncateSignature = truncateSignature;
|
||||||
|
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, 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,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
import javax.ws.rs.WebApplicationException;
|
||||||
|
import javax.ws.rs.core.Response.Status;
|
||||||
|
|
||||||
|
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||||
|
import org.signal.libsignal.zkgroup.groupsend.GroupSendCredentialPresentation;
|
||||||
|
|
||||||
|
public record GroupSendCredentialHeader(GroupSendCredentialPresentation presentation) {
|
||||||
|
|
||||||
|
public static GroupSendCredentialHeader valueOf(String header) {
|
||||||
|
try {
|
||||||
|
return new GroupSendCredentialHeader(new GroupSendCredentialPresentation(Base64.getDecoder().decode(header)));
|
||||||
|
} catch (InvalidInputException | IllegalArgumentException e) {
|
||||||
|
// Base64 throws IllegalArgumentException; GroupSendCredentialPresentation ctor throws InvalidInputException
|
||||||
|
throw new WebApplicationException(e, Status.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2020 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
|
||||||
|
import javax.ws.rs.WebApplicationException;
|
||||||
|
import javax.ws.rs.core.Response.Status;
|
||||||
|
|
||||||
|
public class InvalidAuthorizationHeaderException extends WebApplicationException {
|
||||||
|
public InvalidAuthorizationHeaderException(String s) {
|
||||||
|
super(s, Status.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvalidAuthorizationHeaderException(Exception e) {
|
||||||
|
super(e, Status.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2020 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;
|
||||||
|
|
||||||
|
import javax.ws.rs.NotAuthorizedException;
|
||||||
|
import javax.ws.rs.NotFoundException;
|
||||||
|
import javax.ws.rs.WebApplicationException;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||||
|
public class OptionalAccess {
|
||||||
|
|
||||||
|
public static void verify(Optional<Account> requestAccount,
|
||||||
|
Optional<Anonymous> accessKey,
|
||||||
|
Optional<Account> targetAccount,
|
||||||
|
String deviceSelector)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
verify(requestAccount, accessKey, targetAccount);
|
||||||
|
|
||||||
|
if (!deviceSelector.equals("*")) {
|
||||||
|
byte deviceId = Byte.parseByte(deviceSelector);
|
||||||
|
|
||||||
|
Optional<Device> targetDevice = targetAccount.get().getDevice(deviceId);
|
||||||
|
|
||||||
|
if (targetDevice.isPresent() && targetDevice.get().isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestAccount.isPresent()) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
} else {
|
||||||
|
throw new NotAuthorizedException(Response.Status.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new WebApplicationException(Response.status(422).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void verify(Optional<Account> requestAccount,
|
||||||
|
Optional<Anonymous> accessKey,
|
||||||
|
Optional<Account> targetAccount)
|
||||||
|
{
|
||||||
|
if (requestAccount.isPresent() && targetAccount.isPresent() && targetAccount.get().isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
if (requestAccount.isPresent() && (targetAccount.isEmpty() || (targetAccount.isPresent() && !targetAccount.get().isEnabled()))) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessKey.isPresent() && targetAccount.isPresent() && targetAccount.get().isEnabled() && targetAccount.get().isUnrestrictedUnidentifiedAccess()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessKey.isPresent() &&
|
||||||
|
targetAccount.isPresent() &&
|
||||||
|
targetAccount.get().getUnidentifiedAccessKey().isPresent() &&
|
||||||
|
targetAccount.get().isEnabled() &&
|
||||||
|
MessageDigest.isEqual(accessKey.get().getAccessKey(), targetAccount.get().getUnidentifiedAccessKey().get()))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new NotAuthorizedException(Response.Status.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* 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, Byte>> 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,113 @@
|
|||||||
|
/*
|
||||||
|
* 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 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 javax.ws.rs.BadRequestException;
|
||||||
|
import javax.ws.rs.ForbiddenException;
|
||||||
|
import javax.ws.rs.NotAuthorizedException;
|
||||||
|
import javax.ws.rs.ServerErrorException;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
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.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 RegistrationServiceClient registrationServiceClient;
|
||||||
|
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
|
||||||
|
|
||||||
|
public PhoneVerificationTokenManager(final RegistrationServiceClient registrationServiceClient,
|
||||||
|
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager) {
|
||||||
|
this.registrationServiceClient = registrationServiceClient;
|
||||||
|
this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a {@link PhoneVerificationRequest} has a token that verifies the caller has confirmed access to the e164
|
||||||
|
* number
|
||||||
|
*
|
||||||
|
* @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 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(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 String number, final byte[] recoveryPassword)
|
||||||
|
throws InterruptedException {
|
||||||
|
try {
|
||||||
|
final boolean verified = registrationRecoveryPasswordsManager.verify(number, 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,178 @@
|
|||||||
|
/*
|
||||||
|
* 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 java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.ws.rs.WebApplicationException;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
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.limits.RateLimiters;
|
||||||
|
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
||||||
|
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 ClientPresenceManager clientPresenceManager;
|
||||||
|
private final ExternalServiceCredentialsGenerator svr2CredentialGenerator;
|
||||||
|
private final RateLimiters rateLimiters;
|
||||||
|
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
|
||||||
|
private final PushNotificationManager pushNotificationManager;
|
||||||
|
|
||||||
|
public RegistrationLockVerificationManager(
|
||||||
|
final AccountsManager accounts, final ClientPresenceManager clientPresenceManager,
|
||||||
|
final ExternalServiceCredentialsGenerator svr2CredentialGenerator,
|
||||||
|
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
|
||||||
|
final PushNotificationManager pushNotificationManager,
|
||||||
|
final RateLimiters rateLimiters) {
|
||||||
|
this.accounts = accounts;
|
||||||
|
this.clientPresenceManager = clientPresenceManager;
|
||||||
|
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 ExternalServiceCredentials existingSvr2Credentials = svr2CredentialGenerator.generateForUuid(account.getUuid());
|
||||||
|
|
||||||
|
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.removeForNumber(updatedAccount.getNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<Byte> deviceIds = updatedAccount.getDevices().stream().map(Device::getId).toList();
|
||||||
|
clientPresenceManager.disconnectAllPresences(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(),
|
||||||
|
existingRegistrationLock.needsFailureCredentials() ? existingSvr2Credentials : null))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
rateLimiters.getPinLimiter().clear(phoneNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import org.signal.libsignal.protocol.kdf.HKDF;
|
||||||
|
|
||||||
|
public record SaltedTokenHash(String hash, String salt) {
|
||||||
|
|
||||||
|
public enum Version {
|
||||||
|
V1,
|
||||||
|
V2,
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Version CURRENT_VERSION = Version.V2;
|
||||||
|
|
||||||
|
private static final String V2_PREFIX = "2.";
|
||||||
|
|
||||||
|
private static final byte[] AUTH_TOKEN_HKDF_INFO = "authtoken".getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
private static final int SALT_SIZE = 16;
|
||||||
|
|
||||||
|
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||||
|
|
||||||
|
|
||||||
|
public static SaltedTokenHash generateFor(final String token) {
|
||||||
|
final String salt = generateSalt();
|
||||||
|
final String hash = calculateV2Hash(salt, token);
|
||||||
|
return new SaltedTokenHash(hash, salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Version getVersion() {
|
||||||
|
return hash.startsWith(V2_PREFIX) ? Version.V2 : Version.V1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean verify(final String token) {
|
||||||
|
final String theirValue = switch (getVersion()) {
|
||||||
|
case V1 -> calculateV1Hash(salt, token);
|
||||||
|
case V2 -> calculateV2Hash(salt, token);
|
||||||
|
};
|
||||||
|
return MessageDigest.isEqual(
|
||||||
|
theirValue.getBytes(StandardCharsets.UTF_8),
|
||||||
|
hash.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String generateSalt() {
|
||||||
|
final byte[] salt = new byte[SALT_SIZE];
|
||||||
|
SECURE_RANDOM.nextBytes(salt);
|
||||||
|
return HexFormat.of().formatHex(salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String calculateV1Hash(final String salt, final String token) {
|
||||||
|
try {
|
||||||
|
return HexFormat.of()
|
||||||
|
.formatHex(MessageDigest.getInstance("SHA1").digest((salt + token).getBytes(StandardCharsets.UTF_8)));
|
||||||
|
} catch (final NoSuchAlgorithmException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String calculateV2Hash(final String salt, final String token) {
|
||||||
|
final byte[] secret = HKDF.deriveSecrets(
|
||||||
|
token.getBytes(StandardCharsets.UTF_8), // key
|
||||||
|
salt.getBytes(StandardCharsets.UTF_8), // salt
|
||||||
|
AUTH_TOKEN_HKDF_INFO,
|
||||||
|
32);
|
||||||
|
return V2_PREFIX + HexFormat.of().formatHex(secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 javax.annotation.Nullable;
|
||||||
|
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 Instant lastSeen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return milliseconds since the last time the account was seen.
|
||||||
|
*/
|
||||||
|
private long timeSinceLastSeen() {
|
||||||
|
return System.currentTimeMillis() - lastSeen.toEpochMilli();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if the registration lock and salt are both set.
|
||||||
|
*/
|
||||||
|
private boolean hasLockAndSalt() {
|
||||||
|
return registrationLock.isPresent() && registrationLockSalt.isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Status getStatus() {
|
||||||
|
if (!isPresent()) {
|
||||||
|
return Status.ABSENT;
|
||||||
|
}
|
||||||
|
if (getTimeRemaining().toMillis() > 0) {
|
||||||
|
return Status.REQUIRED;
|
||||||
|
}
|
||||||
|
return Status.EXPIRED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean needsFailureCredentials() {
|
||||||
|
return hasLockAndSalt();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Duration getTimeRemaining() {
|
||||||
|
return REGISTRATION_LOCK_EXPIRATION_DAYS.minus(timeSinceLastSeen(), ChronoUnit.MILLIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean verify(@Nullable String 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, Instant.ofEpochMilli(timestamp));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2020 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record TurnToken(String username, String password, List<String> urls) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* 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.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class TurnTokenGenerator {
|
||||||
|
|
||||||
|
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||||
|
|
||||||
|
private final byte[] turnSecret;
|
||||||
|
|
||||||
|
private static final String ALGORITHM = "HmacSHA1";
|
||||||
|
|
||||||
|
public TurnTokenGenerator(final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
|
||||||
|
final byte[] turnSecret) {
|
||||||
|
|
||||||
|
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||||
|
this.turnSecret = turnSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TurnToken generate(final UUID aci) {
|
||||||
|
try {
|
||||||
|
final List<String> urls = urls(aci);
|
||||||
|
final Mac mac = Mac.getInstance(ALGORITHM);
|
||||||
|
final long validUntilSeconds = Instant.now().plus(Duration.ofDays(1)).getEpochSecond();
|
||||||
|
final long user = Util.ensureNonNegativeInt(new SecureRandom().nextInt());
|
||||||
|
final String userTime = validUntilSeconds + ":" + user;
|
||||||
|
|
||||||
|
mac.init(new SecretKeySpec(turnSecret, ALGORITHM));
|
||||||
|
final String password = Base64.getEncoder().encodeToString(mac.doFinal(userTime.getBytes()));
|
||||||
|
|
||||||
|
return new TurnToken(userTime, password, urls);
|
||||||
|
} catch (final NoSuchAlgorithmException | InvalidKeyException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> urls(final UUID aci) {
|
||||||
|
final DynamicTurnConfiguration turnConfig = dynamicConfigurationManager.getConfiguration().getTurnConfiguration();
|
||||||
|
|
||||||
|
// Check if number is enrolled to test out specific turn servers
|
||||||
|
final Optional<TurnUriConfiguration> enrolled = turnConfig.getUriConfigs().stream()
|
||||||
|
.filter(config -> config.getEnrolledAcis().contains(aci))
|
||||||
|
.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<>(c.getUris(), c.getWeight())).toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2020 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
public class UnidentifiedAccessChecksum {
|
||||||
|
|
||||||
|
public static byte[] generateFor(byte[] unidentifiedAccessKey) {
|
||||||
|
try {
|
||||||
|
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, "HmacSHA256"));
|
||||||
|
|
||||||
|
return mac.doFinal(new byte[32]);
|
||||||
|
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
|
||||||
|
public class UnidentifiedAccessUtil {
|
||||||
|
|
||||||
|
public static final int UNIDENTIFIED_ACCESS_KEY_LENGTH = 16;
|
||||||
|
|
||||||
|
private UnidentifiedAccessUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether an action (e.g. sending a message or retrieving pre-keys) may be taken on the target account by an
|
||||||
|
* actor presenting the given unidentified access key.
|
||||||
|
*
|
||||||
|
* @param targetAccount the account on which an actor wishes to take an action
|
||||||
|
* @param unidentifiedAccessKey the unidentified access key presented by the actor
|
||||||
|
*
|
||||||
|
* @return {@code true} if an actor presenting the given unidentified access key has permission to take an action on
|
||||||
|
* the target account or {@code false} otherwise
|
||||||
|
*/
|
||||||
|
public static boolean checkUnidentifiedAccess(final Account targetAccount, final byte[] unidentifiedAccessKey) {
|
||||||
|
return targetAccount.isUnrestrictedUnidentifiedAccess()
|
||||||
|
|| targetAccount.getUnidentifiedAccessKey()
|
||||||
|
.map(targetUnidentifiedAccessKey -> MessageDigest.isEqual(targetUnidentifiedAccessKey, unidentifiedAccessKey))
|
||||||
|
.orElse(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
import org.glassfish.jersey.server.monitoring.ApplicationEvent;
|
||||||
|
import org.glassfish.jersey.server.monitoring.ApplicationEventListener;
|
||||||
|
import org.glassfish.jersey.server.monitoring.RequestEvent;
|
||||||
|
import org.glassfish.jersey.server.monitoring.RequestEventListener;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegates request events to a listener that watches for intra-request changes that require websocket refreshes
|
||||||
|
*/
|
||||||
|
public class WebsocketRefreshApplicationEventListener implements ApplicationEventListener {
|
||||||
|
|
||||||
|
private final WebsocketRefreshRequestEventListener websocketRefreshRequestEventListener;
|
||||||
|
|
||||||
|
public WebsocketRefreshApplicationEventListener(final AccountsManager accountsManager,
|
||||||
|
final ClientPresenceManager clientPresenceManager) {
|
||||||
|
|
||||||
|
this.websocketRefreshRequestEventListener = new WebsocketRefreshRequestEventListener(clientPresenceManager,
|
||||||
|
new AuthEnablementRefreshRequirementProvider(accountsManager),
|
||||||
|
new PhoneNumberChangeRefreshRequirementProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEvent(final ApplicationEvent event) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequestEventListener onRequest(final RequestEvent requestEvent) {
|
||||||
|
return websocketRefreshRequestEventListener;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2021 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||||
|
|
||||||
|
import io.micrometer.core.instrument.Counter;
|
||||||
|
import io.micrometer.core.instrument.Metrics;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import javax.ws.rs.container.ResourceInfo;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
|
import org.glassfish.jersey.server.monitoring.RequestEvent;
|
||||||
|
import org.glassfish.jersey.server.monitoring.RequestEvent.Type;
|
||||||
|
import org.glassfish.jersey.server.monitoring.RequestEventListener;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
||||||
|
|
||||||
|
public class WebsocketRefreshRequestEventListener implements RequestEventListener {
|
||||||
|
|
||||||
|
private final ClientPresenceManager clientPresenceManager;
|
||||||
|
private final WebsocketRefreshRequirementProvider[] providers;
|
||||||
|
|
||||||
|
private static final Counter DISPLACED_ACCOUNTS = Metrics.counter(
|
||||||
|
name(WebsocketRefreshRequestEventListener.class, "displacedAccounts"));
|
||||||
|
|
||||||
|
private static final Counter DISPLACED_DEVICES = Metrics.counter(
|
||||||
|
name(WebsocketRefreshRequestEventListener.class, "displacedDevices"));
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(WebsocketRefreshRequestEventListener.class);
|
||||||
|
|
||||||
|
public WebsocketRefreshRequestEventListener(
|
||||||
|
final ClientPresenceManager clientPresenceManager,
|
||||||
|
final WebsocketRefreshRequirementProvider... providers) {
|
||||||
|
|
||||||
|
this.clientPresenceManager = clientPresenceManager;
|
||||||
|
this.providers = providers;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Context
|
||||||
|
private ResourceInfo resourceInfo;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEvent(final RequestEvent event) {
|
||||||
|
if (event.getType() == Type.REQUEST_FILTERED) {
|
||||||
|
for (final WebsocketRefreshRequirementProvider provider : providers) {
|
||||||
|
provider.handleRequestFiltered(event);
|
||||||
|
}
|
||||||
|
} else if (event.getType() == Type.FINISHED) {
|
||||||
|
final AtomicInteger displacedDevices = new AtomicInteger(0);
|
||||||
|
|
||||||
|
Arrays.stream(providers)
|
||||||
|
.flatMap(provider -> provider.handleRequestFinished(event).stream())
|
||||||
|
.distinct()
|
||||||
|
.forEach(pair -> {
|
||||||
|
try {
|
||||||
|
displacedDevices.incrementAndGet();
|
||||||
|
clientPresenceManager.disconnectPresence(pair.first(), pair.second());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
logger.error("Could not displace device presence", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (displacedDevices.get() > 0) {
|
||||||
|
DISPLACED_ACCOUNTS.increment();
|
||||||
|
DISPLACED_DEVICES.increment(displacedDevices.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2021 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.auth;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.glassfish.jersey.server.monitoring.RequestEvent;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Pair;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A websocket refresh requirement provider watches for intra-request changes (e.g. to authentication status) that
|
||||||
|
* require a websocket refresh.
|
||||||
|
*/
|
||||||
|
public interface WebsocketRefreshRequirementProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a request after filters have run and the request has been mapped to a destination controller.
|
||||||
|
*
|
||||||
|
* @param requestEvent the request event to observe
|
||||||
|
*/
|
||||||
|
void handleRequestFiltered(RequestEvent requestEvent);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a request after all normal request handling has been completed.
|
||||||
|
*
|
||||||
|
* @param requestEvent the request event to observe
|
||||||
|
* @return a list of pairs of account UUID/device ID pairs identifying websockets that need to be refreshed as a
|
||||||
|
* result of the observed request
|
||||||
|
*/
|
||||||
|
List<Pair<UUID, Byte>> handleRequestFinished(RequestEvent requestEvent);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.auth.grpc;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record AuthenticatedDevice(UUID accountIdentifier, byte deviceId) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.auth.grpc;
|
||||||
|
|
||||||
|
import io.grpc.Context;
|
||||||
|
import io.grpc.Status;
|
||||||
|
import java.util.UUID;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides utility methods for working with authentication in the context of gRPC calls.
|
||||||
|
*/
|
||||||
|
public class AuthenticationUtil {
|
||||||
|
|
||||||
|
static final Context.Key<UUID> CONTEXT_AUTHENTICATED_ACCOUNT_IDENTIFIER_KEY = Context.key("authenticated-aci");
|
||||||
|
static final Context.Key<Byte> CONTEXT_AUTHENTICATED_DEVICE_IDENTIFIER_KEY = Context.key("authenticated-device-id");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the account/device authenticated in the current gRPC context or throws an "unauthenticated" exception if
|
||||||
|
* no authenticated account/device is available.
|
||||||
|
*
|
||||||
|
* @return the account/device identifier authenticated in the current gRPC context
|
||||||
|
*
|
||||||
|
* @throws io.grpc.StatusRuntimeException with a status of {@code UNAUTHENTICATED} if no authenticated account/device
|
||||||
|
* could be retrieved from the current gRPC context
|
||||||
|
*/
|
||||||
|
public static AuthenticatedDevice requireAuthenticatedDevice() {
|
||||||
|
@Nullable final UUID accountIdentifier = CONTEXT_AUTHENTICATED_ACCOUNT_IDENTIFIER_KEY.get();
|
||||||
|
@Nullable final Byte deviceId = CONTEXT_AUTHENTICATED_DEVICE_IDENTIFIER_KEY.get();
|
||||||
|
|
||||||
|
if (accountIdentifier != null && deviceId != null) {
|
||||||
|
return new AuthenticatedDevice(accountIdentifier, deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Status.UNAUTHENTICATED.asRuntimeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the account/device authenticated in the current gRPC context or throws an "unauthenticated" exception if
|
||||||
|
* no authenticated account/device is available or "permission denied" if the authenticated device is not the primary
|
||||||
|
* device for the account.
|
||||||
|
*
|
||||||
|
* @return the account/device identifier authenticated in the current gRPC context
|
||||||
|
*
|
||||||
|
* @throws io.grpc.StatusRuntimeException with a status of {@code UNAUTHENTICATED} if no authenticated account/device
|
||||||
|
* could be retrieved from the current gRPC context or a status of {@code PERMISSION_DENIED} if the authenticated
|
||||||
|
* device is not the primary device for the authenticated account
|
||||||
|
*/
|
||||||
|
public static AuthenticatedDevice requireAuthenticatedPrimaryDevice() {
|
||||||
|
final AuthenticatedDevice authenticatedDevice = requireAuthenticatedDevice();
|
||||||
|
|
||||||
|
if (authenticatedDevice.deviceId() != Device.PRIMARY_ID) {
|
||||||
|
throw Status.PERMISSION_DENIED.asRuntimeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return authenticatedDevice;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.auth.grpc;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import io.dropwizard.auth.basic.BasicCredentials;
|
||||||
|
import io.grpc.Context;
|
||||||
|
import io.grpc.Contexts;
|
||||||
|
import io.grpc.Metadata;
|
||||||
|
import io.grpc.ServerCall;
|
||||||
|
import io.grpc.ServerCallHandler;
|
||||||
|
import io.grpc.ServerInterceptor;
|
||||||
|
import io.grpc.Status;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
|
||||||
|
import org.whispersystems.textsecuregcm.util.HeaderUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A basic credential authentication interceptor enforces the presence of a valid username and password on every call.
|
||||||
|
* Callers supply credentials by providing a username (UUID and optional device ID) and password pair in the
|
||||||
|
* {@code x-signal-basic-auth-credentials} call header.
|
||||||
|
* <p/>
|
||||||
|
* Downstream services can retrieve the identity of the authenticated caller using methods in
|
||||||
|
* {@link AuthenticationUtil}.
|
||||||
|
* <p/>
|
||||||
|
* Note that this authentication, while fully functional, is intended only for development and testing purposes and is
|
||||||
|
* intended to be replaced with a more robust and efficient strategy before widespread client adoption.
|
||||||
|
*
|
||||||
|
* @see AuthenticationUtil
|
||||||
|
* @see AccountAuthenticator
|
||||||
|
*/
|
||||||
|
public class BasicCredentialAuthenticationInterceptor implements ServerInterceptor {
|
||||||
|
|
||||||
|
private final AccountAuthenticator accountAuthenticator;
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static final Metadata.Key<String> BASIC_CREDENTIALS =
|
||||||
|
Metadata.Key.of("x-signal-auth", Metadata.ASCII_STRING_MARSHALLER);
|
||||||
|
|
||||||
|
private static final Metadata EMPTY_TRAILERS = new Metadata();
|
||||||
|
|
||||||
|
public BasicCredentialAuthenticationInterceptor(final AccountAuthenticator accountAuthenticator) {
|
||||||
|
this.accountAuthenticator = accountAuthenticator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
|
||||||
|
final ServerCall<ReqT, RespT> call,
|
||||||
|
final Metadata headers,
|
||||||
|
final ServerCallHandler<ReqT, RespT> next) {
|
||||||
|
|
||||||
|
final String authHeader = headers.get(BASIC_CREDENTIALS);
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(authHeader)) {
|
||||||
|
final Optional<BasicCredentials> maybeCredentials = HeaderUtils.basicCredentialsFromAuthHeader(authHeader);
|
||||||
|
if (maybeCredentials.isEmpty()) {
|
||||||
|
call.close(Status.UNAUTHENTICATED.withDescription("Could not parse credentials"), EMPTY_TRAILERS);
|
||||||
|
} else {
|
||||||
|
final Optional<AuthenticatedAccount> maybeAuthenticatedAccount =
|
||||||
|
accountAuthenticator.authenticate(maybeCredentials.get());
|
||||||
|
|
||||||
|
if (maybeAuthenticatedAccount.isPresent()) {
|
||||||
|
final AuthenticatedAccount authenticatedAccount = maybeAuthenticatedAccount.get();
|
||||||
|
|
||||||
|
final Context context = Context.current()
|
||||||
|
.withValue(AuthenticationUtil.CONTEXT_AUTHENTICATED_ACCOUNT_IDENTIFIER_KEY, authenticatedAccount.getAccount().getUuid())
|
||||||
|
.withValue(AuthenticationUtil.CONTEXT_AUTHENTICATED_DEVICE_IDENTIFIER_KEY, authenticatedAccount.getAuthenticatedDevice().getId());
|
||||||
|
|
||||||
|
return Contexts.interceptCall(context, call, headers, next);
|
||||||
|
} else {
|
||||||
|
call.close(Status.UNAUTHENTICATED.withDescription("Credentials not accepted"), EMPTY_TRAILERS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
call.close(Status.UNAUTHENTICATED.withDescription("No credentials provided"), EMPTY_TRAILERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ServerCall.Listener<>() {};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.backup;
|
||||||
|
|
||||||
|
import io.grpc.Status;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
|
||||||
|
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||||
|
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequest;
|
||||||
|
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialResponse;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Issues ZK backup auth credentials for authenticated accounts
|
||||||
|
* <p>
|
||||||
|
* Authenticated callers can create ZK credentials that contain a blinded backup-id, so that they can later use that
|
||||||
|
* backup id without the verifier learning that the id is associated with this account.
|
||||||
|
* <p>
|
||||||
|
* First use {@link #commitBackupId} to provide a blinded backup-id. This is stored in durable storage. Then the caller
|
||||||
|
* can use {@link #getBackupAuthCredentials} to retrieve credentials that can subsequently be used to make anonymously
|
||||||
|
* authenticated requests against their backup-id.
|
||||||
|
*/
|
||||||
|
public class BackupAuthManager {
|
||||||
|
|
||||||
|
private static final Duration MAX_REDEMPTION_DURATION = Duration.ofDays(7);
|
||||||
|
final static String BACKUP_EXPERIMENT_NAME = "backup";
|
||||||
|
final static String BACKUP_MEDIA_EXPERIMENT_NAME = "backupMedia";
|
||||||
|
|
||||||
|
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||||
|
private final GenericServerSecretParams serverSecretParams;
|
||||||
|
private final Clock clock;
|
||||||
|
private final RateLimiters rateLimiters;
|
||||||
|
private final AccountsManager accountsManager;
|
||||||
|
|
||||||
|
public BackupAuthManager(
|
||||||
|
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
|
||||||
|
final RateLimiters rateLimiters,
|
||||||
|
final AccountsManager accountsManager,
|
||||||
|
final GenericServerSecretParams serverSecretParams,
|
||||||
|
final Clock clock) {
|
||||||
|
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||||
|
this.rateLimiters = rateLimiters;
|
||||||
|
this.accountsManager = accountsManager;
|
||||||
|
this.serverSecretParams = serverSecretParams;
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a credential request containing a blinded backup-id for future use.
|
||||||
|
*
|
||||||
|
* @param account The account using the backup-id
|
||||||
|
* @param backupAuthCredentialRequest A request containing the blinded backup-id
|
||||||
|
* @return A future that completes when the credentialRequest has been stored
|
||||||
|
* @throws RateLimitExceededException If too many backup-ids have been committed
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Void> commitBackupId(final Account account,
|
||||||
|
final BackupAuthCredentialRequest backupAuthCredentialRequest) throws RateLimitExceededException {
|
||||||
|
if (receiptLevel(account).isEmpty()) {
|
||||||
|
throw Status.PERMISSION_DENIED.withDescription("Backups not allowed on account").asRuntimeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] serializedRequest = backupAuthCredentialRequest.serialize();
|
||||||
|
byte[] existingRequest = account.getBackupCredentialRequest();
|
||||||
|
if (existingRequest != null && MessageDigest.isEqual(serializedRequest, existingRequest)) {
|
||||||
|
// No need to update or enforce rate limits, this is the credential that the user has already
|
||||||
|
// committed to.
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
rateLimiters.forDescriptor(RateLimiters.For.SET_BACKUP_ID).validate(account.getUuid());
|
||||||
|
|
||||||
|
return this.accountsManager
|
||||||
|
.updateAsync(account, acc -> acc.setBackupCredentialRequest(serializedRequest))
|
||||||
|
.thenRun(Util.NOOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Credential(BackupAuthCredentialResponse credential, Instant redemptionTime) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a credential for every day between redemptionStart and redemptionEnd
|
||||||
|
* <p>
|
||||||
|
* This uses a {@link BackupAuthCredentialRequest} previous stored via {@link this#commitBackupId} to generate the
|
||||||
|
* credentials.
|
||||||
|
*
|
||||||
|
* @param account The account to create the credentials for
|
||||||
|
* @param redemptionStart The day (must be truncated to a day boundary) the first credential should be valid
|
||||||
|
* @param redemptionEnd The day (must be truncated to a day boundary) the last credential should be valid
|
||||||
|
* @return Credentials and the day on which they may be redeemed
|
||||||
|
*/
|
||||||
|
public CompletableFuture<List<Credential>> getBackupAuthCredentials(
|
||||||
|
final Account account,
|
||||||
|
final Instant redemptionStart,
|
||||||
|
final Instant redemptionEnd) {
|
||||||
|
|
||||||
|
final long receiptLevel = receiptLevel(account).orElseThrow(
|
||||||
|
() -> Status.PERMISSION_DENIED.withDescription("Backups not allowed on account").asRuntimeException());
|
||||||
|
|
||||||
|
final Instant startOfDay = clock.instant().truncatedTo(ChronoUnit.DAYS);
|
||||||
|
if (redemptionStart.isAfter(redemptionEnd) ||
|
||||||
|
redemptionStart.isBefore(startOfDay) ||
|
||||||
|
redemptionEnd.isAfter(startOfDay.plus(MAX_REDEMPTION_DURATION)) ||
|
||||||
|
!redemptionStart.equals(redemptionStart.truncatedTo(ChronoUnit.DAYS)) ||
|
||||||
|
!redemptionEnd.equals(redemptionEnd.truncatedTo(ChronoUnit.DAYS))) {
|
||||||
|
|
||||||
|
throw Status.INVALID_ARGUMENT.withDescription("invalid redemption window").asRuntimeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch the blinded backup-id the account should have previously committed to
|
||||||
|
final byte[] committedBytes = account.getBackupCredentialRequest();
|
||||||
|
if (committedBytes == null) {
|
||||||
|
throw Status.NOT_FOUND.withDescription("No blinded backup-id has been added to the account").asRuntimeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// create a credential for every day in the requested period
|
||||||
|
final BackupAuthCredentialRequest credentialReq = new BackupAuthCredentialRequest(committedBytes);
|
||||||
|
return CompletableFuture.completedFuture(Stream
|
||||||
|
.iterate(redemptionStart, curr -> curr.plus(Duration.ofDays(1)))
|
||||||
|
.takeWhile(redemptionTime -> !redemptionTime.isAfter(redemptionEnd))
|
||||||
|
.map(redemption -> new Credential(
|
||||||
|
credentialReq.issueCredential(redemption, receiptLevel, serverSecretParams),
|
||||||
|
redemption))
|
||||||
|
.toList());
|
||||||
|
} catch (InvalidInputException e) {
|
||||||
|
throw Status.INTERNAL
|
||||||
|
.withDescription("Could not deserialize stored request credential")
|
||||||
|
.withCause(e)
|
||||||
|
.asRuntimeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Long> receiptLevel(final Account account) {
|
||||||
|
if (inExperiment(BACKUP_MEDIA_EXPERIMENT_NAME, account)) {
|
||||||
|
return Optional.of(BackupTier.MEDIA.getReceiptLevel());
|
||||||
|
}
|
||||||
|
if (inExperiment(BACKUP_EXPERIMENT_NAME, account)) {
|
||||||
|
return Optional.of(BackupTier.MESSAGES.getReceiptLevel());
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean inExperiment(final String experimentName, final Account account) {
|
||||||
|
return dynamicConfigurationManager.getConfiguration()
|
||||||
|
.getExperimentEnrollmentConfiguration(experimentName)
|
||||||
|
.map(config -> config.getEnrolledUuids().contains(account.getUuid()))
|
||||||
|
.orElse(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,542 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.backup;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import io.grpc.Status;
|
||||||
|
import io.micrometer.core.instrument.Metrics;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionStage;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||||
|
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||||
|
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
|
||||||
|
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||||
|
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialPresentation;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
|
||||||
|
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
public class BackupManager {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(BackupManager.class);
|
||||||
|
|
||||||
|
static final String MEDIA_DIRECTORY_NAME = "media";
|
||||||
|
static final String MESSAGE_BACKUP_NAME = "messageBackup";
|
||||||
|
static final long MAX_TOTAL_BACKUP_MEDIA_BYTES = 1024L * 1024L * 1024L * 50L;
|
||||||
|
static final long MAX_MEDIA_OBJECT_SIZE = 1024L * 1024L * 101L;
|
||||||
|
// If the last media usage recalculation is over MAX_QUOTA_STALENESS, force a recalculation before quota enforcement.
|
||||||
|
static final Duration MAX_QUOTA_STALENESS = Duration.ofDays(1);
|
||||||
|
private static final String ZK_AUTHN_COUNTER_NAME = MetricsUtil.name(BackupManager.class, "authentication");
|
||||||
|
private static final String ZK_AUTHZ_FAILURE_COUNTER_NAME = MetricsUtil.name(BackupManager.class,
|
||||||
|
"authorizationFailure");
|
||||||
|
private static final String USAGE_RECALCULATION_COUNTER_NAME = MetricsUtil.name(BackupManager.class,
|
||||||
|
"usageRecalculation");
|
||||||
|
private static final String SUCCESS_TAG_NAME = "success";
|
||||||
|
private static final String FAILURE_REASON_TAG_NAME = "reason";
|
||||||
|
|
||||||
|
private final BackupsDb backupsDb;
|
||||||
|
private final GenericServerSecretParams serverSecretParams;
|
||||||
|
private final Cdn3BackupCredentialGenerator cdn3BackupCredentialGenerator;
|
||||||
|
private final RemoteStorageManager remoteStorageManager;
|
||||||
|
private final Map<Integer, String> attachmentCdnBaseUris;
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
|
|
||||||
|
public BackupManager(
|
||||||
|
final BackupsDb backupsDb,
|
||||||
|
final GenericServerSecretParams serverSecretParams,
|
||||||
|
final Cdn3BackupCredentialGenerator cdn3BackupCredentialGenerator,
|
||||||
|
final RemoteStorageManager remoteStorageManager,
|
||||||
|
final Map<Integer, String> attachmentCdnBaseUris,
|
||||||
|
final Clock clock) {
|
||||||
|
this.backupsDb = backupsDb;
|
||||||
|
this.serverSecretParams = serverSecretParams;
|
||||||
|
this.cdn3BackupCredentialGenerator = cdn3BackupCredentialGenerator;
|
||||||
|
this.remoteStorageManager = remoteStorageManager;
|
||||||
|
this.clock = clock;
|
||||||
|
// strip trailing "/" for easier URI construction
|
||||||
|
this.attachmentCdnBaseUris = attachmentCdnBaseUris.entrySet().stream().collect(Collectors.toMap(
|
||||||
|
Map.Entry::getKey,
|
||||||
|
entry -> StringUtils.removeEnd(entry.getValue(), "/")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the public key for the backup-id.
|
||||||
|
* <p>
|
||||||
|
* Once set, calls {@link BackupManager#authenticateBackupUser} can succeed if the presentation is signed with the
|
||||||
|
* private key corresponding to this public key.
|
||||||
|
*
|
||||||
|
* @param presentation a ZK credential presentation that encodes the backupId
|
||||||
|
* @param signature the signature of the presentation
|
||||||
|
* @param publicKey the public key of a key-pair that the presentation must be signed with
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Void> setPublicKey(
|
||||||
|
final BackupAuthCredentialPresentation presentation,
|
||||||
|
final byte[] signature,
|
||||||
|
final ECPublicKey publicKey) {
|
||||||
|
|
||||||
|
// Note: this is a special case where we can't validate the presentation signature against the stored public key
|
||||||
|
// because we are currently setting it. We check against the provided public key, but we must also verify that
|
||||||
|
// there isn't an existing, different stored public key for the backup-id (verified with a condition expression)
|
||||||
|
final BackupTier backupTier = verifySignatureAndCheckPresentation(presentation, signature, publicKey);
|
||||||
|
if (backupTier.compareTo(BackupTier.MESSAGES) < 0) {
|
||||||
|
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
|
||||||
|
throw Status.PERMISSION_DENIED
|
||||||
|
.withDescription("credential does not support setting public key")
|
||||||
|
.asRuntimeException();
|
||||||
|
}
|
||||||
|
return backupsDb.setPublicKey(presentation.getBackupId(), backupTier, publicKey)
|
||||||
|
.exceptionally(ExceptionUtils.exceptionallyHandler(PublicKeyConflictException.class, ex -> {
|
||||||
|
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
|
||||||
|
SUCCESS_TAG_NAME, String.valueOf(false),
|
||||||
|
FAILURE_REASON_TAG_NAME, "public_key_conflict")
|
||||||
|
.increment();
|
||||||
|
throw Status.UNAUTHENTICATED
|
||||||
|
.withDescription("public key does not match existing public key for the backup-id")
|
||||||
|
.asRuntimeException();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a form that may be used to upload a backup file for the backupId encoded in the presentation.
|
||||||
|
* <p>
|
||||||
|
* If successful, this also updates the TTL of the backup.
|
||||||
|
*
|
||||||
|
* @param backupUser an already ZK authenticated backup user
|
||||||
|
* @return the upload form
|
||||||
|
*/
|
||||||
|
public CompletableFuture<MessageBackupUploadDescriptor> createMessageBackupUploadDescriptor(
|
||||||
|
final AuthenticatedBackupUser backupUser) {
|
||||||
|
// this could race with concurrent updates, but the only effect would be last-writer-wins on the timestamp
|
||||||
|
return backupsDb
|
||||||
|
.addMessageBackup(backupUser)
|
||||||
|
.thenApply(result -> cdn3BackupCredentialGenerator.generateUpload(cdnMessageBackupName(backupUser)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the last update timestamps for the backupId in the presentation
|
||||||
|
*
|
||||||
|
* @param backupUser an already ZK authenticated backup user
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Void> ttlRefresh(final AuthenticatedBackupUser backupUser) {
|
||||||
|
if (backupUser.backupTier().compareTo(BackupTier.MESSAGES) < 0) {
|
||||||
|
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
|
||||||
|
throw Status.PERMISSION_DENIED
|
||||||
|
.withDescription("credential does not support ttl operation")
|
||||||
|
.asRuntimeException();
|
||||||
|
}
|
||||||
|
// update message backup TTL
|
||||||
|
return backupsDb.ttlRefresh(backupUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record BackupInfo(int cdn, String backupSubdir, String messageBackupKey, Optional<Long> mediaUsedSpace) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve information about the existing backup
|
||||||
|
*
|
||||||
|
* @param backupUser an already ZK authenticated backup user
|
||||||
|
* @return Information about the existing backup
|
||||||
|
*/
|
||||||
|
public CompletableFuture<BackupInfo> backupInfo(final AuthenticatedBackupUser backupUser) {
|
||||||
|
if (backupUser.backupTier().compareTo(BackupTier.MESSAGES) < 0) {
|
||||||
|
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
|
||||||
|
throw Status.PERMISSION_DENIED.withDescription("credential does not support info operation")
|
||||||
|
.asRuntimeException();
|
||||||
|
}
|
||||||
|
return backupsDb.describeBackup(backupUser)
|
||||||
|
.thenApply(backupDescription -> new BackupInfo(
|
||||||
|
backupDescription.cdn(),
|
||||||
|
encodeBackupIdForCdn(backupUser),
|
||||||
|
MESSAGE_BACKUP_NAME,
|
||||||
|
backupDescription.mediaUsedSpace()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there is enough capacity to store the requested amount of media
|
||||||
|
*
|
||||||
|
* @param backupUser an already ZK authenticated backup user
|
||||||
|
* @param mediaLength the desired number of media bytes to store
|
||||||
|
* @return true if mediaLength bytes can be stored
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Boolean> canStoreMedia(final AuthenticatedBackupUser backupUser, final long mediaLength) {
|
||||||
|
if (backupUser.backupTier().compareTo(BackupTier.MEDIA) < 0) {
|
||||||
|
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
|
||||||
|
throw Status.PERMISSION_DENIED
|
||||||
|
.withDescription("credential does not support storing media")
|
||||||
|
.asRuntimeException();
|
||||||
|
}
|
||||||
|
return backupsDb.getMediaUsage(backupUser)
|
||||||
|
.thenComposeAsync(info -> {
|
||||||
|
final boolean canStore = MAX_TOTAL_BACKUP_MEDIA_BYTES - info.usageInfo().bytesUsed() >= mediaLength;
|
||||||
|
if (canStore || info.lastRecalculationTime().isAfter(clock.instant().minus(MAX_QUOTA_STALENESS))) {
|
||||||
|
return CompletableFuture.completedFuture(canStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The user is out of quota, and we have not recently recalculated the user's usage. Double check by doing a
|
||||||
|
// hard recalculation before actually forbidding the user from storing additional media.
|
||||||
|
return this.remoteStorageManager.calculateBytesUsed(cdnMediaDirectory(backupUser))
|
||||||
|
.thenCompose(usage -> backupsDb
|
||||||
|
.setMediaUsage(backupUser, usage)
|
||||||
|
.thenApply(ignored -> usage))
|
||||||
|
.whenComplete((newUsage, throwable) -> {
|
||||||
|
boolean usageChanged = throwable == null && !newUsage.equals(info.usageInfo());
|
||||||
|
Metrics.counter(USAGE_RECALCULATION_COUNTER_NAME, "usageChanged", String.valueOf(usageChanged))
|
||||||
|
.increment();
|
||||||
|
})
|
||||||
|
.thenApply(newUsage -> MAX_TOTAL_BACKUP_MEDIA_BYTES - newUsage.bytesUsed() >= mediaLength);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public record StorageDescriptor(int cdn, byte[] key) {}
|
||||||
|
|
||||||
|
public record StorageDescriptorWithLength(int cdn, byte[] key, long length) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy an encrypted object to the backup cdn, adding a layer of encryption
|
||||||
|
* <p>
|
||||||
|
* Implementation notes: <p> This method guarantees that any object that gets successfully copied to the backup cdn
|
||||||
|
* will also be deducted from the user's quota. </p>
|
||||||
|
* <p>
|
||||||
|
* However, the converse isn't true. It's possible we may charge the user for media they failed to copy. As a result,
|
||||||
|
* the quota may be over reported and it should be recalculated before taking quota enforcement actions.
|
||||||
|
*
|
||||||
|
* @return A stage that completes successfully with location of the twice-encrypted object on the backup cdn. The
|
||||||
|
* returned CompletionStage can be completed exceptionally with the following exceptions.
|
||||||
|
* <ul>
|
||||||
|
* <li> {@link InvalidLengthException} If the expectedSourceLength does not match the length of the sourceUri </li>
|
||||||
|
* <li> {@link SourceObjectNotFoundException} If the no object at sourceUri is found </li>
|
||||||
|
* <li> {@link java.io.IOException} If there was a generic IO issue </li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public CompletableFuture<StorageDescriptor> copyToBackup(
|
||||||
|
final AuthenticatedBackupUser backupUser,
|
||||||
|
final int sourceCdn,
|
||||||
|
final String sourceKey,
|
||||||
|
final int sourceLength,
|
||||||
|
final MediaEncryptionParameters encryptionParameters,
|
||||||
|
final byte[] destinationMediaId) {
|
||||||
|
if (backupUser.backupTier().compareTo(BackupTier.MEDIA) < 0) {
|
||||||
|
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
|
||||||
|
throw Status.PERMISSION_DENIED
|
||||||
|
.withDescription("credential does not support storing media")
|
||||||
|
.asRuntimeException();
|
||||||
|
}
|
||||||
|
if (sourceLength > MAX_MEDIA_OBJECT_SIZE) {
|
||||||
|
throw Status.INVALID_ARGUMENT
|
||||||
|
.withDescription("Invalid sourceObject size")
|
||||||
|
.asRuntimeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
final MessageBackupUploadDescriptor dst = cdn3BackupCredentialGenerator.generateUpload(
|
||||||
|
cdnMediaPath(backupUser, destinationMediaId));
|
||||||
|
|
||||||
|
final int destinationLength = encryptionParameters.outputSize(sourceLength);
|
||||||
|
|
||||||
|
final URI sourceUri = attachmentReadUri(sourceCdn, sourceKey);
|
||||||
|
return this.backupsDb
|
||||||
|
// Write the ddb updates before actually updating backing storage
|
||||||
|
.trackMedia(backupUser, 1, destinationLength)
|
||||||
|
|
||||||
|
// Actually copy the objects. If the copy fails, our estimated quota usage may not be exact
|
||||||
|
.thenComposeAsync(ignored -> remoteStorageManager.copy(sourceUri, sourceLength, encryptionParameters, dst))
|
||||||
|
.exceptionallyCompose(throwable -> {
|
||||||
|
final Throwable unwrapped = ExceptionUtils.unwrap(throwable);
|
||||||
|
if (!(unwrapped instanceof SourceObjectNotFoundException) && !(unwrapped instanceof InvalidLengthException)) {
|
||||||
|
throw ExceptionUtils.wrap(unwrapped);
|
||||||
|
}
|
||||||
|
// In cases where we know the copy fails without writing anything, we can try to restore the user's quota
|
||||||
|
return this.backupsDb.trackMedia(backupUser, -1, -destinationLength).whenComplete((ignored, ignoredEx) -> {
|
||||||
|
throw ExceptionUtils.wrap(unwrapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
// indicates where the backup was stored
|
||||||
|
.thenApply(ignore -> new StorageDescriptor(dst.cdn(), destinationMediaId));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct the URI for an attachment with the specified key
|
||||||
|
*
|
||||||
|
* @param cdn where the attachment is located
|
||||||
|
* @param key the attachment key
|
||||||
|
* @return A {@link URI} where the attachment can be retrieved
|
||||||
|
*/
|
||||||
|
private URI attachmentReadUri(final int cdn, final String key) {
|
||||||
|
final String baseUri = attachmentCdnBaseUris.get(cdn);
|
||||||
|
if (baseUri == null) {
|
||||||
|
throw Status.INVALID_ARGUMENT.withDescription("Unknown cdn " + cdn).asRuntimeException();
|
||||||
|
}
|
||||||
|
return URI.create("%s/%s".formatted(baseUri, key));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate credentials that can be used to read from the backup CDN
|
||||||
|
*
|
||||||
|
* @param backupUser an already ZK authenticated backup user
|
||||||
|
* @return A map of headers to include with CDN requests
|
||||||
|
*/
|
||||||
|
public Map<String, String> generateReadAuth(final AuthenticatedBackupUser backupUser) {
|
||||||
|
if (backupUser.backupTier().compareTo(BackupTier.MESSAGES) < 0) {
|
||||||
|
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
|
||||||
|
throw Status.PERMISSION_DENIED
|
||||||
|
.withDescription("credential does not support read auth operation")
|
||||||
|
.asRuntimeException();
|
||||||
|
}
|
||||||
|
final String encodedBackupId = encodeBackupIdForCdn(backupUser);
|
||||||
|
return cdn3BackupCredentialGenerator.readHeaders(encodedBackupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of media stored for a particular backup id
|
||||||
|
*
|
||||||
|
* @param media A page of media entries
|
||||||
|
* @param cursor If set, can be passed back to a subsequent list request to resume listing from the previous point
|
||||||
|
*/
|
||||||
|
public record ListMediaResult(List<StorageDescriptorWithLength> media, Optional<String> cursor) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List the media stored by the backupUser
|
||||||
|
*
|
||||||
|
* @param backupUser An already ZK authenticated backup user
|
||||||
|
* @param cursor A cursor returned by a previous call that can be used to resume listing
|
||||||
|
* @param limit The maximum number of list results to return
|
||||||
|
* @return A {@link ListMediaResult}
|
||||||
|
*/
|
||||||
|
public CompletionStage<ListMediaResult> list(
|
||||||
|
final AuthenticatedBackupUser backupUser,
|
||||||
|
final Optional<String> cursor,
|
||||||
|
final int limit) {
|
||||||
|
if (backupUser.backupTier().compareTo(BackupTier.MESSAGES) < 0) {
|
||||||
|
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
|
||||||
|
throw Status.PERMISSION_DENIED
|
||||||
|
.withDescription("credential does not support list operation")
|
||||||
|
.asRuntimeException();
|
||||||
|
}
|
||||||
|
return remoteStorageManager.list(cdnMediaDirectory(backupUser), cursor, limit)
|
||||||
|
.thenApply(result ->
|
||||||
|
new ListMediaResult(
|
||||||
|
result
|
||||||
|
.objects()
|
||||||
|
.stream()
|
||||||
|
.map(entry -> new StorageDescriptorWithLength(
|
||||||
|
remoteStorageManager.cdnNumber(),
|
||||||
|
decodeFromCdn(entry.key()),
|
||||||
|
entry.length()
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
result.cursor()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private sealed interface Either permits DeleteSuccess, DeleteFailure {}
|
||||||
|
|
||||||
|
private record DeleteSuccess(long usage) implements Either {}
|
||||||
|
|
||||||
|
private record DeleteFailure(Throwable e) implements Either {}
|
||||||
|
|
||||||
|
public CompletableFuture<Void> delete(final AuthenticatedBackupUser backupUser,
|
||||||
|
final List<StorageDescriptor> storageDescriptors) {
|
||||||
|
if (backupUser.backupTier().compareTo(BackupTier.MESSAGES) < 0) {
|
||||||
|
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
|
||||||
|
throw Status.PERMISSION_DENIED
|
||||||
|
.withDescription("credential does not support list operation")
|
||||||
|
.asRuntimeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storageDescriptors.stream().anyMatch(sd -> sd.cdn() != remoteStorageManager.cdnNumber())) {
|
||||||
|
throw Status.INVALID_ARGUMENT
|
||||||
|
.withDescription("unsupported media cdn provided")
|
||||||
|
.asRuntimeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Flux
|
||||||
|
.fromIterable(storageDescriptors)
|
||||||
|
|
||||||
|
// Issue deletes for all storage descriptors (proceeds with default flux concurrency)
|
||||||
|
.flatMap(descriptor -> Mono.fromCompletionStage(
|
||||||
|
remoteStorageManager
|
||||||
|
.delete(cdnMediaPath(backupUser, descriptor.key))
|
||||||
|
// Squash errors/success into a single type
|
||||||
|
.handle((bytesDeleted, throwable) -> throwable != null
|
||||||
|
? new DeleteFailure(throwable)
|
||||||
|
: new DeleteSuccess(bytesDeleted))
|
||||||
|
))
|
||||||
|
|
||||||
|
// Update backupsDb with the change in usage
|
||||||
|
.collectList()
|
||||||
|
.<Void>flatMap(eithers -> {
|
||||||
|
// count up usage changes
|
||||||
|
long totalBytesDeleted = 0;
|
||||||
|
long totalCountDeleted = 0;
|
||||||
|
final List<Throwable> toThrow = new ArrayList<>();
|
||||||
|
for (Either either : eithers) {
|
||||||
|
switch (either) {
|
||||||
|
case DeleteFailure f:
|
||||||
|
toThrow.add(f.e());
|
||||||
|
break;
|
||||||
|
case DeleteSuccess s when s.usage() > 0:
|
||||||
|
totalBytesDeleted += s.usage();
|
||||||
|
totalCountDeleted++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final Mono<Void> result = toThrow.isEmpty()
|
||||||
|
? Mono.empty()
|
||||||
|
: Mono.error(toThrow.stream().reduce((t1, t2) -> {
|
||||||
|
t1.addSuppressed(t2);
|
||||||
|
return t1;
|
||||||
|
}).get());
|
||||||
|
return Mono
|
||||||
|
.fromCompletionStage(this.backupsDb.trackMedia(backupUser, -totalCountDeleted, -totalBytesDeleted))
|
||||||
|
.then(result);
|
||||||
|
})
|
||||||
|
.toFuture();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate the ZK anonymous backup credential's presentation
|
||||||
|
* <p>
|
||||||
|
* This validates:
|
||||||
|
* <li> The presentation was for a credential issued by the server </li>
|
||||||
|
* <li> The credential is in its redemption window </li>
|
||||||
|
* <li> The backup-id matches a previously committed blinded backup-id and server issued receipt level </li>
|
||||||
|
* <li> The signature of the credential matches an existing publicKey associated with this backup-id </li>
|
||||||
|
*
|
||||||
|
* @param presentation A {@link BackupAuthCredentialPresentation}
|
||||||
|
* @param signature An XEd25519 signature of the presentation bytes
|
||||||
|
* @return On authentication success, the authenticated backup-id and backup-tier encoded in the presentation
|
||||||
|
*/
|
||||||
|
public CompletableFuture<AuthenticatedBackupUser> authenticateBackupUser(
|
||||||
|
final BackupAuthCredentialPresentation presentation,
|
||||||
|
final byte[] signature) {
|
||||||
|
return backupsDb
|
||||||
|
.retrievePublicKey(presentation.getBackupId())
|
||||||
|
.thenApply(optionalPublicKey -> {
|
||||||
|
final byte[] publicKeyBytes = optionalPublicKey
|
||||||
|
.orElseThrow(() -> {
|
||||||
|
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
|
||||||
|
SUCCESS_TAG_NAME, String.valueOf(false),
|
||||||
|
FAILURE_REASON_TAG_NAME, "missing_public_key")
|
||||||
|
.increment();
|
||||||
|
return Status.NOT_FOUND.withDescription("Backup not found").asRuntimeException();
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
final ECPublicKey publicKey = new ECPublicKey(publicKeyBytes);
|
||||||
|
return new AuthenticatedBackupUser(
|
||||||
|
presentation.getBackupId(),
|
||||||
|
verifySignatureAndCheckPresentation(presentation, signature, publicKey));
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
|
||||||
|
SUCCESS_TAG_NAME, String.valueOf(false),
|
||||||
|
FAILURE_REASON_TAG_NAME, "invalid_public_key")
|
||||||
|
.increment();
|
||||||
|
logger.error("Invalid publicKey for backupId hash {}",
|
||||||
|
HexFormat.of().formatHex(BackupsDb.hashedBackupId(presentation.getBackupId())), e);
|
||||||
|
throw Status.INTERNAL
|
||||||
|
.withCause(e)
|
||||||
|
.withDescription("Could not deserialize stored public key")
|
||||||
|
.asRuntimeException();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.thenApply(result -> {
|
||||||
|
Metrics.counter(ZK_AUTHN_COUNTER_NAME, SUCCESS_TAG_NAME, String.valueOf(true)).increment();
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify the presentation and return the extracted backup tier
|
||||||
|
*
|
||||||
|
* @param presentation A ZK credential presentation that encodes the backupId and the receipt level of the requester
|
||||||
|
* @return The backup tier this presentation supports
|
||||||
|
*/
|
||||||
|
private BackupTier verifySignatureAndCheckPresentation(
|
||||||
|
final BackupAuthCredentialPresentation presentation,
|
||||||
|
final byte[] signature,
|
||||||
|
final ECPublicKey publicKey) {
|
||||||
|
if (!publicKey.verifySignature(presentation.serialize(), signature)) {
|
||||||
|
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
|
||||||
|
SUCCESS_TAG_NAME, String.valueOf(false),
|
||||||
|
FAILURE_REASON_TAG_NAME, "signature_validation")
|
||||||
|
.increment();
|
||||||
|
throw Status.UNAUTHENTICATED
|
||||||
|
.withDescription("backup auth credential presentation signature verification failed")
|
||||||
|
.asRuntimeException();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
presentation.verify(clock.instant(), serverSecretParams);
|
||||||
|
} catch (VerificationFailedException e) {
|
||||||
|
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
|
||||||
|
SUCCESS_TAG_NAME, String.valueOf(false),
|
||||||
|
FAILURE_REASON_TAG_NAME, "presentation_verification")
|
||||||
|
.increment();
|
||||||
|
throw Status.UNAUTHENTICATED
|
||||||
|
.withDescription("backup auth credential presentation verification failed")
|
||||||
|
.withCause(e)
|
||||||
|
.asRuntimeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return BackupTier
|
||||||
|
.fromReceiptLevel(presentation.getReceiptLevel())
|
||||||
|
.orElseThrow(() -> {
|
||||||
|
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
|
||||||
|
SUCCESS_TAG_NAME, String.valueOf(false),
|
||||||
|
FAILURE_REASON_TAG_NAME, "invalid_receipt_level")
|
||||||
|
.increment();
|
||||||
|
return Status.PERMISSION_DENIED.withDescription("invalid receipt level").asRuntimeException();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static String encodeBackupIdForCdn(final AuthenticatedBackupUser backupUser) {
|
||||||
|
return encodeForCdn(BackupsDb.hashedBackupId(backupUser.backupId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static String encodeForCdn(final byte[] bytes) {
|
||||||
|
return Base64.getUrlEncoder().encodeToString(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] decodeFromCdn(final String base64) {
|
||||||
|
return Base64.getUrlDecoder().decode(base64);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String cdnMessageBackupName(final AuthenticatedBackupUser backupUser) {
|
||||||
|
return "%s/%s".formatted(encodeBackupIdForCdn(backupUser), MESSAGE_BACKUP_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String cdnMediaDirectory(final AuthenticatedBackupUser backupUser) {
|
||||||
|
return "%s/%s/".formatted(encodeBackupIdForCdn(backupUser), MEDIA_DIRECTORY_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String cdnMediaPath(final AuthenticatedBackupUser backupUser, final byte[] mediaId) {
|
||||||
|
return "%s%s".formatted(cdnMediaDirectory(backupUser), encodeForCdn(mediaId));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.backup;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Flow;
|
||||||
|
import javax.crypto.BadPaddingException;
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
import org.reactivestreams.FlowAdapters;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
public class BackupMediaEncrypter {
|
||||||
|
|
||||||
|
private final Cipher cipher;
|
||||||
|
private final Mac mac;
|
||||||
|
|
||||||
|
public BackupMediaEncrypter(final MediaEncryptionParameters encryptionParameters) {
|
||||||
|
cipher = initializeCipher(encryptionParameters);
|
||||||
|
mac = initializeMac(encryptionParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int outputSize(final int inputSize) {
|
||||||
|
return cipher.getIV().length + cipher.getOutputSize(inputSize) + mac.getMacLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform streaming encryption
|
||||||
|
*
|
||||||
|
* @param sourceBody A source of ByteBuffers, typically from an asynchronous HttpResponse
|
||||||
|
* @return A publisher that returns IV + AES/CBC/PKCS5Padding encrypted source + HMAC(IV + encrypted source) suitable
|
||||||
|
* to write with an asynchronous HttpRequest
|
||||||
|
*/
|
||||||
|
public Flow.Publisher<ByteBuffer> encryptBody(Flow.Publisher<List<ByteBuffer>> sourceBody) {
|
||||||
|
|
||||||
|
// Write IV, encrypted payload, mac
|
||||||
|
final Flux<ByteBuffer> encryptedBody = Flux.concat(
|
||||||
|
Mono.fromSupplier(() -> {
|
||||||
|
mac.update(cipher.getIV());
|
||||||
|
return ByteBuffer.wrap(cipher.getIV());
|
||||||
|
}),
|
||||||
|
Flux.from(FlowAdapters.toPublisher(sourceBody))
|
||||||
|
.flatMap(buffers -> Flux.fromIterable(buffers))
|
||||||
|
.concatMap(byteBuffer -> {
|
||||||
|
final byte[] copy = new byte[byteBuffer.remaining()];
|
||||||
|
byteBuffer.get(copy);
|
||||||
|
final byte[] res = cipher.update(copy);
|
||||||
|
if (res == null) {
|
||||||
|
return Mono.empty();
|
||||||
|
} else {
|
||||||
|
mac.update(res);
|
||||||
|
return Mono.just(ByteBuffer.wrap(res));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Mono.fromSupplier(() -> {
|
||||||
|
try {
|
||||||
|
final byte[] finalBytes = cipher.doFinal();
|
||||||
|
mac.update(finalBytes);
|
||||||
|
return ByteBuffer.wrap(finalBytes);
|
||||||
|
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||||
|
throw ExceptionUtils.wrap(e);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Mono.fromSupplier(() -> ByteBuffer.wrap(mac.doFinal())));
|
||||||
|
return FlowAdapters.toFlowPublisher(encryptedBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Mac initializeMac(final MediaEncryptionParameters encryptionParameters) {
|
||||||
|
try {
|
||||||
|
final Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
|
mac.init(encryptionParameters.hmacSHA256Key());
|
||||||
|
return mac;
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new IllegalArgumentException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Cipher initializeCipher(final MediaEncryptionParameters encryptionParameters) {
|
||||||
|
try {
|
||||||
|
final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||||
|
cipher.init(
|
||||||
|
Cipher.ENCRYPT_MODE,
|
||||||
|
encryptionParameters.aesEncryptionKey(),
|
||||||
|
encryptionParameters.iv());
|
||||||
|
return cipher;
|
||||||
|
|
||||||
|
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
} catch (InvalidAlgorithmParameterException | InvalidKeyException e) {
|
||||||
|
throw new IllegalArgumentException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.backup;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public enum BackupTier {
|
||||||
|
NONE(0),
|
||||||
|
MESSAGES(10),
|
||||||
|
MEDIA(20);
|
||||||
|
|
||||||
|
private static Map<Long, BackupTier> LOOKUP = Arrays.stream(BackupTier.values())
|
||||||
|
.collect(Collectors.toMap(BackupTier::getReceiptLevel, Function.identity()));
|
||||||
|
private long receiptLevel;
|
||||||
|
|
||||||
|
private BackupTier(long receiptLevel) {
|
||||||
|
this.receiptLevel = receiptLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
long getReceiptLevel() {
|
||||||
|
return receiptLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Optional<BackupTier> fromReceiptLevel(long receiptLevel) {
|
||||||
|
return Optional.ofNullable(LOOKUP.get(receiptLevel));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.backup;
|
||||||
|
|
||||||
|
import io.grpc.Status;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
|
||||||
|
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||||
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
import software.amazon.awssdk.core.SdkBytes;
|
||||||
|
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||||
|
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
|
||||||
|
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
|
||||||
|
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
|
||||||
|
import software.amazon.awssdk.services.dynamodb.model.Update;
|
||||||
|
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks backup metadata in a persistent store.
|
||||||
|
* <p>
|
||||||
|
* It's assumed that the caller has already validated that the backupUser being operated on has valid credentials and
|
||||||
|
* possesses the appropriate {@link BackupTier} to perform the current operation.
|
||||||
|
*/
|
||||||
|
public class BackupsDb {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(BackupsDb.class);
|
||||||
|
static final int BACKUP_CDN = 3;
|
||||||
|
|
||||||
|
private final DynamoDbAsyncClient dynamoClient;
|
||||||
|
private final String backupTableName;
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
|
// The backups table
|
||||||
|
|
||||||
|
// B: 16 bytes that identifies the backup
|
||||||
|
public static final String KEY_BACKUP_ID_HASH = "U";
|
||||||
|
// N: Time in seconds since epoch of the last backup refresh. This timestamp must be periodically updated to avoid
|
||||||
|
// garbage collection of archive objects.
|
||||||
|
public static final String ATTR_LAST_REFRESH = "R";
|
||||||
|
// N: Time in seconds since epoch of the last backup media refresh. This timestamp can only be updated if the client
|
||||||
|
// has BackupTier.MEDIA, and must be periodically updated to avoid garbage collection of media objects.
|
||||||
|
public static final String ATTR_LAST_MEDIA_REFRESH = "MR";
|
||||||
|
// B: A 32 byte public key that should be used to sign the presentation used to authenticate requests against the
|
||||||
|
// backup-id
|
||||||
|
public static final String ATTR_PUBLIC_KEY = "P";
|
||||||
|
// N: Bytes consumed by this backup
|
||||||
|
public static final String ATTR_MEDIA_BYTES_USED = "MB";
|
||||||
|
// N: Number of media objects in the backup
|
||||||
|
public static final String ATTR_MEDIA_COUNT = "MC";
|
||||||
|
// N: The cdn number where the message backup is stored
|
||||||
|
public static final String ATTR_CDN = "CDN";
|
||||||
|
// N: Time in seconds since epoch of last backup media usage recalculation. This timestamp is updated whenever we
|
||||||
|
// recalculate the up-to-date bytes used by querying the cdn(s) directly.
|
||||||
|
public static final String ATTR_MEDIA_USAGE_LAST_RECALCULATION = "MBTS";
|
||||||
|
|
||||||
|
public BackupsDb(
|
||||||
|
final DynamoDbAsyncClient dynamoClient,
|
||||||
|
final String backupTableName,
|
||||||
|
final Clock clock) {
|
||||||
|
this.dynamoClient = dynamoClient;
|
||||||
|
this.backupTableName = backupTableName;
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the public key associated with a backupId.
|
||||||
|
*
|
||||||
|
* @param authenticatedBackupId The backup-id bytes that should be associated with the provided public key
|
||||||
|
* @param authenticatedBackupTier The backup tier
|
||||||
|
* @param publicKey The public key to associate with the backup id
|
||||||
|
* @return A stage that completes when the public key has been set. If the backup-id already has a set public key that
|
||||||
|
* does not match, the stage will be completed exceptionally with a {@link PublicKeyConflictException}
|
||||||
|
*/
|
||||||
|
CompletableFuture<Void> setPublicKey(
|
||||||
|
final byte[] authenticatedBackupId,
|
||||||
|
final BackupTier authenticatedBackupTier,
|
||||||
|
final ECPublicKey publicKey) {
|
||||||
|
final byte[] hashedBackupId = hashedBackupId(authenticatedBackupId);
|
||||||
|
return dynamoClient.updateItem(new UpdateBuilder(backupTableName, authenticatedBackupTier, hashedBackupId)
|
||||||
|
.addSetExpression("#publicKey = :publicKey",
|
||||||
|
Map.entry("#publicKey", ATTR_PUBLIC_KEY),
|
||||||
|
Map.entry(":publicKey", AttributeValues.b(publicKey.serialize())))
|
||||||
|
.setRefreshTimes(clock)
|
||||||
|
.withConditionExpression("attribute_not_exists(#publicKey) OR #publicKey = :publicKey")
|
||||||
|
.updateItemBuilder()
|
||||||
|
.build())
|
||||||
|
.exceptionally(throwable -> {
|
||||||
|
// There was already a row for this backup-id and it contained a different publicKey
|
||||||
|
if (ExceptionUtils.unwrap(throwable) instanceof ConditionalCheckFailedException) {
|
||||||
|
throw ExceptionUtils.wrap(new PublicKeyConflictException());
|
||||||
|
}
|
||||||
|
throw ExceptionUtils.wrap(throwable);
|
||||||
|
})
|
||||||
|
.thenRun(Util.NOOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletableFuture<Optional<byte[]>> retrievePublicKey(byte[] backupId) {
|
||||||
|
final byte[] hashedBackupId = hashedBackupId(backupId);
|
||||||
|
return dynamoClient.getItem(GetItemRequest.builder()
|
||||||
|
.tableName(backupTableName)
|
||||||
|
.key(Map.of(KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId)))
|
||||||
|
.consistentRead(true)
|
||||||
|
.projectionExpression("#publicKey")
|
||||||
|
.expressionAttributeNames(Map.of("#publicKey", ATTR_PUBLIC_KEY))
|
||||||
|
.build())
|
||||||
|
.thenApply(response ->
|
||||||
|
AttributeValues.get(response.item(), ATTR_PUBLIC_KEY)
|
||||||
|
.map(AttributeValue::b)
|
||||||
|
.map(SdkBytes::asByteArray));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the quota in the backup table
|
||||||
|
*
|
||||||
|
* @param backupUser The backup user
|
||||||
|
* @param mediaBytesDelta The length of the media after encryption. A negative length implies media being removed
|
||||||
|
* @param mediaCountDelta The number of media objects being added, or if negative, removed
|
||||||
|
* @return A stage that completes successfully once the table are updated.
|
||||||
|
*/
|
||||||
|
CompletableFuture<Void> trackMedia(final AuthenticatedBackupUser backupUser, final long mediaCountDelta, final long mediaBytesDelta) {
|
||||||
|
final Instant now = clock.instant();
|
||||||
|
return dynamoClient
|
||||||
|
.updateItem(
|
||||||
|
// Update the media quota and TTL
|
||||||
|
UpdateBuilder.forUser(backupTableName, backupUser)
|
||||||
|
.setRefreshTimes(now)
|
||||||
|
.incrementMediaBytes(mediaBytesDelta)
|
||||||
|
.incrementMediaCount(mediaCountDelta)
|
||||||
|
.updateItemBuilder()
|
||||||
|
.build())
|
||||||
|
.thenRun(Util.NOOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the last update timestamps for the backupId in the presentation
|
||||||
|
*
|
||||||
|
* @param backupUser an already authorized backup user
|
||||||
|
*/
|
||||||
|
CompletableFuture<Void> ttlRefresh(final AuthenticatedBackupUser backupUser) {
|
||||||
|
// update message backup TTL
|
||||||
|
return dynamoClient.updateItem(UpdateBuilder.forUser(backupTableName, backupUser)
|
||||||
|
.setRefreshTimes(clock)
|
||||||
|
.updateItemBuilder()
|
||||||
|
.build())
|
||||||
|
.thenRun(Util.NOOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track that a backup will be stored for the user
|
||||||
|
*
|
||||||
|
* @param backupUser an already authorized backup user
|
||||||
|
*/
|
||||||
|
CompletableFuture<Void> addMessageBackup(final AuthenticatedBackupUser backupUser) {
|
||||||
|
// this could race with concurrent updates, but the only effect would be last-writer-wins on the timestamp
|
||||||
|
return dynamoClient.updateItem(
|
||||||
|
UpdateBuilder.forUser(backupTableName, backupUser)
|
||||||
|
.setRefreshTimes(clock)
|
||||||
|
.setCdn(BACKUP_CDN)
|
||||||
|
.updateItemBuilder()
|
||||||
|
.build())
|
||||||
|
.thenRun(Util.NOOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
record BackupDescription(int cdn, Optional<Long> mediaUsedSpace) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve information about the backup
|
||||||
|
*
|
||||||
|
* @param backupUser an already authorized backup user
|
||||||
|
* @return A {@link BackupDescription} containing the cdn of the message backup and the total number of media space
|
||||||
|
* bytes used by the backup user.
|
||||||
|
*/
|
||||||
|
CompletableFuture<BackupDescription> describeBackup(final AuthenticatedBackupUser backupUser) {
|
||||||
|
return dynamoClient.getItem(GetItemRequest.builder()
|
||||||
|
.tableName(backupTableName)
|
||||||
|
.key(Map.of(KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId(backupUser))))
|
||||||
|
.projectionExpression("#cdn,#mediaBytesUsed")
|
||||||
|
.expressionAttributeNames(Map.of("#cdn", ATTR_CDN, "#mediaBytesUsed", ATTR_MEDIA_BYTES_USED))
|
||||||
|
.consistentRead(true)
|
||||||
|
.build())
|
||||||
|
.thenApply(response -> {
|
||||||
|
if (!response.hasItem()) {
|
||||||
|
throw Status.NOT_FOUND.withDescription("Backup not found").asRuntimeException();
|
||||||
|
}
|
||||||
|
final int cdn = AttributeValues.get(response.item(), ATTR_CDN)
|
||||||
|
.map(AttributeValue::n)
|
||||||
|
.map(Integer::parseInt)
|
||||||
|
.orElseThrow(() -> Status.NOT_FOUND.withDescription("Stored backup not found").asRuntimeException());
|
||||||
|
|
||||||
|
final Optional<Long> mediaUsed = AttributeValues.get(response.item(), ATTR_MEDIA_BYTES_USED)
|
||||||
|
.map(AttributeValue::n)
|
||||||
|
.map(Long::parseLong);
|
||||||
|
|
||||||
|
return new BackupDescription(cdn, mediaUsed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public record TimestampedUsageInfo(UsageInfo usageInfo, Instant lastRecalculationTime) {}
|
||||||
|
|
||||||
|
CompletableFuture<TimestampedUsageInfo> getMediaUsage(final AuthenticatedBackupUser backupUser) {
|
||||||
|
return dynamoClient.getItem(GetItemRequest.builder()
|
||||||
|
.tableName(backupTableName)
|
||||||
|
.key(Map.of(KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId(backupUser))))
|
||||||
|
.projectionExpression("#mediaBytesUsed,#mediaCount,#usageRecalc")
|
||||||
|
.expressionAttributeNames(Map.of(
|
||||||
|
"#mediaBytesUsed", ATTR_MEDIA_BYTES_USED,
|
||||||
|
"#mediaCount", ATTR_MEDIA_COUNT,
|
||||||
|
"#usageRecalc", ATTR_MEDIA_USAGE_LAST_RECALCULATION))
|
||||||
|
.consistentRead(true)
|
||||||
|
.build())
|
||||||
|
.thenApply(response -> {
|
||||||
|
final long mediaUsed = AttributeValues.getLong(response.item(), ATTR_MEDIA_BYTES_USED, 0L);
|
||||||
|
final long mediaCount = AttributeValues.getLong(response.item(), ATTR_MEDIA_COUNT, 0L);
|
||||||
|
final long recalcSeconds = AttributeValues.getLong(response.item(), ATTR_MEDIA_USAGE_LAST_RECALCULATION, 0L);
|
||||||
|
return new TimestampedUsageInfo(new UsageInfo(mediaUsed, mediaCount), Instant.ofEpochSecond(recalcSeconds));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletableFuture<Void> setMediaUsage(final AuthenticatedBackupUser backupUser, UsageInfo usageInfo) {
|
||||||
|
return dynamoClient.updateItem(
|
||||||
|
UpdateBuilder.forUser(backupTableName, backupUser)
|
||||||
|
.addSetExpression("#mediaBytesUsed = :mediaBytesUsed",
|
||||||
|
Map.entry("#mediaBytesUsed", ATTR_MEDIA_BYTES_USED),
|
||||||
|
Map.entry(":mediaBytesUsed", AttributeValues.n(usageInfo.bytesUsed())))
|
||||||
|
.addSetExpression("#mediaCount = :mediaCount",
|
||||||
|
Map.entry("#mediaCount", ATTR_MEDIA_COUNT),
|
||||||
|
Map.entry(":mediaCount", AttributeValues.n(usageInfo.numObjects())))
|
||||||
|
.addSetExpression("#mediaRecalc = :mediaRecalc",
|
||||||
|
Map.entry("#mediaRecalc", ATTR_MEDIA_USAGE_LAST_RECALCULATION),
|
||||||
|
Map.entry(":mediaRecalc", AttributeValues.n(clock.instant().getEpochSecond())))
|
||||||
|
.updateItemBuilder()
|
||||||
|
.build())
|
||||||
|
.thenRun(Util.NOOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build ddb update statements for the backups table
|
||||||
|
*/
|
||||||
|
private static class UpdateBuilder {
|
||||||
|
|
||||||
|
private final List<String> setStatements = new ArrayList<>();
|
||||||
|
private final Map<String, AttributeValue> attrValues = new HashMap<>();
|
||||||
|
private final Map<String, String> attrNames = new HashMap<>();
|
||||||
|
|
||||||
|
private final String tableName;
|
||||||
|
private final BackupTier backupTier;
|
||||||
|
private final byte[] hashedBackupId;
|
||||||
|
private String conditionExpression = null;
|
||||||
|
|
||||||
|
static UpdateBuilder forUser(String tableName, AuthenticatedBackupUser backupUser) {
|
||||||
|
return new UpdateBuilder(tableName, backupUser.backupTier(), hashedBackupId(backupUser));
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateBuilder(String tableName, BackupTier backupTier, byte[] hashedBackupId) {
|
||||||
|
this.tableName = tableName;
|
||||||
|
this.backupTier = backupTier;
|
||||||
|
this.hashedBackupId = hashedBackupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAttrValue(Map.Entry<String, AttributeValue> attrValue) {
|
||||||
|
final AttributeValue old = attrValues.put(attrValue.getKey(), attrValue.getValue());
|
||||||
|
if (old != null && !old.equals(attrValue.getValue())) {
|
||||||
|
throw new IllegalArgumentException("duplicate attrValue key used for different values");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAttrName(Map.Entry<String, String> attrName) {
|
||||||
|
final String oldName = attrNames.put(attrName.getKey(), attrName.getValue());
|
||||||
|
if (oldName != null && !oldName.equals(attrName.getValue())) {
|
||||||
|
throw new IllegalArgumentException("duplicate attrName key used for different attribute names");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAttrs(final Map.Entry<String, String> attrName, final Map.Entry<String, AttributeValue> attrValue) {
|
||||||
|
addAttrName(attrName);
|
||||||
|
addAttrValue(attrValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateBuilder addSetExpression(
|
||||||
|
final String update,
|
||||||
|
final Map.Entry<String, String> attrName,
|
||||||
|
final Map.Entry<String, AttributeValue> attrValue) {
|
||||||
|
setStatements.add(update);
|
||||||
|
addAttrs(attrName, attrValue);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateBuilder addSetExpression(final String update) {
|
||||||
|
setStatements.add(update);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateBuilder withConditionExpression(final String conditionExpression) {
|
||||||
|
this.conditionExpression = conditionExpression;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateBuilder withConditionExpression(
|
||||||
|
final String conditionExpression,
|
||||||
|
final Map.Entry<String, String> attrName,
|
||||||
|
final Map.Entry<String, AttributeValue> attrValue) {
|
||||||
|
this.addAttrs(attrName, attrValue);
|
||||||
|
this.conditionExpression = conditionExpression;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateBuilder setCdn(final int cdn) {
|
||||||
|
return addSetExpression(
|
||||||
|
"#cdn = :cdn",
|
||||||
|
Map.entry("#cdn", ATTR_CDN),
|
||||||
|
Map.entry(":cdn", AttributeValues.n(cdn)));
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateBuilder incrementMediaCount(long delta) {
|
||||||
|
addAttrName(Map.entry("#mediaCount", ATTR_MEDIA_COUNT));
|
||||||
|
addAttrValue(Map.entry(":zero", AttributeValues.n(0)));
|
||||||
|
addAttrValue(Map.entry(":mediaCountDelta", AttributeValues.n(delta)));
|
||||||
|
addSetExpression("#mediaCount = if_not_exists(#mediaCount, :zero) + :mediaCountDelta");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateBuilder incrementMediaBytes(long delta) {
|
||||||
|
addAttrName(Map.entry("#mediaBytes", ATTR_MEDIA_BYTES_USED));
|
||||||
|
addAttrValue(Map.entry(":zero", AttributeValues.n(0)));
|
||||||
|
addAttrValue(Map.entry(":mediaBytesDelta", AttributeValues.n(delta)));
|
||||||
|
addSetExpression("#mediaBytes = if_not_exists(#mediaBytes, :zero) + :mediaBytesDelta");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the lastRefresh time as part of the update
|
||||||
|
* <p>
|
||||||
|
* This always updates lastRefreshTime, and updates lastMediaRefreshTime if the backup user has the appropriate
|
||||||
|
* tier.
|
||||||
|
*/
|
||||||
|
UpdateBuilder setRefreshTimes(final Clock clock) {
|
||||||
|
return this.setRefreshTimes(clock.instant());
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateBuilder setRefreshTimes(final Instant refreshTime) {
|
||||||
|
addSetExpression("#lastRefreshTime = :lastRefreshTime",
|
||||||
|
Map.entry("#lastRefreshTime", ATTR_LAST_REFRESH),
|
||||||
|
Map.entry(":lastRefreshTime", AttributeValues.n(refreshTime.getEpochSecond())));
|
||||||
|
|
||||||
|
if (backupTier.compareTo(BackupTier.MEDIA) >= 0) {
|
||||||
|
// update the media time if we have the appropriate tier
|
||||||
|
addSetExpression("#lastMediaRefreshTime = :lastMediaRefreshTime",
|
||||||
|
Map.entry("#lastMediaRefreshTime", ATTR_LAST_MEDIA_REFRESH),
|
||||||
|
Map.entry(":lastMediaRefreshTime", AttributeValues.n(refreshTime.getEpochSecond())));
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare a non-transactional update
|
||||||
|
*
|
||||||
|
* @return An {@link UpdateItemRequest#builder()} that can be used with updateItem
|
||||||
|
*/
|
||||||
|
UpdateItemRequest.Builder updateItemBuilder() {
|
||||||
|
final UpdateItemRequest.Builder bldr = UpdateItemRequest.builder()
|
||||||
|
.tableName(tableName)
|
||||||
|
.key(Map.of(KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId)))
|
||||||
|
.updateExpression("SET %s".formatted(String.join(",", setStatements)))
|
||||||
|
.expressionAttributeNames(attrNames)
|
||||||
|
.expressionAttributeValues(attrValues);
|
||||||
|
if (this.conditionExpression != null) {
|
||||||
|
bldr.conditionExpression(conditionExpression);
|
||||||
|
}
|
||||||
|
return bldr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare a transactional update
|
||||||
|
*
|
||||||
|
* @return An {@link Update#builder()} that can be used with transactItem
|
||||||
|
*/
|
||||||
|
Update.Builder transactItemBuilder() {
|
||||||
|
final Update.Builder bldr = Update.builder()
|
||||||
|
.tableName(tableName)
|
||||||
|
.key(Map.of(KEY_BACKUP_ID_HASH, AttributeValues.b(hashedBackupId)))
|
||||||
|
.updateExpression("SET %s".formatted(String.join(",", setStatements)))
|
||||||
|
.expressionAttributeNames(attrNames)
|
||||||
|
.expressionAttributeValues(attrValues);
|
||||||
|
if (this.conditionExpression != null) {
|
||||||
|
bldr.conditionExpression(conditionExpression);
|
||||||
|
}
|
||||||
|
return bldr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static byte[] hashedBackupId(final AuthenticatedBackupUser backupId) {
|
||||||
|
return hashedBackupId(backupId.backupId());
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] hashedBackupId(final byte[] backupId) {
|
||||||
|
try {
|
||||||
|
return Arrays.copyOf(MessageDigest.getInstance("SHA-256").digest(backupId), 16);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.backup;
|
||||||
|
|
||||||
|
import org.apache.http.HttpHeaders;
|
||||||
|
import org.whispersystems.textsecuregcm.attachments.TusConfiguration;
|
||||||
|
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 Cdn3BackupCredentialGenerator {
|
||||||
|
|
||||||
|
public static final String CDN_PATH = "backups";
|
||||||
|
public static final int BACKUP_CDN = 3;
|
||||||
|
|
||||||
|
private static String READ_PERMISSION = "read";
|
||||||
|
private static String WRITE_PERMISSION = "write";
|
||||||
|
private static String PERMISSION_SEPARATOR = "$";
|
||||||
|
|
||||||
|
// Write entities will be of the form 'write$backups/<string>
|
||||||
|
private static final String WRITE_ENTITY_PREFIX = String.format("%s%s%s/", WRITE_PERMISSION, PERMISSION_SEPARATOR,
|
||||||
|
CDN_PATH);
|
||||||
|
// Read entities will be of the form 'read$backups/<string>
|
||||||
|
private static final String READ_ENTITY_PREFIX = String.format("%s%s%s/", READ_PERMISSION, PERMISSION_SEPARATOR,
|
||||||
|
CDN_PATH);
|
||||||
|
|
||||||
|
private final ExternalServiceCredentialsGenerator credentialsGenerator;
|
||||||
|
private final String tusUri;
|
||||||
|
|
||||||
|
public Cdn3BackupCredentialGenerator(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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessageBackupUploadDescriptor generateUpload(final String key) {
|
||||||
|
if (key.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("Upload descriptors must have non-empty keys");
|
||||||
|
}
|
||||||
|
final String entity = WRITE_ENTITY_PREFIX + key;
|
||||||
|
final ExternalServiceCredentials credentials = credentialsGenerator.generateFor(entity);
|
||||||
|
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 MessageBackupUploadDescriptor(
|
||||||
|
BACKUP_CDN,
|
||||||
|
key,
|
||||||
|
headers,
|
||||||
|
tusUri + "/" + CDN_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> readHeaders(final String hashedBackupId) {
|
||||||
|
if (hashedBackupId.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("Backup subdir name must be non-empty");
|
||||||
|
}
|
||||||
|
final ExternalServiceCredentials credentials = credentialsGenerator.generateFor(
|
||||||
|
READ_ENTITY_PREFIX + hashedBackupId);
|
||||||
|
return Map.of(HttpHeaders.AUTHORIZATION, HeaderUtils.basicAuthHeader(credentials));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.backup;
|
||||||
|
|
||||||
|
import io.micrometer.core.instrument.Metrics;
|
||||||
|
import io.micrometer.core.instrument.Timer;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
import java.util.concurrent.CompletionStage;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.Cdn3StorageManagerConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.RetryConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
|
||||||
|
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||||
|
import org.whispersystems.textsecuregcm.util.HttpUtils;
|
||||||
|
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||||
|
|
||||||
|
public class Cdn3RemoteStorageManager implements RemoteStorageManager {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(Cdn3RemoteStorageManager.class);
|
||||||
|
|
||||||
|
private final FaultTolerantHttpClient cdnHttpClient;
|
||||||
|
private final FaultTolerantHttpClient storageManagerHttpClient;
|
||||||
|
private final String storageManagerBaseUrl;
|
||||||
|
private final String clientId;
|
||||||
|
private final String clientSecret;
|
||||||
|
static final String CLIENT_ID_HEADER = "CF-Access-Client-Id";
|
||||||
|
static final String CLIENT_SECRET_HEADER = "CF-Access-Client-Secret";
|
||||||
|
|
||||||
|
private static String TUS_UPLOAD_LENGTH_HEADER = "Upload-Length";
|
||||||
|
private static String TUS_UPLOAD_OFFSET_HEADER = "Upload-Offset";
|
||||||
|
private static String TUS_VERSION_HEADER = "Tus-Resumable";
|
||||||
|
private static String TUS_VERSION = "1.0.0";
|
||||||
|
private static String TUS_CONTENT_TYPE = "application/offset+octet-stream";
|
||||||
|
|
||||||
|
private static final String STORAGE_MANAGER_STATUS_COUNTER_NAME = MetricsUtil.name(Cdn3RemoteStorageManager.class,
|
||||||
|
"storageManagerStatus");
|
||||||
|
|
||||||
|
private static final String STORAGE_MANAGER_TIMER_NAME = MetricsUtil.name(Cdn3RemoteStorageManager.class,
|
||||||
|
"storageManager");
|
||||||
|
private static final String OPERATION_TAG_NAME = "op";
|
||||||
|
private static final String STATUS_TAG_NAME = "status";
|
||||||
|
|
||||||
|
public Cdn3RemoteStorageManager(
|
||||||
|
final ScheduledExecutorService retryExecutor,
|
||||||
|
final CircuitBreakerConfiguration circuitBreakerConfiguration,
|
||||||
|
final RetryConfiguration retryConfiguration,
|
||||||
|
final List<String> cdnCaCertificates,
|
||||||
|
final Cdn3StorageManagerConfiguration configuration) throws CertificateException {
|
||||||
|
|
||||||
|
// strip trailing "/" for easier URI construction
|
||||||
|
this.storageManagerBaseUrl = StringUtils.removeEnd(configuration.baseUri(), "/");
|
||||||
|
this.clientId = configuration.clientId();
|
||||||
|
this.clientSecret = configuration.clientSecret().value();
|
||||||
|
|
||||||
|
// Client used to read/write to cdn
|
||||||
|
this.cdnHttpClient = FaultTolerantHttpClient.newBuilder()
|
||||||
|
.withName("cdn-client")
|
||||||
|
.withCircuitBreaker(circuitBreakerConfiguration)
|
||||||
|
.withExecutor(Executors.newCachedThreadPool())
|
||||||
|
.withRetryExecutor(retryExecutor)
|
||||||
|
.withRetry(retryConfiguration)
|
||||||
|
.withConnectTimeout(Duration.ofSeconds(10))
|
||||||
|
.withVersion(HttpClient.Version.HTTP_2)
|
||||||
|
.withTrustedServerCertificates(cdnCaCertificates.toArray(new String[0]))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Client used for calls to storage-manager
|
||||||
|
// storage-manager has an external CA so uses a different client
|
||||||
|
this.storageManagerHttpClient = FaultTolerantHttpClient.newBuilder()
|
||||||
|
.withName("cdn3-storage-manager")
|
||||||
|
.withCircuitBreaker(circuitBreakerConfiguration)
|
||||||
|
.withExecutor(Executors.newCachedThreadPool())
|
||||||
|
.withRetryExecutor(retryExecutor)
|
||||||
|
.withRetry(retryConfiguration)
|
||||||
|
.withConnectTimeout(Duration.ofSeconds(10))
|
||||||
|
.withVersion(HttpClient.Version.HTTP_2)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int cdnNumber() {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletionStage<Void> copy(
|
||||||
|
final URI sourceUri,
|
||||||
|
final int expectedSourceLength,
|
||||||
|
final MediaEncryptionParameters encryptionParameters,
|
||||||
|
final MessageBackupUploadDescriptor uploadDescriptor) {
|
||||||
|
|
||||||
|
if (uploadDescriptor.cdn() != cdnNumber()) {
|
||||||
|
throw new IllegalArgumentException("Cdn3RemoteStorageManager can only copy to cdn3");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Timer.Sample sample = Timer.start();
|
||||||
|
final BackupMediaEncrypter encrypter = new BackupMediaEncrypter(encryptionParameters);
|
||||||
|
final HttpRequest request = HttpRequest.newBuilder().GET().uri(sourceUri).build();
|
||||||
|
final int expectedEncryptedLength = encrypter.outputSize(expectedSourceLength);
|
||||||
|
return cdnHttpClient.sendAsync(request, HttpResponse.BodyHandlers.ofPublisher()).thenCompose(response -> {
|
||||||
|
if (response.statusCode() == Response.Status.NOT_FOUND.getStatusCode()) {
|
||||||
|
throw new CompletionException(new SourceObjectNotFoundException());
|
||||||
|
} else if (response.statusCode() != Response.Status.OK.getStatusCode()) {
|
||||||
|
throw new CompletionException(new IOException("error reading from source: " + response.statusCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
final int actualSourceLength = Math.toIntExact(response.headers().firstValueAsLong("Content-Length")
|
||||||
|
.orElseThrow(() -> new CompletionException(new IOException("upstream missing Content-Length"))));
|
||||||
|
|
||||||
|
if (actualSourceLength != expectedSourceLength) {
|
||||||
|
throw new CompletionException(
|
||||||
|
new InvalidLengthException("Provided sourceLength " + expectedSourceLength + " was " + actualSourceLength));
|
||||||
|
}
|
||||||
|
|
||||||
|
final HttpRequest.BodyPublisher encryptedBody = HttpRequest.BodyPublishers.fromPublisher(
|
||||||
|
encrypter.encryptBody(response.body()), expectedEncryptedLength);
|
||||||
|
|
||||||
|
final String[] headers = Stream.concat(
|
||||||
|
uploadDescriptor.headers().entrySet()
|
||||||
|
.stream()
|
||||||
|
.flatMap(e -> Stream.of(e.getKey(), e.getValue())),
|
||||||
|
Stream.of(
|
||||||
|
TUS_VERSION_HEADER, TUS_VERSION,
|
||||||
|
TUS_UPLOAD_LENGTH_HEADER, Integer.toString(expectedEncryptedLength),
|
||||||
|
HttpHeaders.CONTENT_TYPE, TUS_CONTENT_TYPE))
|
||||||
|
.toArray(String[]::new);
|
||||||
|
|
||||||
|
final HttpRequest post = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(uploadDescriptor.signedUploadLocation()))
|
||||||
|
.headers(headers)
|
||||||
|
.POST(encryptedBody)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return cdnHttpClient.sendAsync(post, HttpResponse.BodyHandlers.discarding());
|
||||||
|
})
|
||||||
|
.thenAccept(response -> {
|
||||||
|
if (response.statusCode() != Response.Status.CREATED.getStatusCode() &&
|
||||||
|
response.statusCode() != Response.Status.OK.getStatusCode()) {
|
||||||
|
throw new CompletionException(new IOException("Failed to copy object: " + response.statusCode()));
|
||||||
|
}
|
||||||
|
long uploadOffset = response.headers().firstValueAsLong(TUS_UPLOAD_OFFSET_HEADER)
|
||||||
|
.orElseThrow(() -> new CompletionException(new IOException("Tus server did not return Upload-Offset")));
|
||||||
|
if (uploadOffset != expectedEncryptedLength) {
|
||||||
|
throw new CompletionException(new IOException(
|
||||||
|
"Expected to upload %d bytes, uploaded %d".formatted(expectedEncryptedLength, uploadOffset)));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.whenComplete((ignored, ignoredException) ->
|
||||||
|
sample.stop(Metrics.timer(STORAGE_MANAGER_TIMER_NAME, OPERATION_TAG_NAME, "copy")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletionStage<ListResult> list(
|
||||||
|
final String prefix,
|
||||||
|
final Optional<String> cursor,
|
||||||
|
final long limit) {
|
||||||
|
final Timer.Sample sample = Timer.start();
|
||||||
|
|
||||||
|
final Map<String, String> queryParams = new HashMap<>();
|
||||||
|
queryParams.put("prefix", prefix);
|
||||||
|
queryParams.put("limit", Long.toString(limit));
|
||||||
|
cursor.ifPresent(s -> queryParams.put("cursor", cursor.get()));
|
||||||
|
|
||||||
|
final HttpRequest request = HttpRequest.newBuilder().GET()
|
||||||
|
.uri(URI.create("%s%s".formatted(listUrl(), HttpUtils.queryParamString(queryParams.entrySet()))))
|
||||||
|
.header(CLIENT_ID_HEADER, clientId)
|
||||||
|
.header(CLIENT_SECRET_HEADER, clientSecret)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return this.storageManagerHttpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())
|
||||||
|
.thenApply(response -> {
|
||||||
|
Metrics.counter(STORAGE_MANAGER_STATUS_COUNTER_NAME,
|
||||||
|
OPERATION_TAG_NAME, "list",
|
||||||
|
STATUS_TAG_NAME, Integer.toString(response.statusCode()))
|
||||||
|
.increment();
|
||||||
|
try {
|
||||||
|
return parseListResponse(response, prefix);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw ExceptionUtils.wrap(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.whenComplete((ignored, ignoredException) ->
|
||||||
|
sample.stop(Metrics.timer(STORAGE_MANAGER_TIMER_NAME, OPERATION_TAG_NAME, "list")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialized list response from storage manager
|
||||||
|
*/
|
||||||
|
record Cdn3ListResponse(@NotNull List<Entry> objects, @Nullable String cursor) {
|
||||||
|
|
||||||
|
record Entry(@NotNull String key, @NotNull long size) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ListResult parseListResponse(final HttpResponse<InputStream> httpListResponse, final String prefix)
|
||||||
|
throws IOException {
|
||||||
|
if (!HttpUtils.isSuccessfulResponse(httpListResponse.statusCode())) {
|
||||||
|
throw new IOException("Failed to list objects: " + httpListResponse.statusCode());
|
||||||
|
}
|
||||||
|
final Cdn3ListResponse result = SystemMapper.jsonMapper()
|
||||||
|
.readValue(httpListResponse.body(), Cdn3ListResponse.class);
|
||||||
|
|
||||||
|
final List<ListResult.Entry> objects = new ArrayList<>(result.objects.size());
|
||||||
|
for (Cdn3ListResponse.Entry entry : result.objects) {
|
||||||
|
if (!entry.key().startsWith(prefix)) {
|
||||||
|
logger.error("unexpected listing result from cdn3 - entry {} does not contain requested prefix {}",
|
||||||
|
entry.key(), prefix);
|
||||||
|
throw new IOException("prefix listing returned unexpected result");
|
||||||
|
}
|
||||||
|
objects.add(new ListResult.Entry(entry.key().substring(prefix.length()), entry.size()));
|
||||||
|
}
|
||||||
|
return new ListResult(objects, Optional.ofNullable(result.cursor));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialized usage response from storage manager
|
||||||
|
*/
|
||||||
|
record UsageResponse(@NotNull long numObjects, @NotNull long bytesUsed) {}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletionStage<UsageInfo> calculateBytesUsed(final String prefix) {
|
||||||
|
final Timer.Sample sample = Timer.start();
|
||||||
|
final HttpRequest request = HttpRequest.newBuilder().GET()
|
||||||
|
.uri(URI.create("%s%s".formatted(
|
||||||
|
usageUrl(),
|
||||||
|
HttpUtils.queryParamString(Map.of("prefix", prefix).entrySet()))))
|
||||||
|
.header(CLIENT_ID_HEADER, clientId)
|
||||||
|
.header(CLIENT_SECRET_HEADER, clientSecret)
|
||||||
|
.build();
|
||||||
|
return this.storageManagerHttpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())
|
||||||
|
.thenApply(response -> {
|
||||||
|
Metrics.counter(STORAGE_MANAGER_STATUS_COUNTER_NAME,
|
||||||
|
OPERATION_TAG_NAME, "usage",
|
||||||
|
STATUS_TAG_NAME, Integer.toString(response.statusCode()))
|
||||||
|
.increment();
|
||||||
|
try {
|
||||||
|
return parseUsageResponse(response);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw ExceptionUtils.wrap(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.whenComplete((ignored, ignoredException) ->
|
||||||
|
sample.stop(Metrics.timer(STORAGE_MANAGER_TIMER_NAME, OPERATION_TAG_NAME, "usage")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UsageInfo parseUsageResponse(final HttpResponse<InputStream> httpUsageResponse) throws IOException {
|
||||||
|
if (!HttpUtils.isSuccessfulResponse(httpUsageResponse.statusCode())) {
|
||||||
|
throw new IOException("Failed to retrieve usage: " + httpUsageResponse.statusCode());
|
||||||
|
}
|
||||||
|
final UsageResponse response = SystemMapper.jsonMapper().readValue(httpUsageResponse.body(), UsageResponse.class);
|
||||||
|
return new UsageInfo(response.bytesUsed(), response.numObjects);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialized delete response from storage manager
|
||||||
|
*/
|
||||||
|
record DeleteResponse(@NotNull long bytesDeleted) {}
|
||||||
|
|
||||||
|
public CompletionStage<Long> delete(final String key) {
|
||||||
|
final HttpRequest request = HttpRequest.newBuilder().DELETE()
|
||||||
|
.uri(URI.create(deleteUrl(key)))
|
||||||
|
.header(CLIENT_ID_HEADER, clientId)
|
||||||
|
.header(CLIENT_SECRET_HEADER, clientSecret)
|
||||||
|
.build();
|
||||||
|
return this.storageManagerHttpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())
|
||||||
|
.thenApply(response -> {
|
||||||
|
Metrics.counter(STORAGE_MANAGER_STATUS_COUNTER_NAME,
|
||||||
|
OPERATION_TAG_NAME, "delete",
|
||||||
|
STATUS_TAG_NAME, Integer.toString(response.statusCode()))
|
||||||
|
.increment();
|
||||||
|
try {
|
||||||
|
return parseDeleteResponse(response);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw ExceptionUtils.wrap(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private long parseDeleteResponse(final HttpResponse<InputStream> httpDeleteResponse) throws IOException {
|
||||||
|
if (!HttpUtils.isSuccessfulResponse(httpDeleteResponse.statusCode())) {
|
||||||
|
throw new IOException("Failed to retrieve usage: " + httpDeleteResponse.statusCode());
|
||||||
|
}
|
||||||
|
return SystemMapper.jsonMapper().readValue(httpDeleteResponse.body(), DeleteResponse.class).bytesDeleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String deleteUrl(final String key) {
|
||||||
|
return "%s/%s/%s".formatted(storageManagerBaseUrl, Cdn3BackupCredentialGenerator.CDN_PATH, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String usageUrl() {
|
||||||
|
return "%s/usage".formatted(storageManagerBaseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String listUrl() {
|
||||||
|
return "%s/%s/".formatted(storageManagerBaseUrl, Cdn3BackupCredentialGenerator.CDN_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package org.whispersystems.textsecuregcm.backup;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class InvalidLengthException extends IOException {
|
||||||
|
|
||||||
|
public InvalidLengthException(String s) {
|
||||||
|
super(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user