Compare commits
983 commits
neilalexan
...
main
Author | SHA1 | Date | |
---|---|---|---|
signaryk | 73bd01f7b6 | ||
signaryk | 6a9a344d9a | ||
signaryk | 64cad580d1 | ||
signaryk | 48e3701b85 | ||
signaryk | 0c22b9ea58 | ||
signaryk | d428ae7f62 | ||
signaryk | b86c5110e1 | ||
signaryk | 09587775df | ||
signaryk | af0eadd4fe | ||
signaryk | 5f187e42d3 | ||
signaryk | 77264c3c20 | ||
signaryk | a595be09a2 | ||
signaryk | 0bfe418b18 | ||
20aa36ada7 | |||
f9c6fbab69 | |||
3e62b986d1 | |||
46902e5766 | |||
5547bf8ca6 | |||
14a6c10097 | |||
5c0ceec2a6 | |||
8aa088f713 | |||
b732eede27 | |||
ad0a7d09e8 | |||
81f73c9f8d | |||
79072c3dcd | |||
1bdf0cc541 | |||
a00b976a00 | |||
b9abbf7b20 | |||
de95499178 | |||
928c8c8c4a | |||
09f15a3d3f | |||
ad3a3e7bed | |||
66865597e2 | |||
4452833099 | |||
4892b08dd5 | |||
58bc289a37 | |||
e4a579f10f | |||
865fff5f03 | |||
4ccf6d6f67 | |||
f4e77453cb | |||
8f944f6434 | |||
ecb7b383e9 | |||
e9deb5244e | |||
be0c27e688 | |||
436773ab71 | |||
0f6b81f456 | |||
a3a18fbcce | |||
87f028db27 | |||
8f68f1ff53 | |||
a4817f31c0 | |||
00217a69d1 | |||
d58daf9665 | |||
8e4dc6b4ae | |||
d357615452 | |||
bebf701dce | |||
dae1ef2e46 | |||
3a4b5f49ac | |||
e34242008b | |||
57646d5b86 | |||
9510fa00cc | |||
13c5173273 | |||
edd02ec468 | |||
9a5a56718e | |||
f93d1c4790 | |||
d65449c782 | |||
b7054f4274 | |||
1555b3542d | |||
185ad6b00d | |||
fd11e65a9d | |||
61e5dc47d7 | |||
210bce9938 | |||
4f943771fa | |||
b8f91485b4 | |||
c4528b2de8 | |||
f25cce237e | |||
210123bab5 | |||
06e079abac | |||
fde4225469 | |||
7863a405a5 | |||
699f5ca8c1 | |||
ee73a90aea | |||
5f872f4a82 | |||
5c67eb99b3 | |||
8b4043473c | |||
da7bca0224 | |||
32f7c4b166 | |||
317b1018a3 | |||
89482ad790 | |||
a0375d41fb | |||
e02a7948d8 | |||
4fa8512d57 | |||
1b124fe9cb | |||
c1d6b9aa8e | |||
8b3adaf244 | |||
8c23c1150c | |||
fe2955a4db | |||
933ae2db91 | |||
5888329b13 | |||
2259e71c0c | |||
3d02c81031 | |||
1853f58cb4 | |||
b341a66152 | |||
4d344b65b2 | |||
f1db57c7f8 | |||
f02d998253 | |||
10b4fbc66d | |||
05a8f1ede3 | |||
16d922de70 | |||
d065219de1 | |||
db83789654 | |||
8245b24100 | |||
058081e68e | |||
bea73c765a | |||
478827459c | |||
bb2ab62cbf | |||
11fd2f019b | |||
b538f237df | |||
e3a7039c81 | |||
43b1ddb89b | |||
1c4ec67bb6 | |||
9b5be6b9c5 | |||
a721294e2b | |||
845800abfa | |||
57ddbe015d | |||
9a12420428 | |||
fa6c7ba456 | |||
35804f8493 | |||
294eff8a7f | |||
c7193e24d0 | |||
af13fa1c75 | |||
3f727485d6 | |||
79d4a0e399 | |||
7899f47e71 | |||
a48c7d33a5 | |||
c809e95335 | |||
e216c2fbf0 | |||
9582827493 | |||
297479ea49 | |||
a01faee17c | |||
33ff309572 | |||
6011ddc0a8 | |||
3e314e028e | |||
5267cc0f54 | |||
f12982472c | |||
0df982a2e5 | |||
99f94fc735 | |||
69b2069dea | |||
b965a08faa | |||
ef32de928d | |||
74a5ab6c24 | |||
eb9e90379d | |||
e93bdd56fd | |||
c08c7405db | |||
cc9b695c1e | |||
3a125fd8fa | |||
d507c5fc95 | |||
fea946d914 | |||
9f7e14e4d0 | |||
4a666932f5 | |||
e1d76de6c6 | |||
49d75d3cf6 | |||
5a87c703fa | |||
4c3a526e1b | |||
2ee03fd657 | |||
de1ed9d486 | |||
939ee325f8 | |||
23cd7877a1 | |||
4722f12fab | |||
a5ea928d0f | |||
45082d4dce | |||
a734b112c6 | |||
d13466c1ee | |||
420e7ec81f | |||
8cf6c381e2 | |||
3f4df25b31 | |||
5aaa539e3e | |||
e4665979bf | |||
7a2e325d10 | |||
2c87972a3a | |||
82b73a4906 | |||
77d9e4e93d | |||
832ccc32f6 | |||
5713c5715c | |||
8ea1a11105 | |||
7a1fd7f512 | |||
725ff5567d | |||
d11da6ec7c | |||
ea6b368ad4 | |||
cbdc601f1b | |||
61341aca50 | |||
3dcca4017c | |||
f956a8c1d9 | |||
11b557097c | |||
5d6221d191 | |||
2eae8dc489 | |||
027a9b8ce0 | |||
345f025ee3 | |||
67d6876857 | |||
0489d16f95 | |||
a49c9f01e2 | |||
2b34f88fde | |||
d5c11a3c86 | |||
99b143d4d0 | |||
6284790f98 | |||
9b98e5a102 | |||
f5b3144dc3 | |||
696cbb70b8 | |||
b00e272e6f | |||
9e9617ff84 | |||
6b47cf0f6a | |||
1432743d1a | |||
d23d0369cc | |||
6171310307 | |||
c6457cd4e5 | |||
b189edf4f4 | |||
2475cf4b61 | |||
dd5e47a9a7 | |||
ed19efc5d7 | |||
4679098a64 | |||
1647213fac | |||
71eeccf34a | |||
72285b2659 | |||
9fa39263c0 | |||
f66862958d | |||
914e6145a5 | |||
2d822356ff | |||
c45d8cd688 | |||
ca63b414da | |||
94e81cc3f3 | |||
ee57400afd | |||
0db43f13a6 | |||
e093005bc2 | |||
3691423626 | |||
985298cfc4 | |||
682a7d0a66 | |||
560ba46272 | |||
c2db38d295 | |||
4cb9cd7842 | |||
10ef1fb11a | |||
675926967d | |||
8223e1f2e1 | |||
01dd02dad2 | |||
44ed0a3279 | |||
2854ffeb7d | |||
28d3e296a8 | |||
f4104b4b5d | |||
69e3bd82a9 | |||
fa7710315a | |||
e8b2162a01 | |||
aa1bda4c58 | |||
e2d2482ca6 | |||
05f72fc4be | |||
234ed603e6 | |||
cb18ba0230 | |||
14085d30ac | |||
a4400bdd76 | |||
b741d38e10 | |||
6948d16527 | |||
5e85a00cb3 | |||
ec6879e5ae | |||
0459d2b9e5 | |||
5579121c6f | |||
d88f71ab71 | |||
2c58bab6a8 | |||
74dc54684b | |||
232aef016c | |||
689b5ee72f | |||
c7303cbf76 | |||
70322699ab | |||
baef523cb0 | |||
11a3fcc6cb | |||
a684b850b9 | |||
7d83f8b633 | |||
7fc839f751 | |||
56b28b01db | |||
9bcd0a2105 | |||
7cde99a7a7 | |||
6b1c9eafa9 | |||
6c20f8f742 | |||
1aa70b0f56 | |||
f1ccfcf150 | |||
086e205eba | |||
7fff7cd2ac | |||
eddf31f915 | |||
b28406c7d0 | |||
3d31b131fc | |||
ad07b169b8 | |||
e6aa0955ff | |||
d34277a6c0 | |||
c8ca23acdb | |||
7f114cc538 | |||
4594233f89 | |||
bd6f0c14e5 | |||
22c4736495 | |||
f0805071d5 | |||
11d9b9db0e | |||
cc59879faa | |||
e64ed0934d | |||
eb29a31550 | |||
cf254ba044 | |||
4ed61740ab | |||
26f86a76b6 | |||
baf118b08c | |||
9c826d064d | |||
a666c06da1 | |||
048e35026c | |||
dbc2869cbd | |||
d4f64f91ca | |||
2f8377e94b | |||
529feb07ee | |||
be43b9c0ea | |||
4738fe656f | |||
4af88ff0e6 | |||
b935da6c33 | |||
7b3334778f | |||
f98003c030 | |||
0f998e3af3 | |||
63df85db6d | |||
2debabf0f0 | |||
24a865aeb7 | |||
80738cc2a0 | |||
ace44458b2 | |||
5b73592f5a | |||
48fa869fa3 | |||
430932f0f1 | |||
25cb65acdb | |||
caf310fd79 | |||
a2b4860912 | |||
ce2bfc3f2e | |||
738686ae68 | |||
67f5c5bc1e | |||
b55a7c238f | |||
0d0280cf5f | |||
8582c7520a | |||
eeeb3017d6 | |||
477a44faa6 | |||
0491a8e343 | |||
25dfbc6ec3 | |||
6ae1dd565c | |||
b297ea7379 | |||
8fef692741 | |||
11a07d855d | |||
97ebd72b5a | |||
7482cd2b47 | |||
b0c5af6674 | |||
0995dc4822 | |||
54b47a98e5 | |||
3fd95e60cc | |||
002310390f | |||
d579ddb8e7 | |||
2e1fe58937 | |||
e449d174cc | |||
f762ce1050 | |||
f47515e38b | |||
5eed31fea3 | |||
09dff951d6 | |||
d1d2d16738 | |||
beea2432e6 | |||
d3db542fbf | |||
76db8e90de | |||
7d2344049d | |||
aaf4e5c865 | |||
8846de7312 | |||
c136a450d5 | |||
0351618ff4 | |||
27a1dea522 | |||
ba2ffb7da9 | |||
ded43e0f2d | |||
7583478305 | |||
b99349b18c | |||
3dc06bea81 | |||
0e6d94757b | |||
07e8ed13f6 | |||
e245a26f6b | |||
b65f89e61e | |||
9a46d8d95c | |||
934056f21f | |||
1be0afa181 | |||
6f000e9801 | |||
f009e54181 | |||
ac5f3f025e | |||
ed497aa8b2 | |||
f8d1dc521d | |||
1990c154e9 | |||
1ed5fb5e98 | |||
f6f1445cfa | |||
5e4b461e01 | |||
31f56ac3f4 | |||
7ad87eace3 | |||
8299da5905 | |||
a8e7ffc7ab | |||
ffd8e21ce5 | |||
16325203af | |||
607819f425 | |||
df76a17234 | |||
163dabc498 | |||
a916b041b1 | |||
1e714bc3b6 | |||
a2f72dd966 | |||
d558da1c87 | |||
deddf686b9 | |||
9b8bb55430 | |||
5c9aed6af9 | |||
6650712a1c | |||
f4ee397734 | |||
2a77a910eb | |||
858a4af224 | |||
1e79b0557e | |||
529df30b56 | |||
e177e0ae73 | |||
72ce6acf71 | |||
c648c671a3 | |||
d35a5642e8 | |||
0193549201 | |||
efa50253f6 | |||
503d9c7586 | |||
bdaae060cc | |||
a5cabdbac5 | |||
205a15621a | |||
c125203eb6 | |||
a7b74176e3 | |||
b2712cd2b1 | |||
7c73b131f4 | |||
efe28db631 | |||
b13cb43785 | |||
eeabe892a9 | |||
98d3f88bfb | |||
fb2e7d1b05 | |||
9625a79926 | |||
23a25be904 | |||
8704e84898 | |||
1fcbb9b5e5 | |||
85d740ea1b | |||
f5b11e30a4 | |||
4afadebd99 | |||
ef52731e9f | |||
9c0725feac | |||
ca8bc87380 | |||
51ab0a8ccf | |||
16c2a95900 | |||
86b25a6337 | |||
b367cfeddf | |||
75a508cc27 | |||
3db9e98456 | |||
8a1904ffe5 | |||
52478dac3c | |||
501977f6fe | |||
5aaa60227a | |||
42d7e3ee0d | |||
6663728eb1 | |||
2acc1d65fb | |||
0b21cb78aa | |||
7bd6631935 | |||
869bf4d0ac | |||
8c7b274e4e | |||
4c38bd76ce | |||
7307701a24 | |||
66a82e0fa4 | |||
f10c6f26e5 | |||
69aff372f3 | |||
0782011f54 | |||
f6035822e7 | |||
a2706e6498 | |||
a785532463 | |||
444b4bbdb8 | |||
a169a9121a | |||
fa96811e64 | |||
238b6ef2cd | |||
97491a174b | |||
a74aea0714 | |||
5298dd1133 | |||
f6dea712d2 | |||
2a4c7f45b3 | |||
c62ac3d6ad | |||
8b7bf5e7d7 | |||
db6a214b04 | |||
313cb3fd19 | |||
7506e3303e | |||
a553fe7705 | |||
0843bd776e | |||
411db6083b | |||
3cf42a1d64 | |||
9e4c3171da | |||
e98d75fd63 | |||
e57b301722 | |||
40cfb9a4ea | |||
73e02463cf | |||
9041491201 | |||
b58c9bb094 | |||
539c61b3db | |||
6a93858125 | |||
e79bfd8fd5 | |||
8cbe14bd6d | |||
c1463db6c9 | |||
f3dae0e749 | |||
241d5c47df | |||
3aa92efaa3 | |||
9c189b1b80 | |||
07bfb791ca | |||
d72d4f8d5d | |||
83c9dde219 | |||
81dbad39a3 | |||
cd8f7e1251 | |||
eac5678449 | |||
f76969831e | |||
82d1d434c5 | |||
a8bc558a60 | |||
fb44e33909 | |||
088ad1dd21 | |||
f3be4b3185 | |||
dcedd1b6bf | |||
23a3e04579 | |||
3c1474f68f | |||
0a9aebdf01 | |||
3920b9f9b6 | |||
9ed8ff6b93 | |||
6bf1912525 | |||
b000db81ca | |||
39581af3ba | |||
dcc0116287 | |||
0f09e9d196 | |||
fb6cb2dbcb | |||
80a0ab6246 | |||
b32b6d6e8e | |||
04bab14290 | |||
980fa55846 | |||
f1b8df0f49 | |||
1ca3f3efb5 | |||
8e231130e9 | |||
1b5460a920 | |||
8d8f4689a0 | |||
b9d0e9f7ed | |||
453b50e1d3 | |||
8c5b166784 | |||
d605d928bc | |||
ec5d1d681d | |||
3f82bceb70 | |||
e53dcb25a9 | |||
9ba3103f88 | |||
0f777d421c | |||
c85bc3434f | |||
6f602bb096 | |||
8c0c3441d8 | |||
ebd137cf6b | |||
e070352293 | |||
21f8881985 | |||
ae10aac456 | |||
3da182212e | |||
a767102f8a | |||
085bf5e28b | |||
98b73652e0 | |||
ede4632835 | |||
e6c992ba8b | |||
34ed316584 | |||
ba66b5a3b9 | |||
34451d21b8 | |||
50fa50a343 | |||
fec3ee384b | |||
c1e16fd41e | |||
b7f4bab358 | |||
d32f60249d | |||
d4710217f8 | |||
a050503d8d | |||
0116db79c6 | |||
16048be236 | |||
7d9545ceea | |||
3617d5a0ff | |||
60ec9180e6 | |||
ee40a29e55 | |||
aa8ec1acbf | |||
0e2fb63b4f | |||
6348486a13 | |||
8a82f10046 | |||
9005e5b4a8 | |||
e45ba35e97 | |||
90f1985bf3 | |||
b28bbadeb0 | |||
1b65c97ad1 | |||
e1bf709eb3 | |||
6f3dbee5f4 | |||
68d6eb0a6f | |||
3f9e38e80a | |||
a574ed5369 | |||
083ae01520 | |||
34993717fd | |||
87be32ca26 | |||
6c67552bf9 | |||
249b32c4f3 | |||
f18bce93cc | |||
d531202b0e | |||
b5bfff47d2 | |||
12649ccedd | |||
40fec70d13 | |||
f022fc1397 | |||
3e87096a21 | |||
3c416517b0 | |||
8d64c24b23 | |||
d5978d98fd | |||
14b7322003 | |||
a50556dcf0 | |||
c53f284fdb | |||
f40e280327 | |||
0ddfb0cad4 | |||
852d856db8 | |||
61a34d7cfb | |||
d8b19c857f | |||
bd39748b5c | |||
201ac05943 | |||
97d7cf2232 | |||
e007b8038f | |||
47af4bff5b | |||
cf01d29277 | |||
99f6b6a952 | |||
7bfc3074d1 | |||
fc1d8e479b | |||
a5f8c07184 | |||
0ea948c705 | |||
e6960d0b15 | |||
7f89fed1e4 | |||
482914aef4 | |||
b05e028f7d | |||
3e55856254 | |||
c366ccdfca | |||
100fa9b235 | |||
62afb936a5 | |||
47b2a5d6b8 | |||
2792d0490f | |||
7595fbf58c | |||
e9af30b3fe | |||
3a9dde28fd | |||
5997c32452 | |||
af9a204cc0 | |||
e8687f6f82 | |||
955e69a3b7 | |||
6ee758df63 | |||
e1bc4f6a1e | |||
c0e17bbe1b | |||
8196b29657 | |||
646de03d60 | |||
34e1dc210b | |||
64472d9aab | |||
42a82091a8 | |||
d5876abbe9 | |||
31f4ae8997 | |||
5014b35bd7 | |||
0d697f6754 | |||
7e8c605f98 | |||
4e352390b6 | |||
2cfcfddecc | |||
440eb0f3a2 | |||
847032df36 | |||
489ccc1c60 | |||
70cd9a902c | |||
fd7661f69c | |||
5992b4c7ed | |||
cd22ba22b0 | |||
ecee5f10f4 | |||
bea3dbe77b | |||
1c1d09abd4 | |||
d1f87e63f1 | |||
350a5e5393 | |||
fea869b41f | |||
304acd7adc | |||
51d229b025 | |||
ad6b902b84 | |||
175f65407a | |||
ba0b3adab4 | |||
02ec00b1bb | |||
2be43560ca | |||
93a6e2f4d3 | |||
7313f56f44 | |||
b0e2ea0f37 | |||
bbb3ade4a2 | |||
21ef487ff8 | |||
704cc5c9f5 | |||
aba171d9bc | |||
a3eb4e5e98 | |||
38bed30b41 | |||
ed79e8626a | |||
07dd9bd995 | |||
8ff3f1a7c9 | |||
cd7fa34595 | |||
16156b0b09 | |||
522bd2999f | |||
78e5d05efc | |||
14fea600bb | |||
95a509757a | |||
33129c02f7 | |||
2668050e53 | |||
9dc57122d9 | |||
56b55a28f5 | |||
5513f182cc | |||
365da70a23 | |||
5cacca92d2 | |||
a379d3e814 | |||
6b48ce0d75 | |||
606cb67506 | |||
7484689ad1 | |||
59bc0a6f4e | |||
8d9c8f11c5 | |||
ad4ac2c016 | |||
ec16c944eb | |||
804653e551 | |||
5424b88f30 | |||
0642ffc0f6 | |||
bcdbd5c00a | |||
65b7db633c | |||
da7d209b10 | |||
30dbc5ac44 | |||
ef7262a7a2 | |||
e55cd6ea78 | |||
6d0d7a0bc3 | |||
9fa30f5d3c | |||
20bf00b743 | |||
f1e9108db8 | |||
b4647fbb7e | |||
48600d5540 | |||
fad3ac8e78 | |||
2b352915a1 | |||
a01af55ec6 | |||
05cafbd197 | |||
371336c6b5 | |||
25ad40f5e5 | |||
c45d0936b5 | |||
240ae257de | |||
e930959e49 | |||
03ddd98f5e | |||
10a151cb55 | |||
3a156a434a | |||
3efc646f25 | |||
cecd11be9a | |||
2a1df0129e | |||
c8935fb53f | |||
1b7f84250a | |||
de78eab63a | |||
9a655cb5e7 | |||
a2bed259dd | |||
3bf5ae5ffe | |||
9fe509b18d | |||
2250768be1 | |||
bbff41b44b | |||
376391d1c7 | |||
f7f2453a85 | |||
ac2dbb3513 | |||
df5d4dc7a3 | |||
e384eb683f | |||
f4345dafde | |||
7ec70272d2 | |||
ca3fa58388 | |||
eab87ef07d | |||
6b6b420b9f | |||
3d51624fef | |||
e94ef84aab | |||
119cde3766 | |||
05c83923e3 | |||
c7f7aec4d0 | |||
645f31ae24 | |||
962b76da44 | |||
497ab4e1b7 | |||
ccea23cd40 | |||
b836243a24 | |||
081f5e7226 | |||
c7d978274d | |||
f0c8a03649 | |||
a201b4400d | |||
35ce551c8f | |||
9507966ebd | |||
84a7797883 | |||
5c01306bb5 | |||
583b8ea273 | |||
bcff14adea | |||
99b696e775 | |||
a7e92f8cb9 | |||
c0c909d306 | |||
4ff57993ab | |||
90bf01d8b1 | |||
69c86295f7 | |||
950659320e | |||
a1f9b02edf | |||
9cd8e9d4b9 | |||
09f0ff14c8 | |||
3ea21273bc | |||
eb8dc50a97 | |||
f76f28e6db | |||
f3e8a9a4cb | |||
460dccf93d | |||
c0f824d437 | |||
d4341a2d97 | |||
5087b36af0 | |||
f29cdb26f6 | |||
43147bd654 | |||
b5c55faf98 | |||
b50a24c666 | |||
89cd0e8fc1 | |||
086f182e24 | |||
54bed4c593 | |||
561c159ad7 | |||
519bc1124b | |||
2dea466685 | |||
2086992caf | |||
920a20821b | |||
7120eb6bc9 | |||
1b90cc9536 | |||
4c2a10f1a6 | |||
c500958583 | |||
e1136f4d3e | |||
0a7f7dc716 | |||
89d2adadbd | |||
1030072285 | |||
16ed1633b6 | |||
e2a64773ce | |||
660f7839f5 | |||
83797573be | |||
289b3c5608 | |||
3cdefcf765 | |||
c4df6d7723 | |||
b21a2223ef | |||
6d4bd5d890 | |||
27948fb304 | |||
aafb7bf120 | |||
0d7020fbaf | |||
2cb609c428 | |||
02597f15f0 | |||
3e9c734da5 | |||
f41931b566 | |||
02e5c74101 | |||
70cd8c68c2 | |||
3d9fe20748 | |||
ea16614f71 | |||
ae7b6dd516 | |||
beed39a8f4 | |||
9f8b3136b2 | |||
fb52b6cedc | |||
9869dc2cbe | |||
247604979a | |||
500124dd18 | |||
b541f3043f | |||
45c4c00672 | |||
9e46d5025a | |||
5a878b6e14 | |||
9eb4fec33b | |||
015465d496 | |||
6940c7c7dd | |||
ff53398635 | |||
ac4d0072cf | |||
9ce3898d03 | |||
81843e8836 | |||
d621dd2986 | |||
ead0112aa1 | |||
7379b02b70 | |||
c83837e684 | |||
20844942a8 | |||
4472267901 | |||
8a7567c652 | |||
b5a497a0c0 | |||
028963af1c | |||
a53c9300aa | |||
21dd5a7176 | |||
f321a7d55e | |||
b3162755a9 | |||
ac92e04772 | |||
6de29c1cd2 | |||
cd82460513 | |||
05607d6b87 | |||
6af35385ba | |||
b40b548432 | |||
1698c39579 | |||
be9be2553f | |||
cafc2d2c10 | |||
b57fdcc82d | |||
870f9b0c3f | |||
fc670f03a2 | |||
0d1505a4c1 | |||
3437adf597 | |||
58af7f61b6 | |||
24f7be968d | |||
19a9166eb0 | |||
9599b3686e | |||
c15bfefd0d | |||
6db08b2874 | |||
1897e2f1c0 | |||
e2a932ec0b | |||
77722c5a4f | |||
1b3fa9689c | |||
6b3c183396 | |||
236b16aa6c | |||
a443d1e5f3 | |||
79da75d483 | |||
1a7f4c8aa9 | |||
f69ebc6af2 | |||
09d754cfbf | |||
79e2fbc663 | |||
4c15c73b3a | |||
633ca06eb9 | |||
6bc6184d70 | |||
85c00208c5 | |||
6493c0c0f2 | |||
507f63d0fc | |||
a1a5357f79 | |||
85704eff20 | |||
4705f5761e | |||
9957752a9d | |||
e4da04e75b | |||
42f35a57ac | |||
530fd488a9 | |||
506de4bb3d | |||
d9e71b93b6 | |||
1bfe87aa56 | |||
658e82f8bc | |||
5c37f165ae | |||
d86dcbef66 | |||
3c940c428d | |||
b0a9e85c4a | |||
e01d1e1f5b | |||
dd061a172e | |||
4ad5f9c982 | |||
979a551f1e | |||
bfa344e831 | |||
987d7adc5d | |||
31799a3b2a | |||
d28d0ee66e | |||
2a4517f8e6 | |||
0d4b8eadaa | |||
1e083794ef | |||
26a1512808 | |||
2a5b8e0306 | |||
c6ea2c9ff2 | |||
21ee5b36a4 | |||
8683ff78b1 | |||
65034d1f22 | |||
6deb10f3f6 | |||
2ff75b7c80 | |||
74259f296f | |||
8d69e2f0b8 | |||
34221938cc | |||
923f789ca3 | |||
103795d33a | |||
cafa2853c5 | |||
655ac3e8fb | |||
6ee8507955 | |||
f023cdf8c4 | |||
d7cc187ec0 | |||
54ff4cf690 | |||
dca4afd2f0 | |||
66b397b3c6 | |||
6c5c6d73d7 | |||
b527e33c16 | |||
f6d07768a8 | |||
6892e0f0e0 | |||
4c19f22725 | |||
5306c73b00 | |||
e8be2b234f | |||
feac9db43f | |||
7df5d69a5b | |||
e8ab2154aa | |||
e95fc5c5e3 | |||
aad81b7b4d | |||
446819e4ac | |||
67fb086c13 | |||
6d78c4d67d | |||
c07f347f00 | |||
0eb5bd1e13 | |||
144c060fa7 | |||
2258387d39 | |||
54e7ea41c6 | |||
bb987cd64b | |||
a9f0a390c6 | |||
073972646f | |||
85b1631ecf | |||
711e377b9c | |||
7e745665a4 | |||
abf71649b0 | |||
57e3622b85 | |||
3ddbffd59e | |||
3a5e9a0f28 | |||
1140f39993 | |||
29f2168789 | |||
ea92f80c12 | |||
69f2ff7c82 | |||
b4b2fbc36b | |||
6d25bd6ca5 | |||
986d27a128 | |||
9bd9f2beba |
|
@ -1,3 +1,2 @@
|
||||||
bin
|
bin
|
||||||
*.wasm
|
*.wasm
|
||||||
.git
|
|
59
.forgejo/workflows/docker.yml
Normal file
59
.forgejo/workflows/docker.yml
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
env:
|
||||||
|
GHCR_NAMESPACE: sigb.us
|
||||||
|
PLATFORMS: linux/amd64
|
||||||
|
FORGEJO_USER: signaryk
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
monolith:
|
||||||
|
name: Monolith image
|
||||||
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: ghcr.io/catthehacker/ubuntu:act-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Get release tag & build flags
|
||||||
|
if: github.event_name == 'release' # Only for GitHub releases
|
||||||
|
run: |
|
||||||
|
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Login to sigb.us container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.sigb.us
|
||||||
|
username: ${{ env.FORGEJO_USER }}
|
||||||
|
password: ${{ secrets.FORGEJO_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build main monolith image
|
||||||
|
id: docker_build_monolith
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: ${{ env.PLATFORMS }}
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:${{ github.ref_name }}
|
||||||
|
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:latest
|
||||||
|
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:devel
|
||||||
|
|
||||||
|
- name: Build release monolith image
|
||||||
|
if: github.event_name == 'release' # Only for GitHub releases
|
||||||
|
id: docker_build_monolith_release
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: ${{ env.PLATFORMS }}
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:latest
|
||||||
|
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:stable
|
||||||
|
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:${{ env.RELEASE_VERSION }}
|
18
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
18
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
|
@ -7,24 +7,27 @@ about: Create a report to help us improve
|
||||||
<!--
|
<!--
|
||||||
All bug reports must provide the following background information
|
All bug reports must provide the following background information
|
||||||
Text between <!-- and --> marks will be invisible in the report.
|
Text between <!-- and --> marks will be invisible in the report.
|
||||||
|
|
||||||
|
IF YOUR ISSUE IS CONSIDERED A SECURITY VULNERABILITY THEN PLEASE STOP
|
||||||
|
AND DO NOT POST IT AS A GITHUB ISSUE! Please report the issue responsibly by
|
||||||
|
disclosing in private by email to security@matrix.org instead. For more details, please
|
||||||
|
see: https://www.matrix.org/security-disclosure-policy/
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Background information
|
### Background information
|
||||||
<!-- Please include versions of all software when known e.g database versions, docker versions, client versions -->
|
<!-- Please include versions of all software when known e.g database versions, docker versions, client versions -->
|
||||||
- **Dendrite version or git SHA**:
|
- **Dendrite version or git SHA**:
|
||||||
- **Monolith or Polylith?**:
|
|
||||||
- **SQLite3 or Postgres?**:
|
- **SQLite3 or Postgres?**:
|
||||||
- **Running in Docker?**:
|
- **Running in Docker?**:
|
||||||
- **`go version`**:
|
- **`go version`**:
|
||||||
- **Client used (if applicable)**:
|
- **Client used (if applicable)**:
|
||||||
|
|
||||||
|
|
||||||
### Description
|
### Description
|
||||||
|
|
||||||
- **What** is the problem:
|
- **What** is the problem:
|
||||||
- **Who** is affected:
|
- **Who** is affected:
|
||||||
- **How** is this bug manifesting:
|
- **How** is this bug manifesting:
|
||||||
- **When** did this first appear:
|
- **When** did this first appear:
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Examples of good descriptions:
|
Examples of good descriptions:
|
||||||
|
@ -38,7 +41,6 @@ Examples of good descriptions:
|
||||||
- How: "Lots of logs about device change updates"
|
- How: "Lots of logs about device change updates"
|
||||||
- When: "After my server joined Matrix HQ"
|
- When: "After my server joined Matrix HQ"
|
||||||
|
|
||||||
|
|
||||||
Examples of bad descriptions:
|
Examples of bad descriptions:
|
||||||
- What: "Can't send messages" - This is bad because it isn't specfic enough. Which endpoint isn't working and what is the response code? Does the message send but encryption fail?
|
- What: "Can't send messages" - This is bad because it isn't specfic enough. Which endpoint isn't working and what is the response code? Does the message send but encryption fail?
|
||||||
- Who: "Me" - Who are you? Running the server or a user on a Dendrite server?
|
- Who: "Me" - Who are you? Running the server or a user on a Dendrite server?
|
||||||
|
@ -60,6 +62,6 @@ If you can identify any relevant log snippets from server logs, please include
|
||||||
those (please be careful to remove any personal or private data). Please surround them with
|
those (please be careful to remove any personal or private data). Please surround them with
|
||||||
``` (three backticks, on a line on their own), so that they are formatted legibly.
|
``` (three backticks, on a line on their own), so that they are formatted legibly.
|
||||||
|
|
||||||
Alternatively, please send logs to @kegan:matrix.org or @neilalexander:matrix.org
|
Alternatively, please send logs to @kegan:matrix.org, @s7evink:matrix.org or @devonh:one.ems.host
|
||||||
with a link to the respective Github issue, thanks!
|
with a link to the respective Github issue, thanks!
|
||||||
-->
|
-->
|
||||||
|
|
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -1,8 +1,8 @@
|
||||||
### Pull Request Checklist
|
### Pull Request Checklist
|
||||||
|
|
||||||
<!-- Please read docs/CONTRIBUTING.md before submitting your pull request -->
|
<!-- Please read https://matrix-org.github.io/dendrite/development/contributing before submitting your pull request -->
|
||||||
|
|
||||||
* [ ] I have added added tests for PR _or_ I have justified why this PR doesn't need tests.
|
* [ ] I have added Go unit tests or [Complement integration tests](https://github.com/matrix-org/complement) for this PR _or_ I have justified why this PR doesn't need tests
|
||||||
* [ ] Pull request includes a [sign off](https://github.com/matrix-org/dendrite/blob/main/docs/CONTRIBUTING.md#sign-off)
|
* [ ] Pull request includes a [sign off below using a legally identifiable name](https://matrix-org.github.io/dendrite/development/contributing#sign-off) _or_ I have already signed off privately
|
||||||
|
|
||||||
Signed-off-by: `Your Name <your@email.example.org>`
|
Signed-off-by: `Your Name <your@email.example.org>`
|
||||||
|
|
20
.github/codecov.yaml
vendored
Normal file
20
.github/codecov.yaml
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
flag_management:
|
||||||
|
default_rules:
|
||||||
|
carryforward: true
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
project:
|
||||||
|
default:
|
||||||
|
target: auto
|
||||||
|
threshold: 0.1%
|
||||||
|
base: auto
|
||||||
|
flags:
|
||||||
|
- unittests
|
||||||
|
patch:
|
||||||
|
default:
|
||||||
|
target: 75%
|
||||||
|
threshold: 0%
|
||||||
|
base: auto
|
||||||
|
flags:
|
||||||
|
- unittests
|
287
.github/workflows/dendrite.yml
vendored
287
.github/workflows/dendrite.yml
vendored
|
@ -4,9 +4,18 @@ on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths:
|
||||||
|
- '**.go' # only execute on changes to go files
|
||||||
|
- 'go.sum' # or dependency updates
|
||||||
|
- '.github/workflows/**' # or workflow changes
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**.go'
|
||||||
|
- 'go.sum' # or dependency updates
|
||||||
|
- '.github/workflows/**'
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
@ -17,29 +26,22 @@ jobs:
|
||||||
name: WASM build test
|
name: WASM build test
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ false }} # disable for now
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: 1.16
|
go-version: "stable"
|
||||||
|
cache: true
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cache/go-build
|
|
||||||
~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-wasm-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-wasm
|
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 14
|
||||||
|
|
||||||
- uses: actions/cache@v2
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.npm
|
path: ~/.npm
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
@ -64,14 +66,20 @@ jobs:
|
||||||
name: Linting
|
name: Linting
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install libolm
|
||||||
|
run: sudo apt-get install libolm-dev libolm3
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: "stable"
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v2
|
uses: golangci/golangci-lint-action@v3
|
||||||
|
|
||||||
# run go test with different go versions
|
# run go test with different go versions
|
||||||
test:
|
test:
|
||||||
timeout-minutes: 5
|
timeout-minutes: 10
|
||||||
name: Unit tests (Go ${{ matrix.go }})
|
name: Unit tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Service containers to run with `container-job`
|
# Service containers to run with `container-job`
|
||||||
services:
|
services:
|
||||||
|
@ -93,25 +101,29 @@ jobs:
|
||||||
--health-interval 10s
|
--health-interval 10s
|
||||||
--health-timeout 5s
|
--health-timeout 5s
|
||||||
--health-retries 5
|
--health-retries 5
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
go: ["1.16", "1.17", "1.18"]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install libolm
|
||||||
|
run: sudo apt-get install libolm-dev libolm3
|
||||||
- name: Setup go
|
- name: Setup go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: "stable"
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v4
|
||||||
|
# manually set up caches, as they otherwise clash with different steps using setup-go with cache=true
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go${{ matrix.go }}-test-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-stable-unit-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go${{ matrix.go }}-test-
|
${{ runner.os }}-go-stable-unit-
|
||||||
- run: go test ./...
|
- name: Set up gotestfmt
|
||||||
|
uses: gotesttools/gotestfmt-action@v2
|
||||||
|
with:
|
||||||
|
# Optional: pass GITHUB_TOKEN to avoid rate limiting.
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- run: go test -json -v ./... 2>&1 | gotestfmt -hide all
|
||||||
env:
|
env:
|
||||||
POSTGRES_HOST: localhost
|
POSTGRES_HOST: localhost
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
|
@ -126,30 +138,30 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
go: ["1.16", "1.17", "1.18"]
|
|
||||||
goos: ["linux"]
|
goos: ["linux"]
|
||||||
goarch: ["amd64", "386"]
|
goarch: ["amd64", "386"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup go
|
- name: Setup go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: "stable"
|
||||||
- name: Install dependencies x86
|
- uses: actions/cache@v4
|
||||||
if: ${{ matrix.goarch == '386' }}
|
|
||||||
run: sudo apt update && sudo apt-get install -y gcc-multilib
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go${{ matrix.go }}-${{ matrix.goarch }}-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-stable-${{ matrix.goos }}-${{ matrix.goarch }}-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go${{ matrix.go }}-${{ matrix.goarch }}-
|
key: ${{ runner.os }}-go-stable-${{ matrix.goos }}-${{ matrix.goarch }}-
|
||||||
|
- name: Install dependencies x86
|
||||||
|
if: ${{ matrix.goarch == '386' }}
|
||||||
|
run: sudo apt update && sudo apt-get install -y gcc-multilib
|
||||||
- env:
|
- env:
|
||||||
GOOS: ${{ matrix.goos }}
|
GOOS: ${{ matrix.goos }}
|
||||||
GOARCH: ${{ matrix.goarch }}
|
GOARCH: ${{ matrix.goarch }}
|
||||||
CGO_ENABLED: 1
|
CGO_ENABLED: 1
|
||||||
|
CGO_CFLAGS: -fno-stack-protector
|
||||||
run: go build -trimpath -v -o "bin/" ./cmd/...
|
run: go build -trimpath -v -o "bin/" ./cmd/...
|
||||||
|
|
||||||
# build for Windows 64-bit
|
# build for Windows 64-bit
|
||||||
|
@ -159,25 +171,24 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go: ["1.16", "1.17", "1.18"]
|
|
||||||
goos: ["windows"]
|
goos: ["windows"]
|
||||||
goarch: ["amd64"]
|
goarch: ["amd64"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Go ${{ matrix.go }}
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: "stable"
|
||||||
- name: Install dependencies
|
- uses: actions/cache@v4
|
||||||
run: sudo apt update && sudo apt install -y gcc-mingw-w64-x86-64 # install required gcc
|
|
||||||
- uses: actions/cache@v3
|
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go${{ matrix.go }}-${{ matrix.goos }}-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-stable-${{ matrix.goos }}-${{ matrix.goarch }}-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go${{ matrix.go }}-${{ matrix.goos }}
|
key: ${{ runner.os }}-go-stable-${{ matrix.goos }}-${{ matrix.goarch }}-
|
||||||
|
- name: Install dependencies
|
||||||
|
run: sudo apt update && sudo apt install -y gcc-mingw-w64-x86-64 # install required gcc
|
||||||
- env:
|
- env:
|
||||||
GOOS: ${{ matrix.goos }}
|
GOOS: ${{ matrix.goos }}
|
||||||
GOARCH: ${{ matrix.goarch }}
|
GOARCH: ${{ matrix.goarch }}
|
||||||
|
@ -197,6 +208,66 @@ jobs:
|
||||||
with:
|
with:
|
||||||
jobs: ${{ toJSON(needs) }}
|
jobs: ${{ toJSON(needs) }}
|
||||||
|
|
||||||
|
# run go test with different go versions
|
||||||
|
integration:
|
||||||
|
timeout-minutes: 20
|
||||||
|
needs: initial-tests-done
|
||||||
|
name: Integration tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Service containers to run with `container-job`
|
||||||
|
services:
|
||||||
|
# Label used to access the service container
|
||||||
|
postgres:
|
||||||
|
# Docker Hub image
|
||||||
|
image: postgres:13-alpine
|
||||||
|
# Provide the password for postgres
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: dendrite
|
||||||
|
ports:
|
||||||
|
# Maps tcp port 5432 on service container to the host
|
||||||
|
- 5432:5432
|
||||||
|
# Set health checks to wait until postgres has started
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install libolm
|
||||||
|
run: sudo apt-get install libolm-dev libolm3
|
||||||
|
- name: Setup go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: "stable"
|
||||||
|
- name: Set up gotestfmt
|
||||||
|
uses: gotesttools/gotestfmt-action@v2
|
||||||
|
with:
|
||||||
|
# Optional: pass GITHUB_TOKEN to avoid rate limiting.
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-stable-test-race-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-stable-test-race-
|
||||||
|
- run: go test -race -json -v -coverpkg=./... -coverprofile=cover.out $(go list ./... | grep -v /cmd/dendrite*) 2>&1 | gotestfmt -hide all
|
||||||
|
env:
|
||||||
|
POSTGRES_HOST: localhost
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: dendrite
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
|
with:
|
||||||
|
flags: unittests
|
||||||
|
fail_ci_if_error: true
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
# run database upgrade tests
|
# run database upgrade tests
|
||||||
upgrade_test:
|
upgrade_test:
|
||||||
name: Upgrade tests
|
name: Upgrade tests
|
||||||
|
@ -204,23 +275,58 @@ jobs:
|
||||||
needs: initial-tests-done
|
needs: initial-tests-done
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup go
|
- name: Setup go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "1.16"
|
go-version: "stable"
|
||||||
- uses: actions/cache@v3
|
cache: true
|
||||||
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go-upgrade-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-upgrade-test-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go-upgrade
|
${{ runner.os }}-go-upgrade-test-
|
||||||
|
- name: Docker version
|
||||||
|
run: docker version
|
||||||
- name: Build upgrade-tests
|
- name: Build upgrade-tests
|
||||||
run: go build ./cmd/dendrite-upgrade-tests
|
run: go build ./cmd/dendrite-upgrade-tests
|
||||||
- name: Test upgrade
|
- name: Test upgrade (PostgreSQL)
|
||||||
run: ./dendrite-upgrade-tests --head .
|
run: ./dendrite-upgrade-tests --head .
|
||||||
|
- name: Test upgrade (SQLite)
|
||||||
|
run: ./dendrite-upgrade-tests --sqlite --head .
|
||||||
|
|
||||||
|
# run database upgrade tests, skipping over one version
|
||||||
|
upgrade_test_direct:
|
||||||
|
name: Upgrade tests from HEAD-2
|
||||||
|
timeout-minutes: 20
|
||||||
|
needs: initial-tests-done
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: "stable"
|
||||||
|
cache: true
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-upgrade-direct-test-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-upgrade-direct-test-
|
||||||
|
- name: Docker version
|
||||||
|
run: docker version
|
||||||
|
- name: Build upgrade-tests
|
||||||
|
run: go build ./cmd/dendrite-upgrade-tests
|
||||||
|
- name: Test upgrade (PostgreSQL)
|
||||||
|
run: ./dendrite-upgrade-tests -direct -from HEAD-2 --head .
|
||||||
|
- name: Test upgrade (SQLite)
|
||||||
|
run: ./dendrite-upgrade-tests -direct -from HEAD-2 --head .
|
||||||
|
|
||||||
# run Sytest in different variations
|
# run Sytest in different variations
|
||||||
sytest:
|
sytest:
|
||||||
|
@ -232,26 +338,34 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- label: SQLite
|
- label: SQLite native
|
||||||
|
|
||||||
- label: SQLite, full HTTP APIs
|
- label: SQLite Cgo
|
||||||
api: full-http
|
cgo: 1
|
||||||
|
|
||||||
- label: PostgreSQL
|
- label: PostgreSQL
|
||||||
postgres: postgres
|
postgres: postgres
|
||||||
|
|
||||||
- label: PostgreSQL, full HTTP APIs
|
|
||||||
postgres: postgres
|
|
||||||
api: full-http
|
|
||||||
container:
|
container:
|
||||||
image: matrixdotorg/sytest-dendrite:latest
|
image: matrixdotorg/sytest-dendrite
|
||||||
volumes:
|
volumes:
|
||||||
- ${{ github.workspace }}:/src
|
- ${{ github.workspace }}:/src
|
||||||
|
- /root/.cache/go-build:/github/home/.cache/go-build
|
||||||
|
- /root/.cache/go-mod:/gopath/pkg/mod
|
||||||
env:
|
env:
|
||||||
POSTGRES: ${{ matrix.postgres && 1}}
|
POSTGRES: ${{ matrix.postgres && 1}}
|
||||||
API: ${{ matrix.api && 1 }}
|
SYTEST_BRANCH: ${{ github.head_ref }}
|
||||||
|
CGO_ENABLED: ${{ matrix.cgo && 1 }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
/gopath/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-sytest-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-sytest-
|
||||||
- name: Run Sytest
|
- name: Run Sytest
|
||||||
run: /bootstrap.sh dendrite
|
run: /bootstrap.sh dendrite
|
||||||
working-directory: /src
|
working-directory: /src
|
||||||
|
@ -267,7 +381,7 @@ jobs:
|
||||||
run: /src/are-we-synapse-yet.py /logs/results.tap -v
|
run: /src/are-we-synapse-yet.py /logs/results.tap -v
|
||||||
continue-on-error: true # not fatal
|
continue-on-error: true # not fatal
|
||||||
- name: Upload Sytest logs
|
- name: Upload Sytest logs
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v4
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
with:
|
with:
|
||||||
name: Sytest Logs - ${{ job.status }} - (Dendrite, ${{ join(matrix.*, ', ') }})
|
name: Sytest Logs - ${{ job.status }} - (Dendrite, ${{ join(matrix.*, ', ') }})
|
||||||
|
@ -285,17 +399,15 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- label: SQLite
|
- label: SQLite native
|
||||||
|
cgo: 0
|
||||||
|
|
||||||
- label: SQLite, full HTTP APIs
|
- label: SQLite Cgo
|
||||||
api: full-http
|
cgo: 1
|
||||||
|
|
||||||
- label: PostgreSQL
|
- label: PostgreSQL
|
||||||
postgres: Postgres
|
postgres: Postgres
|
||||||
|
cgo: 0
|
||||||
- label: PostgreSQL, full HTTP APIs
|
|
||||||
postgres: Postgres
|
|
||||||
api: full-http
|
|
||||||
steps:
|
steps:
|
||||||
# Env vars are set file a file given by $GITHUB_PATH. We need both Go 1.17 and GOPATH on env to run Complement.
|
# Env vars are set file a file given by $GITHUB_PATH. We need both Go 1.17 and GOPATH on env to run Complement.
|
||||||
# See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-system-path
|
# See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-system-path
|
||||||
|
@ -303,16 +415,14 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
echo "$GOROOT_1_17_X64/bin" >> $GITHUB_PATH
|
echo "$GOROOT_1_17_X64/bin" >> $GITHUB_PATH
|
||||||
echo "~/go/bin" >> $GITHUB_PATH
|
echo "~/go/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
- name: "Install Complement Dependencies"
|
- name: "Install Complement Dependencies"
|
||||||
# We don't need to install Go because it is included on the Ubuntu 20.04 image:
|
# We don't need to install Go because it is included on the Ubuntu 20.04 image:
|
||||||
# See https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-Readme.md specifically GOROOT_1_17_X64
|
# See https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-Readme.md specifically GOROOT_1_17_X64
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update && sudo apt-get install -y libolm3 libolm-dev
|
sudo apt-get update && sudo apt-get install -y libolm3 libolm-dev
|
||||||
go get -v github.com/haveyoudebuggedit/gotestfmt/v2/cmd/gotestfmt@latest
|
go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest
|
||||||
|
- name: Run actions/checkout@v4 for dendrite
|
||||||
- name: Run actions/checkout@v2 for dendrite
|
uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
with:
|
||||||
path: dendrite
|
path: dendrite
|
||||||
|
|
||||||
|
@ -336,28 +446,36 @@ jobs:
|
||||||
if [[ -z "$BRANCH_NAME" || $BRANCH_NAME =~ ^refs/pull/.* ]]; then
|
if [[ -z "$BRANCH_NAME" || $BRANCH_NAME =~ ^refs/pull/.* ]]; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
(wget -O - "https://github.com/matrix-org/complement/archive/$BRANCH_NAME.tar.gz" | tar -xz --strip-components=1 -C complement) && break
|
(wget -O - "https://github.com/matrix-org/complement/archive/$BRANCH_NAME.tar.gz" | tar -xz --strip-components=1 -C complement) && break
|
||||||
done
|
done
|
||||||
|
|
||||||
# Build initial Dendrite image
|
# Build initial Dendrite image
|
||||||
- run: docker build -t complement-dendrite -f build/scripts/Complement${{ matrix.postgres }}.Dockerfile .
|
- run: docker build --build-arg=CGO=${{ matrix.cgo }} -t complement-dendrite:${{ matrix.postgres }}${{ matrix.cgo }} -f build/scripts/Complement${{ matrix.postgres }}.Dockerfile .
|
||||||
working-directory: dendrite
|
working-directory: dendrite
|
||||||
|
env:
|
||||||
|
DOCKER_BUILDKIT: 1
|
||||||
|
|
||||||
# Run Complement
|
# Run Complement
|
||||||
- run: |
|
- run: |
|
||||||
set -o pipefail &&
|
set -o pipefail &&
|
||||||
go test -v -json -tags dendrite_blacklist ./tests/... 2>&1 | gotestfmt
|
go test -v -json -tags dendrite_blacklist ./tests ./tests/csapi 2>&1 | gotestfmt -hide all
|
||||||
shell: bash
|
shell: bash
|
||||||
name: Run Complement Tests
|
name: Run Complement Tests
|
||||||
env:
|
env:
|
||||||
COMPLEMENT_BASE_IMAGE: complement-dendrite:latest
|
COMPLEMENT_BASE_IMAGE: complement-dendrite:${{ matrix.postgres }}${{ matrix.cgo }}
|
||||||
API: ${{ matrix.api && 1 }}
|
COMPLEMENT_SHARE_ENV_PREFIX: COMPLEMENT_DENDRITE_
|
||||||
working-directory: complement
|
working-directory: complement
|
||||||
|
|
||||||
integration-tests-done:
|
integration-tests-done:
|
||||||
name: Integration tests passed
|
name: Integration tests passed
|
||||||
needs: [initial-tests-done, upgrade_test, sytest, complement]
|
needs:
|
||||||
|
[
|
||||||
|
initial-tests-done,
|
||||||
|
upgrade_test,
|
||||||
|
upgrade_test_direct,
|
||||||
|
sytest,
|
||||||
|
complement,
|
||||||
|
integration
|
||||||
|
]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ !cancelled() }} # Run this even if prior jobs were skipped
|
if: ${{ !cancelled() }} # Run this even if prior jobs were skipped
|
||||||
steps:
|
steps:
|
||||||
|
@ -371,6 +489,7 @@ jobs:
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
contents: read
|
contents: read
|
||||||
|
security-events: write # To upload Trivy sarif files
|
||||||
if: github.repository == 'matrix-org/dendrite' && github.ref_name == 'main'
|
if: github.repository == 'matrix-org/dendrite' && github.ref_name == 'main'
|
||||||
needs: [integration-tests-done]
|
needs: [integration-tests-done]
|
||||||
uses: matrix-org/dendrite/.github/workflows/docker.yml@main
|
uses: matrix-org/dendrite/.github/workflows/docker.yml@main
|
||||||
|
|
146
.github/workflows/docker.yml
vendored
146
.github/workflows/docker.yml
vendored
|
@ -24,23 +24,25 @@ jobs:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
security-events: write # To upload Trivy sarif files
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: Get release tag
|
- name: Get release tag & build flags
|
||||||
if: github.event_name == 'release' # Only for GitHub releases
|
if: github.event_name == 'release' # Only for GitHub releases
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
run: |
|
||||||
|
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ env.DOCKER_HUB_USER }}
|
username: ${{ env.DOCKER_HUB_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
- name: Login to GitHub Containers
|
- name: Login to GitHub Containers
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
@ -49,12 +51,11 @@ jobs:
|
||||||
- name: Build main monolith image
|
- name: Build main monolith image
|
||||||
if: github.ref_name == 'main'
|
if: github.ref_name == 'main'
|
||||||
id: docker_build_monolith
|
id: docker_build_monolith
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
cache-from: type=gha
|
cache-from: type=registry,ref=ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:buildcache
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=registry,ref=ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:buildcache,mode=max
|
||||||
context: .
|
context: .
|
||||||
file: ./build/docker/Dockerfile.monolith
|
|
||||||
platforms: ${{ env.PLATFORMS }}
|
platforms: ${{ env.PLATFORMS }}
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
|
@ -64,12 +65,11 @@ jobs:
|
||||||
- name: Build release monolith image
|
- name: Build release monolith image
|
||||||
if: github.event_name == 'release' # Only for GitHub releases
|
if: github.event_name == 'release' # Only for GitHub releases
|
||||||
id: docker_build_monolith_release
|
id: docker_build_monolith_release
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
context: .
|
context: .
|
||||||
file: ./build/docker/Dockerfile.monolith
|
|
||||||
platforms: ${{ env.PLATFORMS }}
|
platforms: ${{ env.PLATFORMS }}
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
|
@ -78,62 +78,136 @@ jobs:
|
||||||
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:latest
|
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:latest
|
||||||
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:${{ env.RELEASE_VERSION }}
|
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:${{ env.RELEASE_VERSION }}
|
||||||
|
|
||||||
polylith:
|
- name: Run Trivy vulnerability scanner
|
||||||
name: Polylith image
|
uses: aquasecurity/trivy-action@master
|
||||||
|
with:
|
||||||
|
image-ref: ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:${{ github.ref_name }}
|
||||||
|
format: "sarif"
|
||||||
|
output: "trivy-results.sarif"
|
||||||
|
|
||||||
|
- name: Upload Trivy scan results to GitHub Security tab
|
||||||
|
uses: github/codeql-action/upload-sarif@v2
|
||||||
|
with:
|
||||||
|
sarif_file: "trivy-results.sarif"
|
||||||
|
|
||||||
|
demo-pinecone:
|
||||||
|
name: Pinecone demo image
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
- name: Get release tag
|
- name: Get release tag & build flags
|
||||||
if: github.event_name == 'release' # Only for GitHub releases
|
if: github.event_name == 'release' # Only for GitHub releases
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
run: |
|
||||||
|
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ env.DOCKER_HUB_USER }}
|
username: ${{ env.DOCKER_HUB_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
- name: Login to GitHub Containers
|
- name: Login to GitHub Containers
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build main polylith image
|
- name: Build main Pinecone demo image
|
||||||
if: github.ref_name == 'main'
|
if: github.ref_name == 'main'
|
||||||
id: docker_build_polylith
|
id: docker_build_demo_pinecone
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
context: .
|
context: .
|
||||||
file: ./build/docker/Dockerfile.polylith
|
file: ./build/docker/Dockerfile.demo-pinecone
|
||||||
platforms: ${{ env.PLATFORMS }}
|
platforms: ${{ env.PLATFORMS }}
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.DOCKER_NAMESPACE }}/dendrite-polylith:${{ github.ref_name }}
|
${{ env.DOCKER_NAMESPACE }}/dendrite-demo-pinecone:${{ github.ref_name }}
|
||||||
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-polylith:${{ github.ref_name }}
|
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-demo-pinecone:${{ github.ref_name }}
|
||||||
|
|
||||||
- name: Build release polylith image
|
- name: Build release Pinecone demo image
|
||||||
if: github.event_name == 'release' # Only for GitHub releases
|
if: github.event_name == 'release' # Only for GitHub releases
|
||||||
id: docker_build_polylith_release
|
id: docker_build_demo_pinecone_release
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
context: .
|
context: .
|
||||||
file: ./build/docker/Dockerfile.polylith
|
file: ./build/docker/Dockerfile.demo-pinecone
|
||||||
platforms: ${{ env.PLATFORMS }}
|
platforms: ${{ env.PLATFORMS }}
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.DOCKER_NAMESPACE }}/dendrite-polylith:latest
|
${{ env.DOCKER_NAMESPACE }}/dendrite-demo-yggdrasil:latest
|
||||||
${{ env.DOCKER_NAMESPACE }}/dendrite-polylith:${{ env.RELEASE_VERSION }}
|
${{ env.DOCKER_NAMESPACE }}/dendrite-demo-yggdrasil:${{ env.RELEASE_VERSION }}
|
||||||
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-polylith:latest
|
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-demo-yggdrasil:latest
|
||||||
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-polylith:${{ env.RELEASE_VERSION }}
|
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-demo-yggdrasil:${{ env.RELEASE_VERSION }}
|
||||||
|
|
||||||
|
demo-yggdrasil:
|
||||||
|
name: Yggdrasil demo image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Get release tag & build flags
|
||||||
|
if: github.event_name == 'release' # Only for GitHub releases
|
||||||
|
run: |
|
||||||
|
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ env.DOCKER_HUB_USER }}
|
||||||
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
- name: Login to GitHub Containers
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build main Yggdrasil demo image
|
||||||
|
if: github.ref_name == 'main'
|
||||||
|
id: docker_build_demo_yggdrasil
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
context: .
|
||||||
|
file: ./build/docker/Dockerfile.demo-yggdrasil
|
||||||
|
platforms: ${{ env.PLATFORMS }}
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.DOCKER_NAMESPACE }}/dendrite-demo-yggdrasil:${{ github.ref_name }}
|
||||||
|
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-demo-yggdrasil:${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Build release Yggdrasil demo image
|
||||||
|
if: github.event_name == 'release' # Only for GitHub releases
|
||||||
|
id: docker_build_demo_yggdrasil_release
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
context: .
|
||||||
|
file: ./build/docker/Dockerfile.demo-yggdrasil
|
||||||
|
platforms: ${{ env.PLATFORMS }}
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.DOCKER_NAMESPACE }}/dendrite-demo-yggdrasil:latest
|
||||||
|
${{ env.DOCKER_NAMESPACE }}/dendrite-demo-yggdrasil:${{ env.RELEASE_VERSION }}
|
||||||
|
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-demo-yggdrasil:latest
|
||||||
|
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-demo-yggdrasil:${{ env.RELEASE_VERSION }}
|
||||||
|
|
52
.github/workflows/gh-pages.yml
vendored
Normal file
52
.github/workflows/gh-pages.yml
vendored
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# Sample workflow for building and deploying a Jekyll site to GitHub Pages
|
||||||
|
name: Deploy GitHub Pages dependencies preinstalled
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Runs on pushes targeting the default branch
|
||||||
|
push:
|
||||||
|
branches: ["gh-pages"]
|
||||||
|
paths:
|
||||||
|
- 'docs/**' # only execute if we have docs changes
|
||||||
|
|
||||||
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
# Allow one concurrent deployment
|
||||||
|
concurrency:
|
||||||
|
group: "pages"
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Build job
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v2
|
||||||
|
- name: Build with Jekyll
|
||||||
|
uses: actions/jekyll-build-pages@v1
|
||||||
|
with:
|
||||||
|
source: ./docs
|
||||||
|
destination: ./_site
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v1
|
||||||
|
|
||||||
|
# Deployment job
|
||||||
|
deploy:
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v1
|
41
.github/workflows/helm.yml
vendored
Normal file
41
.github/workflows/helm.yml
vendored
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
name: Release Charts
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'helm/**' # only execute if we have helm chart changes
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
# depending on default permission settings for your org (contents being read-only or read-write for workloads), you will have to add permissions
|
||||||
|
# see: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure Git
|
||||||
|
run: |
|
||||||
|
git config user.name "$GITHUB_ACTOR"
|
||||||
|
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Install Helm
|
||||||
|
uses: azure/setup-helm@v3
|
||||||
|
with:
|
||||||
|
version: v3.10.0
|
||||||
|
|
||||||
|
- name: Run chart-releaser
|
||||||
|
uses: helm/chart-releaser-action@ed43eb303604cbc0eeec8390544f7748dc6c790d # specific commit, since `mark_as_latest` is not yet in a release
|
||||||
|
env:
|
||||||
|
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
with:
|
||||||
|
config: helm/cr.yaml
|
||||||
|
charts_dir: helm/
|
||||||
|
mark_as_latest: false
|
91
.github/workflows/k8s.yml
vendored
Normal file
91
.github/workflows/k8s.yml
vendored
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
name: k8s
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
paths:
|
||||||
|
- 'helm/**' # only execute if we have helm chart changes
|
||||||
|
pull_request:
|
||||||
|
branches: ["main"]
|
||||||
|
paths:
|
||||||
|
- 'helm/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Lint Helm chart
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
changed: ${{ steps.list-changed.outputs.changed }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: azure/setup-helm@v3
|
||||||
|
with:
|
||||||
|
version: v3.10.0
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: 3.11
|
||||||
|
check-latest: true
|
||||||
|
- uses: helm/chart-testing-action@v2.3.1
|
||||||
|
- name: Get changed status
|
||||||
|
id: list-changed
|
||||||
|
run: |
|
||||||
|
changed=$(ct list-changed --config helm/ct.yaml --target-branch ${{ github.event.repository.default_branch }})
|
||||||
|
if [[ -n "$changed" ]]; then
|
||||||
|
echo "::set-output name=changed::true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run lint
|
||||||
|
run: ct lint --config helm/ct.yaml
|
||||||
|
|
||||||
|
# only bother to run if lint step reports a change to the helm chart
|
||||||
|
install:
|
||||||
|
needs:
|
||||||
|
- lint
|
||||||
|
if: ${{ needs.lint.outputs.changed == 'true' }}
|
||||||
|
name: Install Helm charts
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ inputs.checkoutCommit }}
|
||||||
|
- name: Install Kubernetes tools
|
||||||
|
uses: yokawasa/action-setup-kube-tools@v0.8.2
|
||||||
|
with:
|
||||||
|
setup-tools: |
|
||||||
|
helmv3
|
||||||
|
helm: "3.10.3"
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.10"
|
||||||
|
- name: Set up chart-testing
|
||||||
|
uses: helm/chart-testing-action@v2.3.1
|
||||||
|
- name: Create k3d cluster
|
||||||
|
uses: nolar/setup-k3d-k3s@v1
|
||||||
|
with:
|
||||||
|
version: v1.28
|
||||||
|
- name: Remove node taints
|
||||||
|
run: |
|
||||||
|
kubectl taint --all=true nodes node.cloudprovider.kubernetes.io/uninitialized- || true
|
||||||
|
- name: Run chart-testing (install)
|
||||||
|
run: ct install --config helm/ct.yaml
|
||||||
|
|
||||||
|
# Install the chart using helm directly and test with create-account
|
||||||
|
- name: Install chart
|
||||||
|
run: |
|
||||||
|
helm install --values helm/dendrite/ci/ct-postgres-sharedsecret-values.yaml dendrite helm/dendrite
|
||||||
|
- name: Wait for Postgres and Dendrite to be up
|
||||||
|
run: |
|
||||||
|
kubectl wait --for=condition=ready --timeout=90s pod -l app.kubernetes.io/name=postgresql || kubectl get pods -A
|
||||||
|
kubectl wait --for=condition=ready --timeout=90s pod -l app.kubernetes.io/name=dendrite || kubectl get pods -A
|
||||||
|
kubectl get pods -A
|
||||||
|
kubectl get services
|
||||||
|
kubectl get ingress
|
||||||
|
kubectl logs -l app.kubernetes.io/name=dendrite
|
||||||
|
- name: Run create account
|
||||||
|
run: |
|
||||||
|
podName=$(kubectl get pods -l app.kubernetes.io/name=dendrite -o name)
|
||||||
|
kubectl exec "${podName}" -- /usr/bin/create-account -username alice -password somerandompassword
|
322
.github/workflows/schedules.yaml
vendored
Normal file
322
.github/workflows/schedules.yaml
vendored
Normal file
|
@ -0,0 +1,322 @@
|
||||||
|
name: Scheduled
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *' # every day at midnight
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check_date: # https://stackoverflow.com/questions/63014786/how-to-schedule-a-github-actions-nightly-build-but-run-it-only-when-there-where
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Check latest commit
|
||||||
|
outputs:
|
||||||
|
should_run: ${{ steps.should_run.outputs.should_run }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: print latest_commit
|
||||||
|
run: echo ${{ github.sha }}
|
||||||
|
|
||||||
|
- id: should_run
|
||||||
|
continue-on-error: true
|
||||||
|
name: check latest commit is less than a day
|
||||||
|
if: ${{ github.event_name == 'schedule' }}
|
||||||
|
run: test -z $(git rev-list --after="24 hours" ${{ github.sha }}) && echo "::set-output name=should_run::false"
|
||||||
|
|
||||||
|
# run Sytest in different variations
|
||||||
|
sytest:
|
||||||
|
needs: check_date
|
||||||
|
if: ${{ needs.check_date.outputs.should_run != 'false' }}
|
||||||
|
timeout-minutes: 60
|
||||||
|
name: "Sytest (${{ matrix.label }})"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- label: SQLite native
|
||||||
|
|
||||||
|
- label: SQLite Cgo
|
||||||
|
cgo: 1
|
||||||
|
|
||||||
|
- label: PostgreSQL
|
||||||
|
postgres: postgres
|
||||||
|
container:
|
||||||
|
image: matrixdotorg/sytest-dendrite:latest
|
||||||
|
volumes:
|
||||||
|
- ${{ github.workspace }}:/src
|
||||||
|
- /root/.cache/go-build:/github/home/.cache/go-build
|
||||||
|
- /root/.cache/go-mod:/gopath/pkg/mod
|
||||||
|
env:
|
||||||
|
POSTGRES: ${{ matrix.postgres && 1}}
|
||||||
|
SYTEST_BRANCH: ${{ github.head_ref }}
|
||||||
|
RACE_DETECTION: 1
|
||||||
|
COVER: 1
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
/gopath/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-sytest-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-sytest-
|
||||||
|
- name: Run Sytest
|
||||||
|
run: /bootstrap.sh dendrite
|
||||||
|
working-directory: /src
|
||||||
|
- name: Summarise results.tap
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: /sytest/scripts/tap_to_gha.pl /logs/results.tap
|
||||||
|
- name: Sytest List Maintenance
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: /src/show-expected-fail-tests.sh /logs/results.tap /src/sytest-whitelist /src/sytest-blacklist
|
||||||
|
continue-on-error: true # not fatal
|
||||||
|
- name: Are We Synapse Yet?
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: /src/are-we-synapse-yet.py /logs/results.tap -v
|
||||||
|
continue-on-error: true # not fatal
|
||||||
|
- name: Upload Sytest logs
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: Sytest Logs - ${{ job.status }} - (Dendrite ${{ join(matrix.*, ' ') }})
|
||||||
|
path: |
|
||||||
|
/logs/results.tap
|
||||||
|
/logs/**/*.log*
|
||||||
|
/logs/**/covdatafiles/**
|
||||||
|
|
||||||
|
sytest-coverage:
|
||||||
|
timeout-minutes: 5
|
||||||
|
name: "Sytest Coverage"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [ sytest, check_date ] # only run once Sytest is done and there was a commit
|
||||||
|
if: ${{ always() && needs.check_date.outputs.should_run != 'false' }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: 'stable'
|
||||||
|
cache: true
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
- name: Collect coverage
|
||||||
|
run: |
|
||||||
|
go tool covdata textfmt -i="$(find Sytest* -name 'covmeta*' -type f -exec dirname {} \; | uniq | paste -s -d ',' -)" -o sytest.cov
|
||||||
|
grep -Ev 'relayapi|setup/mscs|api_trace' sytest.cov > final.cov
|
||||||
|
go tool covdata func -i="$(find Sytest* -name 'covmeta*' -type f -exec dirname {} \; | uniq | paste -s -d ',' -)"
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
|
with:
|
||||||
|
files: ./final.cov
|
||||||
|
flags: sytest
|
||||||
|
fail_ci_if_error: true
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
|
# run Complement
|
||||||
|
complement:
|
||||||
|
needs: check_date
|
||||||
|
if: ${{ needs.check_date.outputs.should_run != 'false' }}
|
||||||
|
name: "Complement (${{ matrix.label }})"
|
||||||
|
timeout-minutes: 60
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- label: SQLite native
|
||||||
|
cgo: 0
|
||||||
|
|
||||||
|
- label: SQLite Cgo
|
||||||
|
cgo: 1
|
||||||
|
|
||||||
|
- label: PostgreSQL
|
||||||
|
postgres: Postgres
|
||||||
|
cgo: 0
|
||||||
|
steps:
|
||||||
|
# Env vars are set file a file given by $GITHUB_PATH. We need both Go 1.17 and GOPATH on env to run Complement.
|
||||||
|
# See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-system-path
|
||||||
|
- name: "Set Go Version"
|
||||||
|
run: |
|
||||||
|
echo "$GOROOT_1_17_X64/bin" >> $GITHUB_PATH
|
||||||
|
echo "~/go/bin" >> $GITHUB_PATH
|
||||||
|
- name: "Install Complement Dependencies"
|
||||||
|
# We don't need to install Go because it is included on the Ubuntu 20.04 image:
|
||||||
|
# See https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-Readme.md specifically GOROOT_1_17_X64
|
||||||
|
run: |
|
||||||
|
sudo apt-get update && sudo apt-get install -y libolm3 libolm-dev
|
||||||
|
go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest
|
||||||
|
- name: Run actions/checkout@v4 for dendrite
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
path: dendrite
|
||||||
|
|
||||||
|
# Attempt to check out the same branch of Complement as the PR. If it
|
||||||
|
# doesn't exist, fallback to main.
|
||||||
|
- name: Checkout complement
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p complement
|
||||||
|
# Attempt to use the version of complement which best matches the current
|
||||||
|
# build. Depending on whether this is a PR or release, etc. we need to
|
||||||
|
# use different fallbacks.
|
||||||
|
#
|
||||||
|
# 1. First check if there's a similarly named branch (GITHUB_HEAD_REF
|
||||||
|
# for pull requests, otherwise GITHUB_REF).
|
||||||
|
# 2. Attempt to use the base branch, e.g. when merging into release-vX.Y
|
||||||
|
# (GITHUB_BASE_REF for pull requests).
|
||||||
|
# 3. Use the default complement branch ("master").
|
||||||
|
for BRANCH_NAME in "$GITHUB_HEAD_REF" "$GITHUB_BASE_REF" "${GITHUB_REF#refs/heads/}" "master"; do
|
||||||
|
# Skip empty branch names and merge commits.
|
||||||
|
if [[ -z "$BRANCH_NAME" || $BRANCH_NAME =~ ^refs/pull/.* ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
(wget -O - "https://github.com/matrix-org/complement/archive/$BRANCH_NAME.tar.gz" | tar -xz --strip-components=1 -C complement) && break
|
||||||
|
done
|
||||||
|
# Build initial Dendrite image
|
||||||
|
- run: docker build --build-arg=CGO=${{ matrix.cgo }} -t complement-dendrite:${{ matrix.postgres }}${{ matrix.cgo }} -f build/scripts/Complement${{ matrix.postgres }}.Dockerfile .
|
||||||
|
working-directory: dendrite
|
||||||
|
env:
|
||||||
|
DOCKER_BUILDKIT: 1
|
||||||
|
|
||||||
|
- name: Create post test script
|
||||||
|
run: |
|
||||||
|
cat <<EOF > /tmp/posttest.sh
|
||||||
|
#!/bin/bash
|
||||||
|
mkdir -p /tmp/Complement/logs/\$2/\$1/
|
||||||
|
docker cp \$1:/tmp/covdatafiles/. /tmp/Complement/logs/\$2/\$1/
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x /tmp/posttest.sh
|
||||||
|
# Run Complement
|
||||||
|
- run: |
|
||||||
|
set -o pipefail &&
|
||||||
|
go test -v -json -tags dendrite_blacklist ./tests ./tests/csapi 2>&1 | gotestfmt -hide all
|
||||||
|
shell: bash
|
||||||
|
name: Run Complement Tests
|
||||||
|
env:
|
||||||
|
COMPLEMENT_BASE_IMAGE: complement-dendrite:${{ matrix.postgres }}${{ matrix.cgo }}
|
||||||
|
COMPLEMENT_SHARE_ENV_PREFIX: COMPLEMENT_DENDRITE_
|
||||||
|
COMPLEMENT_DENDRITE_COVER: 1
|
||||||
|
COMPLEMENT_POST_TEST_SCRIPT: /tmp/posttest.sh
|
||||||
|
working-directory: complement
|
||||||
|
|
||||||
|
- name: Upload Complement logs
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: Complement Logs - (Dendrite ${{ join(matrix.*, ' ') }})
|
||||||
|
path: |
|
||||||
|
/tmp/Complement/logs/**
|
||||||
|
|
||||||
|
complement-coverage:
|
||||||
|
timeout-minutes: 5
|
||||||
|
name: "Complement Coverage"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [ complement, check_date ] # only run once Complements is done and there was a commit
|
||||||
|
if: ${{ always() && needs.check_date.outputs.should_run != 'false' }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: 'stable'
|
||||||
|
cache: true
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
- name: Collect coverage
|
||||||
|
run: |
|
||||||
|
go tool covdata textfmt -i="$(find Complement* -name 'covmeta*' -type f -exec dirname {} \; | uniq | paste -s -d ',' -)" -o complement.cov
|
||||||
|
grep -Ev 'relayapi|setup/mscs|api_trace' complement.cov > final.cov
|
||||||
|
go tool covdata func -i="$(find Complement* -name 'covmeta*' -type f -exec dirname {} \; | uniq | paste -s -d ',' -)"
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
|
with:
|
||||||
|
files: ./final.cov
|
||||||
|
flags: complement
|
||||||
|
fail_ci_if_error: true
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }} # required
|
||||||
|
|
||||||
|
element-web:
|
||||||
|
if: ${{ false }} # disable for now, as Cypress has been replaced by Playwright
|
||||||
|
timeout-minutes: 120
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: tecolicom/actions-use-apt-tools@v1
|
||||||
|
with:
|
||||||
|
# Our test suite includes some screenshot tests with unusual diacritics, which are
|
||||||
|
# supposed to be covered by STIXGeneral.
|
||||||
|
tools: fonts-stix
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: matrix-org/matrix-react-sdk
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: 'yarn'
|
||||||
|
- name: Fetch layered build
|
||||||
|
run: scripts/ci/layered.sh
|
||||||
|
- name: Copy config
|
||||||
|
run: cp element.io/develop/config.json config.json
|
||||||
|
working-directory: ./element-web
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
CI_PACKAGE: true
|
||||||
|
NODE_OPTIONS: "--openssl-legacy-provider"
|
||||||
|
run: yarn build
|
||||||
|
working-directory: ./element-web
|
||||||
|
- name: Edit Test Config
|
||||||
|
run: |
|
||||||
|
sed -i '/HOMESERVER/c\ HOMESERVER: "dendrite",' cypress.config.ts
|
||||||
|
- name: "Run cypress tests"
|
||||||
|
uses: cypress-io/github-action@v4.1.1
|
||||||
|
with:
|
||||||
|
browser: chrome
|
||||||
|
start: npx serve -p 8080 ./element-web/webapp
|
||||||
|
wait-on: 'http://localhost:8080'
|
||||||
|
env:
|
||||||
|
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
|
||||||
|
TMPDIR: ${{ runner.temp }}
|
||||||
|
|
||||||
|
element-web-pinecone:
|
||||||
|
if: ${{ false }} # disable for now, as Cypress has been replaced by Playwright
|
||||||
|
timeout-minutes: 120
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: tecolicom/actions-use-apt-tools@v1
|
||||||
|
with:
|
||||||
|
# Our test suite includes some screenshot tests with unusual diacritics, which are
|
||||||
|
# supposed to be covered by STIXGeneral.
|
||||||
|
tools: fonts-stix
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: matrix-org/matrix-react-sdk
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
cache: 'yarn'
|
||||||
|
- name: Fetch layered build
|
||||||
|
run: scripts/ci/layered.sh
|
||||||
|
- name: Copy config
|
||||||
|
run: cp element.io/develop/config.json config.json
|
||||||
|
working-directory: ./element-web
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
CI_PACKAGE: true
|
||||||
|
NODE_OPTIONS: "--openssl-legacy-provider"
|
||||||
|
run: yarn build
|
||||||
|
working-directory: ./element-web
|
||||||
|
- name: Edit Test Config
|
||||||
|
run: |
|
||||||
|
sed -i '/HOMESERVER/c\ HOMESERVER: "dendritePinecone",' cypress.config.ts
|
||||||
|
- name: "Run cypress tests"
|
||||||
|
uses: cypress-io/github-action@v4.1.1
|
||||||
|
with:
|
||||||
|
browser: chrome
|
||||||
|
start: npx serve -p 8080 ./element-web/webapp
|
||||||
|
wait-on: 'http://localhost:8080'
|
||||||
|
env:
|
||||||
|
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
|
||||||
|
TMPDIR: ${{ runner.temp }}
|
16
.gitignore
vendored
16
.gitignore
vendored
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
# Allow GitHub config
|
# Allow GitHub config
|
||||||
!.github
|
!.github
|
||||||
|
!.forgejo
|
||||||
|
|
||||||
# Downloads
|
# Downloads
|
||||||
/.downloads
|
/.downloads
|
||||||
|
@ -41,6 +42,10 @@ _testmain.go
|
||||||
*.test
|
*.test
|
||||||
*.prof
|
*.prof
|
||||||
*.wasm
|
*.wasm
|
||||||
|
*.aar
|
||||||
|
*.jar
|
||||||
|
*.framework
|
||||||
|
*.xcframework
|
||||||
|
|
||||||
# Generated keys
|
# Generated keys
|
||||||
*.pem
|
*.pem
|
||||||
|
@ -52,6 +57,7 @@ dendrite.yaml
|
||||||
|
|
||||||
# Database files
|
# Database files
|
||||||
*.db
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
|
||||||
# Log files
|
# Log files
|
||||||
*.log*
|
*.log*
|
||||||
|
@ -65,4 +71,14 @@ test/wasm/node_modules
|
||||||
# Ignore complement folder when running locally
|
# Ignore complement folder when running locally
|
||||||
complement/
|
complement/
|
||||||
|
|
||||||
|
# Stuff from GitHub Pages
|
||||||
|
docs/_site
|
||||||
|
|
||||||
media_store/
|
media_store/
|
||||||
|
build
|
||||||
|
|
||||||
|
# golang workspaces
|
||||||
|
go.work*
|
||||||
|
|
||||||
|
# helm chart
|
||||||
|
helm/dendrite/charts/
|
||||||
|
|
|
@ -6,7 +6,7 @@ run:
|
||||||
concurrency: 4
|
concurrency: 4
|
||||||
|
|
||||||
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||||
deadline: 30m
|
timeout: 5m
|
||||||
|
|
||||||
# exit code when at least one issue was found, default is 1
|
# exit code when at least one issue was found, default is 1
|
||||||
issues-exit-code: 1
|
issues-exit-code: 1
|
||||||
|
@ -18,24 +18,6 @@ run:
|
||||||
#build-tags:
|
#build-tags:
|
||||||
# - mytag
|
# - mytag
|
||||||
|
|
||||||
# which dirs to skip: they won't be analyzed;
|
|
||||||
# can use regexp here: generated.*, regexp is applied on full path;
|
|
||||||
# default value is empty list, but next dirs are always skipped independently
|
|
||||||
# from this option's value:
|
|
||||||
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
|
|
||||||
skip-dirs:
|
|
||||||
- bin
|
|
||||||
- docs
|
|
||||||
|
|
||||||
# which files to skip: they will be analyzed, but issues from them
|
|
||||||
# won't be reported. Default value is empty list, but there is
|
|
||||||
# no need to include all autogenerated files, we confidently recognize
|
|
||||||
# autogenerated files. If it's not please let us know.
|
|
||||||
skip-files:
|
|
||||||
- ".*\\.md$"
|
|
||||||
- ".*\\.sh$"
|
|
||||||
- "^cmd/syncserver-integration-tests/testdata.go$"
|
|
||||||
|
|
||||||
# by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules":
|
# by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules":
|
||||||
# If invoked with -mod=readonly, the go command is disallowed from the implicit
|
# If invoked with -mod=readonly, the go command is disallowed from the implicit
|
||||||
# automatic updating of go.mod described above. Instead, it fails when any changes
|
# automatic updating of go.mod described above. Instead, it fails when any changes
|
||||||
|
@ -50,7 +32,8 @@ run:
|
||||||
# output configuration options
|
# output configuration options
|
||||||
output:
|
output:
|
||||||
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
|
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
|
||||||
format: colored-line-number
|
formats:
|
||||||
|
- format: colored-line-number
|
||||||
|
|
||||||
# print lines of code with issue, default is true
|
# print lines of code with issue, default is true
|
||||||
print-issued-lines: true
|
print-issued-lines: true
|
||||||
|
@ -79,9 +62,8 @@ linters-settings:
|
||||||
# see https://github.com/kisielk/errcheck#excluding-functions for details
|
# see https://github.com/kisielk/errcheck#excluding-functions for details
|
||||||
#exclude: /path/to/file.txt
|
#exclude: /path/to/file.txt
|
||||||
govet:
|
govet:
|
||||||
# report about shadowed variables
|
enable:
|
||||||
check-shadowing: true
|
- shadow
|
||||||
|
|
||||||
# settings per analyzer
|
# settings per analyzer
|
||||||
settings:
|
settings:
|
||||||
printf: # analyzer name, run `go tool vet help` to see all analyzers
|
printf: # analyzer name, run `go tool vet help` to see all analyzers
|
||||||
|
@ -179,9 +161,7 @@ linters-settings:
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
enable:
|
enable:
|
||||||
- deadcode
|
|
||||||
- errcheck
|
- errcheck
|
||||||
- goconst
|
|
||||||
- gocyclo
|
- gocyclo
|
||||||
- goimports # Does everything gofmt does
|
- goimports # Does everything gofmt does
|
||||||
- gosimple
|
- gosimple
|
||||||
|
@ -191,10 +171,8 @@ linters:
|
||||||
- misspell # Check code comments, whereas misspell in CI checks *.md files
|
- misspell # Check code comments, whereas misspell in CI checks *.md files
|
||||||
- nakedret
|
- nakedret
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- structcheck
|
|
||||||
- unparam
|
- unparam
|
||||||
- unused
|
- unused
|
||||||
- varcheck
|
|
||||||
enable-all: false
|
enable-all: false
|
||||||
disable:
|
disable:
|
||||||
- bodyclose
|
- bodyclose
|
||||||
|
@ -214,12 +192,31 @@ linters:
|
||||||
- stylecheck
|
- stylecheck
|
||||||
- typecheck # Should turn back on soon
|
- typecheck # Should turn back on soon
|
||||||
- unconvert # Should turn back on soon
|
- unconvert # Should turn back on soon
|
||||||
|
- goconst # Slightly annoying, as it reports "issues" in SQL statements
|
||||||
disable-all: false
|
disable-all: false
|
||||||
presets:
|
presets:
|
||||||
fast: false
|
fast: false
|
||||||
|
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
|
# which files to skip: they will be analyzed, but issues from them
|
||||||
|
# won't be reported. Default value is empty list, but there is
|
||||||
|
# no need to include all autogenerated files, we confidently recognize
|
||||||
|
# autogenerated files. If it's not please let us know.
|
||||||
|
exclude-files:
|
||||||
|
- ".*\\.md$"
|
||||||
|
- ".*\\.sh$"
|
||||||
|
- "^cmd/syncserver-integration-tests/testdata.go$"
|
||||||
|
|
||||||
|
# which dirs to skip: they won't be analyzed;
|
||||||
|
# can use regexp here: generated.*, regexp is applied on full path;
|
||||||
|
# default value is empty list, but next dirs are always skipped independently
|
||||||
|
# from this option's value:
|
||||||
|
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
|
||||||
|
exclude-dirs:
|
||||||
|
- bin
|
||||||
|
- docs
|
||||||
|
|
||||||
# List of regexps of issue texts to exclude, empty list by default.
|
# List of regexps of issue texts to exclude, empty list by default.
|
||||||
# But independently from this option we use default exclude patterns,
|
# But independently from this option we use default exclude patterns,
|
||||||
# it can be disabled by `exclude-use-default: false`. To list all
|
# it can be disabled by `exclude-use-default: false`. To list all
|
||||||
|
|
752
CHANGES.md
752
CHANGES.md
|
@ -1,5 +1,757 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Dendrite 0.13.7 (2024-04-09)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Fixed an issue where the displayname/avatar of an invited user was replaced with the inviter's details
|
||||||
|
- Improved server startup performance by avoiding unnecessary room ACL queries
|
||||||
|
- This change reduces memory footprint as it caches ACL regex patterns once instead of for each room
|
||||||
|
- Unnecessary Relay related queries have been removed. **Note**: To use relays, you now need to explicitly enable them using the `federation_api.enable_relays` config
|
||||||
|
- Fixed space summaries over federation
|
||||||
|
- Improved usage of external NATS JetStream by reusing existing connections instead of opening new ones unnecessarily
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Modernized Appservices (contributed by [tulir](https://github.com/tulir))
|
||||||
|
- Added event reporting with Synapse Admin endpoints for querying them
|
||||||
|
- Updated dependencies
|
||||||
|
|
||||||
|
## Dendrite 0.13.6 (2024-01-26)
|
||||||
|
|
||||||
|
Upgrading to this version is **highly** recommended, as it contains several QoL improvements.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Use `AckExplicitPolicy` for JetStream consumers, so messages don't pile up in NATS
|
||||||
|
- A rare panic when assigning a state key NID has been fixed
|
||||||
|
- A rare panic when checking powerlevels has been fixed
|
||||||
|
- Notary keys requests for all keys now work correctly
|
||||||
|
- Spec compliance:
|
||||||
|
- Return `M_INVALID_PARAM` when querying room aliases
|
||||||
|
- Handle empty `from` parameter when requesting `/messages`
|
||||||
|
- Add CORP headers on media endpoints
|
||||||
|
- Remove `aliases` from `/publicRooms` responses
|
||||||
|
- Allow `+` in MXIDs (Contributed by [RosstheRoss](https://github.com/RosstheRoss))
|
||||||
|
- Fixes membership transitions from `knock` to `join` in `knock_restricted` rooms
|
||||||
|
- Incremental syncs now batch querying events (Contributed by [recht](https://github.com/recht))
|
||||||
|
- Move `/joined_members` back to the clientAPI/roomserver, which should make bridges happier again
|
||||||
|
- Backfilling from other servers now only uses at max 100 events instead of potentially thousands
|
||||||
|
|
||||||
|
## Dendrite 0.13.5 (2023-12-12)
|
||||||
|
|
||||||
|
Upgrading to this version is **highly** recommended, as it fixes several long-standing bugs in
|
||||||
|
our CanonicalJSON implementation.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Convert unicode escapes to lowercase (gomatrixserverlib)
|
||||||
|
- Fix canonical json utf-16 surrogate pair detection logic (gomatrixserverlib)
|
||||||
|
- Handle negative zero and exponential numbers in Canonical JSON verification (gomatrixserverlib)
|
||||||
|
- Avoid logging unnecessary messages when unable to fetch server keys if multiple fetchers are used (gomatrixserverlib)
|
||||||
|
- Issues around the device list updater have been fixed, which should ensure that there are always
|
||||||
|
workers available to process incoming device list updates.
|
||||||
|
- A panic in the `/hierarchy` endpoints used for spaces has been fixed (client-server and server-server API)
|
||||||
|
- Fixes around the way we handle database transactions (including a potential connection leak)
|
||||||
|
- ACLs are now updated when received as outliers
|
||||||
|
- A race condition, which could lead to bridges instantly leaving a room after joining it, between the SyncAPI and
|
||||||
|
Appservices has been fixed
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Appservice login is now supported!**
|
||||||
|
- Users can now kick themselves (used by some bridges)
|
||||||
|
|
||||||
|
## Dendrite 0.13.4 (2023-10-25)
|
||||||
|
|
||||||
|
Upgrading to this version is **highly** recommended, as it fixes a long-standing bug in the state resolution
|
||||||
|
algorithm.
|
||||||
|
|
||||||
|
### Fixes:
|
||||||
|
|
||||||
|
- The "device list updater" now de-duplicates the servers to fetch devices from on startup. (This also
|
||||||
|
avoids spamming the logs when shutting down.)
|
||||||
|
- A bug in the state resolution algorithm has been fixed. This bug could result in users "being reset"
|
||||||
|
out of rooms and other missing state events due to calculating the wrong state.
|
||||||
|
- A bug when setting notifications from Element Android has been fixed by implementing MSC3987
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Updated dependencies
|
||||||
|
- Internal NATS Server has been updated from v2.9.19 to v2.9.23
|
||||||
|
|
||||||
|
## Dendrite 0.13.3 (2023-09-28)
|
||||||
|
|
||||||
|
### Fixes:
|
||||||
|
|
||||||
|
- The `user_id` query parameter when authenticating is now used correctly (contributed by [tulir](https://github.com/tulir))
|
||||||
|
- Invitations are now correctly pushed to devices
|
||||||
|
- A bug which could result in the corruption of `m.direct` account data has been fixed
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- [Sliding Sync proxy](https://github.com/matrix-org/sliding-sync) can be configured in the `/.well-known/matrix/client` response
|
||||||
|
- Room version 11 is now supported
|
||||||
|
- Clients can request the `federation` `event_format` when creating filters
|
||||||
|
- Many under the hood improvements for [MSC4014: Pseudonymous Identities](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/pseudo-ids/proposals/4014-pseudonymous-identities.md)
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
|
- Dendrite now requires Go 1.20 if building from source
|
||||||
|
|
||||||
|
## Dendrite 0.13.2 (2023-08-23)
|
||||||
|
|
||||||
|
### Fixes:
|
||||||
|
|
||||||
|
- Migrations in SQLite are now prepared on the correct context (transaction or database)
|
||||||
|
- The `InputRoomEvent` stream now has a maximum age of 24h, which should help with slow start up times of NATS JetStream (contributed by [neilalexander](https://github.com/neilalexander))
|
||||||
|
- Event size checks are more in line with Synapse
|
||||||
|
- Requests to `/messages` have been optimized, possibly reducing database round trips
|
||||||
|
- Re-add the revision of Dendrite when building from source (Note: This only works if git is installed)
|
||||||
|
- Getting local members to notify has been optimized, which should significantly reduce memory allocation and cache usage
|
||||||
|
- When getting queried about user profiles, we now return HTTP404 if the user/profiles does not exist
|
||||||
|
- Background federated joins should now be fixed and not timeout after a short time
|
||||||
|
- Database connections are now correctly re-used
|
||||||
|
- Restored the old behavior of the `/purgeRoom` admin endpoint (does not evacuate the room before purging)
|
||||||
|
- Don't expose information about the system when trying to download files that don't exist
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Further improvements and fixes for [MSC4014: Pseudonymous Identities](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/pseudo-ids/proposals/4014-pseudonymous-identities.md)
|
||||||
|
- Lookup correct prev events in the sync API
|
||||||
|
- Populate `prev_sender` correctly in the sync API
|
||||||
|
- Event federation should work better
|
||||||
|
- Added new `dendrite_up` Prometheus metric, containing the version of Dendrite
|
||||||
|
- Space summaries ([MSC2946](https://github.com/matrix-org/matrix-spec-proposals/pull/2946)) have been moved from MSC to being natively supported
|
||||||
|
- For easier issue investigation, logs for application services now contain the application service ID (contributed by [maxberger](https://github.com/maxberger))
|
||||||
|
- The default room version to use when creating rooms can now be configured using `room_server.default_room_version`
|
||||||
|
|
||||||
|
## Dendrite 0.13.1 (2023-07-06)
|
||||||
|
|
||||||
|
This releases fixes a long-standing "off-by-one" error which could result in state resets. Upgrading to this version is **highly** recommended.
|
||||||
|
|
||||||
|
When deduplicating state events, we were checking if the event in question was already in a state snapshot. If it was in a previous state snapshot, we would
|
||||||
|
then remove it from the list of events to store. If this happened, we were, unfortunately, skipping the next event to check. This resulted in
|
||||||
|
events getting stored in state snapshots where they may not be needed. When we now compared two of those state snapshots, one of them
|
||||||
|
contained the skipped event, while the other didn't. This difference possibly shouldn't exist, resulting in unexpected state resets and explains
|
||||||
|
reports of missing state events as well.
|
||||||
|
|
||||||
|
Rooms where a state reset occurred earlier should, hopefully, reconcile over time.
|
||||||
|
|
||||||
|
### Fixes:
|
||||||
|
|
||||||
|
- A long-standing "off-by-one" error has been fixed, which could result in state resets
|
||||||
|
- Roomserver Prometheus Metrics are available again
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Updated dependencies
|
||||||
|
- Internal NATS Server has been updated from v2.9.15 to v2.9.19
|
||||||
|
|
||||||
|
## Dendrite 0.13.0 (2023-06-30)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Results in responses to `/search` now highlight words more accurately and not only the search terms as before
|
||||||
|
- Support for connecting to appservices listening on unix sockets has been added (contributed by [cyberb](https://github.com/cyberb))
|
||||||
|
- Admin APIs for token authenticated registration have been added (contributed by [santhoshivan23](https://github.com/santhoshivan23))
|
||||||
|
- Initial support for [MSC4014: Pseudonymous Identities](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/pseudo-ids/proposals/4014-pseudonymous-identities.md)
|
||||||
|
- This is **highly experimental**, things like changing usernames/avatars, inviting users, upgrading rooms isn't working
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- `m.upload.size` is now optional, finally allowing uploads with unlimited file size
|
||||||
|
- A bug while resolving server names has been fixed (contributed by [anton-molyboha](https://github.com/anton-molyboha))
|
||||||
|
- Application services should only receive one invitation instead of 2 (or worse), which could result in state resets previously
|
||||||
|
- Several admin endpoints are now using `POST` instead of `GET`
|
||||||
|
- `/delete_devices` now uses user-interactive authentication
|
||||||
|
- Several "membership" (e.g `/kick`, `/ban`) endpoints are using less heavy database queries to check if the user is allowed to perform this action
|
||||||
|
- `/3pid` endpoints are now available on `/v3` instead of the `/unstable` prefix
|
||||||
|
- Upgrading rooms ignores state events of other users, which could result in failed upgrades before
|
||||||
|
- Uploading key backups with a wrong version now returns `M_WRONG_ROOM_KEYS_VERSION`
|
||||||
|
- A potential state reset when joining the same room multiple times in short sequence has been fixed
|
||||||
|
- A bug where we returned the full event as `redacted_because` in redaction events has been fixed
|
||||||
|
- The `displayname` and `avatar_url` can now be set to empty strings
|
||||||
|
- Unsafe hotserving of files has been fixed (contributed by [joshqou](https://github.com/joshqou))
|
||||||
|
- Joining new rooms would potentially return "redacted" events, due to history visibility not being set correctly, this could result in events being rejected
|
||||||
|
- Backfilling resulting in `unsuported room version ''` should now be solved
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
|
- Huge refactoring of Dendrite and gomatrixserverlib
|
||||||
|
|
||||||
|
## Dendrite 0.12.0 (2023-03-13)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- The userapi and keyserver have been merged (no actions needed regarding the database)
|
||||||
|
- The internal NATS JetStream server is now using logrus for logging (contributed by [dvob](https://github.com/dvob))
|
||||||
|
- The roomserver database has been refactored to have separate interfaces when working with rooms and events. Also includes increased usage of the cache to avoid database round trips. (database is unchanged)
|
||||||
|
- The pinecone demo now shuts down more cleanly
|
||||||
|
- The Helm chart now has the ability to deploy a Grafana chart as well (contributed by [genofire](https://github.com/genofire))
|
||||||
|
- Support for listening on unix sockets has been added (contributed by [cyberb](https://github.com/cyberb))
|
||||||
|
- The internal NATS server was updated to v2.9.15
|
||||||
|
- Initial support for `runtime/trace` has been added, to further track down long-running tasks
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- The `session_id` is now correctly set when using SQLite
|
||||||
|
- An issue where device keys could be removed if a device ID is reused has been fixed
|
||||||
|
- A possible DoS issue related to relations has been fixed (reported by [sleroq](https://github.com/sleroq))
|
||||||
|
- When backfilling events, errors are now ignored if we still could fetch events
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
|
- **⚠️ DEPRECATION: Polylith/HTTP API mode has been removed**
|
||||||
|
- The default endpoint to report usages stats to has been updated
|
||||||
|
|
||||||
|
## Dendrite 0.11.1 (2023-02-10)
|
||||||
|
|
||||||
|
**⚠️ DEPRECATION WARNING: This is the last release to have polylith and HTTP API mode. Future releases are monolith only.**
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Dendrite can now be compiled against Go 1.20
|
||||||
|
* Initial store and forward support has been added
|
||||||
|
* A landing page showing that Dendrite is running has been added (contributed by [LukasLJL](https://github.com/LukasLJL))
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- `/sync` is now using significantly less database round trips when using Postgres, resulting in faster initial syncs, allowing larger accounts to login again
|
||||||
|
- Many under the hood pinecone improvements
|
||||||
|
- Publishing rooms is now possible again
|
||||||
|
|
||||||
|
## Dendrite 0.11.0 (2023-01-20)
|
||||||
|
|
||||||
|
The last three missing federation API Sytests have been fixed - bringing us to 100% server-server Synapse parity, with client-server parity at 93% 🎉
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Added `/_dendrite/admin/purgeRoom/{roomID}` to clean up the database
|
||||||
|
* The default room version was updated to 10 (contributed by [FSG-Cat](https://github.com/FSG-Cat))
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* An oversight in the `create-config` binary, which now correctly sets the media path if specified (contributed by [BieHDC](https://github.com/BieHDC))
|
||||||
|
* The Helm chart now uses the `$.Chart.AppVersion` as the default image version to pull, with the possibility to override it (contributed by [genofire](https://github.com/genofire))
|
||||||
|
|
||||||
|
## Dendrite 0.10.9 (2023-01-17)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Stale device lists are now cleaned up on startup, removing entries for users the server doesn't share a room with anymore
|
||||||
|
* Dendrite now has its own Helm chart
|
||||||
|
* Guest access is now handled correctly (disallow joins, kick guests on revocation of guest access, as well as over federation)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* Push rules have seen several tweaks and fixes, which should, for example, fix notifications for `m.read_receipts`
|
||||||
|
* Outgoing presence will now correctly be sent to newly joined hosts
|
||||||
|
* Fixes the `/_dendrite/admin/resetPassword/{userID}` admin endpoint to use the correct variable
|
||||||
|
* Federated backfilling for medium/large rooms has been fixed
|
||||||
|
* `/login` causing wrong device list updates has been resolved
|
||||||
|
* `/sync` should now return the correct room summary heroes
|
||||||
|
* The default config options for `recaptcha_sitekey_class` and `recaptcha_form_field` are now set correctly
|
||||||
|
* `/messages` now omits empty `state` to be more spec compliant (contributed by [handlerug](https://github.com/handlerug))
|
||||||
|
* `/sync` has been optimised to only query state events for history visibility if they are really needed
|
||||||
|
|
||||||
|
## Dendrite 0.10.8 (2022-11-29)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* The built-in NATS Server has been updated to version 2.9.8
|
||||||
|
* A number of under-the-hood changes have been merged for future virtual hosting support in Dendrite (running multiple domain names on the same Dendrite deployment)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* Event auth handling of invites has been refactored, which should fix some edge cases being handled incorrectly
|
||||||
|
* Fix a bug when returning an empty protocol list, which could cause Element to display "The homeserver may be too old to support third party networks" when opening the public room directory
|
||||||
|
* The sync API will no longer filter out the user's own membership when using lazy-loading
|
||||||
|
* Dendrite will now correctly detect JetStream consumers being deleted, stopping the consumer goroutine as needed
|
||||||
|
* A panic in the federation API where the server list could go out of bounds has been fixed
|
||||||
|
* Blacklisted servers will now be excluded when querying joined servers, which improves CPU usage and performs less unnecessary outbound requests
|
||||||
|
* A database writer will now be used to assign state key NIDs when requesting NIDs that may not exist yet
|
||||||
|
* Dendrite will now correctly move local aliases for an upgraded room when the room is upgraded remotely
|
||||||
|
* Dendrite will now correctly move account data for an upgraded room when the room is upgraded remotely
|
||||||
|
* Missing state key NIDs will now be allocated on request rather than returning an error
|
||||||
|
* Guest access is now correctly denied on a number of endpoints
|
||||||
|
* Presence information will now be correctly sent for new private chats
|
||||||
|
* A number of unspecced fields have been removed from outbound `/send` transactions
|
||||||
|
|
||||||
|
## Dendrite 0.10.7 (2022-11-04)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Dendrite will now use a native SQLite port when building with `CGO_ENABLED=0`
|
||||||
|
* A number of `thirdparty` endpoints have been added, improving support for appservices
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* The `"state"` section of the `/sync` response is no longer limited, so state events should not be dropped unexpectedly
|
||||||
|
* The deduplication of the `"timeline"` and `"state"` sections in `/sync` is now performed after applying history visibility, so state events should not be dropped unexpectedly
|
||||||
|
* The `prev_batch` token returned by `/sync` is now calculated after applying history visibility, so that the pagination boundaries are correct
|
||||||
|
* The room summary membership counts in `/sync` should now be calculated properly in more cases
|
||||||
|
* A false membership leave event should no longer be sent down `/sync` as a result of retiring an accepted invite (contributed by [tak-hntlabs](https://github.com/tak-hntlabs))
|
||||||
|
* Presence updates are now only sent to other servers for which the user shares rooms
|
||||||
|
* A bug which could cause a panic when converting events into the `ClientEvent` format has been fixed
|
||||||
|
|
||||||
|
## Dendrite 0.10.6 (2022-11-01)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* History visibility checks have been optimised, which should speed up response times on a variety of endpoints (including `/sync`, `/messages`, `/context` and others) and reduce database load
|
||||||
|
* The built-in NATS Server has been updated to version 2.9.4
|
||||||
|
* Some other minor dependencies have been updated
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* A panic has been fixed in the sync API PDU stream which could cause requests to fail
|
||||||
|
* The `/members` response now contains the `room_id` field, which may fix some E2EE problems with clients using the JS SDK (contributed by [ashkitten](https://github.com/ashkitten))
|
||||||
|
* The auth difference calculation in state resolution v2 has been tweaked for clarity (and moved into gomatrixserverlib with the rest of the state resolution code)
|
||||||
|
|
||||||
|
## Dendrite 0.10.5 (2022-10-31)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* It is now possible to use hCaptcha instead of reCAPTCHA for protecting registration
|
||||||
|
* A new `auto_join_rooms` configuration option has been added for automatically joining new users to a set of rooms
|
||||||
|
* A new `/_dendrite/admin/downloadState/{serverName}/{roomID}` endpoint has been added, which allows a server administrator to attempt to repair a room with broken room state by downloading a state snapshot from another federated server in the room
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* Querying cross-signing keys for users should now be considerably faster
|
||||||
|
* A bug in state resolution where some events were not correctly selected for third-party invites has been fixed
|
||||||
|
* A bug in state resolution which could result in `not in room` event rejections has been fixed
|
||||||
|
* When accepting a DM invite, it should now be possible to see messages that were sent before the invite was accepted
|
||||||
|
* Claiming remote E2EE one-time keys has been refactored and should be more reliable now
|
||||||
|
* Various fixes have been made to the `/members` endpoint, which may help with E2EE reliability and clients rendering memberships
|
||||||
|
* A race condition in the federation API destination queues has been fixed when associating queued events with remote server destinations
|
||||||
|
* A bug in the sync API where too many events were selected resulting in high CPU usage has been fixed
|
||||||
|
* Configuring the avatar URL for the Server Notices user should work correctly now
|
||||||
|
|
||||||
|
## Dendrite 0.10.4 (2022-10-21)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Various tables belonging to the user API will be renamed so that they are namespaced with the `userapi_` prefix
|
||||||
|
* Note that, after upgrading to this version, you should not revert to an older version of Dendrite as the database changes **will not** be reverted automatically
|
||||||
|
* The backoff and retry behaviour in the federation API has been refactored and improved
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* Private read receipt support is now advertised in the client `/versions` endpoint
|
||||||
|
* Private read receipts will now clear notification counts properly
|
||||||
|
* A bug where a false `leave` membership transition was inserted into the timeline after accepting an invite has been fixed
|
||||||
|
* Some panics caused by concurrent map writes in the key server have been fixed
|
||||||
|
* The sync API now calculates membership transitions from state deltas more accurately
|
||||||
|
* Transaction IDs are now scoped to endpoints, which should fix some bugs where transaction ID reuse could cause nonsensical cached responses from some endpoints
|
||||||
|
* The length of the `type`, `sender`, `state_key` and `room_id` fields in events are now verified by number of bytes rather than codepoints after a spec clarification, reverting a change made in Dendrite 0.9.6
|
||||||
|
|
||||||
|
## Dendrite 0.10.3 (2022-10-14)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Event relations are now tracked and support for the `/room/{roomID}/relations/...` client API endpoints have been added
|
||||||
|
* Support has been added for private read receipts
|
||||||
|
* The built-in NATS Server has been updated to version 2.9.3
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* The `unread_notifications` are now always populated in joined room responses
|
||||||
|
* The `/get_missing_events` federation API endpoint should now work correctly for rooms with `joined` and `invited` visibility settings, returning redacted events for events that other servers are not allowed to see
|
||||||
|
* The `/event` client API endpoint now applies history visibility correctly
|
||||||
|
* Read markers should now be updated much more reliably
|
||||||
|
* A rare bug in the sync API which could cause some `join` memberships to be incorrectly overwritten by other memberships when working out which rooms to populate has been fixed
|
||||||
|
* The federation API now correctly updates the joined hosts table during a state rewrite
|
||||||
|
|
||||||
|
## Dendrite 0.10.2 (2022-10-07)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Dendrite will now fail to start if there is an obvious problem with the configured `max_open_conns` when using PostgreSQL database backends, since this can lead to instability and performance issues
|
||||||
|
* More information on this is available [in the documentation](https://matrix-org.github.io/dendrite/installation/start/optimisation#postgresql-connection-limit)
|
||||||
|
* Unnecessary/empty fields will no longer be sent in `/sync` responses
|
||||||
|
* It is now possible to configure `old_private_keys` from previous Matrix installations on the same domain if only public key is known, to make it easier to expire old keys correctly
|
||||||
|
* You can configure either just the `private_key` path, or you can supply both the `public_key` and `key_id`
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* The sync transaction behaviour has been modified further so that errors in one stream should not propagate to other streams unnecessarily
|
||||||
|
* Rooms should now be classified as DM rooms correctly by passing through `is_direct` and unsigned hints
|
||||||
|
* A bug which caused marking device lists as stale to consume lots of CPU has been fixed
|
||||||
|
* Users accepting invites should no longer cause unnecessary federated joins if there are already other local users in the room
|
||||||
|
* The sync API state range queries have been optimised by adding missing indexes
|
||||||
|
* It should now be possible to configure non-English languages for full-text search in `search.language`
|
||||||
|
* The roomserver will no longer attempt to perform federated requests to the local server when trying to fetch missing events
|
||||||
|
* The `/keys/upload` endpoint will now always return the `one_time_keys_counts`, which may help with E2EE reliability
|
||||||
|
* The sync API will now retrieve the latest stream position before processing each stream rather than at the beginning of the request, to hopefully reduce the number of round-trips to `/sync`
|
||||||
|
|
||||||
|
## Dendrite 0.10.1 (2022-09-30)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* The built-in NATS Server has been updated to version 2.9.2
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* A regression introduced in 0.10.0 in `/sync` as a result of transaction errors has been fixed
|
||||||
|
* Account data updates will no longer send duplicate output events
|
||||||
|
|
||||||
|
## Dendrite 0.10.0 (2022-09-30)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* High performance full-text searching has been added to Dendrite
|
||||||
|
* Search must be enabled in the [`search` section of the `sync_api` config](https://github.com/matrix-org/dendrite/blob/6348486a1365c7469a498101f5035a9b6bd16d22/dendrite-sample.monolith.yaml#L279-L290) before it can be used
|
||||||
|
* The search index is stored on the filesystem rather than the sync API database, so a path to a suitable storage location on disk must be configured
|
||||||
|
* Sync requests should now complete faster and use considerably less database connections as a result of better transactional isolation
|
||||||
|
* The notifications code has been refactored to hopefully make notifications more reliable
|
||||||
|
* A new `/_dendrite/admin/refreshDevices/{userID}` admin endpoint has been added for forcing a refresh of a remote user's device lists without having to modify the database by hand
|
||||||
|
* A new `/_dendrite/admin/fulltext/reindex` admin endpoint has been added for rebuilding the search index (although this may take some time)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* A number of bugs in the device list updater have been fixed, which should help considerably with federated device list synchronisation and E2EE reliability
|
||||||
|
* A state resolution bug has been fixed which should help to prevent unexpected state resets
|
||||||
|
* The deprecated `"origin"` field in events will now be correctly ignored in all cases
|
||||||
|
* Room versions 8 and 9 will now correctly evaluate `"knock"` join rules and membership states
|
||||||
|
* A database index has been added to speed up finding room memberships in the sync API (contributed by [PiotrKozimor](https://github.com/PiotrKozimor))
|
||||||
|
* The client API will now return an `M_UNRECOGNIZED` error for unknown endpoints/methods, which should help with client error handling
|
||||||
|
* A bug has been fixed when updating push rules which could result in `database is locked` on SQLite
|
||||||
|
|
||||||
|
## Dendrite 0.9.9 (2022-09-22)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Dendrite will now try to keep HTTP connections open to remote federated servers for a few minutes after a request and attempt to reuse those connections where possible
|
||||||
|
* This should reduce the amount of time spent on TLS handshakes and often speed up requests to remote servers
|
||||||
|
* This new behaviour can be disabled with the `federation_api.disable_http_keepalives` option if needed
|
||||||
|
* A number of dependencies have been updated
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* A bug where the roomserver did not correctly propagate rewritten room state to downstream components (like the federation API and sync API) has been fixed, which could cause issues when performing a federated join to a previously left room
|
||||||
|
* Event auth now correctly parses the `join_authorised_via_users_server` field in the membership event content
|
||||||
|
* Database migrations should no longer produce unique constraint errors at Dendrite startup
|
||||||
|
* The `origin` of device list updates should now be populated correctly
|
||||||
|
* Send-to-device messages will no longer be dropped if we fail to publish them to specific devices
|
||||||
|
* The roomserver query to find state after events will now always resolve state if there are multiple prev events
|
||||||
|
* The roomserver will now return no memberships if querying history visibility for an event which has no state snapshot
|
||||||
|
* The device list updater will now mark a device list as stale if a requesting device ID is not known
|
||||||
|
* Transactions sent to appservices should no longer have accidental duplicated transaction IDs (contributed by [tak-hntlabs](https://github.com/tak-hntlabs))
|
||||||
|
|
||||||
|
## Dendrite 0.9.8 (2022-09-12)
|
||||||
|
|
||||||
|
### Important
|
||||||
|
|
||||||
|
* This is a **security release** to fix a vulnerability where missing events retrieved from other servers did not have their signatures verified in all cases, affecting all versions of Dendrite before 0.9.8. Upgrading to this version is highly recommended. For more information, [see here](https://github.com/matrix-org/dendrite/security/advisories/GHSA-pfw4-xjgm-267c).
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* The built-in NATS Server has been updated to the final 2.9.0 release version
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* Dendrite will now correctly verify the signatures of events retrieved using `/get_missing_events`
|
||||||
|
|
||||||
|
## Dendrite 0.9.7 (2022-09-09)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Initial supporting code to enable full-text search has been merged (although not ready for use yet)
|
||||||
|
* Newly created rooms now have higher default power levels for enabling encryption, setting server ACLs or sending tombstone events
|
||||||
|
* Incoming signing key updates over federation are now queued in JetStream for processing, so that they cannot be dropped accidentally
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* A race condition between the roomserver output events being generated, forward extremities being updated and room info being updated has been fixed
|
||||||
|
* Appservices will no longer receive invite events which they are not interested in, which caused heavy load in some cases or excessive request sizes in others
|
||||||
|
* A bug in state resolution v2 where events could incorrectly be classified as control events has been fixed
|
||||||
|
* A bug in state resolution v2 where some specific events with unexpected non-empty state keys are dropped has been fixed
|
||||||
|
* A bug in state resolution v2 when fetching auth events vs partial state has been fixed
|
||||||
|
* Stale device lists should now be handled correctly for all user IDs, which may help with E2EE reliability
|
||||||
|
* A number of database writer issues have been fixed in the user API and sync API, which should help to reduce `database is locked` errors with SQLite databases
|
||||||
|
* Database migrations should now be detected more reliably to prevent unexpected errors at startup
|
||||||
|
* A number of minor database transaction issues have been fixed, particularly for assigning NIDs in the roomserver, cleaning up device keys and cleaning up notifications
|
||||||
|
* The database query for finding shared users in the sync API has been optimised, using significantly less CPU time as a result
|
||||||
|
|
||||||
|
## Dendrite 0.9.6 (2022-09-01)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* The appservice API has been refactored for improved performance and stability
|
||||||
|
* The appservice database has been deprecated, as the roomserver output stream is now used as the data source instead
|
||||||
|
* The `generate-config` tool has been updated to support additional scenarios, i.e. for CI configuration generation and generating both monolith and polylith skeleton config files
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* The username length check has been fixed on new account creation
|
||||||
|
* The length of the `type`, `sender`, `state_key` and `room_id` fields in events are now verified by number of codepoints rather than bytes, fixing the "Cat Overflow" bug
|
||||||
|
* UTF-16 surrogate handling in the canonical JSON implementation has been fixed
|
||||||
|
* A race condition when starting the keyserver has been fixed
|
||||||
|
* A race condition when configuring HTTP servers and routing at startup has been fixed
|
||||||
|
* A bug where the incorrect limit was used for lazy-loading memberships has been fixed
|
||||||
|
* The number of push notifications will now be sent to the push gateway
|
||||||
|
* A missing index causing slow performance on the sync API send-to-device table has been added (contributed by [PiotrKozimor](https://github.com/PiotrKozimor))
|
||||||
|
* Event auth will now correctly check for the existence of the `"creator"` field in create events
|
||||||
|
|
||||||
|
## Dendrite 0.9.5 (2022-08-25)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* The roomserver will now correctly unreject previously rejected events if necessary when reprocessing
|
||||||
|
* The handling of event soft-failure has been improved on the roomserver input by no longer applying rejection rules and still calculating state before the event if possible
|
||||||
|
* The federation `/state` and `/state_ids` endpoints should now return the correct error code when the state isn't known instead of returning a HTTP 500
|
||||||
|
* The federation `/event` should now return outlier events correctly instead of returning a HTTP 500
|
||||||
|
* A bug in the federation backoff allowing zero intervals has been corrected
|
||||||
|
* The `create-account` utility will no longer error if the homeserver URL ends in a trailing slash
|
||||||
|
* A regression in `/sync` introduced in 0.9.4 should be fixed
|
||||||
|
|
||||||
|
## Dendrite 0.9.4 (2022-08-19)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* A bug in the roomserver around handling rejected outliers has been fixed
|
||||||
|
* Backfilled events will now use the correct history visibility where possible
|
||||||
|
* The device list updater backoff has been fixed, which should reduce the number of outbound HTTP requests and `Failed to query device keys for some users` log entries for dead servers
|
||||||
|
* The `/sync` endpoint will no longer incorrectly return room entries for retired invites which could cause some rooms to show up in the client "Historical" section
|
||||||
|
* The `/createRoom` endpoint will now correctly populate `is_direct` in invite membership events, which may help clients to classify direct messages correctly
|
||||||
|
* The `create-account` tool will now log an error if the shared secret is not set in the Dendrite config
|
||||||
|
* A couple of minor bugs have been fixed in the membership lazy-loading
|
||||||
|
* Queued EDUs in the federation API are now cached properly
|
||||||
|
|
||||||
|
## Dendrite 0.9.3 (2022-08-15)
|
||||||
|
|
||||||
|
### Important
|
||||||
|
|
||||||
|
* This is a **security release** to fix a vulnerability within event auth, affecting all versions of Dendrite before 0.9.3. Upgrading to this version is highly recommended. For more information, [see here](https://github.com/matrix-org/gomatrixserverlib/security/advisories/GHSA-grvv-h2f9-7v9c).
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* Dendrite will now correctly parse the `"events_default"` power level value for event auth.
|
||||||
|
|
||||||
|
## Dendrite 0.9.2 (2022-08-12)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Dendrite now supports history visibility on the `/sync`, `/messages` and `/context` endpoints
|
||||||
|
* It should now be possible to view the history of a room in more cases (as opposed to limiting scrollback to the join event or defaulting to the restrictive `"join"` visibility rule as before)
|
||||||
|
* The default room version for newly created rooms is now room version 9
|
||||||
|
* New admin endpoint `/_dendrite/admin/resetPassword/{userID}` has been added, which replaces the `-reset-password` flag in `create-account`
|
||||||
|
* The `create-account` binary now uses shared secret registration over HTTP to create new accounts, which fixes a number of problems with account data and push rules not being configured correctly for new accounts
|
||||||
|
* The internal HTTP APIs for polylith deployments have been refactored for correctness and consistency
|
||||||
|
* The federation API will now automatically clean up some EDUs that have failed to send within a certain period of time
|
||||||
|
* The `/hierarchy` endpoint will now return potentially joinable rooms (contributed by [texuf](https://github.com/texuf))
|
||||||
|
* The user directory will now show or hide users correctly
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* Send-to-device messages should no longer be incorrectly duplicated in `/sync`
|
||||||
|
* The federation sender will no longer create unnecessary destination queues as a result of a logic error
|
||||||
|
* A bug where database migrations may not execute properly when upgrading from older versions has been fixed
|
||||||
|
* A crash when failing to update user account data has been fixed
|
||||||
|
* A race condition when generating notification counts has been fixed
|
||||||
|
* A race condition when setting up NATS has been fixed (contributed by [brianathere](https://github.com/brianathere))
|
||||||
|
* Stale cache data for membership lazy-loading is now correctly invalidated when doing a complete sync
|
||||||
|
* Data races within user-interactive authentication have been fixed (contributed by [tak-hntlabs](https://github.com/tak-hntlabs))
|
||||||
|
|
||||||
|
## Dendrite 0.9.1 (2022-08-03)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* Upgrades a dependency which caused issues building Dendrite with Go 1.19
|
||||||
|
* The roomserver will no longer give up prematurely after failing to call `/state_ids`
|
||||||
|
* Removes the faulty room info cache, which caused of a number of race conditions and occasional bugs (including when creating and joining rooms)
|
||||||
|
* The media endpoint now sets the `Cache-Control` header correctly to prevent web-based clients from hitting media endpoints excessively
|
||||||
|
* The sync API will now advance the PDU stream position correctly in all cases (contributed by [sergekh2](https://github.com/sergekh2))
|
||||||
|
* The sync API will now delete the correct range of send-to-device messages when advancing the stream position
|
||||||
|
* The device list `changed` key in the `/sync` response should now return the correct users
|
||||||
|
* A data race when looking up missing state has been fixed
|
||||||
|
* The `/send_join` API is now applying stronger validation to the received membership event
|
||||||
|
|
||||||
|
## Dendrite 0.9.0 (2022-08-01)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Dendrite now uses Ristretto for managing in-memory caches
|
||||||
|
* Should improve cache utilisation considerably over time by more intelligently selecting and managing cache entries compared to the previous LRU-based cache
|
||||||
|
* Defaults to a 1GB cache size if not configured otherwise
|
||||||
|
* The estimated cache size in memory and maximum age can now be configured with new [configuration options](https://github.com/matrix-org/dendrite/blob/e94ef84aaba30e12baf7f524c4e7a36d2fdeb189/dendrite-sample.monolith.yaml#L44-L61) to prevent unbounded cache growth
|
||||||
|
* Added support for serving the `/.well-known/matrix/client` hint directly from Dendrite
|
||||||
|
* Configurable with the new [configuration option](https://github.com/matrix-org/dendrite/blob/e94ef84aaba30e12baf7f524c4e7a36d2fdeb189/dendrite-sample.monolith.yaml#L67-L69)
|
||||||
|
* Refactored membership updater, which should eliminate some bugs caused by the membership table getting out of sync with the room state
|
||||||
|
* The User API is now responsible for sending account data updates to other components, which may fix some races and duplicate account data events
|
||||||
|
* Optimised database query for checking whether a remote server is allowed to request an event over federation without using anywhere near as much CPU time (PostgreSQL only)
|
||||||
|
* Database migrations have been refactored to eliminate some problems that were present with `goose` and upgrading from older Dendrite versions
|
||||||
|
* Media fetching will now use the `/v3` endpoints for downloading media from remote homeservers
|
||||||
|
* HTTP 404 and HTTP 405 errors from the client-facing APIs should now be returned with CORS headers so that web-based clients do not produce incorrect access control warnings for unknown endpoints
|
||||||
|
* Some preparation work for full history visibility support
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* Fixes a crash that could occur during event redaction
|
||||||
|
* The `/members` endpoint will no longer incorrectly return HTTP 500 as a result of some invite events
|
||||||
|
* Send-to-device messages should now be ordered more reliably and the last position in the stream updated correctly
|
||||||
|
* Parsing of appservice configuration files is now less strict (contributed by [Kab1r](https://github.com/Kab1r))
|
||||||
|
* The sync API should now identify shared users correctly when waking up for E2EE key changes
|
||||||
|
* The federation `/state` endpoint will now return a HTTP 403 when the state before an event isn't known instead of a HTTP 500
|
||||||
|
* Presence timestamps should now be calculated with the correct precision
|
||||||
|
* A race condition in the roomserver's room info has been fixed
|
||||||
|
* A race condition in the sync API has been fixed
|
||||||
|
|
||||||
|
## Dendrite 0.8.9 (2022-07-01)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Incoming device list updates over federation are now queued in JetStream for processing so that they will no longer block incoming federation transactions and should never end up dropped, which will hopefully help E2EE reliability
|
||||||
|
* The `/context` endpoint now returns `"start"` and `"end"` parameters to allow pagination from a context call
|
||||||
|
* The `/messages` endpoint will no longer return `"end"` when there are no more messages remaining
|
||||||
|
* Deactivated user accounts will now leave all rooms automatically
|
||||||
|
* New admin endpoint `/_dendrite/admin/evacuateUser/{userID}` has been added for forcing a local user to leave all joined rooms
|
||||||
|
* Dendrite will now automatically attempt to raise the file descriptor limit at startup if it is too low
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* A rare crash when retrieving remote device lists has been fixed
|
||||||
|
* Fixes a bug where events were not redacted properly over federation
|
||||||
|
* The `/invite` endpoints will now return an error instead of silently proceeding if the user ID is obviously malformed
|
||||||
|
|
||||||
|
## Dendrite 0.8.8 (2022-06-09)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* The performance of state resolution has been increased significantly for larger rooms
|
||||||
|
* A number of changes have been made to rate limiting:
|
||||||
|
* Logged in users will now be rate-limited on a per-session basis rather than by remote IP
|
||||||
|
* Rate limiting no longer applies to admin or appservice users
|
||||||
|
* It is now possible to configure additional users that are exempt from rate limiting using the `exempt_user_ids` option in the `rate_limiting` section of the Dendrite config
|
||||||
|
* Setting state is now idempotent via the client API state endpoints
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* Room upgrades now properly propagate tombstone events to remote servers
|
||||||
|
* Room upgrades will no longer send tombstone events if creating the upgraded room fails
|
||||||
|
* A crash has been fixed when evaluating restricted room joins
|
||||||
|
|
||||||
|
## Dendrite 0.8.7 (2022-06-01)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Support added for room version 10
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* A number of state handling bugs have been fixed, which previously resulted in missing state events, unexpected state deletions, reverted memberships and unexpectedly rejected/soft-failed events in some specific cases
|
||||||
|
* Fixed destination queue performance issues as a result of missing indexes, which speeds up outbound federation considerably
|
||||||
|
* A bug which could cause the `/register` endpoint to return HTTP 500 has been fixed
|
||||||
|
|
||||||
|
## Dendrite 0.8.6 (2022-05-26)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Room versions 8 and 9 are now marked as stable
|
||||||
|
* Dendrite can now assist remote users to join restricted rooms via `/make_join` and `/send_join`
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* The sync API no longer returns immediately on `/sync` requests unnecessarily if it can be avoided
|
||||||
|
* A race condition has been fixed in the sync API when updating presence via `/sync`
|
||||||
|
* A race condition has been fixed sending E2EE keys to remote servers over federation when joining rooms
|
||||||
|
* The `trusted_private_chat` preset should now grant power level 100 to all participant users, which should improve the user experience of direct messages
|
||||||
|
* Invited users are now authed correctly in restricted rooms
|
||||||
|
* The `join_authorised_by_users_server` key is now correctly stripped in restricted rooms when updating the membership event
|
||||||
|
* Appservices should now receive invite events correctly
|
||||||
|
* Device list updates should no longer contain optional fields with `null` values
|
||||||
|
* The `/deactivate` endpoint has been fixed to no longer confuse Element with incorrect completed flows
|
||||||
|
|
||||||
|
## Dendrite 0.8.5 (2022-05-13)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* New living documentation available at <https://matrix-org.github.io/dendrite/>, including new installation instructions
|
||||||
|
* The built-in NATS Server has been updated to version 2.8.2
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* Monolith deployments will no longer panic at startup if given a config file that does not include the `internal_api` and `external_api` options
|
||||||
|
* State resolution v2 now correctly identifies other events related to power events, which should fix some event auth issues
|
||||||
|
* The latest events updater will no longer implicitly trust the new forward extremities when calculating the current room state, which may help to avoid some state resets
|
||||||
|
* The one-time key count is now correctly returned in `/sync` even if the request otherwise timed out, which should reduce the chance that unnecessary one-time keys will be uploaded by clients
|
||||||
|
* The `create-account` tool should now work properly when the database is configured using the global connection pool
|
||||||
|
|
||||||
|
## Dendrite 0.8.4 (2022-05-10)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* Fixes a regression introduced in the previous version where appservices, push and phone-home statistics would not work over plain HTTP
|
||||||
|
* Adds missing indexes to the sync API output events table, which should significantly improve `/sync` performance and reduce database CPU usage
|
||||||
|
* Building Dendrite with the `bimg` thumbnailer should now work again (contributed by [database64128](https://github.com/database64128))
|
||||||
|
|
||||||
|
## Dendrite 0.8.3 (2022-05-09)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Open registration is now harder to enable, which should reduce the chance that Dendrite servers will be used to conduct spam or abuse attacks
|
||||||
|
* Dendrite will only enable open registration if you pass the `--really-enable-open-registration` command line flag at startup
|
||||||
|
* If open registration is enabled but this command line flag is not passed, Dendrite will fail to start up
|
||||||
|
* Dendrite now supports phone-home statistic reporting
|
||||||
|
* These statistics include things like the number of registered and active users, some configuration options and platform/environment details, to help us to understand how Dendrite is used
|
||||||
|
* This is not enabled by default — it must be enabled in the `global.report_stats` section of the config file
|
||||||
|
* Monolith installations can now be configured with a single global database connection pool (in `global.database` in the config) rather than having to configure each component separately
|
||||||
|
* This also means that you no longer need to balance connection counts between different components, as they will share the same larger pool
|
||||||
|
* Specific components can override the global database settings by specifying their own `database` block
|
||||||
|
* To use only the global pool, you must configure `global.database` and then remove the `database` block from all of the component sections of the config file
|
||||||
|
* A new admin API endpoint `/_dendrite/admin/evacuateRoom/{roomID}` has been added, allowing server admins to forcefully part all local users from a given room
|
||||||
|
* The sync notifier now only loads members for the relevant rooms, which should reduce CPU usage and load on the database
|
||||||
|
* A number of component interfaces have been refactored for cleanliness and developer ease
|
||||||
|
* Event auth errors in the log should now be much more useful, including the reason for the event failures
|
||||||
|
* The forward extremity calculation in the roomserver has been simplified
|
||||||
|
* A new index has been added to the one-time keys table in the keyserver which should speed up key count lookups
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* Dendrite will no longer process events for rooms where there are no local users joined, which should help to reduce CPU and RAM usage
|
||||||
|
* A bug has been fixed in event auth when changing the user levels in `m.room.power_levels` events
|
||||||
|
* Usernames should no longer be duplicated when no room name is set
|
||||||
|
* Device display names should now be correctly propagated over federation
|
||||||
|
* A panic when uploading cross-signing signatures has been fixed
|
||||||
|
* Presence is now correctly limited in `/sync` based on the filters
|
||||||
|
* The presence stream position returned by `/sync` will now be correct if no presence events were returned
|
||||||
|
* The media `/config` endpoint will no longer return a maximum upload size field if it is configured to be unlimited in the Dendrite config
|
||||||
|
* The server notices room will no longer produce "User is already joined to the room" errors
|
||||||
|
* Consumer errors will no longer flood the logs during a graceful shutdown
|
||||||
|
* Sync API and federation API consumers will no longer unnecessarily query added state events matching the one in the output event
|
||||||
|
* The Sync API will no longer unnecessarily track invites for remote users
|
||||||
|
|
||||||
|
## Dendrite 0.8.2 (2022-04-27)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Lazy-loading has been added to the `/sync` endpoint, which should speed up syncs considerably
|
||||||
|
* Filtering has been added to the `/messages` endpoint
|
||||||
|
* The room summary now contains "heroes" (up to 5 users in the room) for clients to display when no room name is set
|
||||||
|
* The existing lazy-loading caches will now be used by `/messages` and `/context` so that member events will not be sent to clients more times than necessary
|
||||||
|
* The account data stream now uses the provided filters
|
||||||
|
* The built-in NATS Server has been updated to version 2.8.0
|
||||||
|
* The `/state` and `/state_ids` endpoints will now return `M_NOT_FOUND` for rejected events
|
||||||
|
* Repeated calls to the `/redact` endpoint will now be idempotent when a transaction ID is given
|
||||||
|
* Dendrite should now be able to run as a Windows service under Service Control Manager
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* Fictitious presence updates will no longer be created for users which have not sent us presence updates, which should speed up complete syncs considerably
|
||||||
|
* Uploading cross-signing device signatures should now be more reliable, fixing a number of bugs with cross-signing
|
||||||
|
* All account data should now be sent properly on a complete sync, which should eliminate problems with client settings or key backups appearing to be missing
|
||||||
|
* Account data will now be limited correctly on incremental syncs, returning the stream position of the most recent update rather than the latest stream position
|
||||||
|
* Account data will not be sent for parted rooms, which should reduce the number of left/forgotten rooms reappearing in clients as empty rooms
|
||||||
|
* The TURN username hash has been fixed which should help to resolve some problems when using TURN for voice calls (contributed by [fcwoknhenuxdfiyv](https://github.com/fcwoknhenuxdfiyv))
|
||||||
|
* Push rules can no longer be modified using the account data endpoints
|
||||||
|
* Querying account availability should now work properly in polylith deployments
|
||||||
|
* A number of bugs with sync filters have been fixed
|
||||||
|
* A default sync filter will now be used if the request contains a filter ID that does not exist
|
||||||
|
* The `pushkey_ts` field is now using seconds instead of milliseconds
|
||||||
|
* A race condition when gracefully shutting down has been fixed, so JetStream should no longer cause the process to exit before other Dendrite components are finished shutting down
|
||||||
|
|
||||||
## Dendrite 0.8.1 (2022-04-07)
|
## Dendrite 0.8.1 (2022-04-07)
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
49
Dockerfile
Normal file
49
Dockerfile
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
#syntax=docker/dockerfile:1.2
|
||||||
|
|
||||||
|
#
|
||||||
|
# base installs required dependencies and runs go mod download to cache dependencies
|
||||||
|
#
|
||||||
|
# Pinned to alpine3.18 until https://github.com/mattn/go-sqlite3/issues/1164 is solved
|
||||||
|
FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21-alpine3.18 AS base
|
||||||
|
RUN apk --update --no-cache add bash build-base curl git
|
||||||
|
|
||||||
|
#
|
||||||
|
# build creates all needed binaries
|
||||||
|
#
|
||||||
|
FROM --platform=${BUILDPLATFORM} base AS build
|
||||||
|
WORKDIR /src
|
||||||
|
ARG TARGETOS
|
||||||
|
ARG TARGETARCH
|
||||||
|
RUN --mount=target=. \
|
||||||
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
--mount=type=cache,target=/go/pkg/mod \
|
||||||
|
USERARCH=`go env GOARCH` \
|
||||||
|
GOARCH="$TARGETARCH" \
|
||||||
|
GOOS="linux" \
|
||||||
|
CGO_ENABLED=$([ "$TARGETARCH" = "$USERARCH" ] && echo "1" || echo "0") \
|
||||||
|
go build -v -trimpath -o /out/ ./cmd/...
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Builds the Dendrite image containing all required binaries
|
||||||
|
#
|
||||||
|
FROM alpine:latest
|
||||||
|
RUN apk --update --no-cache add curl
|
||||||
|
LABEL org.opencontainers.image.title="Dendrite"
|
||||||
|
LABEL org.opencontainers.image.description="Next-generation Matrix homeserver written in Go"
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/matrix-org/dendrite"
|
||||||
|
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
||||||
|
LABEL org.opencontainers.image.documentation="https://matrix-org.github.io/dendrite/"
|
||||||
|
LABEL org.opencontainers.image.vendor="The Matrix.org Foundation C.I.C."
|
||||||
|
|
||||||
|
COPY --from=build /out/create-account /usr/bin/create-account
|
||||||
|
COPY --from=build /out/generate-config /usr/bin/generate-config
|
||||||
|
COPY --from=build /out/generate-keys /usr/bin/generate-keys
|
||||||
|
COPY --from=build /out/dendrite /usr/bin/dendrite
|
||||||
|
|
||||||
|
VOLUME /etc/dendrite
|
||||||
|
WORKDIR /etc/dendrite
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/dendrite"]
|
||||||
|
EXPOSE 8008 8448
|
||||||
|
|
100
README.md
100
README.md
|
@ -1,4 +1,5 @@
|
||||||
# Dendrite
|
# Dendrite
|
||||||
|
|
||||||
[![Build status](https://github.com/matrix-org/dendrite/actions/workflows/dendrite.yml/badge.svg?event=push)](https://github.com/matrix-org/dendrite/actions/workflows/dendrite.yml) [![Dendrite](https://img.shields.io/matrix/dendrite:matrix.org.svg?label=%23dendrite%3Amatrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#dendrite:matrix.org) [![Dendrite Dev](https://img.shields.io/matrix/dendrite-dev:matrix.org.svg?label=%23dendrite-dev%3Amatrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#dendrite-dev:matrix.org)
|
[![Build status](https://github.com/matrix-org/dendrite/actions/workflows/dendrite.yml/badge.svg?event=push)](https://github.com/matrix-org/dendrite/actions/workflows/dendrite.yml) [![Dendrite](https://img.shields.io/matrix/dendrite:matrix.org.svg?label=%23dendrite%3Amatrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#dendrite:matrix.org) [![Dendrite Dev](https://img.shields.io/matrix/dendrite-dev:matrix.org.svg?label=%23dendrite-dev%3Amatrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#dendrite-dev:matrix.org)
|
||||||
|
|
||||||
Dendrite is a second-generation Matrix homeserver written in Go.
|
Dendrite is a second-generation Matrix homeserver written in Go.
|
||||||
|
@ -6,26 +7,23 @@ It intends to provide an **efficient**, **reliable** and **scalable** alternativ
|
||||||
|
|
||||||
- Efficient: A small memory footprint with better baseline performance than an out-of-the-box Synapse.
|
- Efficient: A small memory footprint with better baseline performance than an out-of-the-box Synapse.
|
||||||
- Reliable: Implements the Matrix specification as written, using the
|
- Reliable: Implements the Matrix specification as written, using the
|
||||||
[same test suite](https://github.com/matrix-org/sytest) as Synapse as well as
|
[same test suite](https://github.com/matrix-org/sytest) as Synapse as well as
|
||||||
a [brand new Go test suite](https://github.com/matrix-org/complement).
|
a [brand new Go test suite](https://github.com/matrix-org/complement).
|
||||||
- Scalable: can run on multiple machines and eventually scale to massive homeserver deployments.
|
- Scalable: can run on multiple machines and eventually scale to massive homeserver deployments.
|
||||||
|
|
||||||
As of October 2020, Dendrite has now entered **beta** which means:
|
Dendrite is **beta** software, which means:
|
||||||
|
|
||||||
- Dendrite is ready for early adopters. We recommend running in Monolith mode with a PostgreSQL database.
|
- Dendrite is ready for early adopters. We recommend running Dendrite with a PostgreSQL database.
|
||||||
- Dendrite has periodic semver releases. We intend to release new versions as we land significant features.
|
- Dendrite has periodic releases. We intend to release new versions as we fix bugs and land significant features.
|
||||||
- Dendrite supports database schema upgrades between releases. This means you should never lose your messages when upgrading Dendrite.
|
- Dendrite supports database schema upgrades between releases. This means you should never lose your messages when upgrading Dendrite.
|
||||||
- Breaking changes will not occur on minor releases. This means you can safely upgrade Dendrite without modifying your database or config file.
|
|
||||||
|
|
||||||
This does not mean:
|
This does not mean:
|
||||||
|
|
||||||
- Dendrite is bug-free. It has not yet been battle-tested in the real world and so will be error prone initially.
|
- Dendrite is bug-free. It has not yet been battle-tested in the real world and so will be error prone initially.
|
||||||
- All of the CS/Federation APIs are implemented. We are tracking progress via a script called 'Are We Synapse Yet?'. In particular,
|
- Dendrite is feature-complete. There may be client or federation APIs that are not implemented.
|
||||||
presence and push notifications are entirely missing from Dendrite. See [CHANGES.md](CHANGES.md) for updates.
|
- Dendrite is ready for massive homeserver deployments. There is no high-availability/clustering support.
|
||||||
- Dendrite is ready for massive homeserver deployments. You cannot shard each microservice, only run each one on a different machine.
|
|
||||||
|
|
||||||
Currently, we expect Dendrite to function well for small (10s/100s of users) homeserver deployments as well as P2P Matrix nodes in-browser or on mobile devices.
|
Currently, we expect Dendrite to function well for small (10s/100s of users) homeserver deployments as well as P2P Matrix nodes in-browser or on mobile devices.
|
||||||
In the future, we will be able to scale up to gigantic servers (equivalent to matrix.org) via polylith mode.
|
|
||||||
|
|
||||||
If you have further questions, please take a look at [our FAQ](docs/FAQ.md) or join us in:
|
If you have further questions, please take a look at [our FAQ](docs/FAQ.md) or join us in:
|
||||||
|
|
||||||
|
@ -35,7 +33,10 @@ If you have further questions, please take a look at [our FAQ](docs/FAQ.md) or j
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
To build Dendrite, you will need Go 1.16 or later.
|
See the [Planning your Installation](https://matrix-org.github.io/dendrite/installation/planning) page for
|
||||||
|
more information on requirements.
|
||||||
|
|
||||||
|
To build Dendrite, you will need Go 1.20 or later.
|
||||||
|
|
||||||
For a usable federating Dendrite deployment, you will also need:
|
For a usable federating Dendrite deployment, you will also need:
|
||||||
|
|
||||||
|
@ -46,20 +47,20 @@ For a usable federating Dendrite deployment, you will also need:
|
||||||
Also recommended are:
|
Also recommended are:
|
||||||
|
|
||||||
- A PostgreSQL database engine, which will perform better than SQLite with many users and/or larger rooms
|
- A PostgreSQL database engine, which will perform better than SQLite with many users and/or larger rooms
|
||||||
- A reverse proxy server, such as nginx, configured [like this sample](https://github.com/matrix-org/dendrite/blob/master/docs/nginx/monolith-sample.conf)
|
- A reverse proxy server, such as nginx, configured [like this sample](https://github.com/matrix-org/dendrite/blob/main/docs/nginx/dendrite-sample.conf)
|
||||||
|
|
||||||
The [Federation Tester](https://federationtester.matrix.org) can be used to verify your deployment.
|
The [Federation Tester](https://federationtester.matrix.org) can be used to verify your deployment.
|
||||||
|
|
||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
If you wish to build a fully-federating Dendrite instance, see [INSTALL.md](docs/INSTALL.md). For running in Docker, see [build/docker](build/docker).
|
If you wish to build a fully-federating Dendrite instance, see [the Installation documentation](https://matrix-org.github.io/dendrite/installation). For running in Docker, see [build/docker](build/docker).
|
||||||
|
|
||||||
The following instructions are enough to get Dendrite started as a non-federating test deployment using self-signed certificates and SQLite databases:
|
The following instructions are enough to get Dendrite started as a non-federating test deployment using self-signed certificates and SQLite databases:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ git clone https://github.com/matrix-org/dendrite
|
$ git clone https://github.com/matrix-org/dendrite
|
||||||
$ cd dendrite
|
$ cd dendrite
|
||||||
$ ./build.sh
|
$ go build -o bin/ ./cmd/...
|
||||||
|
|
||||||
# Generate a Matrix signing key for federation (required)
|
# Generate a Matrix signing key for federation (required)
|
||||||
$ ./bin/generate-keys --private-key matrix_key.pem
|
$ ./bin/generate-keys --private-key matrix_key.pem
|
||||||
|
@ -70,31 +71,34 @@ $ ./bin/generate-keys --tls-cert server.crt --tls-key server.key
|
||||||
|
|
||||||
# Copy and modify the config file - you'll need to set a server name and paths to the keys
|
# Copy and modify the config file - you'll need to set a server name and paths to the keys
|
||||||
# at the very least, along with setting up the database connection strings.
|
# at the very least, along with setting up the database connection strings.
|
||||||
$ cp dendrite-config.yaml dendrite.yaml
|
$ cp dendrite-sample.yaml dendrite.yaml
|
||||||
|
|
||||||
# Build and run the server:
|
# Build and run the server:
|
||||||
$ ./bin/dendrite-monolith-server --tls-cert server.crt --tls-key server.key --config dendrite.yaml
|
$ ./bin/dendrite --tls-cert server.crt --tls-key server.key --config dendrite.yaml
|
||||||
|
|
||||||
|
# Create an user account (add -admin for an admin user).
|
||||||
|
# Specify the localpart only, e.g. 'alice' for '@alice:domain.com'
|
||||||
|
$ ./bin/create-account --config dendrite.yaml --username alice
|
||||||
```
|
```
|
||||||
|
|
||||||
Then point your favourite Matrix client at `http://localhost:8008` or `https://localhost:8448`.
|
Then point your favourite Matrix client at `http://localhost:8008` or `https://localhost:8448`.
|
||||||
|
|
||||||
## Progress
|
## Progress
|
||||||
|
|
||||||
We use a script called Are We Synapse Yet which checks Sytest compliance rates. Sytest is a black-box homeserver
|
We use a script called "Are We Synapse Yet" which checks Sytest compliance rates. Sytest is a black-box homeserver
|
||||||
test rig with around 900 tests. The script works out how many of these tests are passing on Dendrite and it
|
test rig with around 900 tests. The script works out how many of these tests are passing on Dendrite and it
|
||||||
updates with CI. As of April 2022 we're at around 83% CS API coverage and 95% Federation coverage, though check
|
updates with CI. As of January 2023, we have 100% server-server parity with Synapse, and the client-server parity is at 93% , though check
|
||||||
CI for the latest numbers. In practice, this means you can communicate locally and via federation with Synapse
|
CI for the latest numbers. In practice, this means you can communicate locally and via federation with Synapse
|
||||||
servers such as matrix.org reasonably well, although there are still some missing features (like Search).
|
servers such as matrix.org reasonably well, although there are still some missing features (like SSO and Third-party ID APIs).
|
||||||
|
|
||||||
We are prioritising features that will benefit single-user homeservers first (e.g Receipts, E2E) rather
|
We are prioritising features that will benefit single-user homeservers first (e.g Receipts, E2E) rather
|
||||||
than features that massive deployments may be interested in (User Directory, OpenID, Guests, Admin APIs, AS API).
|
than features that massive deployments may be interested in (OpenID, Guests, Admin APIs, AS API).
|
||||||
This means Dendrite supports amongst others:
|
This means Dendrite supports amongst others:
|
||||||
|
|
||||||
- Core room functionality (creating rooms, invites, auth rules)
|
- Core room functionality (creating rooms, invites, auth rules)
|
||||||
- Full support for room versions 1 to 7
|
- Room versions 1 to 10 supported
|
||||||
- Experimental support for room versions 8 to 9
|
|
||||||
- Backfilling locally and via federation
|
- Backfilling locally and via federation
|
||||||
- Accounts, Profiles and Devices
|
- Accounts, profiles and devices
|
||||||
- Published room lists
|
- Published room lists
|
||||||
- Typing
|
- Typing
|
||||||
- Media APIs
|
- Media APIs
|
||||||
|
@ -107,6 +111,7 @@ This means Dendrite supports amongst others:
|
||||||
- Guests
|
- Guests
|
||||||
- User Directory
|
- User Directory
|
||||||
- Presence
|
- Presence
|
||||||
|
- Fulltext search
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
@ -115,53 +120,8 @@ We would be grateful for any help on issues marked as
|
||||||
all have related Sytests which need to pass in order for the issue to be closed. Once you've written your
|
all have related Sytests which need to pass in order for the issue to be closed. Once you've written your
|
||||||
code, you can quickly run Sytest to ensure that the test names are now passing.
|
code, you can quickly run Sytest to ensure that the test names are now passing.
|
||||||
|
|
||||||
For example, if the test `Local device key changes get to remote servers` was marked as failing, find the
|
If you're new to the project, see our
|
||||||
test file (e.g via `grep` or via the
|
[Contributing page](https://matrix-org.github.io/dendrite/development/contributing) to get up to speed, then
|
||||||
[CI log output](https://buildkite.com/matrix-dot-org/dendrite/builds/2826#39cff5de-e032-4ad0-ad26-f819e6919c42)
|
|
||||||
it's `tests/50federation/40devicelists.pl` ) then to run Sytest:
|
|
||||||
|
|
||||||
```
|
|
||||||
docker run --rm --name sytest
|
|
||||||
-v "/Users/kegan/github/sytest:/sytest"
|
|
||||||
-v "/Users/kegan/github/dendrite:/src"
|
|
||||||
-v "/Users/kegan/logs:/logs"
|
|
||||||
-v "/Users/kegan/go/:/gopath"
|
|
||||||
-e "POSTGRES=1" -e "DENDRITE_TRACE_HTTP=1"
|
|
||||||
matrixdotorg/sytest-dendrite:latest tests/50federation/40devicelists.pl
|
|
||||||
```
|
|
||||||
|
|
||||||
See [sytest.md](docs/sytest.md) for the full description of these flags.
|
|
||||||
|
|
||||||
You can try running sytest outside of docker for faster runs, but the dependencies can be temperamental
|
|
||||||
and we recommend using docker where possible.
|
|
||||||
|
|
||||||
```
|
|
||||||
cd sytest
|
|
||||||
export PERL5LIB=$HOME/lib/perl5
|
|
||||||
export PERL_MB_OPT=--install_base=$HOME
|
|
||||||
export PERL_MM_OPT=INSTALL_BASE=$HOME
|
|
||||||
./install-deps.pl
|
|
||||||
|
|
||||||
./run-tests.pl -I Dendrite::Monolith -d $PATH_TO_DENDRITE_BINARIES
|
|
||||||
```
|
|
||||||
|
|
||||||
Sometimes Sytest is testing the wrong thing or is flakey, so it will need to be patched.
|
|
||||||
Ask on `#dendrite-dev:matrix.org` if you think this is the case for you and we'll be happy to help.
|
|
||||||
|
|
||||||
If you're new to the project, see [CONTRIBUTING.md](docs/CONTRIBUTING.md) to get up to speed then
|
|
||||||
look for [Good First Issues](https://github.com/matrix-org/dendrite/labels/good%20first%20issue). If you're
|
look for [Good First Issues](https://github.com/matrix-org/dendrite/labels/good%20first%20issue). If you're
|
||||||
familiar with the project, look for [Help Wanted](https://github.com/matrix-org/dendrite/labels/help-wanted)
|
familiar with the project, look for [Help Wanted](https://github.com/matrix-org/dendrite/labels/help-wanted)
|
||||||
issues.
|
issues.
|
||||||
|
|
||||||
## Hardware requirements
|
|
||||||
|
|
||||||
Dendrite in Monolith + SQLite works in a range of environments including iOS and in-browser via WASM.
|
|
||||||
|
|
||||||
For small homeserver installations joined on ~10s rooms on matrix.org with ~100s of users in those rooms, including some
|
|
||||||
encrypted rooms:
|
|
||||||
|
|
||||||
- Memory: uses around 100MB of RAM, with peaks at around 200MB.
|
|
||||||
- Disk space: After a few months of usage, the database grew to around 2GB (in Monolith mode).
|
|
||||||
- CPU: Brief spikes when processing events, typically idles at 1% CPU.
|
|
||||||
|
|
||||||
This means Dendrite should comfortably work on things like Raspberry Pis.
|
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
# Application Service
|
|
||||||
|
|
||||||
This component interfaces with external [Application
|
|
||||||
Services](https://matrix.org/docs/spec/application_service/unstable.html).
|
|
||||||
This includes any HTTP endpoints that application services call, as well as talking
|
|
||||||
to any HTTP endpoints that application services provide themselves.
|
|
||||||
|
|
||||||
## Consumers
|
|
||||||
|
|
||||||
This component consumes and filters events from the Roomserver Kafka stream, passing on any necessary events to subscribing application services.
|
|
|
@ -19,13 +19,34 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AppServiceInternalAPI is used to query user and room alias data from application
|
||||||
|
// services
|
||||||
|
type AppServiceInternalAPI interface {
|
||||||
|
// Check whether a room alias exists within any application service namespaces
|
||||||
|
RoomAliasExists(
|
||||||
|
ctx context.Context,
|
||||||
|
req *RoomAliasExistsRequest,
|
||||||
|
resp *RoomAliasExistsResponse,
|
||||||
|
) error
|
||||||
|
// Check whether a user ID exists within any application service namespaces
|
||||||
|
UserIDExists(
|
||||||
|
ctx context.Context,
|
||||||
|
req *UserIDExistsRequest,
|
||||||
|
resp *UserIDExistsResponse,
|
||||||
|
) error
|
||||||
|
|
||||||
|
Locations(ctx context.Context, req *LocationRequest, resp *LocationResponse) error
|
||||||
|
User(ctx context.Context, request *UserRequest, response *UserResponse) error
|
||||||
|
Protocols(ctx context.Context, req *ProtocolRequest, resp *ProtocolResponse) error
|
||||||
|
}
|
||||||
|
|
||||||
// RoomAliasExistsRequest is a request to an application service
|
// RoomAliasExistsRequest is a request to an application service
|
||||||
// about whether a room alias exists
|
// about whether a room alias exists
|
||||||
type RoomAliasExistsRequest struct {
|
type RoomAliasExistsRequest struct {
|
||||||
|
@ -60,49 +81,97 @@ type UserIDExistsResponse struct {
|
||||||
UserIDExists bool `json:"exists"`
|
UserIDExists bool `json:"exists"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppServiceQueryAPI is used to query user and room alias data from application
|
const (
|
||||||
// services
|
ASProtocolLegacyPath = "/_matrix/app/unstable/thirdparty/protocol/"
|
||||||
type AppServiceQueryAPI interface {
|
ASUserLegacyPath = "/_matrix/app/unstable/thirdparty/user"
|
||||||
// Check whether a room alias exists within any application service namespaces
|
ASLocationLegacyPath = "/_matrix/app/unstable/thirdparty/location"
|
||||||
RoomAliasExists(
|
ASRoomAliasExistsLegacyPath = "/rooms/"
|
||||||
ctx context.Context,
|
ASUserExistsLegacyPath = "/users/"
|
||||||
req *RoomAliasExistsRequest,
|
|
||||||
resp *RoomAliasExistsResponse,
|
ASProtocolPath = "/_matrix/app/v1/thirdparty/protocol/"
|
||||||
) error
|
ASUserPath = "/_matrix/app/v1/thirdparty/user"
|
||||||
// Check whether a user ID exists within any application service namespaces
|
ASLocationPath = "/_matrix/app/v1/thirdparty/location"
|
||||||
UserIDExists(
|
ASRoomAliasExistsPath = "/_matrix/app/v1/rooms/"
|
||||||
ctx context.Context,
|
ASUserExistsPath = "/_matrix/app/v1/users/"
|
||||||
req *UserIDExistsRequest,
|
)
|
||||||
resp *UserIDExistsResponse,
|
|
||||||
) error
|
type ProtocolRequest struct {
|
||||||
|
Protocol string `json:"protocol,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProtocolResponse struct {
|
||||||
|
Protocols map[string]ASProtocolResponse `json:"protocols"`
|
||||||
|
Exists bool `json:"exists"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ASProtocolResponse struct {
|
||||||
|
FieldTypes map[string]FieldType `json:"field_types,omitempty"` // NOTSPEC: field_types is required by the spec
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
Instances []ProtocolInstance `json:"instances"`
|
||||||
|
LocationFields []string `json:"location_fields"`
|
||||||
|
UserFields []string `json:"user_fields"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldType struct {
|
||||||
|
Placeholder string `json:"placeholder"`
|
||||||
|
Regexp string `json:"regexp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProtocolInstance struct {
|
||||||
|
Description string `json:"desc"`
|
||||||
|
Icon string `json:"icon,omitempty"`
|
||||||
|
NetworkID string `json:"network_id,omitempty"` // NOTSPEC: network_id is required by the spec
|
||||||
|
Fields json.RawMessage `json:"fields,omitempty"` // NOTSPEC: fields is required by the spec
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserRequest struct {
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
Params string `json:"params"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserResponse struct {
|
||||||
|
Users []ASUserResponse `json:"users,omitempty"`
|
||||||
|
Exists bool `json:"exists,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ASUserResponse struct {
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
UserID string `json:"userid"`
|
||||||
|
Fields json.RawMessage `json:"fields"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocationRequest struct {
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
Params string `json:"params"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocationResponse struct {
|
||||||
|
Locations []ASLocationResponse `json:"locations,omitempty"`
|
||||||
|
Exists bool `json:"exists,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ASLocationResponse struct {
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
Fields json.RawMessage `json:"fields"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrProfileNotExists is returned when trying to lookup a user's profile that
|
||||||
|
// doesn't exist locally.
|
||||||
|
var ErrProfileNotExists = errors.New("no known profile for given user ID")
|
||||||
|
|
||||||
// RetrieveUserProfile is a wrapper that queries both the local database and
|
// RetrieveUserProfile is a wrapper that queries both the local database and
|
||||||
// application services for a given user's profile
|
// application services for a given user's profile
|
||||||
// TODO: Remove this, it's called from federationapi and clientapi but is a pure function
|
// TODO: Remove this, it's called from federationapi and clientapi but is a pure function
|
||||||
func RetrieveUserProfile(
|
func RetrieveUserProfile(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID string,
|
userID string,
|
||||||
asAPI AppServiceQueryAPI,
|
asAPI AppServiceInternalAPI,
|
||||||
profileAPI userapi.UserProfileAPI,
|
profileAPI userapi.ProfileAPI,
|
||||||
) (*authtypes.Profile, error) {
|
) (*authtypes.Profile, error) {
|
||||||
localpart, _, err := gomatrixserverlib.SplitID('@', userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to query the user from the local database
|
// Try to query the user from the local database
|
||||||
res := &userapi.QueryProfileResponse{}
|
profile, err := profileAPI.QueryProfile(ctx, userID)
|
||||||
err = profileAPI.QueryProfile(ctx, &userapi.QueryProfileRequest{UserID: userID}, res)
|
if err == nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
profile := &authtypes.Profile{
|
|
||||||
Localpart: localpart,
|
|
||||||
DisplayName: res.DisplayName,
|
|
||||||
AvatarURL: res.AvatarURL,
|
|
||||||
}
|
|
||||||
if res.UserExists {
|
|
||||||
return profile, nil
|
return profile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,19 +184,15 @@ func RetrieveUserProfile(
|
||||||
|
|
||||||
// If no user exists, return
|
// If no user exists, return
|
||||||
if !userResp.UserIDExists {
|
if !userResp.UserIDExists {
|
||||||
return nil, errors.New("no known profile for given user ID")
|
return nil, ErrProfileNotExists
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to query the user from the local database again
|
// Try to query the user from the local database again
|
||||||
err = profileAPI.QueryProfile(ctx, &userapi.QueryProfileRequest{UserID: userID}, res)
|
profile, err = profileAPI.QueryProfile(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// profile should not be nil at this point
|
// profile should not be nil at this point
|
||||||
return &authtypes.Profile{
|
return profile, nil
|
||||||
Localpart: localpart,
|
|
||||||
DisplayName: res.DisplayName,
|
|
||||||
AvatarURL: res.AvatarURL,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,100 +16,66 @@ package appservice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
|
"github.com/matrix-org/dendrite/setup/process"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||||
"github.com/matrix-org/dendrite/appservice/consumers"
|
"github.com/matrix-org/dendrite/appservice/consumers"
|
||||||
"github.com/matrix-org/dendrite/appservice/inthttp"
|
|
||||||
"github.com/matrix-org/dendrite/appservice/query"
|
"github.com/matrix-org/dendrite/appservice/query"
|
||||||
"github.com/matrix-org/dendrite/appservice/storage"
|
|
||||||
"github.com/matrix-org/dendrite/appservice/types"
|
|
||||||
"github.com/matrix-org/dendrite/appservice/workers"
|
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/setup/base"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddInternalRoutes registers HTTP handlers for internal API calls
|
|
||||||
func AddInternalRoutes(router *mux.Router, queryAPI appserviceAPI.AppServiceQueryAPI) {
|
|
||||||
inthttp.AddRoutes(queryAPI, router)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewInternalAPI returns a concerete implementation of the internal API. Callers
|
// NewInternalAPI returns a concerete implementation of the internal API. Callers
|
||||||
// can call functions directly on the returned API or via an HTTP interface using AddInternalRoutes.
|
// can call functions directly on the returned API or via an HTTP interface using AddInternalRoutes.
|
||||||
func NewInternalAPI(
|
func NewInternalAPI(
|
||||||
base *base.BaseDendrite,
|
processContext *process.ProcessContext,
|
||||||
userAPI userapi.UserInternalAPI,
|
cfg *config.Dendrite,
|
||||||
|
natsInstance *jetstream.NATSInstance,
|
||||||
|
userAPI userapi.AppserviceUserAPI,
|
||||||
rsAPI roomserverAPI.RoomserverInternalAPI,
|
rsAPI roomserverAPI.RoomserverInternalAPI,
|
||||||
) appserviceAPI.AppServiceQueryAPI {
|
) appserviceAPI.AppServiceInternalAPI {
|
||||||
client := &http.Client{
|
|
||||||
Timeout: time.Second * 30,
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DisableKeepAlives: true,
|
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
InsecureSkipVerify: base.Cfg.AppServiceAPI.DisableTLSValidation,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
js, _ := jetstream.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream)
|
|
||||||
|
|
||||||
// Create a connection to the appservice postgres DB
|
// Create appserivce query API with an HTTP client that will be used for all
|
||||||
appserviceDB, err := storage.NewDatabase(&base.Cfg.AppServiceAPI.Database)
|
// outbound and inbound requests (inbound only for the internal API)
|
||||||
if err != nil {
|
appserviceQueryAPI := &query.AppServiceQueryAPI{
|
||||||
logrus.WithError(err).Panicf("failed to connect to appservice db")
|
Cfg: &cfg.AppServiceAPI,
|
||||||
|
ProtocolCache: map[string]appserviceAPI.ASProtocolResponse{},
|
||||||
|
CacheMu: sync.Mutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.Derived.ApplicationServices) == 0 {
|
||||||
|
return appserviceQueryAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap application services in a type that relates the application service and
|
// Wrap application services in a type that relates the application service and
|
||||||
// a sync.Cond object that can be used to notify workers when there are new
|
// a sync.Cond object that can be used to notify workers when there are new
|
||||||
// events to be sent out.
|
// events to be sent out.
|
||||||
workerStates := make([]types.ApplicationServiceWorkerState, len(base.Cfg.Derived.ApplicationServices))
|
for _, appservice := range cfg.Derived.ApplicationServices {
|
||||||
for i, appservice := range base.Cfg.Derived.ApplicationServices {
|
|
||||||
m := sync.Mutex{}
|
|
||||||
ws := types.ApplicationServiceWorkerState{
|
|
||||||
AppService: appservice,
|
|
||||||
Cond: sync.NewCond(&m),
|
|
||||||
}
|
|
||||||
workerStates[i] = ws
|
|
||||||
|
|
||||||
// Create bot account for this AS if it doesn't already exist
|
// Create bot account for this AS if it doesn't already exist
|
||||||
if err = generateAppServiceAccount(userAPI, appservice); err != nil {
|
if err := generateAppServiceAccount(userAPI, appservice, cfg.Global.ServerName); err != nil {
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"appservice": appservice.ID,
|
"appservice": appservice.ID,
|
||||||
}).WithError(err).Panicf("failed to generate bot account for appservice")
|
}).WithError(err).Panicf("failed to generate bot account for appservice")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create appserivce query API with an HTTP client that will be used for all
|
|
||||||
// outbound and inbound requests (inbound only for the internal API)
|
|
||||||
appserviceQueryAPI := &query.AppServiceQueryAPI{
|
|
||||||
HTTPClient: client,
|
|
||||||
Cfg: base.Cfg,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only consume if we actually have ASes to track, else we'll just chew cycles needlessly.
|
// Only consume if we actually have ASes to track, else we'll just chew cycles needlessly.
|
||||||
// We can't add ASes at runtime so this is safe to do.
|
// We can't add ASes at runtime so this is safe to do.
|
||||||
if len(workerStates) > 0 {
|
js, _ := natsInstance.Prepare(processContext, &cfg.Global.JetStream)
|
||||||
consumer := consumers.NewOutputRoomEventConsumer(
|
consumer := consumers.NewOutputRoomEventConsumer(
|
||||||
base.ProcessContext, base.Cfg, js, appserviceDB,
|
processContext, &cfg.AppServiceAPI,
|
||||||
rsAPI, workerStates,
|
js, rsAPI,
|
||||||
)
|
)
|
||||||
if err := consumer.Start(); err != nil {
|
if err := consumer.Start(); err != nil {
|
||||||
logrus.WithError(err).Panicf("failed to start appservice roomserver consumer")
|
logrus.WithError(err).Panicf("failed to start appservice roomserver consumer")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create application service transaction workers
|
|
||||||
if err := workers.SetupTransactionWorkers(client, appserviceDB, workerStates); err != nil {
|
|
||||||
logrus.WithError(err).Panicf("failed to start app service transaction workers")
|
|
||||||
}
|
|
||||||
return appserviceQueryAPI
|
return appserviceQueryAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,13 +83,15 @@ func NewInternalAPI(
|
||||||
// `sender_localpart` field of each application service if it doesn't
|
// `sender_localpart` field of each application service if it doesn't
|
||||||
// exist already
|
// exist already
|
||||||
func generateAppServiceAccount(
|
func generateAppServiceAccount(
|
||||||
userAPI userapi.UserInternalAPI,
|
userAPI userapi.AppserviceUserAPI,
|
||||||
as config.ApplicationService,
|
as config.ApplicationService,
|
||||||
|
serverName spec.ServerName,
|
||||||
) error {
|
) error {
|
||||||
var accRes userapi.PerformAccountCreationResponse
|
var accRes userapi.PerformAccountCreationResponse
|
||||||
err := userAPI.PerformAccountCreation(context.Background(), &userapi.PerformAccountCreationRequest{
|
err := userAPI.PerformAccountCreation(context.Background(), &userapi.PerformAccountCreationRequest{
|
||||||
AccountType: userapi.AccountTypeAppService,
|
AccountType: userapi.AccountTypeAppService,
|
||||||
Localpart: as.SenderLocalpart,
|
Localpart: as.SenderLocalpart,
|
||||||
|
ServerName: serverName,
|
||||||
AppServiceID: as.ID,
|
AppServiceID: as.ID,
|
||||||
OnConflict: userapi.ConflictUpdate,
|
OnConflict: userapi.ConflictUpdate,
|
||||||
}, &accRes)
|
}, &accRes)
|
||||||
|
@ -133,6 +101,7 @@ func generateAppServiceAccount(
|
||||||
var devRes userapi.PerformDeviceCreationResponse
|
var devRes userapi.PerformDeviceCreationResponse
|
||||||
err = userAPI.PerformDeviceCreation(context.Background(), &userapi.PerformDeviceCreationRequest{
|
err = userAPI.PerformDeviceCreation(context.Background(), &userapi.PerformDeviceCreationRequest{
|
||||||
Localpart: as.SenderLocalpart,
|
Localpart: as.SenderLocalpart,
|
||||||
|
ServerName: serverName,
|
||||||
AccessToken: as.ASToken,
|
AccessToken: as.ASToken,
|
||||||
DeviceID: &as.SenderLocalpart,
|
DeviceID: &as.SenderLocalpart,
|
||||||
DeviceDisplayName: &as.SenderLocalpart,
|
DeviceDisplayName: &as.SenderLocalpart,
|
||||||
|
|
606
appservice/appservice_test.go
Normal file
606
appservice/appservice_test.go
Normal file
|
@ -0,0 +1,606 @@
|
||||||
|
package appservice_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"path"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
|
"github.com/matrix-org/dendrite/federationapi/statistics"
|
||||||
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver/types"
|
||||||
|
"github.com/matrix-org/dendrite/syncapi"
|
||||||
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/appservice"
|
||||||
|
"github.com/matrix-org/dendrite/appservice/api"
|
||||||
|
"github.com/matrix-org/dendrite/appservice/consumers"
|
||||||
|
"github.com/matrix-org/dendrite/internal/caching"
|
||||||
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver"
|
||||||
|
rsapi "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
|
"github.com/matrix-org/dendrite/test"
|
||||||
|
"github.com/matrix-org/dendrite/userapi"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/test/testrig"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testIsBlacklistedOrBackingOff = func(s spec.ServerName) (*statistics.ServerStatistics, error) {
|
||||||
|
return &statistics.ServerStatistics{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppserviceInternalAPI(t *testing.T) {
|
||||||
|
|
||||||
|
// Set expected results
|
||||||
|
existingProtocol := "irc"
|
||||||
|
wantLocationResponse := []api.ASLocationResponse{{Protocol: existingProtocol, Fields: []byte("{}")}}
|
||||||
|
wantUserResponse := []api.ASUserResponse{{Protocol: existingProtocol, Fields: []byte("{}")}}
|
||||||
|
wantProtocolResponse := api.ASProtocolResponse{Instances: []api.ProtocolInstance{{Fields: []byte("{}")}}}
|
||||||
|
wantProtocolResult := map[string]api.ASProtocolResponse{
|
||||||
|
existingProtocol: wantProtocolResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a dummy AS url, handling some cases
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case strings.Contains(r.URL.Path, "location"):
|
||||||
|
// Check if we've got an existing protocol, if so, return a proper response.
|
||||||
|
if r.URL.Path[len(r.URL.Path)-len(existingProtocol):] == existingProtocol {
|
||||||
|
if err := json.NewEncoder(w).Encode(wantLocationResponse); err != nil {
|
||||||
|
t.Fatalf("failed to encode response: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(w).Encode([]api.ASLocationResponse{}); err != nil {
|
||||||
|
t.Fatalf("failed to encode response: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case strings.Contains(r.URL.Path, "user"):
|
||||||
|
if r.URL.Path[len(r.URL.Path)-len(existingProtocol):] == existingProtocol {
|
||||||
|
if err := json.NewEncoder(w).Encode(wantUserResponse); err != nil {
|
||||||
|
t.Fatalf("failed to encode response: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(w).Encode([]api.UserResponse{}); err != nil {
|
||||||
|
t.Fatalf("failed to encode response: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case strings.Contains(r.URL.Path, "protocol"):
|
||||||
|
if r.URL.Path[len(r.URL.Path)-len(existingProtocol):] == existingProtocol {
|
||||||
|
if err := json.NewEncoder(w).Encode(wantProtocolResponse); err != nil {
|
||||||
|
t.Fatalf("failed to encode response: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(w).Encode(nil); err != nil {
|
||||||
|
t.Fatalf("failed to encode response: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
t.Logf("hit location: %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// The test cases to run
|
||||||
|
runCases := func(t *testing.T, testAPI api.AppServiceInternalAPI) {
|
||||||
|
t.Run("UserIDExists", func(t *testing.T) {
|
||||||
|
testUserIDExists(t, testAPI, "@as-testing:test", true)
|
||||||
|
testUserIDExists(t, testAPI, "@as1-testing:test", false)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("AliasExists", func(t *testing.T) {
|
||||||
|
testAliasExists(t, testAPI, "@asroom-testing:test", true)
|
||||||
|
testAliasExists(t, testAPI, "@asroom1-testing:test", false)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Locations", func(t *testing.T) {
|
||||||
|
testLocations(t, testAPI, existingProtocol, wantLocationResponse)
|
||||||
|
testLocations(t, testAPI, "abc", nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("User", func(t *testing.T) {
|
||||||
|
testUser(t, testAPI, existingProtocol, wantUserResponse)
|
||||||
|
testUser(t, testAPI, "abc", nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Protocols", func(t *testing.T) {
|
||||||
|
testProtocol(t, testAPI, existingProtocol, wantProtocolResult)
|
||||||
|
testProtocol(t, testAPI, existingProtocol, wantProtocolResult) // tests the cache
|
||||||
|
testProtocol(t, testAPI, "", wantProtocolResult) // tests getting all protocols
|
||||||
|
testProtocol(t, testAPI, "abc", nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
|
cfg, ctx, close := testrig.CreateConfig(t, dbType)
|
||||||
|
defer close()
|
||||||
|
|
||||||
|
// Create a dummy application service
|
||||||
|
as := &config.ApplicationService{
|
||||||
|
ID: "someID",
|
||||||
|
URL: srv.URL,
|
||||||
|
ASToken: "",
|
||||||
|
HSToken: "",
|
||||||
|
SenderLocalpart: "senderLocalPart",
|
||||||
|
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
||||||
|
"users": {{RegexpObject: regexp.MustCompile("as-.*")}},
|
||||||
|
"aliases": {{RegexpObject: regexp.MustCompile("asroom-.*")}},
|
||||||
|
},
|
||||||
|
Protocols: []string{existingProtocol},
|
||||||
|
}
|
||||||
|
as.CreateHTTPClient(cfg.AppServiceAPI.DisableTLSValidation)
|
||||||
|
cfg.AppServiceAPI.Derived.ApplicationServices = []config.ApplicationService{*as}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ctx.ShutdownDendrite()
|
||||||
|
ctx.WaitForShutdown()
|
||||||
|
})
|
||||||
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
|
// Create required internal APIs
|
||||||
|
natsInstance := jetstream.NATSInstance{}
|
||||||
|
cm := sqlutil.NewConnectionManager(ctx, cfg.Global.DatabaseOptions)
|
||||||
|
rsAPI := roomserver.NewInternalAPI(ctx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
|
||||||
|
rsAPI.SetFederationAPI(nil, nil)
|
||||||
|
usrAPI := userapi.NewInternalAPI(ctx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
|
||||||
|
asAPI := appservice.NewInternalAPI(ctx, cfg, &natsInstance, usrAPI, rsAPI)
|
||||||
|
|
||||||
|
runCases(t, asAPI)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppserviceInternalAPI_UnixSocket_Simple(t *testing.T) {
|
||||||
|
|
||||||
|
// Set expected results
|
||||||
|
existingProtocol := "irc"
|
||||||
|
wantLocationResponse := []api.ASLocationResponse{{Protocol: existingProtocol, Fields: []byte("{}")}}
|
||||||
|
wantUserResponse := []api.ASUserResponse{{Protocol: existingProtocol, Fields: []byte("{}")}}
|
||||||
|
wantProtocolResponse := api.ASProtocolResponse{Instances: []api.ProtocolInstance{{Fields: []byte("{}")}}}
|
||||||
|
|
||||||
|
// create a dummy AS url, handling some cases
|
||||||
|
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case strings.Contains(r.URL.Path, "location"):
|
||||||
|
// Check if we've got an existing protocol, if so, return a proper response.
|
||||||
|
if r.URL.Path[len(r.URL.Path)-len(existingProtocol):] == existingProtocol {
|
||||||
|
if err := json.NewEncoder(w).Encode(wantLocationResponse); err != nil {
|
||||||
|
t.Fatalf("failed to encode response: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(w).Encode([]api.ASLocationResponse{}); err != nil {
|
||||||
|
t.Fatalf("failed to encode response: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case strings.Contains(r.URL.Path, "user"):
|
||||||
|
if r.URL.Path[len(r.URL.Path)-len(existingProtocol):] == existingProtocol {
|
||||||
|
if err := json.NewEncoder(w).Encode(wantUserResponse); err != nil {
|
||||||
|
t.Fatalf("failed to encode response: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(w).Encode([]api.UserResponse{}); err != nil {
|
||||||
|
t.Fatalf("failed to encode response: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case strings.Contains(r.URL.Path, "protocol"):
|
||||||
|
if r.URL.Path[len(r.URL.Path)-len(existingProtocol):] == existingProtocol {
|
||||||
|
if err := json.NewEncoder(w).Encode(wantProtocolResponse); err != nil {
|
||||||
|
t.Fatalf("failed to encode response: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(w).Encode(nil); err != nil {
|
||||||
|
t.Fatalf("failed to encode response: %s", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
t.Logf("hit location: %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
socket := path.Join(tmpDir, "socket")
|
||||||
|
l, err := net.Listen("unix", socket)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_ = srv.Listener.Close()
|
||||||
|
srv.Listener = l
|
||||||
|
srv.Start()
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
cfg, ctx, tearDown := testrig.CreateConfig(t, test.DBTypeSQLite)
|
||||||
|
defer tearDown()
|
||||||
|
|
||||||
|
// Create a dummy application service
|
||||||
|
as := &config.ApplicationService{
|
||||||
|
ID: "someID",
|
||||||
|
URL: fmt.Sprintf("unix://%s", socket),
|
||||||
|
ASToken: "",
|
||||||
|
HSToken: "",
|
||||||
|
SenderLocalpart: "senderLocalPart",
|
||||||
|
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
||||||
|
"users": {{RegexpObject: regexp.MustCompile("as-.*")}},
|
||||||
|
"aliases": {{RegexpObject: regexp.MustCompile("asroom-.*")}},
|
||||||
|
},
|
||||||
|
Protocols: []string{existingProtocol},
|
||||||
|
}
|
||||||
|
as.CreateHTTPClient(cfg.AppServiceAPI.DisableTLSValidation)
|
||||||
|
cfg.AppServiceAPI.Derived.ApplicationServices = []config.ApplicationService{*as}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ctx.ShutdownDendrite()
|
||||||
|
ctx.WaitForShutdown()
|
||||||
|
})
|
||||||
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
|
// Create required internal APIs
|
||||||
|
natsInstance := jetstream.NATSInstance{}
|
||||||
|
cm := sqlutil.NewConnectionManager(ctx, cfg.Global.DatabaseOptions)
|
||||||
|
rsAPI := roomserver.NewInternalAPI(ctx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
|
||||||
|
rsAPI.SetFederationAPI(nil, nil)
|
||||||
|
usrAPI := userapi.NewInternalAPI(ctx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
|
||||||
|
asAPI := appservice.NewInternalAPI(ctx, cfg, &natsInstance, usrAPI, rsAPI)
|
||||||
|
|
||||||
|
t.Run("UserIDExists", func(t *testing.T) {
|
||||||
|
testUserIDExists(t, asAPI, "@as-testing:test", true)
|
||||||
|
testUserIDExists(t, asAPI, "@as1-testing:test", false)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUserIDExists(t *testing.T, asAPI api.AppServiceInternalAPI, userID string, wantExists bool) {
|
||||||
|
ctx := context.Background()
|
||||||
|
userResp := &api.UserIDExistsResponse{}
|
||||||
|
|
||||||
|
if err := asAPI.UserIDExists(ctx, &api.UserIDExistsRequest{
|
||||||
|
UserID: userID,
|
||||||
|
}, userResp); err != nil {
|
||||||
|
t.Errorf("failed to get userID: %s", err)
|
||||||
|
}
|
||||||
|
if userResp.UserIDExists != wantExists {
|
||||||
|
t.Errorf("unexpected result for UserIDExists(%s): %v, expected %v", userID, userResp.UserIDExists, wantExists)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAliasExists(t *testing.T, asAPI api.AppServiceInternalAPI, alias string, wantExists bool) {
|
||||||
|
ctx := context.Background()
|
||||||
|
aliasResp := &api.RoomAliasExistsResponse{}
|
||||||
|
|
||||||
|
if err := asAPI.RoomAliasExists(ctx, &api.RoomAliasExistsRequest{
|
||||||
|
Alias: alias,
|
||||||
|
}, aliasResp); err != nil {
|
||||||
|
t.Errorf("failed to get alias: %s", err)
|
||||||
|
}
|
||||||
|
if aliasResp.AliasExists != wantExists {
|
||||||
|
t.Errorf("unexpected result for RoomAliasExists(%s): %v, expected %v", alias, aliasResp.AliasExists, wantExists)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLocations(t *testing.T, asAPI api.AppServiceInternalAPI, proto string, wantResult []api.ASLocationResponse) {
|
||||||
|
ctx := context.Background()
|
||||||
|
locationResp := &api.LocationResponse{}
|
||||||
|
|
||||||
|
if err := asAPI.Locations(ctx, &api.LocationRequest{
|
||||||
|
Protocol: proto,
|
||||||
|
}, locationResp); err != nil {
|
||||||
|
t.Errorf("failed to get locations: %s", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(locationResp.Locations, wantResult) {
|
||||||
|
t.Errorf("unexpected result for Locations(%s): %+v, expected %+v", proto, locationResp.Locations, wantResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUser(t *testing.T, asAPI api.AppServiceInternalAPI, proto string, wantResult []api.ASUserResponse) {
|
||||||
|
ctx := context.Background()
|
||||||
|
userResp := &api.UserResponse{}
|
||||||
|
|
||||||
|
if err := asAPI.User(ctx, &api.UserRequest{
|
||||||
|
Protocol: proto,
|
||||||
|
}, userResp); err != nil {
|
||||||
|
t.Errorf("failed to get user: %s", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(userResp.Users, wantResult) {
|
||||||
|
t.Errorf("unexpected result for User(%s): %+v, expected %+v", proto, userResp.Users, wantResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testProtocol(t *testing.T, asAPI api.AppServiceInternalAPI, proto string, wantResult map[string]api.ASProtocolResponse) {
|
||||||
|
ctx := context.Background()
|
||||||
|
protoResp := &api.ProtocolResponse{}
|
||||||
|
|
||||||
|
if err := asAPI.Protocols(ctx, &api.ProtocolRequest{
|
||||||
|
Protocol: proto,
|
||||||
|
}, protoResp); err != nil {
|
||||||
|
t.Errorf("failed to get Protocols: %s", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(protoResp.Protocols, wantResult) {
|
||||||
|
t.Errorf("unexpected result for Protocols(%s): %+v, expected %+v", proto, protoResp.Protocols[proto], wantResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that the roomserver consumer only receives one invite
|
||||||
|
func TestRoomserverConsumerOneInvite(t *testing.T) {
|
||||||
|
|
||||||
|
alice := test.NewUser(t)
|
||||||
|
bob := test.NewUser(t)
|
||||||
|
room := test.NewRoom(t, alice)
|
||||||
|
|
||||||
|
// Invite Bob
|
||||||
|
room.CreateAndInsert(t, alice, spec.MRoomMember, map[string]interface{}{
|
||||||
|
"membership": "invite",
|
||||||
|
}, test.WithStateKey(bob.ID))
|
||||||
|
|
||||||
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
|
cfg, processCtx, closeDB := testrig.CreateConfig(t, dbType)
|
||||||
|
defer closeDB()
|
||||||
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
natsInstance := &jetstream.NATSInstance{}
|
||||||
|
|
||||||
|
evChan := make(chan struct{})
|
||||||
|
// create a dummy AS url, handling the events
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var txn consumers.ApplicationServiceTransaction
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&txn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, ev := range txn.Events {
|
||||||
|
if ev.Type != spec.MRoomMember {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Usually we would check the event content for the membership, but since
|
||||||
|
// we only invited bob, this should be fine for this test.
|
||||||
|
if ev.StateKey != nil && *ev.StateKey == bob.ID {
|
||||||
|
evChan <- struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
as := &config.ApplicationService{
|
||||||
|
ID: "someID",
|
||||||
|
URL: srv.URL,
|
||||||
|
ASToken: "",
|
||||||
|
HSToken: "",
|
||||||
|
SenderLocalpart: "senderLocalPart",
|
||||||
|
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
||||||
|
"users": {{RegexpObject: regexp.MustCompile(bob.ID)}},
|
||||||
|
"aliases": {{RegexpObject: regexp.MustCompile(room.ID)}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
as.CreateHTTPClient(cfg.AppServiceAPI.DisableTLSValidation)
|
||||||
|
|
||||||
|
// Create a dummy application service
|
||||||
|
cfg.AppServiceAPI.Derived.ApplicationServices = []config.ApplicationService{*as}
|
||||||
|
|
||||||
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
|
// Create required internal APIs
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, natsInstance, caches, caching.DisableMetrics)
|
||||||
|
rsAPI.SetFederationAPI(nil, nil)
|
||||||
|
usrAPI := userapi.NewInternalAPI(processCtx, cfg, cm, natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
|
||||||
|
// start the consumer
|
||||||
|
appservice.NewInternalAPI(processCtx, cfg, natsInstance, usrAPI, rsAPI)
|
||||||
|
|
||||||
|
// Create the room
|
||||||
|
if err := rsapi.SendEvents(context.Background(), rsAPI, rsapi.KindNew, room.Events(), "test", "test", "test", nil, false); err != nil {
|
||||||
|
t.Fatalf("failed to send events: %v", err)
|
||||||
|
}
|
||||||
|
var seenInvitesForBob int
|
||||||
|
waitLoop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Millisecond * 50): // wait for the AS to process the events
|
||||||
|
break waitLoop
|
||||||
|
case <-evChan:
|
||||||
|
seenInvitesForBob++
|
||||||
|
if seenInvitesForBob != 1 {
|
||||||
|
t.Fatalf("received unexpected invites: %d", seenInvitesForBob)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(evChan)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: If this test panics, it is because we timed out waiting for the
|
||||||
|
// join event to come through to the appservice and we close the DB/shutdown Dendrite. This makes the
|
||||||
|
// syncAPI unhappy, as it is unable to write to the database.
|
||||||
|
func TestOutputAppserviceEvent(t *testing.T) {
|
||||||
|
alice := test.NewUser(t)
|
||||||
|
bob := test.NewUser(t)
|
||||||
|
|
||||||
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
|
cfg, processCtx, closeDB := testrig.CreateConfig(t, dbType)
|
||||||
|
defer closeDB()
|
||||||
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
natsInstance := &jetstream.NATSInstance{}
|
||||||
|
|
||||||
|
evChan := make(chan struct{})
|
||||||
|
|
||||||
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
|
// Create required internal APIs
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, natsInstance, caches, caching.DisableMetrics)
|
||||||
|
rsAPI.SetFederationAPI(nil, nil)
|
||||||
|
|
||||||
|
// Create the router, so we can hit `/joined_members`
|
||||||
|
routers := httputil.NewRouters()
|
||||||
|
|
||||||
|
accessTokens := map[*test.User]userDevice{
|
||||||
|
bob: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
usrAPI := userapi.NewInternalAPI(processCtx, cfg, cm, natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
|
||||||
|
clientapi.AddPublicRoutes(processCtx, routers, cfg, natsInstance, nil, rsAPI, nil, nil, nil, usrAPI, nil, nil, caching.DisableMetrics)
|
||||||
|
createAccessTokens(t, accessTokens, usrAPI, processCtx.Context(), routers)
|
||||||
|
|
||||||
|
room := test.NewRoom(t, alice)
|
||||||
|
|
||||||
|
// Invite Bob
|
||||||
|
room.CreateAndInsert(t, alice, spec.MRoomMember, map[string]interface{}{
|
||||||
|
"membership": "invite",
|
||||||
|
}, test.WithStateKey(bob.ID))
|
||||||
|
|
||||||
|
// create a dummy AS url, handling the events
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var txn consumers.ApplicationServiceTransaction
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&txn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, ev := range txn.Events {
|
||||||
|
if ev.Type != spec.MRoomMember {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ev.StateKey != nil && *ev.StateKey == bob.ID {
|
||||||
|
membership := gjson.GetBytes(ev.Content, "membership").Str
|
||||||
|
t.Logf("Processing membership: %s", membership)
|
||||||
|
switch membership {
|
||||||
|
case spec.Invite:
|
||||||
|
// Accept the invite
|
||||||
|
joinEv := room.CreateAndInsert(t, bob, spec.MRoomMember, map[string]interface{}{
|
||||||
|
"membership": "join",
|
||||||
|
}, test.WithStateKey(bob.ID))
|
||||||
|
|
||||||
|
if err := rsapi.SendEvents(context.Background(), rsAPI, rsapi.KindNew, []*types.HeaderedEvent{joinEv}, "test", "test", "test", nil, false); err != nil {
|
||||||
|
t.Fatalf("failed to send events: %v", err)
|
||||||
|
}
|
||||||
|
case spec.Join: // the AS has received the join event, now hit `/joined_members` to validate that
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/rooms/"+room.ID+"/joined_members", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessTokens[bob].accessToken)
|
||||||
|
routers.Client.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both Alice and Bob should be joined. If not, we have a race condition
|
||||||
|
if !gjson.GetBytes(rec.Body.Bytes(), "joined."+alice.ID).Exists() {
|
||||||
|
t.Errorf("Alice is not joined to the room") // in theory should not happen
|
||||||
|
}
|
||||||
|
if !gjson.GetBytes(rec.Body.Bytes(), "joined."+bob.ID).Exists() {
|
||||||
|
t.Errorf("Bob is not joined to the room")
|
||||||
|
}
|
||||||
|
evChan <- struct{}{}
|
||||||
|
default:
|
||||||
|
t.Fatalf("Unexpected membership: %s", membership)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
as := &config.ApplicationService{
|
||||||
|
ID: "someID",
|
||||||
|
URL: srv.URL,
|
||||||
|
ASToken: "",
|
||||||
|
HSToken: "",
|
||||||
|
SenderLocalpart: "senderLocalPart",
|
||||||
|
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
||||||
|
"users": {{RegexpObject: regexp.MustCompile(bob.ID)}},
|
||||||
|
"aliases": {{RegexpObject: regexp.MustCompile(room.ID)}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
as.CreateHTTPClient(cfg.AppServiceAPI.DisableTLSValidation)
|
||||||
|
|
||||||
|
// Create a dummy application service
|
||||||
|
cfg.AppServiceAPI.Derived.ApplicationServices = []config.ApplicationService{*as}
|
||||||
|
|
||||||
|
// Prepare AS Streams on the old topic to validate that they get deleted
|
||||||
|
jsCtx, _ := natsInstance.Prepare(processCtx, &cfg.Global.JetStream)
|
||||||
|
|
||||||
|
token := jetstream.Tokenise(as.ID)
|
||||||
|
if err := jetstream.JetStreamConsumer(
|
||||||
|
processCtx.Context(), jsCtx, cfg.Global.JetStream.Prefixed(jetstream.OutputRoomEvent),
|
||||||
|
cfg.Global.JetStream.Durable("Appservice_"+token),
|
||||||
|
50, // maximum number of events to send in a single transaction
|
||||||
|
func(ctx context.Context, msgs []*nats.Msg) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the syncAPI to have `/joined_members` available
|
||||||
|
syncapi.AddPublicRoutes(processCtx, routers, cfg, cm, natsInstance, usrAPI, rsAPI, caches, caching.DisableMetrics)
|
||||||
|
|
||||||
|
// start the consumer
|
||||||
|
appservice.NewInternalAPI(processCtx, cfg, natsInstance, usrAPI, rsAPI)
|
||||||
|
|
||||||
|
// At this point, the old JetStream consumers should be deleted
|
||||||
|
for consumer := range jsCtx.Consumers(cfg.Global.JetStream.Prefixed(jetstream.OutputRoomEvent)) {
|
||||||
|
if consumer.Name == cfg.Global.JetStream.Durable("Appservice_"+token)+"Pull" {
|
||||||
|
t.Fatalf("Consumer still exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the room, this triggers the AS to receive an invite for Bob.
|
||||||
|
if err := rsapi.SendEvents(context.Background(), rsAPI, rsapi.KindNew, room.Events(), "test", "test", "test", nil, false); err != nil {
|
||||||
|
t.Fatalf("failed to send events: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
// Pretty generous timeout duration...
|
||||||
|
case <-time.After(time.Millisecond * 1000): // wait for the AS to process the events
|
||||||
|
t.Errorf("Timed out waiting for join event")
|
||||||
|
case <-evChan:
|
||||||
|
}
|
||||||
|
close(evChan)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type userDevice struct {
|
||||||
|
accessToken string
|
||||||
|
deviceID string
|
||||||
|
password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAccessTokens(t *testing.T, accessTokens map[*test.User]userDevice, userAPI uapi.UserInternalAPI, ctx context.Context, routers httputil.Routers) {
|
||||||
|
t.Helper()
|
||||||
|
for u := range accessTokens {
|
||||||
|
localpart, serverName, _ := gomatrixserverlib.SplitID('@', u.ID)
|
||||||
|
userRes := &uapi.PerformAccountCreationResponse{}
|
||||||
|
password := util.RandomString(8)
|
||||||
|
if err := userAPI.PerformAccountCreation(ctx, &uapi.PerformAccountCreationRequest{
|
||||||
|
AccountType: u.AccountType,
|
||||||
|
Localpart: localpart,
|
||||||
|
ServerName: serverName,
|
||||||
|
Password: password,
|
||||||
|
}, userRes); err != nil {
|
||||||
|
t.Errorf("failed to create account: %s", err)
|
||||||
|
}
|
||||||
|
req := test.NewRequest(t, http.MethodPost, "/_matrix/client/v3/login", test.WithJSONBody(t, map[string]interface{}{
|
||||||
|
"type": authtypes.LoginTypePassword,
|
||||||
|
"identifier": map[string]interface{}{
|
||||||
|
"type": "m.id.user",
|
||||||
|
"user": u.ID,
|
||||||
|
},
|
||||||
|
"password": password,
|
||||||
|
}))
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
routers.Client.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("failed to login: %s", rec.Body.String())
|
||||||
|
}
|
||||||
|
accessTokens[u] = userDevice{
|
||||||
|
accessToken: gjson.GetBytes(rec.Body.Bytes(), "access_token").String(),
|
||||||
|
deviceID: gjson.GetBytes(rec.Body.Bytes(), "device_id").String(),
|
||||||
|
password: password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,195 +15,267 @@
|
||||||
package consumers
|
package consumers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/appservice/storage"
|
|
||||||
"github.com/matrix-org/dendrite/appservice/types"
|
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver/types"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
"github.com/matrix-org/dendrite/setup/process"
|
"github.com/matrix-org/dendrite/setup/process"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/dendrite/syncapi/synctypes"
|
||||||
"github.com/nats-io/nats.go"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ApplicationServiceTransaction is the transaction that is sent off to an
|
||||||
|
// application service.
|
||||||
|
type ApplicationServiceTransaction struct {
|
||||||
|
Events []synctypes.ClientEvent `json:"events"`
|
||||||
|
}
|
||||||
|
|
||||||
// OutputRoomEventConsumer consumes events that originated in the room server.
|
// OutputRoomEventConsumer consumes events that originated in the room server.
|
||||||
type OutputRoomEventConsumer struct {
|
type OutputRoomEventConsumer struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
jetstream nats.JetStreamContext
|
cfg *config.AppServiceAPI
|
||||||
durable string
|
jetstream nats.JetStreamContext
|
||||||
topic string
|
topic string
|
||||||
asDB storage.Database
|
rsAPI api.AppserviceRoomserverAPI
|
||||||
rsAPI api.RoomserverInternalAPI
|
}
|
||||||
serverName string
|
|
||||||
workerStates []types.ApplicationServiceWorkerState
|
type appserviceState struct {
|
||||||
|
*config.ApplicationService
|
||||||
|
backoff int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOutputRoomEventConsumer creates a new OutputRoomEventConsumer. Call
|
// NewOutputRoomEventConsumer creates a new OutputRoomEventConsumer. Call
|
||||||
// Start() to begin consuming from room servers.
|
// Start() to begin consuming from room servers.
|
||||||
func NewOutputRoomEventConsumer(
|
func NewOutputRoomEventConsumer(
|
||||||
process *process.ProcessContext,
|
process *process.ProcessContext,
|
||||||
cfg *config.Dendrite,
|
cfg *config.AppServiceAPI,
|
||||||
js nats.JetStreamContext,
|
js nats.JetStreamContext,
|
||||||
appserviceDB storage.Database,
|
rsAPI api.AppserviceRoomserverAPI,
|
||||||
rsAPI api.RoomserverInternalAPI,
|
|
||||||
workerStates []types.ApplicationServiceWorkerState,
|
|
||||||
) *OutputRoomEventConsumer {
|
) *OutputRoomEventConsumer {
|
||||||
return &OutputRoomEventConsumer{
|
return &OutputRoomEventConsumer{
|
||||||
ctx: process.Context(),
|
ctx: process.Context(),
|
||||||
jetstream: js,
|
cfg: cfg,
|
||||||
durable: cfg.Global.JetStream.Durable("AppserviceRoomserverConsumer"),
|
jetstream: js,
|
||||||
topic: cfg.Global.JetStream.Prefixed(jetstream.OutputRoomEvent),
|
topic: cfg.Matrix.JetStream.Prefixed(jetstream.OutputAppserviceEvent),
|
||||||
asDB: appserviceDB,
|
rsAPI: rsAPI,
|
||||||
rsAPI: rsAPI,
|
|
||||||
serverName: string(cfg.Global.ServerName),
|
|
||||||
workerStates: workerStates,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start consuming from room servers
|
// Start consuming from room servers
|
||||||
func (s *OutputRoomEventConsumer) Start() error {
|
func (s *OutputRoomEventConsumer) Start() error {
|
||||||
return jetstream.JetStreamConsumer(
|
durableNames := make([]string, 0, len(s.cfg.Derived.ApplicationServices))
|
||||||
s.ctx, s.jetstream, s.topic, s.durable, s.onMessage,
|
for _, as := range s.cfg.Derived.ApplicationServices {
|
||||||
nats.DeliverAll(), nats.ManualAck(),
|
appsvc := as
|
||||||
)
|
state := &appserviceState{
|
||||||
|
ApplicationService: &appsvc,
|
||||||
|
}
|
||||||
|
token := jetstream.Tokenise(as.ID)
|
||||||
|
if err := jetstream.JetStreamConsumer(
|
||||||
|
s.ctx, s.jetstream, s.topic,
|
||||||
|
s.cfg.Matrix.JetStream.Durable("Appservice_"+token),
|
||||||
|
50, // maximum number of events to send in a single transaction
|
||||||
|
func(ctx context.Context, msgs []*nats.Msg) bool {
|
||||||
|
return s.onMessage(ctx, state, msgs)
|
||||||
|
},
|
||||||
|
nats.DeliverNew(), nats.ManualAck(),
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("failed to create %q consumer: %w", token, err)
|
||||||
|
}
|
||||||
|
durableNames = append(durableNames, s.cfg.Matrix.JetStream.Durable("Appservice_"+token))
|
||||||
|
}
|
||||||
|
// Cleanup any consumers still existing on the OutputRoomEvent stream
|
||||||
|
// to avoid messages not being deleted
|
||||||
|
for _, consumerName := range durableNames {
|
||||||
|
err := s.jetstream.DeleteConsumer(s.cfg.Matrix.JetStream.Prefixed(jetstream.OutputRoomEvent), consumerName+"Pull")
|
||||||
|
if err != nil && err != nats.ErrConsumerNotFound {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// onMessage is called when the appservice component receives a new event from
|
// onMessage is called when the appservice component receives a new event from
|
||||||
// the room server output log.
|
// the room server output log.
|
||||||
func (s *OutputRoomEventConsumer) onMessage(ctx context.Context, msg *nats.Msg) bool {
|
func (s *OutputRoomEventConsumer) onMessage(
|
||||||
// Parse out the event JSON
|
ctx context.Context, state *appserviceState, msgs []*nats.Msg,
|
||||||
var output api.OutputEvent
|
) bool {
|
||||||
if err := json.Unmarshal(msg.Data, &output); err != nil {
|
log.WithField("appservice", state.ID).Tracef("Appservice worker received %d message(s) from roomserver", len(msgs))
|
||||||
// If the message was invalid, log it and move on to the next message in the stream
|
events := make([]*types.HeaderedEvent, 0, len(msgs))
|
||||||
log.WithError(err).Errorf("roomserver output log: message parse failure")
|
for _, msg := range msgs {
|
||||||
return true
|
// Only handle events we care about
|
||||||
}
|
receivedType := api.OutputType(msg.Header.Get(jetstream.RoomEventType))
|
||||||
|
if receivedType != api.OutputTypeNewRoomEvent && receivedType != api.OutputTypeNewInviteEvent {
|
||||||
if output.Type != api.OutputTypeNewRoomEvent || output.NewRoomEvent == nil {
|
continue
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
newEventID := output.NewRoomEvent.Event.EventID()
|
|
||||||
events := make([]*gomatrixserverlib.HeaderedEvent, 0, len(output.NewRoomEvent.AddsStateEventIDs))
|
|
||||||
events = append(events, output.NewRoomEvent.Event)
|
|
||||||
if len(output.NewRoomEvent.AddsStateEventIDs) > 0 {
|
|
||||||
eventsReq := &api.QueryEventsByIDRequest{
|
|
||||||
EventIDs: make([]string, 0, len(output.NewRoomEvent.AddsStateEventIDs)),
|
|
||||||
}
|
}
|
||||||
eventsRes := &api.QueryEventsByIDResponse{}
|
// Parse out the event JSON
|
||||||
for _, eventID := range output.NewRoomEvent.AddsStateEventIDs {
|
var output api.OutputEvent
|
||||||
if eventID != newEventID {
|
if err := json.Unmarshal(msg.Data, &output); err != nil {
|
||||||
eventsReq.EventIDs = append(eventsReq.EventIDs, eventID)
|
// If the message was invalid, log it and move on to the next message in the stream
|
||||||
|
log.WithField("appservice", state.ID).WithError(err).Errorf("Appservice failed to parse message, ignoring")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch output.Type {
|
||||||
|
case api.OutputTypeNewRoomEvent:
|
||||||
|
if output.NewRoomEvent == nil || !s.appserviceIsInterestedInEvent(ctx, output.NewRoomEvent.Event, state.ApplicationService) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
events = append(events, output.NewRoomEvent.Event)
|
||||||
if len(eventsReq.EventIDs) > 0 {
|
if len(output.NewRoomEvent.AddsStateEventIDs) > 0 {
|
||||||
if err := s.rsAPI.QueryEventsByID(s.ctx, eventsReq, eventsRes); err != nil {
|
newEventID := output.NewRoomEvent.Event.EventID()
|
||||||
return false
|
eventsReq := &api.QueryEventsByIDRequest{
|
||||||
}
|
RoomID: output.NewRoomEvent.Event.RoomID().String(),
|
||||||
events = append(events, eventsRes.Events...)
|
EventIDs: make([]string, 0, len(output.NewRoomEvent.AddsStateEventIDs)),
|
||||||
}
|
}
|
||||||
}
|
eventsRes := &api.QueryEventsByIDResponse{}
|
||||||
|
for _, eventID := range output.NewRoomEvent.AddsStateEventIDs {
|
||||||
// Send event to any relevant application services
|
if eventID != newEventID {
|
||||||
if err := s.filterRoomserverEvents(context.TODO(), events); err != nil {
|
eventsReq.EventIDs = append(eventsReq.EventIDs, eventID)
|
||||||
log.WithError(err).Errorf("roomserver output log: filter error")
|
}
|
||||||
return true
|
}
|
||||||
}
|
if len(eventsReq.EventIDs) > 0 {
|
||||||
|
if err := s.rsAPI.QueryEventsByID(s.ctx, eventsReq, eventsRes); err != nil {
|
||||||
return true
|
log.WithError(err).Errorf("s.rsAPI.QueryEventsByID failed")
|
||||||
}
|
return false
|
||||||
|
}
|
||||||
// filterRoomserverEvents takes in events and decides whether any of them need
|
events = append(events, eventsRes.Events...)
|
||||||
// to be passed on to an external application service. It does this by checking
|
|
||||||
// each namespace of each registered application service, and if there is a
|
|
||||||
// match, adds the event to the queue for events to be sent to a particular
|
|
||||||
// application service.
|
|
||||||
func (s *OutputRoomEventConsumer) filterRoomserverEvents(
|
|
||||||
ctx context.Context,
|
|
||||||
events []*gomatrixserverlib.HeaderedEvent,
|
|
||||||
) error {
|
|
||||||
for _, ws := range s.workerStates {
|
|
||||||
for _, event := range events {
|
|
||||||
// Check if this event is interesting to this application service
|
|
||||||
if s.appserviceIsInterestedInEvent(ctx, event, ws.AppService) {
|
|
||||||
// Queue this event to be sent off to the application service
|
|
||||||
if err := s.asDB.StoreEvent(ctx, ws.AppService.ID, event); err != nil {
|
|
||||||
log.WithError(err).Warn("failed to insert incoming event into appservices database")
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
// Tell our worker to send out new messages by updating remaining message
|
|
||||||
// count and waking them up with a broadcast
|
|
||||||
ws.NotifyNewEvents()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there are no events selected for sending then we should
|
||||||
|
// ack the messages so that we don't get sent them again in the
|
||||||
|
// future.
|
||||||
|
if len(events) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
txnID := ""
|
||||||
|
// Try to get the message metadata, if we're able to, use the timestamp as the txnID
|
||||||
|
metadata, err := msgs[0].Metadata()
|
||||||
|
if err == nil {
|
||||||
|
txnID = strconv.Itoa(int(metadata.Timestamp.UnixNano()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send event to any relevant application services. If we hit
|
||||||
|
// an error here, return false, so that we negatively ack.
|
||||||
|
log.WithField("appservice", state.ID).Debugf("Appservice worker sending %d events(s) from roomserver", len(events))
|
||||||
|
return s.sendEvents(ctx, state, events, txnID) == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendEvents passes events to the appservice by using the transactions
|
||||||
|
// endpoint. It will block for the backoff period if necessary.
|
||||||
|
func (s *OutputRoomEventConsumer) sendEvents(
|
||||||
|
ctx context.Context, state *appserviceState,
|
||||||
|
events []*types.HeaderedEvent,
|
||||||
|
txnID string,
|
||||||
|
) error {
|
||||||
|
// Create the transaction body.
|
||||||
|
transaction, err := json.Marshal(
|
||||||
|
ApplicationServiceTransaction{
|
||||||
|
Events: synctypes.ToClientEvents(gomatrixserverlib.ToPDUs(events), synctypes.FormatAll, func(roomID spec.RoomID, senderID spec.SenderID) (*spec.UserID, error) {
|
||||||
|
return s.rsAPI.QueryUserIDForSender(ctx, roomID, senderID)
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If txnID is not defined, generate one from the events.
|
||||||
|
if txnID == "" {
|
||||||
|
txnID = fmt.Sprintf("%d_%d", events[0].PDU.OriginServerTS(), len(transaction))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the transaction to the appservice.
|
||||||
|
// https://spec.matrix.org/v1.9/application-service-api/#pushing-events
|
||||||
|
path := "_matrix/app/v1/transactions"
|
||||||
|
if s.cfg.LegacyPaths {
|
||||||
|
path = "transactions"
|
||||||
|
}
|
||||||
|
address := fmt.Sprintf("%s/%s/%s", state.RequestUrl(), path, txnID)
|
||||||
|
if s.cfg.LegacyAuth {
|
||||||
|
address += "?access_token=" + url.QueryEscape(state.HSToken)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "PUT", address, bytes.NewBuffer(transaction))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", state.HSToken))
|
||||||
|
resp, err := state.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return state.backoffAndPause(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the response was fine then we can clear any backoffs in place and
|
||||||
|
// report that everything was OK. Otherwise, back off for a while.
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusOK:
|
||||||
|
state.backoff = 0
|
||||||
|
default:
|
||||||
|
return state.backoffAndPause(fmt.Errorf("received HTTP status code %d from appservice url %s", resp.StatusCode, address))
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// appserviceJoinedAtEvent returns a boolean depending on whether a given
|
// backoff pauses the calling goroutine for a 2^some backoff exponent seconds
|
||||||
// appservice has membership at the time a given event was created.
|
func (s *appserviceState) backoffAndPause(err error) error {
|
||||||
func (s *OutputRoomEventConsumer) appserviceJoinedAtEvent(ctx context.Context, event *gomatrixserverlib.HeaderedEvent, appservice config.ApplicationService) bool {
|
if s.backoff < 6 {
|
||||||
// TODO: This is only checking the current room state, not the state at
|
s.backoff++
|
||||||
// the event in question. Pretty sure this is what Synapse does too, but
|
|
||||||
// until we have a lighter way of checking the state before the event that
|
|
||||||
// doesn't involve state res, then this is probably OK.
|
|
||||||
membershipReq := &api.QueryMembershipsForRoomRequest{
|
|
||||||
RoomID: event.RoomID(),
|
|
||||||
JoinedOnly: true,
|
|
||||||
}
|
}
|
||||||
membershipRes := &api.QueryMembershipsForRoomResponse{}
|
duration := time.Second * time.Duration(math.Pow(2, float64(s.backoff)))
|
||||||
|
log.WithField("appservice", s.ID).WithError(err).Errorf("Unable to send transaction to appservice, backing off for %s", duration.String())
|
||||||
// XXX: This could potentially race if the state for the event is not known yet
|
time.Sleep(duration)
|
||||||
// e.g. the event came over federation but we do not have the full state persisted.
|
return err
|
||||||
if err := s.rsAPI.QueryMembershipsForRoom(ctx, membershipReq, membershipRes); err == nil {
|
|
||||||
for _, ev := range membershipRes.JoinEvents {
|
|
||||||
var membership gomatrixserverlib.MemberContent
|
|
||||||
if err = json.Unmarshal(ev.Content, &membership); err != nil || ev.StateKey == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if appservice.IsInterestedInUserID(*ev.StateKey) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"room_id": event.RoomID(),
|
|
||||||
}).WithError(err).Errorf("Unable to get membership for room")
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// appserviceIsInterestedInEvent returns a boolean depending on whether a given
|
// appserviceIsInterestedInEvent returns a boolean depending on whether a given
|
||||||
// event falls within one of a given application service's namespaces.
|
// event falls within one of a given application service's namespaces.
|
||||||
//
|
//
|
||||||
// TODO: This should be cached, see https://github.com/matrix-org/dendrite/issues/1682
|
// TODO: This should be cached, see https://github.com/matrix-org/dendrite/issues/1682
|
||||||
func (s *OutputRoomEventConsumer) appserviceIsInterestedInEvent(ctx context.Context, event *gomatrixserverlib.HeaderedEvent, appservice config.ApplicationService) bool {
|
func (s *OutputRoomEventConsumer) appserviceIsInterestedInEvent(ctx context.Context, event *types.HeaderedEvent, appservice *config.ApplicationService) bool {
|
||||||
// No reason to queue events if they'll never be sent to the application
|
user := ""
|
||||||
// service
|
userID, err := s.rsAPI.QueryUserIDForSender(ctx, event.RoomID(), event.SenderID())
|
||||||
if appservice.URL == "" {
|
if err == nil {
|
||||||
return false
|
user = userID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check Room ID and Sender of the event
|
switch {
|
||||||
if appservice.IsInterestedInUserID(event.Sender()) ||
|
case appservice.URL == "":
|
||||||
appservice.IsInterestedInRoomID(event.RoomID()) {
|
return false
|
||||||
|
case appservice.IsInterestedInUserID(user):
|
||||||
|
return true
|
||||||
|
case appservice.IsInterestedInRoomID(event.RoomID().String()):
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.Type() == gomatrixserverlib.MRoomMember && event.StateKey() != nil {
|
if event.Type() == spec.MRoomMember && event.StateKey() != nil {
|
||||||
if appservice.IsInterestedInUserID(*event.StateKey()) {
|
if appservice.IsInterestedInUserID(*event.StateKey()) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check all known room aliases of the room the event came from
|
// Check all known room aliases of the room the event came from
|
||||||
queryReq := api.GetAliasesForRoomIDRequest{RoomID: event.RoomID()}
|
queryReq := api.GetAliasesForRoomIDRequest{RoomID: event.RoomID().String()}
|
||||||
var queryRes api.GetAliasesForRoomIDResponse
|
var queryRes api.GetAliasesForRoomIDResponse
|
||||||
if err := s.rsAPI.GetAliasesForRoomID(ctx, &queryReq, &queryRes); err == nil {
|
if err := s.rsAPI.GetAliasesForRoomID(ctx, &queryReq, &queryRes); err == nil {
|
||||||
for _, alias := range queryRes.Aliases {
|
for _, alias := range queryRes.Aliases {
|
||||||
|
@ -213,10 +285,54 @@ func (s *OutputRoomEventConsumer) appserviceIsInterestedInEvent(ctx context.Cont
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"room_id": event.RoomID(),
|
"appservice": appservice.ID,
|
||||||
|
"room_id": event.RoomID().String(),
|
||||||
}).WithError(err).Errorf("Unable to get aliases for room")
|
}).WithError(err).Errorf("Unable to get aliases for room")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if any of the members in the room match the appservice
|
// Check if any of the members in the room match the appservice
|
||||||
return s.appserviceJoinedAtEvent(ctx, event, appservice)
|
return s.appserviceJoinedAtEvent(ctx, event, appservice)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// appserviceJoinedAtEvent returns a boolean depending on whether a given
|
||||||
|
// appservice has membership at the time a given event was created.
|
||||||
|
func (s *OutputRoomEventConsumer) appserviceJoinedAtEvent(ctx context.Context, event *types.HeaderedEvent, appservice *config.ApplicationService) bool {
|
||||||
|
// TODO: This is only checking the current room state, not the state at
|
||||||
|
// the event in question. Pretty sure this is what Synapse does too, but
|
||||||
|
// until we have a lighter way of checking the state before the event that
|
||||||
|
// doesn't involve state res, then this is probably OK.
|
||||||
|
membershipReq := &api.QueryMembershipsForRoomRequest{
|
||||||
|
RoomID: event.RoomID().String(),
|
||||||
|
JoinedOnly: true,
|
||||||
|
}
|
||||||
|
membershipRes := &api.QueryMembershipsForRoomResponse{}
|
||||||
|
|
||||||
|
// XXX: This could potentially race if the state for the event is not known yet
|
||||||
|
// e.g. the event came over federation but we do not have the full state persisted.
|
||||||
|
if err := s.rsAPI.QueryMembershipsForRoom(ctx, membershipReq, membershipRes); err == nil {
|
||||||
|
for _, ev := range membershipRes.JoinEvents {
|
||||||
|
switch {
|
||||||
|
case ev.StateKey == nil:
|
||||||
|
continue
|
||||||
|
case ev.Type != spec.MRoomMember:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var membership gomatrixserverlib.MemberContent
|
||||||
|
err = json.Unmarshal(ev.Content, &membership)
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
continue
|
||||||
|
case membership.Membership == spec.Join:
|
||||||
|
if appservice.IsInterestedInUserID(*ev.StateKey) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"appservice": appservice.ID,
|
||||||
|
"room_id": event.RoomID().String(),
|
||||||
|
}).WithError(err).Errorf("Unable to get membership for room")
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
package inthttp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/appservice/api"
|
|
||||||
"github.com/matrix-org/dendrite/internal/httputil"
|
|
||||||
"github.com/opentracing/opentracing-go"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HTTP paths for the internal HTTP APIs
|
|
||||||
const (
|
|
||||||
AppServiceRoomAliasExistsPath = "/appservice/RoomAliasExists"
|
|
||||||
AppServiceUserIDExistsPath = "/appservice/UserIDExists"
|
|
||||||
)
|
|
||||||
|
|
||||||
// httpAppServiceQueryAPI contains the URL to an appservice query API and a
|
|
||||||
// reference to a httpClient used to reach it
|
|
||||||
type httpAppServiceQueryAPI struct {
|
|
||||||
appserviceURL string
|
|
||||||
httpClient *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAppserviceClient creates a AppServiceQueryAPI implemented by talking
|
|
||||||
// to a HTTP POST API.
|
|
||||||
// If httpClient is nil an error is returned
|
|
||||||
func NewAppserviceClient(
|
|
||||||
appserviceURL string,
|
|
||||||
httpClient *http.Client,
|
|
||||||
) (api.AppServiceQueryAPI, error) {
|
|
||||||
if httpClient == nil {
|
|
||||||
return nil, errors.New("NewRoomserverAliasAPIHTTP: httpClient is <nil>")
|
|
||||||
}
|
|
||||||
return &httpAppServiceQueryAPI{appserviceURL, httpClient}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoomAliasExists implements AppServiceQueryAPI
|
|
||||||
func (h *httpAppServiceQueryAPI) RoomAliasExists(
|
|
||||||
ctx context.Context,
|
|
||||||
request *api.RoomAliasExistsRequest,
|
|
||||||
response *api.RoomAliasExistsResponse,
|
|
||||||
) error {
|
|
||||||
span, ctx := opentracing.StartSpanFromContext(ctx, "appserviceRoomAliasExists")
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
apiURL := h.appserviceURL + AppServiceRoomAliasExistsPath
|
|
||||||
return httputil.PostJSON(ctx, span, h.httpClient, apiURL, request, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserIDExists implements AppServiceQueryAPI
|
|
||||||
func (h *httpAppServiceQueryAPI) UserIDExists(
|
|
||||||
ctx context.Context,
|
|
||||||
request *api.UserIDExistsRequest,
|
|
||||||
response *api.UserIDExistsResponse,
|
|
||||||
) error {
|
|
||||||
span, ctx := opentracing.StartSpanFromContext(ctx, "appserviceUserIDExists")
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
apiURL := h.appserviceURL + AppServiceUserIDExistsPath
|
|
||||||
return httputil.PostJSON(ctx, span, h.httpClient, apiURL, request, response)
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
package inthttp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/matrix-org/dendrite/appservice/api"
|
|
||||||
"github.com/matrix-org/dendrite/internal/httputil"
|
|
||||||
"github.com/matrix-org/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AddRoutes adds the AppServiceQueryAPI handlers to the http.ServeMux.
|
|
||||||
func AddRoutes(a api.AppServiceQueryAPI, internalAPIMux *mux.Router) {
|
|
||||||
internalAPIMux.Handle(
|
|
||||||
AppServiceRoomAliasExistsPath,
|
|
||||||
httputil.MakeInternalAPI("appserviceRoomAliasExists", func(req *http.Request) util.JSONResponse {
|
|
||||||
var request api.RoomAliasExistsRequest
|
|
||||||
var response api.RoomAliasExistsResponse
|
|
||||||
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
if err := a.RoomAliasExists(req.Context(), &request, &response); err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
return util.JSONResponse{Code: http.StatusOK, JSON: &response}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
internalAPIMux.Handle(
|
|
||||||
AppServiceUserIDExistsPath,
|
|
||||||
httputil.MakeInternalAPI("appserviceUserIDExists", func(req *http.Request) util.JSONResponse {
|
|
||||||
var request api.UserIDExistsRequest
|
|
||||||
var response api.UserIDExistsResponse
|
|
||||||
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
if err := a.UserIDExists(req.Context(), &request, &response); err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
return util.JSONResponse{Code: http.StatusOK, JSON: &response}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -18,22 +18,25 @@ package query
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/appservice/api"
|
"github.com/matrix-org/dendrite/appservice/api"
|
||||||
|
"github.com/matrix-org/dendrite/internal"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
opentracing "github.com/opentracing/opentracing-go"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const roomAliasExistsPath = "/rooms/"
|
|
||||||
const userIDExistsPath = "/users/"
|
|
||||||
|
|
||||||
// AppServiceQueryAPI is an implementation of api.AppServiceQueryAPI
|
// AppServiceQueryAPI is an implementation of api.AppServiceQueryAPI
|
||||||
type AppServiceQueryAPI struct {
|
type AppServiceQueryAPI struct {
|
||||||
HTTPClient *http.Client
|
Cfg *config.AppServiceAPI
|
||||||
Cfg *config.Dendrite
|
ProtocolCache map[string]api.ASProtocolResponse
|
||||||
|
CacheMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoomAliasExists performs a request to '/room/{roomAlias}' on all known
|
// RoomAliasExists performs a request to '/room/{roomAlias}' on all known
|
||||||
|
@ -43,20 +46,29 @@ func (a *AppServiceQueryAPI) RoomAliasExists(
|
||||||
request *api.RoomAliasExistsRequest,
|
request *api.RoomAliasExistsRequest,
|
||||||
response *api.RoomAliasExistsResponse,
|
response *api.RoomAliasExistsResponse,
|
||||||
) error {
|
) error {
|
||||||
span, ctx := opentracing.StartSpanFromContext(ctx, "ApplicationServiceRoomAlias")
|
trace, ctx := internal.StartRegion(ctx, "ApplicationServiceRoomAlias")
|
||||||
defer span.Finish()
|
defer trace.EndRegion()
|
||||||
|
|
||||||
// Determine which application service should handle this request
|
// Determine which application service should handle this request
|
||||||
for _, appservice := range a.Cfg.Derived.ApplicationServices {
|
for _, appservice := range a.Cfg.Derived.ApplicationServices {
|
||||||
if appservice.URL != "" && appservice.IsInterestedInRoomAlias(request.Alias) {
|
if appservice.URL != "" && appservice.IsInterestedInRoomAlias(request.Alias) {
|
||||||
|
path := api.ASRoomAliasExistsPath
|
||||||
|
if a.Cfg.LegacyPaths {
|
||||||
|
path = api.ASRoomAliasExistsLegacyPath
|
||||||
|
}
|
||||||
// The full path to the rooms API, includes hs token
|
// The full path to the rooms API, includes hs token
|
||||||
URL, err := url.Parse(appservice.URL + roomAliasExistsPath)
|
URL, err := url.Parse(appservice.RequestUrl() + path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
URL.Path += request.Alias
|
URL.Path += request.Alias
|
||||||
apiURL := URL.String() + "?access_token=" + appservice.HSToken
|
if a.Cfg.LegacyAuth {
|
||||||
|
q := URL.Query()
|
||||||
|
q.Set("access_token", appservice.HSToken)
|
||||||
|
URL.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
|
apiURL := URL.String()
|
||||||
|
|
||||||
// Send a request to each application service. If one responds that it has
|
// Send a request to each application service. If one responds that it has
|
||||||
// created the room, immediately return.
|
// created the room, immediately return.
|
||||||
|
@ -64,9 +76,10 @@ func (a *AppServiceQueryAPI) RoomAliasExists(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", appservice.HSToken))
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
resp, err := a.HTTPClient.Do(req)
|
resp, err := appservice.HTTPClient.Do(req)
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
defer func() {
|
defer func() {
|
||||||
err = resp.Body.Close()
|
err = resp.Body.Close()
|
||||||
|
@ -110,19 +123,28 @@ func (a *AppServiceQueryAPI) UserIDExists(
|
||||||
request *api.UserIDExistsRequest,
|
request *api.UserIDExistsRequest,
|
||||||
response *api.UserIDExistsResponse,
|
response *api.UserIDExistsResponse,
|
||||||
) error {
|
) error {
|
||||||
span, ctx := opentracing.StartSpanFromContext(ctx, "ApplicationServiceUserID")
|
trace, ctx := internal.StartRegion(ctx, "ApplicationServiceUserID")
|
||||||
defer span.Finish()
|
defer trace.EndRegion()
|
||||||
|
|
||||||
// Determine which application service should handle this request
|
// Determine which application service should handle this request
|
||||||
for _, appservice := range a.Cfg.Derived.ApplicationServices {
|
for _, appservice := range a.Cfg.Derived.ApplicationServices {
|
||||||
if appservice.URL != "" && appservice.IsInterestedInUserID(request.UserID) {
|
if appservice.URL != "" && appservice.IsInterestedInUserID(request.UserID) {
|
||||||
// The full path to the rooms API, includes hs token
|
// The full path to the rooms API, includes hs token
|
||||||
URL, err := url.Parse(appservice.URL + userIDExistsPath)
|
path := api.ASUserExistsPath
|
||||||
|
if a.Cfg.LegacyPaths {
|
||||||
|
path = api.ASUserExistsLegacyPath
|
||||||
|
}
|
||||||
|
URL, err := url.Parse(appservice.RequestUrl() + path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
URL.Path += request.UserID
|
URL.Path += request.UserID
|
||||||
apiURL := URL.String() + "?access_token=" + appservice.HSToken
|
if a.Cfg.LegacyAuth {
|
||||||
|
q := URL.Query()
|
||||||
|
q.Set("access_token", appservice.HSToken)
|
||||||
|
URL.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
|
apiURL := URL.String()
|
||||||
|
|
||||||
// Send a request to each application service. If one responds that it has
|
// Send a request to each application service. If one responds that it has
|
||||||
// created the user, immediately return.
|
// created the user, immediately return.
|
||||||
|
@ -130,7 +152,8 @@ func (a *AppServiceQueryAPI) UserIDExists(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
resp, err := a.HTTPClient.Do(req.WithContext(ctx))
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", appservice.HSToken))
|
||||||
|
resp, err := appservice.HTTPClient.Do(req.WithContext(ctx))
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
defer func() {
|
defer func() {
|
||||||
err = resp.Body.Close()
|
err = resp.Body.Close()
|
||||||
|
@ -165,3 +188,191 @@ func (a *AppServiceQueryAPI) UserIDExists(
|
||||||
response.UserIDExists = false
|
response.UserIDExists = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type thirdpartyResponses interface {
|
||||||
|
api.ASProtocolResponse | []api.ASUserResponse | []api.ASLocationResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestDo[T thirdpartyResponses](as *config.ApplicationService, url string, response *T) error {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", as.HSToken))
|
||||||
|
resp, err := as.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close() // nolint: errcheck
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.Unmarshal(body, &response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AppServiceQueryAPI) Locations(
|
||||||
|
ctx context.Context,
|
||||||
|
req *api.LocationRequest,
|
||||||
|
resp *api.LocationResponse,
|
||||||
|
) error {
|
||||||
|
params, err := url.ParseQuery(req.Params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
path := api.ASLocationPath
|
||||||
|
if a.Cfg.LegacyPaths {
|
||||||
|
path = api.ASLocationLegacyPath
|
||||||
|
}
|
||||||
|
for _, as := range a.Cfg.Derived.ApplicationServices {
|
||||||
|
var asLocations []api.ASLocationResponse
|
||||||
|
if a.Cfg.LegacyAuth {
|
||||||
|
params.Set("access_token", as.HSToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
url := as.RequestUrl() + path
|
||||||
|
if req.Protocol != "" {
|
||||||
|
url += "/" + req.Protocol
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := requestDo[[]api.ASLocationResponse](&as, url+"?"+params.Encode(), &asLocations); err != nil {
|
||||||
|
log.WithError(err).WithField("application_service", as.ID).Error("unable to get 'locations' from application service")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Locations = append(resp.Locations, asLocations...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.Locations) == 0 {
|
||||||
|
resp.Exists = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
resp.Exists = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AppServiceQueryAPI) User(
|
||||||
|
ctx context.Context,
|
||||||
|
req *api.UserRequest,
|
||||||
|
resp *api.UserResponse,
|
||||||
|
) error {
|
||||||
|
params, err := url.ParseQuery(req.Params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
path := api.ASUserPath
|
||||||
|
if a.Cfg.LegacyPaths {
|
||||||
|
path = api.ASUserLegacyPath
|
||||||
|
}
|
||||||
|
for _, as := range a.Cfg.Derived.ApplicationServices {
|
||||||
|
var asUsers []api.ASUserResponse
|
||||||
|
if a.Cfg.LegacyAuth {
|
||||||
|
params.Set("access_token", as.HSToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
url := as.RequestUrl() + path
|
||||||
|
if req.Protocol != "" {
|
||||||
|
url += "/" + req.Protocol
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := requestDo[[]api.ASUserResponse](&as, url+"?"+params.Encode(), &asUsers); err != nil {
|
||||||
|
log.WithError(err).WithField("application_service", as.ID).Error("unable to get 'user' from application service")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Users = append(resp.Users, asUsers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.Users) == 0 {
|
||||||
|
resp.Exists = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
resp.Exists = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AppServiceQueryAPI) Protocols(
|
||||||
|
ctx context.Context,
|
||||||
|
req *api.ProtocolRequest,
|
||||||
|
resp *api.ProtocolResponse,
|
||||||
|
) error {
|
||||||
|
protocolPath := api.ASProtocolPath
|
||||||
|
if a.Cfg.LegacyPaths {
|
||||||
|
protocolPath = api.ASProtocolLegacyPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// get a single protocol response
|
||||||
|
if req.Protocol != "" {
|
||||||
|
|
||||||
|
a.CacheMu.Lock()
|
||||||
|
defer a.CacheMu.Unlock()
|
||||||
|
if proto, ok := a.ProtocolCache[req.Protocol]; ok {
|
||||||
|
resp.Exists = true
|
||||||
|
resp.Protocols = map[string]api.ASProtocolResponse{
|
||||||
|
req.Protocol: proto,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
response := api.ASProtocolResponse{}
|
||||||
|
for _, as := range a.Cfg.Derived.ApplicationServices {
|
||||||
|
var proto api.ASProtocolResponse
|
||||||
|
if err := requestDo[api.ASProtocolResponse](&as, as.RequestUrl()+protocolPath+req.Protocol, &proto); err != nil {
|
||||||
|
log.WithError(err).WithField("application_service", as.ID).Error("unable to get 'protocol' from application service")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(response.Instances) != 0 {
|
||||||
|
response.Instances = append(response.Instances, proto.Instances...)
|
||||||
|
} else {
|
||||||
|
response = proto
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(response.Instances) == 0 {
|
||||||
|
resp.Exists = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Exists = true
|
||||||
|
resp.Protocols = map[string]api.ASProtocolResponse{
|
||||||
|
req.Protocol: response,
|
||||||
|
}
|
||||||
|
a.ProtocolCache[req.Protocol] = response
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
response := make(map[string]api.ASProtocolResponse, len(a.Cfg.Derived.ApplicationServices))
|
||||||
|
|
||||||
|
for _, as := range a.Cfg.Derived.ApplicationServices {
|
||||||
|
for _, p := range as.Protocols {
|
||||||
|
var proto api.ASProtocolResponse
|
||||||
|
if err := requestDo[api.ASProtocolResponse](&as, as.RequestUrl()+protocolPath+p, &proto); err != nil {
|
||||||
|
log.WithError(err).WithField("application_service", as.ID).Error("unable to get 'protocol' from application service")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existing, ok := response[p]
|
||||||
|
if !ok {
|
||||||
|
response[p] = proto
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existing.Instances = append(existing.Instances, proto.Instances...)
|
||||||
|
response[p] = existing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(response) == 0 {
|
||||||
|
resp.Exists = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
a.CacheMu.Lock()
|
||||||
|
defer a.CacheMu.Unlock()
|
||||||
|
a.ProtocolCache = response
|
||||||
|
|
||||||
|
resp.Exists = true
|
||||||
|
resp.Protocols = response
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
// Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
//
|
|
||||||
// Licensed 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
|
|
||||||
//
|
|
||||||
// http://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.
|
|
||||||
|
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Database interface {
|
|
||||||
StoreEvent(ctx context.Context, appServiceID string, event *gomatrixserverlib.HeaderedEvent) error
|
|
||||||
GetEventsWithAppServiceID(ctx context.Context, appServiceID string, limit int) (int, int, []gomatrixserverlib.HeaderedEvent, bool, error)
|
|
||||||
CountEventsWithAppServiceID(ctx context.Context, appServiceID string) (int, error)
|
|
||||||
UpdateTxnIDForEvents(ctx context.Context, appserviceID string, maxID, txnID int) error
|
|
||||||
RemoveEventsBeforeAndIncludingID(ctx context.Context, appserviceID string, eventTableID int) error
|
|
||||||
GetLatestTxnID(ctx context.Context) (int, error)
|
|
||||||
}
|
|
|
@ -1,256 +0,0 @@
|
||||||
// Copyright 2018 New Vector Ltd
|
|
||||||
// Copyright 2019-2020 The Matrix.org Foundation C.I.C.
|
|
||||||
//
|
|
||||||
// Licensed 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
|
|
||||||
//
|
|
||||||
// http://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.
|
|
||||||
|
|
||||||
package postgres
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
const appserviceEventsSchema = `
|
|
||||||
-- Stores events to be sent to application services
|
|
||||||
CREATE TABLE IF NOT EXISTS appservice_events (
|
|
||||||
-- An auto-incrementing id unique to each event in the table
|
|
||||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
|
||||||
-- The ID of the application service the event will be sent to
|
|
||||||
as_id TEXT NOT NULL,
|
|
||||||
-- JSON representation of the event
|
|
||||||
headered_event_json TEXT NOT NULL,
|
|
||||||
-- The ID of the transaction that this event is a part of
|
|
||||||
txn_id BIGINT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS appservice_events_as_id ON appservice_events(as_id);
|
|
||||||
`
|
|
||||||
|
|
||||||
const selectEventsByApplicationServiceIDSQL = "" +
|
|
||||||
"SELECT id, headered_event_json, txn_id " +
|
|
||||||
"FROM appservice_events WHERE as_id = $1 ORDER BY txn_id DESC, id ASC"
|
|
||||||
|
|
||||||
const countEventsByApplicationServiceIDSQL = "" +
|
|
||||||
"SELECT COUNT(id) FROM appservice_events WHERE as_id = $1"
|
|
||||||
|
|
||||||
const insertEventSQL = "" +
|
|
||||||
"INSERT INTO appservice_events(as_id, headered_event_json, txn_id) " +
|
|
||||||
"VALUES ($1, $2, $3)"
|
|
||||||
|
|
||||||
const updateTxnIDForEventsSQL = "" +
|
|
||||||
"UPDATE appservice_events SET txn_id = $1 WHERE as_id = $2 AND id <= $3"
|
|
||||||
|
|
||||||
const deleteEventsBeforeAndIncludingIDSQL = "" +
|
|
||||||
"DELETE FROM appservice_events WHERE as_id = $1 AND id <= $2"
|
|
||||||
|
|
||||||
const (
|
|
||||||
// A transaction ID number that no transaction should ever have. Used for
|
|
||||||
// checking again the default value.
|
|
||||||
invalidTxnID = -2
|
|
||||||
)
|
|
||||||
|
|
||||||
type eventsStatements struct {
|
|
||||||
selectEventsByApplicationServiceIDStmt *sql.Stmt
|
|
||||||
countEventsByApplicationServiceIDStmt *sql.Stmt
|
|
||||||
insertEventStmt *sql.Stmt
|
|
||||||
updateTxnIDForEventsStmt *sql.Stmt
|
|
||||||
deleteEventsBeforeAndIncludingIDStmt *sql.Stmt
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *eventsStatements) prepare(db *sql.DB) (err error) {
|
|
||||||
_, err = db.Exec(appserviceEventsSchema)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.selectEventsByApplicationServiceIDStmt, err = db.Prepare(selectEventsByApplicationServiceIDSQL); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if s.countEventsByApplicationServiceIDStmt, err = db.Prepare(countEventsByApplicationServiceIDSQL); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if s.insertEventStmt, err = db.Prepare(insertEventSQL); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if s.updateTxnIDForEventsStmt, err = db.Prepare(updateTxnIDForEventsSQL); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if s.deleteEventsBeforeAndIncludingIDStmt, err = db.Prepare(deleteEventsBeforeAndIncludingIDSQL); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// selectEventsByApplicationServiceID takes in an application service ID and
|
|
||||||
// returns a slice of events that need to be sent to that application service,
|
|
||||||
// as well as an int later used to remove these same events from the database
|
|
||||||
// once successfully sent to an application service.
|
|
||||||
func (s *eventsStatements) selectEventsByApplicationServiceID(
|
|
||||||
ctx context.Context,
|
|
||||||
applicationServiceID string,
|
|
||||||
limit int,
|
|
||||||
) (
|
|
||||||
txnID, maxID int,
|
|
||||||
events []gomatrixserverlib.HeaderedEvent,
|
|
||||||
eventsRemaining bool,
|
|
||||||
err error,
|
|
||||||
) {
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"appservice": applicationServiceID,
|
|
||||||
}).WithError(err).Fatalf("appservice unable to select new events to send")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
// Retrieve events from the database. Unsuccessfully sent events first
|
|
||||||
eventRows, err := s.selectEventsByApplicationServiceIDStmt.QueryContext(ctx, applicationServiceID)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer checkNamedErr(eventRows.Close, &err)
|
|
||||||
events, maxID, txnID, eventsRemaining, err = retrieveEvents(eventRows, limit)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkNamedErr calls fn and overwrite err if it was nil and fn returned non-nil
|
|
||||||
func checkNamedErr(fn func() error, err *error) {
|
|
||||||
if e := fn(); e != nil && *err == nil {
|
|
||||||
*err = e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func retrieveEvents(eventRows *sql.Rows, limit int) (events []gomatrixserverlib.HeaderedEvent, maxID, txnID int, eventsRemaining bool, err error) {
|
|
||||||
// Get current time for use in calculating event age
|
|
||||||
nowMilli := time.Now().UnixNano() / int64(time.Millisecond)
|
|
||||||
|
|
||||||
// Iterate through each row and store event contents
|
|
||||||
// If txn_id changes dramatically, we've switched from collecting old events to
|
|
||||||
// new ones. Send back those events first.
|
|
||||||
lastTxnID := invalidTxnID
|
|
||||||
for eventsProcessed := 0; eventRows.Next(); {
|
|
||||||
var event gomatrixserverlib.HeaderedEvent
|
|
||||||
var eventJSON []byte
|
|
||||||
var id int
|
|
||||||
err = eventRows.Scan(
|
|
||||||
&id,
|
|
||||||
&eventJSON,
|
|
||||||
&txnID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, 0, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal eventJSON
|
|
||||||
if err = json.Unmarshal(eventJSON, &event); err != nil {
|
|
||||||
return nil, 0, 0, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If txnID has changed on this event from the previous event, then we've
|
|
||||||
// reached the end of a transaction's events. Return only those events.
|
|
||||||
if lastTxnID > invalidTxnID && lastTxnID != txnID {
|
|
||||||
return events, maxID, lastTxnID, true, nil
|
|
||||||
}
|
|
||||||
lastTxnID = txnID
|
|
||||||
|
|
||||||
// Limit events that aren't part of an old transaction
|
|
||||||
if txnID == -1 {
|
|
||||||
// Return if we've hit the limit
|
|
||||||
if eventsProcessed++; eventsProcessed > limit {
|
|
||||||
return events, maxID, lastTxnID, true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if id > maxID {
|
|
||||||
maxID = id
|
|
||||||
}
|
|
||||||
|
|
||||||
// Portion of the event that is unsigned due to rapid change
|
|
||||||
// TODO: Consider removing age as not many app services use it
|
|
||||||
if err = event.SetUnsignedField("age", nowMilli-int64(event.OriginServerTS())); err != nil {
|
|
||||||
return nil, 0, 0, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
events = append(events, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// countEventsByApplicationServiceID inserts an event mapped to its corresponding application service
|
|
||||||
// IDs into the db.
|
|
||||||
func (s *eventsStatements) countEventsByApplicationServiceID(
|
|
||||||
ctx context.Context,
|
|
||||||
appServiceID string,
|
|
||||||
) (int, error) {
|
|
||||||
var count int
|
|
||||||
err := s.countEventsByApplicationServiceIDStmt.QueryRowContext(ctx, appServiceID).Scan(&count)
|
|
||||||
if err != nil && err != sql.ErrNoRows {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return count, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// insertEvent inserts an event mapped to its corresponding application service
|
|
||||||
// IDs into the db.
|
|
||||||
func (s *eventsStatements) insertEvent(
|
|
||||||
ctx context.Context,
|
|
||||||
appServiceID string,
|
|
||||||
event *gomatrixserverlib.HeaderedEvent,
|
|
||||||
) (err error) {
|
|
||||||
// Convert event to JSON before inserting
|
|
||||||
eventJSON, err := json.Marshal(event)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.insertEventStmt.ExecContext(
|
|
||||||
ctx,
|
|
||||||
appServiceID,
|
|
||||||
eventJSON,
|
|
||||||
-1, // No transaction ID yet
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateTxnIDForEvents sets the transactionID for a collection of events. Done
|
|
||||||
// before sending them to an AppService. Referenced before sending to make sure
|
|
||||||
// we aren't constructing multiple transactions with the same events.
|
|
||||||
func (s *eventsStatements) updateTxnIDForEvents(
|
|
||||||
ctx context.Context,
|
|
||||||
appserviceID string,
|
|
||||||
maxID, txnID int,
|
|
||||||
) (err error) {
|
|
||||||
_, err = s.updateTxnIDForEventsStmt.ExecContext(ctx, txnID, appserviceID, maxID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteEventsBeforeAndIncludingID removes events matching given IDs from the database.
|
|
||||||
func (s *eventsStatements) deleteEventsBeforeAndIncludingID(
|
|
||||||
ctx context.Context,
|
|
||||||
appserviceID string,
|
|
||||||
eventTableID int,
|
|
||||||
) (err error) {
|
|
||||||
_, err = s.deleteEventsBeforeAndIncludingIDStmt.ExecContext(ctx, appserviceID, eventTableID)
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,115 +0,0 @@
|
||||||
// Copyright 2018 New Vector Ltd
|
|
||||||
// Copyright 2019-2020 The Matrix.org Foundation C.I.C.
|
|
||||||
//
|
|
||||||
// Licensed 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
|
|
||||||
//
|
|
||||||
// http://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.
|
|
||||||
|
|
||||||
package postgres
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
// Import postgres database driver
|
|
||||||
_ "github.com/lib/pq"
|
|
||||||
"github.com/matrix-org/dendrite/internal/sqlutil"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Database stores events intended to be later sent to application services
|
|
||||||
type Database struct {
|
|
||||||
events eventsStatements
|
|
||||||
txnID txnStatements
|
|
||||||
db *sql.DB
|
|
||||||
writer sqlutil.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDatabase opens a new database
|
|
||||||
func NewDatabase(dbProperties *config.DatabaseOptions) (*Database, error) {
|
|
||||||
var result Database
|
|
||||||
var err error
|
|
||||||
if result.db, err = sqlutil.Open(dbProperties); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result.writer = sqlutil.NewDummyWriter()
|
|
||||||
if err = result.prepare(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Database) prepare() error {
|
|
||||||
if err := d.events.prepare(d.db); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return d.txnID.prepare(d.db)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StoreEvent takes in a gomatrixserverlib.HeaderedEvent and stores it in the database
|
|
||||||
// for a transaction worker to pull and later send to an application service.
|
|
||||||
func (d *Database) StoreEvent(
|
|
||||||
ctx context.Context,
|
|
||||||
appServiceID string,
|
|
||||||
event *gomatrixserverlib.HeaderedEvent,
|
|
||||||
) error {
|
|
||||||
return d.events.insertEvent(ctx, appServiceID, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEventsWithAppServiceID returns a slice of events and their IDs intended to
|
|
||||||
// be sent to an application service given its ID.
|
|
||||||
func (d *Database) GetEventsWithAppServiceID(
|
|
||||||
ctx context.Context,
|
|
||||||
appServiceID string,
|
|
||||||
limit int,
|
|
||||||
) (int, int, []gomatrixserverlib.HeaderedEvent, bool, error) {
|
|
||||||
return d.events.selectEventsByApplicationServiceID(ctx, appServiceID, limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CountEventsWithAppServiceID returns the number of events destined for an
|
|
||||||
// application service given its ID.
|
|
||||||
func (d *Database) CountEventsWithAppServiceID(
|
|
||||||
ctx context.Context,
|
|
||||||
appServiceID string,
|
|
||||||
) (int, error) {
|
|
||||||
return d.events.countEventsByApplicationServiceID(ctx, appServiceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateTxnIDForEvents takes in an application service ID and a
|
|
||||||
// and stores them in the DB, unless the pair already exists, in
|
|
||||||
// which case it updates them.
|
|
||||||
func (d *Database) UpdateTxnIDForEvents(
|
|
||||||
ctx context.Context,
|
|
||||||
appserviceID string,
|
|
||||||
maxID, txnID int,
|
|
||||||
) error {
|
|
||||||
return d.events.updateTxnIDForEvents(ctx, appserviceID, maxID, txnID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveEventsBeforeAndIncludingID removes all events from the database that
|
|
||||||
// are less than or equal to a given maximum ID. IDs here are implemented as a
|
|
||||||
// serial, thus this should always delete events in chronological order.
|
|
||||||
func (d *Database) RemoveEventsBeforeAndIncludingID(
|
|
||||||
ctx context.Context,
|
|
||||||
appserviceID string,
|
|
||||||
eventTableID int,
|
|
||||||
) error {
|
|
||||||
return d.events.deleteEventsBeforeAndIncludingID(ctx, appserviceID, eventTableID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLatestTxnID returns the latest available transaction id
|
|
||||||
func (d *Database) GetLatestTxnID(
|
|
||||||
ctx context.Context,
|
|
||||||
) (int, error) {
|
|
||||||
return d.txnID.selectTxnID(ctx)
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
// Copyright 2018 New Vector Ltd
|
|
||||||
// Copyright 2019-2020 The Matrix.org Foundation C.I.C.
|
|
||||||
//
|
|
||||||
// Licensed 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
|
|
||||||
//
|
|
||||||
// http://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.
|
|
||||||
|
|
||||||
package postgres
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
)
|
|
||||||
|
|
||||||
const txnIDSchema = `
|
|
||||||
-- Keeps a count of the current transaction ID
|
|
||||||
CREATE SEQUENCE IF NOT EXISTS txn_id_counter START 1;
|
|
||||||
`
|
|
||||||
|
|
||||||
const selectTxnIDSQL = "SELECT nextval('txn_id_counter')"
|
|
||||||
|
|
||||||
type txnStatements struct {
|
|
||||||
selectTxnIDStmt *sql.Stmt
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *txnStatements) prepare(db *sql.DB) (err error) {
|
|
||||||
_, err = db.Exec(txnIDSchema)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.selectTxnIDStmt, err = db.Prepare(selectTxnIDSQL); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// selectTxnID selects the latest ascending transaction ID
|
|
||||||
func (s *txnStatements) selectTxnID(
|
|
||||||
ctx context.Context,
|
|
||||||
) (txnID int, err error) {
|
|
||||||
err = s.selectTxnIDStmt.QueryRowContext(ctx).Scan(&txnID)
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,267 +0,0 @@
|
||||||
// Copyright 2018 New Vector Ltd
|
|
||||||
// Copyright 2019-2020 The Matrix.org Foundation C.I.C.
|
|
||||||
//
|
|
||||||
// Licensed 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
|
|
||||||
//
|
|
||||||
// http://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.
|
|
||||||
|
|
||||||
package sqlite3
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/internal/sqlutil"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
const appserviceEventsSchema = `
|
|
||||||
-- Stores events to be sent to application services
|
|
||||||
CREATE TABLE IF NOT EXISTS appservice_events (
|
|
||||||
-- An auto-incrementing id unique to each event in the table
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
-- The ID of the application service the event will be sent to
|
|
||||||
as_id TEXT NOT NULL,
|
|
||||||
-- JSON representation of the event
|
|
||||||
headered_event_json TEXT NOT NULL,
|
|
||||||
-- The ID of the transaction that this event is a part of
|
|
||||||
txn_id INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS appservice_events_as_id ON appservice_events(as_id);
|
|
||||||
`
|
|
||||||
|
|
||||||
const selectEventsByApplicationServiceIDSQL = "" +
|
|
||||||
"SELECT id, headered_event_json, txn_id " +
|
|
||||||
"FROM appservice_events WHERE as_id = $1 ORDER BY txn_id DESC, id ASC"
|
|
||||||
|
|
||||||
const countEventsByApplicationServiceIDSQL = "" +
|
|
||||||
"SELECT COUNT(id) FROM appservice_events WHERE as_id = $1"
|
|
||||||
|
|
||||||
const insertEventSQL = "" +
|
|
||||||
"INSERT INTO appservice_events(as_id, headered_event_json, txn_id) " +
|
|
||||||
"VALUES ($1, $2, $3)"
|
|
||||||
|
|
||||||
const updateTxnIDForEventsSQL = "" +
|
|
||||||
"UPDATE appservice_events SET txn_id = $1 WHERE as_id = $2 AND id <= $3"
|
|
||||||
|
|
||||||
const deleteEventsBeforeAndIncludingIDSQL = "" +
|
|
||||||
"DELETE FROM appservice_events WHERE as_id = $1 AND id <= $2"
|
|
||||||
|
|
||||||
const (
|
|
||||||
// A transaction ID number that no transaction should ever have. Used for
|
|
||||||
// checking again the default value.
|
|
||||||
invalidTxnID = -2
|
|
||||||
)
|
|
||||||
|
|
||||||
type eventsStatements struct {
|
|
||||||
db *sql.DB
|
|
||||||
writer sqlutil.Writer
|
|
||||||
selectEventsByApplicationServiceIDStmt *sql.Stmt
|
|
||||||
countEventsByApplicationServiceIDStmt *sql.Stmt
|
|
||||||
insertEventStmt *sql.Stmt
|
|
||||||
updateTxnIDForEventsStmt *sql.Stmt
|
|
||||||
deleteEventsBeforeAndIncludingIDStmt *sql.Stmt
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *eventsStatements) prepare(db *sql.DB, writer sqlutil.Writer) (err error) {
|
|
||||||
s.db = db
|
|
||||||
s.writer = writer
|
|
||||||
_, err = db.Exec(appserviceEventsSchema)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.selectEventsByApplicationServiceIDStmt, err = db.Prepare(selectEventsByApplicationServiceIDSQL); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if s.countEventsByApplicationServiceIDStmt, err = db.Prepare(countEventsByApplicationServiceIDSQL); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if s.insertEventStmt, err = db.Prepare(insertEventSQL); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if s.updateTxnIDForEventsStmt, err = db.Prepare(updateTxnIDForEventsSQL); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if s.deleteEventsBeforeAndIncludingIDStmt, err = db.Prepare(deleteEventsBeforeAndIncludingIDSQL); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// selectEventsByApplicationServiceID takes in an application service ID and
|
|
||||||
// returns a slice of events that need to be sent to that application service,
|
|
||||||
// as well as an int later used to remove these same events from the database
|
|
||||||
// once successfully sent to an application service.
|
|
||||||
func (s *eventsStatements) selectEventsByApplicationServiceID(
|
|
||||||
ctx context.Context,
|
|
||||||
applicationServiceID string,
|
|
||||||
limit int,
|
|
||||||
) (
|
|
||||||
txnID, maxID int,
|
|
||||||
events []gomatrixserverlib.HeaderedEvent,
|
|
||||||
eventsRemaining bool,
|
|
||||||
err error,
|
|
||||||
) {
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"appservice": applicationServiceID,
|
|
||||||
}).WithError(err).Fatalf("appservice unable to select new events to send")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
// Retrieve events from the database. Unsuccessfully sent events first
|
|
||||||
eventRows, err := s.selectEventsByApplicationServiceIDStmt.QueryContext(ctx, applicationServiceID)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer checkNamedErr(eventRows.Close, &err)
|
|
||||||
events, maxID, txnID, eventsRemaining, err = retrieveEvents(eventRows, limit)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkNamedErr calls fn and overwrite err if it was nil and fn returned non-nil
|
|
||||||
func checkNamedErr(fn func() error, err *error) {
|
|
||||||
if e := fn(); e != nil && *err == nil {
|
|
||||||
*err = e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func retrieveEvents(eventRows *sql.Rows, limit int) (events []gomatrixserverlib.HeaderedEvent, maxID, txnID int, eventsRemaining bool, err error) {
|
|
||||||
// Get current time for use in calculating event age
|
|
||||||
nowMilli := time.Now().UnixNano() / int64(time.Millisecond)
|
|
||||||
|
|
||||||
// Iterate through each row and store event contents
|
|
||||||
// If txn_id changes dramatically, we've switched from collecting old events to
|
|
||||||
// new ones. Send back those events first.
|
|
||||||
lastTxnID := invalidTxnID
|
|
||||||
for eventsProcessed := 0; eventRows.Next(); {
|
|
||||||
var event gomatrixserverlib.HeaderedEvent
|
|
||||||
var eventJSON []byte
|
|
||||||
var id int
|
|
||||||
err = eventRows.Scan(
|
|
||||||
&id,
|
|
||||||
&eventJSON,
|
|
||||||
&txnID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, 0, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal eventJSON
|
|
||||||
if err = json.Unmarshal(eventJSON, &event); err != nil {
|
|
||||||
return nil, 0, 0, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If txnID has changed on this event from the previous event, then we've
|
|
||||||
// reached the end of a transaction's events. Return only those events.
|
|
||||||
if lastTxnID > invalidTxnID && lastTxnID != txnID {
|
|
||||||
return events, maxID, lastTxnID, true, nil
|
|
||||||
}
|
|
||||||
lastTxnID = txnID
|
|
||||||
|
|
||||||
// Limit events that aren't part of an old transaction
|
|
||||||
if txnID == -1 {
|
|
||||||
// Return if we've hit the limit
|
|
||||||
if eventsProcessed++; eventsProcessed > limit {
|
|
||||||
return events, maxID, lastTxnID, true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if id > maxID {
|
|
||||||
maxID = id
|
|
||||||
}
|
|
||||||
|
|
||||||
// Portion of the event that is unsigned due to rapid change
|
|
||||||
// TODO: Consider removing age as not many app services use it
|
|
||||||
if err = event.SetUnsignedField("age", nowMilli-int64(event.OriginServerTS())); err != nil {
|
|
||||||
return nil, 0, 0, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
events = append(events, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// countEventsByApplicationServiceID inserts an event mapped to its corresponding application service
|
|
||||||
// IDs into the db.
|
|
||||||
func (s *eventsStatements) countEventsByApplicationServiceID(
|
|
||||||
ctx context.Context,
|
|
||||||
appServiceID string,
|
|
||||||
) (int, error) {
|
|
||||||
var count int
|
|
||||||
err := s.countEventsByApplicationServiceIDStmt.QueryRowContext(ctx, appServiceID).Scan(&count)
|
|
||||||
if err != nil && err != sql.ErrNoRows {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return count, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// insertEvent inserts an event mapped to its corresponding application service
|
|
||||||
// IDs into the db.
|
|
||||||
func (s *eventsStatements) insertEvent(
|
|
||||||
ctx context.Context,
|
|
||||||
appServiceID string,
|
|
||||||
event *gomatrixserverlib.HeaderedEvent,
|
|
||||||
) (err error) {
|
|
||||||
// Convert event to JSON before inserting
|
|
||||||
eventJSON, err := json.Marshal(event)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.writer.Do(s.db, nil, func(txn *sql.Tx) error {
|
|
||||||
_, err := s.insertEventStmt.ExecContext(
|
|
||||||
ctx,
|
|
||||||
appServiceID,
|
|
||||||
eventJSON,
|
|
||||||
-1, // No transaction ID yet
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateTxnIDForEvents sets the transactionID for a collection of events. Done
|
|
||||||
// before sending them to an AppService. Referenced before sending to make sure
|
|
||||||
// we aren't constructing multiple transactions with the same events.
|
|
||||||
func (s *eventsStatements) updateTxnIDForEvents(
|
|
||||||
ctx context.Context,
|
|
||||||
appserviceID string,
|
|
||||||
maxID, txnID int,
|
|
||||||
) (err error) {
|
|
||||||
return s.writer.Do(s.db, nil, func(txn *sql.Tx) error {
|
|
||||||
_, err := s.updateTxnIDForEventsStmt.ExecContext(ctx, txnID, appserviceID, maxID)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteEventsBeforeAndIncludingID removes events matching given IDs from the database.
|
|
||||||
func (s *eventsStatements) deleteEventsBeforeAndIncludingID(
|
|
||||||
ctx context.Context,
|
|
||||||
appserviceID string,
|
|
||||||
eventTableID int,
|
|
||||||
) (err error) {
|
|
||||||
return s.writer.Do(s.db, nil, func(txn *sql.Tx) error {
|
|
||||||
_, err := s.deleteEventsBeforeAndIncludingIDStmt.ExecContext(ctx, appserviceID, eventTableID)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,114 +0,0 @@
|
||||||
// Copyright 2018 New Vector Ltd
|
|
||||||
// Copyright 2019-2020 The Matrix.org Foundation C.I.C.
|
|
||||||
//
|
|
||||||
// Licensed 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
|
|
||||||
//
|
|
||||||
// http://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.
|
|
||||||
|
|
||||||
package sqlite3
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
// Import SQLite database driver
|
|
||||||
"github.com/matrix-org/dendrite/internal/sqlutil"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Database stores events intended to be later sent to application services
|
|
||||||
type Database struct {
|
|
||||||
events eventsStatements
|
|
||||||
txnID txnStatements
|
|
||||||
db *sql.DB
|
|
||||||
writer sqlutil.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDatabase opens a new database
|
|
||||||
func NewDatabase(dbProperties *config.DatabaseOptions) (*Database, error) {
|
|
||||||
var result Database
|
|
||||||
var err error
|
|
||||||
if result.db, err = sqlutil.Open(dbProperties); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result.writer = sqlutil.NewExclusiveWriter()
|
|
||||||
if err = result.prepare(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Database) prepare() error {
|
|
||||||
if err := d.events.prepare(d.db, d.writer); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return d.txnID.prepare(d.db, d.writer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StoreEvent takes in a gomatrixserverlib.HeaderedEvent and stores it in the database
|
|
||||||
// for a transaction worker to pull and later send to an application service.
|
|
||||||
func (d *Database) StoreEvent(
|
|
||||||
ctx context.Context,
|
|
||||||
appServiceID string,
|
|
||||||
event *gomatrixserverlib.HeaderedEvent,
|
|
||||||
) error {
|
|
||||||
return d.events.insertEvent(ctx, appServiceID, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEventsWithAppServiceID returns a slice of events and their IDs intended to
|
|
||||||
// be sent to an application service given its ID.
|
|
||||||
func (d *Database) GetEventsWithAppServiceID(
|
|
||||||
ctx context.Context,
|
|
||||||
appServiceID string,
|
|
||||||
limit int,
|
|
||||||
) (int, int, []gomatrixserverlib.HeaderedEvent, bool, error) {
|
|
||||||
return d.events.selectEventsByApplicationServiceID(ctx, appServiceID, limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CountEventsWithAppServiceID returns the number of events destined for an
|
|
||||||
// application service given its ID.
|
|
||||||
func (d *Database) CountEventsWithAppServiceID(
|
|
||||||
ctx context.Context,
|
|
||||||
appServiceID string,
|
|
||||||
) (int, error) {
|
|
||||||
return d.events.countEventsByApplicationServiceID(ctx, appServiceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateTxnIDForEvents takes in an application service ID and a
|
|
||||||
// and stores them in the DB, unless the pair already exists, in
|
|
||||||
// which case it updates them.
|
|
||||||
func (d *Database) UpdateTxnIDForEvents(
|
|
||||||
ctx context.Context,
|
|
||||||
appserviceID string,
|
|
||||||
maxID, txnID int,
|
|
||||||
) error {
|
|
||||||
return d.events.updateTxnIDForEvents(ctx, appserviceID, maxID, txnID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveEventsBeforeAndIncludingID removes all events from the database that
|
|
||||||
// are less than or equal to a given maximum ID. IDs here are implemented as a
|
|
||||||
// serial, thus this should always delete events in chronological order.
|
|
||||||
func (d *Database) RemoveEventsBeforeAndIncludingID(
|
|
||||||
ctx context.Context,
|
|
||||||
appserviceID string,
|
|
||||||
eventTableID int,
|
|
||||||
) error {
|
|
||||||
return d.events.deleteEventsBeforeAndIncludingID(ctx, appserviceID, eventTableID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLatestTxnID returns the latest available transaction id
|
|
||||||
func (d *Database) GetLatestTxnID(
|
|
||||||
ctx context.Context,
|
|
||||||
) (int, error) {
|
|
||||||
return d.txnID.selectTxnID(ctx)
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
// Copyright 2018 New Vector Ltd
|
|
||||||
// Copyright 2019-2020 The Matrix.org Foundation C.I.C.
|
|
||||||
//
|
|
||||||
// Licensed 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
|
|
||||||
//
|
|
||||||
// http://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.
|
|
||||||
|
|
||||||
package sqlite3
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/internal/sqlutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
const txnIDSchema = `
|
|
||||||
-- Keeps a count of the current transaction ID
|
|
||||||
CREATE TABLE IF NOT EXISTS appservice_counters (
|
|
||||||
name TEXT PRIMARY KEY NOT NULL,
|
|
||||||
last_id INTEGER DEFAULT 1
|
|
||||||
);
|
|
||||||
INSERT OR IGNORE INTO appservice_counters (name, last_id) VALUES('txn_id', 1);
|
|
||||||
`
|
|
||||||
|
|
||||||
const selectTxnIDSQL = `
|
|
||||||
SELECT last_id FROM appservice_counters WHERE name='txn_id'
|
|
||||||
`
|
|
||||||
|
|
||||||
const updateTxnIDSQL = `
|
|
||||||
UPDATE appservice_counters SET last_id=last_id+1 WHERE name='txn_id'
|
|
||||||
`
|
|
||||||
|
|
||||||
type txnStatements struct {
|
|
||||||
db *sql.DB
|
|
||||||
writer sqlutil.Writer
|
|
||||||
selectTxnIDStmt *sql.Stmt
|
|
||||||
updateTxnIDStmt *sql.Stmt
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *txnStatements) prepare(db *sql.DB, writer sqlutil.Writer) (err error) {
|
|
||||||
s.db = db
|
|
||||||
s.writer = writer
|
|
||||||
_, err = db.Exec(txnIDSchema)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.selectTxnIDStmt, err = db.Prepare(selectTxnIDSQL); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.updateTxnIDStmt, err = db.Prepare(updateTxnIDSQL); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// selectTxnID selects the latest ascending transaction ID
|
|
||||||
func (s *txnStatements) selectTxnID(
|
|
||||||
ctx context.Context,
|
|
||||||
) (txnID int, err error) {
|
|
||||||
err = s.writer.Do(s.db, nil, func(txn *sql.Tx) error {
|
|
||||||
err := s.selectTxnIDStmt.QueryRowContext(ctx).Scan(&txnID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.updateTxnIDStmt.ExecContext(ctx)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
// Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
//
|
|
||||||
// Licensed 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
|
|
||||||
//
|
|
||||||
// http://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.
|
|
||||||
|
|
||||||
//go:build !wasm
|
|
||||||
// +build !wasm
|
|
||||||
|
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/appservice/storage/postgres"
|
|
||||||
"github.com/matrix-org/dendrite/appservice/storage/sqlite3"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewDatabase opens a new Postgres or Sqlite database (based on dataSourceName scheme)
|
|
||||||
// and sets DB connection parameters
|
|
||||||
func NewDatabase(dbProperties *config.DatabaseOptions) (Database, error) {
|
|
||||||
switch {
|
|
||||||
case dbProperties.ConnectionString.IsSQLite():
|
|
||||||
return sqlite3.NewDatabase(dbProperties)
|
|
||||||
case dbProperties.ConnectionString.IsPostgres():
|
|
||||||
return postgres.NewDatabase(dbProperties)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unexpected database type")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
// Licensed 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
|
|
||||||
//
|
|
||||||
// http://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.
|
|
||||||
|
|
||||||
package types
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// AppServiceDeviceID is the AS dummy device ID
|
|
||||||
AppServiceDeviceID = "AS_Device"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ApplicationServiceWorkerState is a type that couples an application service,
|
|
||||||
// a lockable condition as well as some other state variables, allowing the
|
|
||||||
// roomserver to notify appservice workers when there are events ready to send
|
|
||||||
// externally to application services.
|
|
||||||
type ApplicationServiceWorkerState struct {
|
|
||||||
AppService config.ApplicationService
|
|
||||||
Cond *sync.Cond
|
|
||||||
// Events ready to be sent
|
|
||||||
EventsReady bool
|
|
||||||
// Backoff exponent (2^x secs). Max 6, aka 64s.
|
|
||||||
Backoff int
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotifyNewEvents wakes up all waiting goroutines, notifying that events remain
|
|
||||||
// in the event queue for this application service worker.
|
|
||||||
func (a *ApplicationServiceWorkerState) NotifyNewEvents() {
|
|
||||||
a.Cond.L.Lock()
|
|
||||||
a.EventsReady = true
|
|
||||||
a.Cond.Broadcast()
|
|
||||||
a.Cond.L.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// FinishEventProcessing marks all events of this worker as being sent to the
|
|
||||||
// application service.
|
|
||||||
func (a *ApplicationServiceWorkerState) FinishEventProcessing() {
|
|
||||||
a.Cond.L.Lock()
|
|
||||||
a.EventsReady = false
|
|
||||||
a.Cond.L.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// WaitForNewEvents causes the calling goroutine to wait on the worker state's
|
|
||||||
// condition for a broadcast or similar wakeup, if there are no events ready.
|
|
||||||
func (a *ApplicationServiceWorkerState) WaitForNewEvents() {
|
|
||||||
a.Cond.L.Lock()
|
|
||||||
if !a.EventsReady {
|
|
||||||
a.Cond.Wait()
|
|
||||||
}
|
|
||||||
a.Cond.L.Unlock()
|
|
||||||
}
|
|
|
@ -1,236 +0,0 @@
|
||||||
// Copyright 2018 Vector Creations Ltd
|
|
||||||
//
|
|
||||||
// Licensed 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
|
|
||||||
//
|
|
||||||
// http://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.
|
|
||||||
|
|
||||||
package workers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/appservice/storage"
|
|
||||||
"github.com/matrix-org/dendrite/appservice/types"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// Maximum size of events sent in each transaction.
|
|
||||||
transactionBatchSize = 50
|
|
||||||
)
|
|
||||||
|
|
||||||
// SetupTransactionWorkers spawns a separate goroutine for each application
|
|
||||||
// service. Each of these "workers" handle taking all events intended for their
|
|
||||||
// app service, batch them up into a single transaction (up to a max transaction
|
|
||||||
// size), then send that off to the AS's /transactions/{txnID} endpoint. It also
|
|
||||||
// handles exponentially backing off in case the AS isn't currently available.
|
|
||||||
func SetupTransactionWorkers(
|
|
||||||
client *http.Client,
|
|
||||||
appserviceDB storage.Database,
|
|
||||||
workerStates []types.ApplicationServiceWorkerState,
|
|
||||||
) error {
|
|
||||||
// Create a worker that handles transmitting events to a single homeserver
|
|
||||||
for _, workerState := range workerStates {
|
|
||||||
// Don't create a worker if this AS doesn't want to receive events
|
|
||||||
if workerState.AppService.URL != "" {
|
|
||||||
go worker(client, appserviceDB, workerState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// worker is a goroutine that sends any queued events to the application service
|
|
||||||
// it is given.
|
|
||||||
func worker(client *http.Client, db storage.Database, ws types.ApplicationServiceWorkerState) {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"appservice": ws.AppService.ID,
|
|
||||||
}).Info("Starting application service")
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Initial check for any leftover events to send from last time
|
|
||||||
eventCount, err := db.CountEventsWithAppServiceID(ctx, ws.AppService.ID)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"appservice": ws.AppService.ID,
|
|
||||||
}).WithError(err).Fatal("appservice worker unable to read queued events from DB")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if eventCount > 0 {
|
|
||||||
ws.NotifyNewEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop forever and keep waiting for more events to send
|
|
||||||
for {
|
|
||||||
// Wait for more events if we've sent all the events in the database
|
|
||||||
ws.WaitForNewEvents()
|
|
||||||
|
|
||||||
// Batch events up into a transaction
|
|
||||||
transactionJSON, txnID, maxEventID, eventsRemaining, err := createTransaction(ctx, db, ws.AppService.ID)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"appservice": ws.AppService.ID,
|
|
||||||
}).WithError(err).Fatal("appservice worker unable to create transaction")
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the events off to the application service
|
|
||||||
// Backoff if the application service does not respond
|
|
||||||
err = send(client, ws.AppService, txnID, transactionJSON)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"appservice": ws.AppService.ID,
|
|
||||||
}).WithError(err).Error("unable to send event")
|
|
||||||
// Backoff
|
|
||||||
backoff(&ws, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// We sent successfully, hooray!
|
|
||||||
ws.Backoff = 0
|
|
||||||
|
|
||||||
// Transactions have a maximum event size, so there may still be some events
|
|
||||||
// left over to send. Keep sending until none are left
|
|
||||||
if !eventsRemaining {
|
|
||||||
ws.FinishEventProcessing()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove sent events from the DB
|
|
||||||
err = db.RemoveEventsBeforeAndIncludingID(ctx, ws.AppService.ID, maxEventID)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"appservice": ws.AppService.ID,
|
|
||||||
}).WithError(err).Fatal("unable to remove appservice events from the database")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// backoff pauses the calling goroutine for a 2^some backoff exponent seconds
|
|
||||||
func backoff(ws *types.ApplicationServiceWorkerState, err error) {
|
|
||||||
// Calculate how long to backoff for
|
|
||||||
backoffDuration := time.Duration(math.Pow(2, float64(ws.Backoff)))
|
|
||||||
backoffSeconds := time.Second * backoffDuration
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"appservice": ws.AppService.ID,
|
|
||||||
}).WithError(err).Warnf("unable to send transactions successfully, backing off for %ds",
|
|
||||||
backoffDuration)
|
|
||||||
|
|
||||||
ws.Backoff++
|
|
||||||
if ws.Backoff > 6 {
|
|
||||||
ws.Backoff = 6
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backoff
|
|
||||||
time.Sleep(backoffSeconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
// createTransaction takes in a slice of AS events, stores them in an AS
|
|
||||||
// transaction, and JSON-encodes the results.
|
|
||||||
func createTransaction(
|
|
||||||
ctx context.Context,
|
|
||||||
db storage.Database,
|
|
||||||
appserviceID string,
|
|
||||||
) (
|
|
||||||
transactionJSON []byte,
|
|
||||||
txnID, maxID int,
|
|
||||||
eventsRemaining bool,
|
|
||||||
err error,
|
|
||||||
) {
|
|
||||||
// Retrieve the latest events from the DB (will return old events if they weren't successfully sent)
|
|
||||||
txnID, maxID, events, eventsRemaining, err := db.GetEventsWithAppServiceID(ctx, appserviceID, transactionBatchSize)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"appservice": appserviceID,
|
|
||||||
}).WithError(err).Fatalf("appservice worker unable to read queued events from DB")
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if these events do not already have a transaction ID
|
|
||||||
if txnID == -1 {
|
|
||||||
// If not, grab next available ID from the DB
|
|
||||||
txnID, err = db.GetLatestTxnID(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, 0, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark new events with current transactionID
|
|
||||||
if err = db.UpdateTxnIDForEvents(ctx, appserviceID, maxID, txnID); err != nil {
|
|
||||||
return nil, 0, 0, false, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var ev []*gomatrixserverlib.HeaderedEvent
|
|
||||||
for i := range events {
|
|
||||||
ev = append(ev, &events[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a transaction and store the events inside
|
|
||||||
transaction := gomatrixserverlib.ApplicationServiceTransaction{
|
|
||||||
Events: gomatrixserverlib.HeaderedToClientEvents(ev, gomatrixserverlib.FormatAll),
|
|
||||||
}
|
|
||||||
|
|
||||||
transactionJSON, err = json.Marshal(transaction)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// send sends events to an application service. Returns an error if an OK was not
|
|
||||||
// received back from the application service or the request timed out.
|
|
||||||
func send(
|
|
||||||
client *http.Client,
|
|
||||||
appservice config.ApplicationService,
|
|
||||||
txnID int,
|
|
||||||
transaction []byte,
|
|
||||||
) (err error) {
|
|
||||||
// PUT a transaction to our AS
|
|
||||||
// https://matrix.org/docs/spec/application_service/r0.1.2#put-matrix-app-v1-transactions-txnid
|
|
||||||
address := fmt.Sprintf("%s/transactions/%d?access_token=%s", appservice.URL, txnID, url.QueryEscape(appservice.HSToken))
|
|
||||||
req, err := http.NewRequest("PUT", address, bytes.NewBuffer(transaction))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer checkNamedErr(resp.Body.Close, &err)
|
|
||||||
|
|
||||||
// Check the AS received the events correctly
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
// TODO: Handle non-200 error codes from application services
|
|
||||||
return fmt.Errorf("non-OK status code %d returned from AS", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkNamedErr calls fn and overwrite err if it was nil and fn returned non-nil
|
|
||||||
func checkNamedErr(fn func() error, err *error) {
|
|
||||||
if e := fn(); e != nil && *err == nil {
|
|
||||||
*err = e
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -212,11 +212,12 @@ plv Users cannot set kick powerlevel higher than their own (2 subtests)
|
||||||
plv Users cannot set redact powerlevel higher than their own (2 subtests)
|
plv Users cannot set redact powerlevel higher than their own (2 subtests)
|
||||||
v1s Check that event streams started after a client joined a room work (SYT-1)
|
v1s Check that event streams started after a client joined a room work (SYT-1)
|
||||||
v1s Event stream catches up fully after many messages
|
v1s Event stream catches up fully after many messages
|
||||||
xxx POST /rooms/:room_id/redact/:event_id as power user redacts message
|
xxx PUT /rooms/:room_id/redact/:event_id/:txn_id as power user redacts message
|
||||||
xxx POST /rooms/:room_id/redact/:event_id as original message sender redacts message
|
xxx PUT /rooms/:room_id/redact/:event_id/:txn_id as original message sender redacts message
|
||||||
xxx POST /rooms/:room_id/redact/:event_id as random user does not redact message
|
xxx PUT /rooms/:room_id/redact/:event_id/:txn_id as random user does not redact message
|
||||||
xxx POST /redact disallows redaction of event in different room
|
xxx PUT /redact disallows redaction of event in different room
|
||||||
xxx Redaction of a redaction redacts the redaction reason
|
xxx Redaction of a redaction redacts the redaction reason
|
||||||
|
xxx PUT /rooms/:room_id/redact/:event_id/:txn_id is idempotent
|
||||||
v1s A departed room is still included in /initialSync (SPEC-216)
|
v1s A departed room is still included in /initialSync (SPEC-216)
|
||||||
v1s Can get rooms/{roomId}/initialSync for a departed room (SPEC-216)
|
v1s Can get rooms/{roomId}/initialSync for a departed room (SPEC-216)
|
||||||
rst Can get rooms/{roomId}/state for a departed room (SPEC-216)
|
rst Can get rooms/{roomId}/state for a departed room (SPEC-216)
|
||||||
|
@ -642,7 +643,7 @@ fed Inbound federation redacts events from erased users
|
||||||
fme Outbound federation can request missing events
|
fme Outbound federation can request missing events
|
||||||
fme Inbound federation can return missing events for world_readable visibility
|
fme Inbound federation can return missing events for world_readable visibility
|
||||||
fme Inbound federation can return missing events for shared visibility
|
fme Inbound federation can return missing events for shared visibility
|
||||||
fme Inbound federation can return missing events for invite visibility
|
fme Inbound federation can return missing events for invited visibility
|
||||||
fme Inbound federation can return missing events for joined visibility
|
fme Inbound federation can return missing events for joined visibility
|
||||||
fme outliers whose auth_events are in a different room are correctly rejected
|
fme outliers whose auth_events are in a different room are correctly rejected
|
||||||
fbk Outbound federation can backfill events
|
fbk Outbound federation can backfill events
|
||||||
|
@ -921,3 +922,34 @@ msc We can't peek into rooms with invited history_visibility
|
||||||
msc We can't peek into rooms with joined history_visibility
|
msc We can't peek into rooms with joined history_visibility
|
||||||
msc Local users can peek by room alias
|
msc Local users can peek by room alias
|
||||||
msc Peeked rooms only turn up in the sync for the device who peeked them
|
msc Peeked rooms only turn up in the sync for the device who peeked them
|
||||||
|
ban 'ban' event respects room powerlevel (2 subtests)
|
||||||
|
inv Test that we can be reinvited to a room we created (11 subtests)
|
||||||
|
fiv Rejecting invite over federation doesn't break incremental /sync
|
||||||
|
pre Presence can be set from sync
|
||||||
|
fst /state returns M_NOT_FOUND for an outlier
|
||||||
|
fst /state_ids returns M_NOT_FOUND for an outlier
|
||||||
|
fst /state returns M_NOT_FOUND for a rejected message event
|
||||||
|
fst /state_ids returns M_NOT_FOUND for a rejected message event
|
||||||
|
fst /state returns M_NOT_FOUND for a rejected state event
|
||||||
|
fst /state_ids returns M_NOT_FOUND for a rejected state event
|
||||||
|
fst Room state after a rejected message event is the same as before
|
||||||
|
fst Room state after a rejected state event is the same as before
|
||||||
|
fpb Federation publicRoom Name/topic keys are correct
|
||||||
|
fed New federated private chats get full presence information (SYN-115) (10 subtests)
|
||||||
|
dvk Rejects invalid device keys
|
||||||
|
rmv User can create and send/receive messages in a room with version 10
|
||||||
|
rmv local user can join room with version 10
|
||||||
|
rmv User can invite local user to room with version 10
|
||||||
|
rmv remote user can join room with version 10
|
||||||
|
rmv User can invite remote user to room with version 10
|
||||||
|
rmv Remote user can backfill in a room with version 10
|
||||||
|
rmv Can reject invites over federation for rooms with version 10
|
||||||
|
rmv Can receive redactions from regular users over federation in room version 10
|
||||||
|
rmv User can create and send/receive messages in a room with version 11
|
||||||
|
rmv local user can join room with version 11
|
||||||
|
rmv User can invite local user to room with version 11
|
||||||
|
rmv remote user can join room with version 11
|
||||||
|
rmv User can invite remote user to room with version 11
|
||||||
|
rmv Remote user can backfill in a room with version 11
|
||||||
|
rmv Can reject invites over federation for rooms with version 11
|
||||||
|
rmv Can receive redactions from regular users over federation in room version 11
|
51
build.cmd
51
build.cmd
|
@ -1,51 +0,0 @@
|
||||||
@echo off
|
|
||||||
|
|
||||||
:ENTRY_POINT
|
|
||||||
setlocal EnableDelayedExpansion
|
|
||||||
|
|
||||||
REM script base dir
|
|
||||||
set SCRIPTDIR=%~dp0
|
|
||||||
set PROJDIR=%SCRIPTDIR:~0,-1%
|
|
||||||
|
|
||||||
REM Put installed packages into ./bin
|
|
||||||
set GOBIN=%PROJDIR%\bin
|
|
||||||
|
|
||||||
set FLAGS=
|
|
||||||
|
|
||||||
REM Check if sources are under Git control
|
|
||||||
if not exist ".git" goto :CHECK_BIN
|
|
||||||
|
|
||||||
REM set BUILD=`git rev-parse --short HEAD \\ ""`
|
|
||||||
FOR /F "tokens=*" %%X IN ('git rev-parse --short HEAD') DO (
|
|
||||||
set BUILD=%%X
|
|
||||||
)
|
|
||||||
|
|
||||||
REM set BRANCH=`(git symbolic-ref --short HEAD \ tr -d \/ ) \\ ""`
|
|
||||||
FOR /F "tokens=*" %%X IN ('git symbolic-ref --short HEAD') DO (
|
|
||||||
set BRANCHRAW=%%X
|
|
||||||
set BRANCH=!BRANCHRAW:/=!
|
|
||||||
)
|
|
||||||
if "%BRANCH%" == "main" set BRANCH=
|
|
||||||
|
|
||||||
set FLAGS=-X github.com/matrix-org/dendrite/internal.branch=%BRANCH% -X github.com/matrix-org/dendrite/internal.build=%BUILD%
|
|
||||||
|
|
||||||
:CHECK_BIN
|
|
||||||
if exist "bin" goto :ALL_SET
|
|
||||||
mkdir "bin"
|
|
||||||
|
|
||||||
:ALL_SET
|
|
||||||
set CGO_ENABLED=1
|
|
||||||
for /D %%P in (cmd\*) do (
|
|
||||||
go build -trimpath -ldflags "%FLAGS%" -v -o ".\bin" ".\%%P"
|
|
||||||
)
|
|
||||||
|
|
||||||
set CGO_ENABLED=0
|
|
||||||
set GOOS=js
|
|
||||||
set GOARCH=wasm
|
|
||||||
go build -trimpath -ldflags "%FLAGS%" -o bin\main.wasm .\cmd\dendritejs-pinecone
|
|
||||||
|
|
||||||
goto :DONE
|
|
||||||
|
|
||||||
:DONE
|
|
||||||
echo Done
|
|
||||||
endlocal
|
|
24
build.sh
24
build.sh
|
@ -1,24 +0,0 @@
|
||||||
#!/bin/sh -eu
|
|
||||||
|
|
||||||
# Put installed packages into ./bin
|
|
||||||
export GOBIN=$PWD/`dirname $0`/bin
|
|
||||||
|
|
||||||
if [ -d ".git" ]
|
|
||||||
then
|
|
||||||
export BUILD=`git rev-parse --short HEAD || ""`
|
|
||||||
export BRANCH=`(git symbolic-ref --short HEAD | tr -d \/ ) || ""`
|
|
||||||
if [ "$BRANCH" = main ]
|
|
||||||
then
|
|
||||||
export BRANCH=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
export FLAGS="-X github.com/matrix-org/dendrite/internal.branch=$BRANCH -X github.com/matrix-org/dendrite/internal.build=$BUILD"
|
|
||||||
else
|
|
||||||
export FLAGS=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p bin
|
|
||||||
|
|
||||||
CGO_ENABLED=1 go build -trimpath -ldflags "$FLAGS" -v -o "bin/" ./cmd/...
|
|
||||||
|
|
||||||
CGO_ENABLED=0 GOOS=js GOARCH=wasm go build -trimpath -ldflags "$FLAGS" -o bin/main.wasm ./cmd/dendritejs-pinecone
|
|
|
@ -29,13 +29,16 @@ import (
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/rooms"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/rooms"
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
|
||||||
"github.com/matrix-org/dendrite/federationapi"
|
"github.com/matrix-org/dendrite/federationapi"
|
||||||
|
"github.com/matrix-org/dendrite/internal/caching"
|
||||||
"github.com/matrix-org/dendrite/internal/httputil"
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
"github.com/matrix-org/dendrite/keyserver"
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
"github.com/matrix-org/dendrite/roomserver"
|
"github.com/matrix-org/dendrite/roomserver"
|
||||||
"github.com/matrix-org/dendrite/setup"
|
"github.com/matrix-org/dendrite/setup"
|
||||||
"github.com/matrix-org/dendrite/setup/base"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
|
"github.com/matrix-org/dendrite/setup/process"
|
||||||
"github.com/matrix-org/dendrite/userapi"
|
"github.com/matrix-org/dendrite/userapi"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
|
||||||
|
@ -158,9 +161,8 @@ func startup() {
|
||||||
pManager.AddPeer("wss://pinecone.matrix.org/public")
|
pManager.AddPeer("wss://pinecone.matrix.org/public")
|
||||||
|
|
||||||
cfg := &config.Dendrite{}
|
cfg := &config.Dendrite{}
|
||||||
cfg.Defaults(true)
|
cfg.Defaults(config.DefaultOpts{Generate: true, SingleDatabase: false})
|
||||||
cfg.UserAPI.AccountDatabase.ConnectionString = "file:/idb/dendritejs_account.db"
|
cfg.UserAPI.AccountDatabase.ConnectionString = "file:/idb/dendritejs_account.db"
|
||||||
cfg.AppServiceAPI.Database.ConnectionString = "file:/idb/dendritejs_appservice.db"
|
|
||||||
cfg.FederationAPI.Database.ConnectionString = "file:/idb/dendritejs_fedsender.db"
|
cfg.FederationAPI.Database.ConnectionString = "file:/idb/dendritejs_fedsender.db"
|
||||||
cfg.MediaAPI.Database.ConnectionString = "file:/idb/dendritejs_mediaapi.db"
|
cfg.MediaAPI.Database.ConnectionString = "file:/idb/dendritejs_mediaapi.db"
|
||||||
cfg.RoomServer.Database.ConnectionString = "file:/idb/dendritejs_roomserver.db"
|
cfg.RoomServer.Database.ConnectionString = "file:/idb/dendritejs_roomserver.db"
|
||||||
|
@ -170,37 +172,37 @@ func startup() {
|
||||||
cfg.Global.TrustedIDServers = []string{}
|
cfg.Global.TrustedIDServers = []string{}
|
||||||
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
||||||
cfg.Global.PrivateKey = sk
|
cfg.Global.PrivateKey = sk
|
||||||
cfg.Global.ServerName = gomatrixserverlib.ServerName(hex.EncodeToString(pk))
|
cfg.Global.ServerName = spec.ServerName(hex.EncodeToString(pk))
|
||||||
|
cfg.ClientAPI.RegistrationDisabled = false
|
||||||
|
cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled = true
|
||||||
|
|
||||||
if err := cfg.Derive(); err != nil {
|
if err := cfg.Derive(); err != nil {
|
||||||
logrus.Fatalf("Failed to derive values from config: %s", err)
|
logrus.Fatalf("Failed to derive values from config: %s", err)
|
||||||
}
|
}
|
||||||
base := base.NewBaseDendrite(cfg, "Monolith")
|
natsInstance := jetstream.NATSInstance{}
|
||||||
defer base.Close() // nolint: errcheck
|
processCtx := process.NewProcessContext()
|
||||||
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
routers := httputil.NewRouters()
|
||||||
|
caches := caching.NewRistrettoCache(cfg.Global.Cache.EstimatedMaxSize, cfg.Global.Cache.MaxAge, caching.EnableMetrics)
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.EnableMetrics)
|
||||||
|
|
||||||
accountDB := base.CreateAccountsDB()
|
federation := conn.CreateFederationClient(cfg, pSessions)
|
||||||
federation := conn.CreateFederationClient(base, pSessions)
|
|
||||||
keyAPI := keyserver.NewInternalAPI(base, &base.Cfg.KeyServer, federation)
|
|
||||||
|
|
||||||
serverKeyAPI := &signing.YggdrasilKeys{}
|
serverKeyAPI := &signing.YggdrasilKeys{}
|
||||||
keyRing := serverKeyAPI.KeyRing()
|
keyRing := serverKeyAPI.KeyRing()
|
||||||
|
|
||||||
rsAPI := roomserver.NewInternalAPI(base)
|
fedSenderAPI := federationapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, federation, rsAPI, caches, keyRing, true)
|
||||||
|
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, federation, caching.EnableMetrics, fedSenderAPI.IsBlacklistedOrBackingOff)
|
||||||
userAPI := userapi.NewInternalAPI(base, accountDB, &cfg.UserAPI, nil, keyAPI, rsAPI, base.PushGatewayHTTPClient())
|
|
||||||
keyAPI.SetUserAPI(userAPI)
|
|
||||||
|
|
||||||
asQuery := appservice.NewInternalAPI(
|
asQuery := appservice.NewInternalAPI(
|
||||||
base, userAPI, rsAPI,
|
processCtx, cfg, &natsInstance, userAPI, rsAPI,
|
||||||
)
|
)
|
||||||
rsAPI.SetAppserviceAPI(asQuery)
|
rsAPI.SetAppserviceAPI(asQuery)
|
||||||
fedSenderAPI := federationapi.NewInternalAPI(base, federation, rsAPI, base.Caches, keyRing, true)
|
|
||||||
rsAPI.SetFederationAPI(fedSenderAPI, keyRing)
|
rsAPI.SetFederationAPI(fedSenderAPI, keyRing)
|
||||||
|
|
||||||
monolith := setup.Monolith{
|
monolith := setup.Monolith{
|
||||||
Config: base.Cfg,
|
Config: cfg,
|
||||||
AccountDB: accountDB,
|
Client: conn.CreateClient(pSessions),
|
||||||
Client: conn.CreateClient(base, pSessions),
|
|
||||||
FedClient: federation,
|
FedClient: federation,
|
||||||
KeyRing: keyRing,
|
KeyRing: keyRing,
|
||||||
|
|
||||||
|
@ -208,28 +210,18 @@ func startup() {
|
||||||
FederationAPI: fedSenderAPI,
|
FederationAPI: fedSenderAPI,
|
||||||
RoomserverAPI: rsAPI,
|
RoomserverAPI: rsAPI,
|
||||||
UserAPI: userAPI,
|
UserAPI: userAPI,
|
||||||
KeyAPI: keyAPI,
|
|
||||||
//ServerKeyAPI: serverKeyAPI,
|
//ServerKeyAPI: serverKeyAPI,
|
||||||
ExtPublicRoomsProvider: rooms.NewPineconeRoomProvider(pRouter, pSessions, fedSenderAPI, federation),
|
ExtPublicRoomsProvider: rooms.NewPineconeRoomProvider(pRouter, pSessions, fedSenderAPI, federation),
|
||||||
}
|
}
|
||||||
monolith.AddAllPublicRoutes(
|
monolith.AddAllPublicRoutes(processCtx, cfg, routers, cm, &natsInstance, caches, caching.EnableMetrics)
|
||||||
base.ProcessContext,
|
|
||||||
base.PublicClientAPIMux,
|
|
||||||
base.PublicFederationAPIMux,
|
|
||||||
base.PublicKeyAPIMux,
|
|
||||||
base.PublicWellKnownAPIMux,
|
|
||||||
base.PublicMediaAPIMux,
|
|
||||||
base.SynapseAdminMux,
|
|
||||||
)
|
|
||||||
|
|
||||||
httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath()
|
httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath()
|
||||||
httpRouter.PathPrefix(httputil.InternalPathPrefix).Handler(base.InternalAPIMux)
|
httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(routers.Client)
|
||||||
httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(base.PublicClientAPIMux)
|
httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(routers.Media)
|
||||||
httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
|
|
||||||
|
|
||||||
p2pRouter := pSessions.Protocol("matrix").HTTP().Mux()
|
p2pRouter := pSessions.Protocol("matrix").HTTP().Mux()
|
||||||
p2pRouter.Handle(httputil.PublicFederationPathPrefix, base.PublicFederationAPIMux)
|
p2pRouter.Handle(httputil.PublicFederationPathPrefix, routers.Federation)
|
||||||
p2pRouter.Handle(httputil.PublicMediaPathPrefix, base.PublicMediaAPIMux)
|
p2pRouter.Handle(httputil.PublicMediaPathPrefix, routers.Media)
|
||||||
|
|
||||||
// Expose the matrix APIs via fetch - for local traffic
|
// Expose the matrix APIs via fetch - for local traffic
|
||||||
go func() {
|
go func() {
|
|
@ -1,4 +1,10 @@
|
||||||
FROM docker.io/golang:1.17-alpine AS base
|
# Pinned to alpine3.18 until https://github.com/mattn/go-sqlite3/issues/1164 is solved
|
||||||
|
FROM docker.io/golang:1.21-alpine3.18 AS base
|
||||||
|
|
||||||
|
#
|
||||||
|
# Needs to be separate from the main Dockerfile for OpenShift,
|
||||||
|
# as --target is not supported there.
|
||||||
|
#
|
||||||
|
|
||||||
RUN apk --update --no-cache add bash build-base
|
RUN apk --update --no-cache add bash build-base
|
||||||
|
|
||||||
|
@ -7,13 +13,13 @@ WORKDIR /build
|
||||||
COPY . /build
|
COPY . /build
|
||||||
|
|
||||||
RUN mkdir -p bin
|
RUN mkdir -p bin
|
||||||
RUN go build -trimpath -o bin/ ./cmd/dendrite-polylith-multi
|
RUN go build -trimpath -o bin/ ./cmd/dendrite-demo-pinecone
|
||||||
RUN go build -trimpath -o bin/ ./cmd/goose
|
|
||||||
RUN go build -trimpath -o bin/ ./cmd/create-account
|
RUN go build -trimpath -o bin/ ./cmd/create-account
|
||||||
RUN go build -trimpath -o bin/ ./cmd/generate-keys
|
RUN go build -trimpath -o bin/ ./cmd/generate-keys
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
LABEL org.opencontainers.image.title="Dendrite (Polylith)"
|
RUN apk --update --no-cache add curl
|
||||||
|
LABEL org.opencontainers.image.title="Dendrite (Pinecone demo)"
|
||||||
LABEL org.opencontainers.image.description="Next-generation Matrix homeserver written in Go"
|
LABEL org.opencontainers.image.description="Next-generation Matrix homeserver written in Go"
|
||||||
LABEL org.opencontainers.image.source="https://github.com/matrix-org/dendrite"
|
LABEL org.opencontainers.image.source="https://github.com/matrix-org/dendrite"
|
||||||
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
||||||
|
@ -23,4 +29,4 @@ COPY --from=base /build/bin/* /usr/bin/
|
||||||
VOLUME /etc/dendrite
|
VOLUME /etc/dendrite
|
||||||
WORKDIR /etc/dendrite
|
WORKDIR /etc/dendrite
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/dendrite-polylith-multi"]
|
ENTRYPOINT ["/usr/bin/dendrite-demo-pinecone"]
|
|
@ -1,4 +1,10 @@
|
||||||
FROM docker.io/golang:1.17-alpine AS base
|
# Pinned to alpine3.18 until https://github.com/mattn/go-sqlite3/issues/1164 is solved
|
||||||
|
FROM docker.io/golang:1.21-alpine3.18 AS base
|
||||||
|
|
||||||
|
#
|
||||||
|
# Needs to be separate from the main Dockerfile for OpenShift,
|
||||||
|
# as --target is not supported there.
|
||||||
|
#
|
||||||
|
|
||||||
RUN apk --update --no-cache add bash build-base
|
RUN apk --update --no-cache add bash build-base
|
||||||
|
|
||||||
|
@ -7,13 +13,12 @@ WORKDIR /build
|
||||||
COPY . /build
|
COPY . /build
|
||||||
|
|
||||||
RUN mkdir -p bin
|
RUN mkdir -p bin
|
||||||
RUN go build -trimpath -o bin/ ./cmd/dendrite-monolith-server
|
RUN go build -trimpath -o bin/ ./cmd/dendrite-demo-yggdrasil
|
||||||
RUN go build -trimpath -o bin/ ./cmd/goose
|
|
||||||
RUN go build -trimpath -o bin/ ./cmd/create-account
|
RUN go build -trimpath -o bin/ ./cmd/create-account
|
||||||
RUN go build -trimpath -o bin/ ./cmd/generate-keys
|
RUN go build -trimpath -o bin/ ./cmd/generate-keys
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
LABEL org.opencontainers.image.title="Dendrite (Monolith)"
|
LABEL org.opencontainers.image.title="Dendrite (Yggdrasil demo)"
|
||||||
LABEL org.opencontainers.image.description="Next-generation Matrix homeserver written in Go"
|
LABEL org.opencontainers.image.description="Next-generation Matrix homeserver written in Go"
|
||||||
LABEL org.opencontainers.image.source="https://github.com/matrix-org/dendrite"
|
LABEL org.opencontainers.image.source="https://github.com/matrix-org/dendrite"
|
||||||
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
||||||
|
@ -23,4 +28,4 @@ COPY --from=base /build/bin/* /usr/bin/
|
||||||
VOLUME /etc/dendrite
|
VOLUME /etc/dendrite
|
||||||
WORKDIR /etc/dendrite
|
WORKDIR /etc/dendrite
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/dendrite-monolith-server"]
|
ENTRYPOINT ["/usr/bin/dendrite-demo-yggdrasil"]
|
|
@ -5,30 +5,28 @@ These are Docker images for Dendrite!
|
||||||
They can be found on Docker Hub:
|
They can be found on Docker Hub:
|
||||||
|
|
||||||
- [matrixdotorg/dendrite-monolith](https://hub.docker.com/r/matrixdotorg/dendrite-monolith) for monolith deployments
|
- [matrixdotorg/dendrite-monolith](https://hub.docker.com/r/matrixdotorg/dendrite-monolith) for monolith deployments
|
||||||
- [matrixdotorg/dendrite-polylith](https://hub.docker.com/r/matrixdotorg/dendrite-polylith) for polylith deployments
|
|
||||||
|
|
||||||
## Dockerfiles
|
## Dockerfile
|
||||||
|
|
||||||
The `Dockerfile` builds the base image which contains all of the Dendrite
|
The `Dockerfile` is a multistage file which can build Dendrite. From the root of the Dendrite
|
||||||
components. The `Dockerfile.component` file takes the given component, as
|
repository, run:
|
||||||
specified with `--buildarg component=` from the base image and produce
|
|
||||||
smaller component-specific images, which are substantially smaller and do
|
|
||||||
not contain the Go toolchain etc.
|
|
||||||
|
|
||||||
## Compose files
|
```
|
||||||
|
docker build . -t matrixdotorg/dendrite-monolith
|
||||||
|
```
|
||||||
|
|
||||||
There are three sample `docker-compose` files:
|
## Compose file
|
||||||
|
|
||||||
- `docker-compose.monolith.yml` which runs a monolith Dendrite deployment
|
There is one sample `docker-compose` files:
|
||||||
- `docker-compose.polylith.yml` which runs a polylith Dendrite deployment
|
|
||||||
|
- `docker-compose.yml` which runs a Dendrite deployment with Postgres
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
The `docker-compose` files refer to the `/etc/dendrite` volume as where the
|
The `docker-compose` files refer to the `/etc/dendrite` volume as where the
|
||||||
runtime config should come from. The mounted folder must contain:
|
runtime config should come from. The mounted folder must contain:
|
||||||
|
|
||||||
- `dendrite.yaml` configuration file (from the [Docker config folder](https://github.com/matrix-org/dendrite/tree/master/build/docker/config)
|
- `dendrite.yaml` configuration file (based on one of the sample config files)
|
||||||
sample in the `build/docker/config` folder of this repository.)
|
|
||||||
- `matrix_key.pem` server key, as generated using `cmd/generate-keys`
|
- `matrix_key.pem` server key, as generated using `cmd/generate-keys`
|
||||||
- `server.crt` certificate file
|
- `server.crt` certificate file
|
||||||
- `server.key` private key file for the above certificate
|
- `server.key` private key file for the above certificate
|
||||||
|
@ -47,24 +45,14 @@ docker run --rm --entrypoint="" \
|
||||||
|
|
||||||
The key files will now exist in your current working directory, and can be mounted into place.
|
The key files will now exist in your current working directory, and can be mounted into place.
|
||||||
|
|
||||||
## Starting Dendrite as a monolith deployment
|
## Starting Dendrite
|
||||||
|
|
||||||
Create your config based on the [`dendrite.yaml`](https://github.com/matrix-org/dendrite/tree/master/build/docker/config) configuration file in the `build/docker/config` folder of this repository.
|
Create your config based on the [`dendrite-sample.yaml`](https://github.com/matrix-org/dendrite/blob/main/dendrite-sample.yaml) sample configuration file.
|
||||||
|
|
||||||
Then start the deployment:
|
Then start the deployment:
|
||||||
|
|
||||||
```
|
```
|
||||||
docker-compose -f docker-compose.monolith.yml up
|
docker-compose -f docker-compose.yml up
|
||||||
```
|
|
||||||
|
|
||||||
## Starting Dendrite as a polylith deployment
|
|
||||||
|
|
||||||
Create your config based on the [`dendrite-config.yaml`](https://github.com/matrix-org/dendrite/tree/master/build/docker/config) configuration file in the `build/docker/config` folder of this repository.
|
|
||||||
|
|
||||||
Then start the deployment:
|
|
||||||
|
|
||||||
```
|
|
||||||
docker-compose -f docker-compose.polylith.yml up
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building the images
|
## Building the images
|
||||||
|
|
|
@ -1,348 +0,0 @@
|
||||||
# This is the Dendrite configuration file.
|
|
||||||
#
|
|
||||||
# The configuration is split up into sections - each Dendrite component has a
|
|
||||||
# configuration section, in addition to the "global" section which applies to
|
|
||||||
# all components.
|
|
||||||
#
|
|
||||||
# At a minimum, to get started, you will need to update the settings in the
|
|
||||||
# "global" section for your deployment, and you will need to check that the
|
|
||||||
# database "connection_string" line in each component section is correct.
|
|
||||||
#
|
|
||||||
# Each component with a "database" section can accept the following formats
|
|
||||||
# for "connection_string":
|
|
||||||
# SQLite: file:filename.db
|
|
||||||
# file:///path/to/filename.db
|
|
||||||
# PostgreSQL: postgresql://user:pass@hostname/database?params=...
|
|
||||||
#
|
|
||||||
# SQLite is embedded into Dendrite and therefore no further prerequisites are
|
|
||||||
# needed for the database when using SQLite mode. However, performance with
|
|
||||||
# PostgreSQL is significantly better and recommended for multi-user deployments.
|
|
||||||
# SQLite is typically around 20-30% slower than PostgreSQL when tested with a
|
|
||||||
# small number of users and likely will perform worse still with a higher volume
|
|
||||||
# of users.
|
|
||||||
#
|
|
||||||
# The "max_open_conns" and "max_idle_conns" settings configure the maximum
|
|
||||||
# number of open/idle database connections. The value 0 will use the database
|
|
||||||
# engine default, and a negative value will use unlimited connections. The
|
|
||||||
# "conn_max_lifetime" option controls the maximum length of time a database
|
|
||||||
# connection can be idle in seconds - a negative value is unlimited.
|
|
||||||
|
|
||||||
# The version of the configuration file.
|
|
||||||
version: 2
|
|
||||||
|
|
||||||
# Global Matrix configuration. This configuration applies to all components.
|
|
||||||
global:
|
|
||||||
# The domain name of this homeserver.
|
|
||||||
server_name: example.com
|
|
||||||
|
|
||||||
# The path to the signing private key file, used to sign requests and events.
|
|
||||||
private_key: matrix_key.pem
|
|
||||||
|
|
||||||
# The paths and expiry timestamps (as a UNIX timestamp in millisecond precision)
|
|
||||||
# to old signing private keys that were formerly in use on this domain. These
|
|
||||||
# keys will not be used for federation request or event signing, but will be
|
|
||||||
# provided to any other homeserver that asks when trying to verify old events.
|
|
||||||
# old_private_keys:
|
|
||||||
# - private_key: old_matrix_key.pem
|
|
||||||
# expired_at: 1601024554498
|
|
||||||
|
|
||||||
# How long a remote server can cache our server signing key before requesting it
|
|
||||||
# again. Increasing this number will reduce the number of requests made by other
|
|
||||||
# servers for our key but increases the period that a compromised key will be
|
|
||||||
# considered valid by other homeservers.
|
|
||||||
key_validity_period: 168h0m0s
|
|
||||||
|
|
||||||
# The server name to delegate server-server communications to, with optional port
|
|
||||||
# e.g. localhost:443
|
|
||||||
well_known_server_name: ""
|
|
||||||
|
|
||||||
# Lists of domains that the server will trust as identity servers to verify third
|
|
||||||
# party identifiers such as phone numbers and email addresses.
|
|
||||||
trusted_third_party_id_servers:
|
|
||||||
- matrix.org
|
|
||||||
- vector.im
|
|
||||||
|
|
||||||
# Disables federation. Dendrite will not be able to make any outbound HTTP requests
|
|
||||||
# to other servers and the federation API will not be exposed.
|
|
||||||
disable_federation: false
|
|
||||||
|
|
||||||
# Configures the handling of presence events.
|
|
||||||
presence:
|
|
||||||
# Whether inbound presence events are allowed, e.g. receiving presence events from other servers
|
|
||||||
enable_inbound: false
|
|
||||||
# Whether outbound presence events are allowed, e.g. sending presence events to other servers
|
|
||||||
enable_outbound: false
|
|
||||||
|
|
||||||
# Configuration for NATS JetStream
|
|
||||||
jetstream:
|
|
||||||
# A list of NATS Server addresses to connect to. If none are specified, an
|
|
||||||
# internal NATS server will be started automatically when running Dendrite
|
|
||||||
# in monolith mode. It is required to specify the address of at least one
|
|
||||||
# NATS Server node if running in polylith mode.
|
|
||||||
addresses:
|
|
||||||
- jetstream:4222
|
|
||||||
|
|
||||||
# Keep all NATS streams in memory, rather than persisting it to the storage
|
|
||||||
# path below. This option is present primarily for integration testing and
|
|
||||||
# should not be used on a real world Dendrite deployment.
|
|
||||||
in_memory: false
|
|
||||||
|
|
||||||
# Persistent directory to store JetStream streams in. This directory
|
|
||||||
# should be preserved across Dendrite restarts.
|
|
||||||
storage_path: ./
|
|
||||||
|
|
||||||
# The prefix to use for stream names for this homeserver - really only
|
|
||||||
# useful if running more than one Dendrite on the same NATS deployment.
|
|
||||||
topic_prefix: Dendrite
|
|
||||||
|
|
||||||
# Configuration for Prometheus metric collection.
|
|
||||||
metrics:
|
|
||||||
# Whether or not Prometheus metrics are enabled.
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
# HTTP basic authentication to protect access to monitoring.
|
|
||||||
basic_auth:
|
|
||||||
username: metrics
|
|
||||||
password: metrics
|
|
||||||
|
|
||||||
# DNS cache options. The DNS cache may reduce the load on DNS servers
|
|
||||||
# if there is no local caching resolver available for use.
|
|
||||||
dns_cache:
|
|
||||||
# Whether or not the DNS cache is enabled.
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
# Maximum number of entries to hold in the DNS cache, and
|
|
||||||
# for how long those items should be considered valid in seconds.
|
|
||||||
cache_size: 256
|
|
||||||
cache_lifetime: 300
|
|
||||||
|
|
||||||
# Configuration for the Appservice API.
|
|
||||||
app_service_api:
|
|
||||||
internal_api:
|
|
||||||
listen: http://0.0.0.0:7777
|
|
||||||
connect: http://appservice_api:7777
|
|
||||||
database:
|
|
||||||
connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_appservice?sslmode=disable
|
|
||||||
max_open_conns: 10
|
|
||||||
max_idle_conns: 2
|
|
||||||
conn_max_lifetime: -1
|
|
||||||
|
|
||||||
# Appservice configuration files to load into this homeserver.
|
|
||||||
config_files: []
|
|
||||||
|
|
||||||
# Configuration for the Client API.
|
|
||||||
client_api:
|
|
||||||
internal_api:
|
|
||||||
listen: http://0.0.0.0:7771
|
|
||||||
connect: http://client_api:7771
|
|
||||||
external_api:
|
|
||||||
listen: http://0.0.0.0:8071
|
|
||||||
|
|
||||||
# Prevents new users from being able to register on this homeserver, except when
|
|
||||||
# using the registration shared secret below.
|
|
||||||
registration_disabled: false
|
|
||||||
|
|
||||||
# If set, allows registration by anyone who knows the shared secret, regardless of
|
|
||||||
# whether registration is otherwise disabled.
|
|
||||||
registration_shared_secret: ""
|
|
||||||
|
|
||||||
# Whether to require reCAPTCHA for registration.
|
|
||||||
enable_registration_captcha: false
|
|
||||||
|
|
||||||
# Settings for ReCAPTCHA.
|
|
||||||
recaptcha_public_key: ""
|
|
||||||
recaptcha_private_key: ""
|
|
||||||
recaptcha_bypass_secret: ""
|
|
||||||
recaptcha_siteverify_api: ""
|
|
||||||
|
|
||||||
# TURN server information that this homeserver should send to clients.
|
|
||||||
turn:
|
|
||||||
turn_user_lifetime: ""
|
|
||||||
turn_uris: []
|
|
||||||
turn_shared_secret: ""
|
|
||||||
turn_username: ""
|
|
||||||
turn_password: ""
|
|
||||||
|
|
||||||
# Settings for rate-limited endpoints. Rate limiting will kick in after the
|
|
||||||
# threshold number of "slots" have been taken by requests from a specific
|
|
||||||
# host. Each "slot" will be released after the cooloff time in milliseconds.
|
|
||||||
rate_limiting:
|
|
||||||
enabled: true
|
|
||||||
threshold: 5
|
|
||||||
cooloff_ms: 500
|
|
||||||
|
|
||||||
# Configuration for the Federation API.
|
|
||||||
federation_api:
|
|
||||||
internal_api:
|
|
||||||
listen: http://0.0.0.0:7772
|
|
||||||
connect: http://federation_api:7772
|
|
||||||
external_api:
|
|
||||||
listen: http://0.0.0.0:8072
|
|
||||||
database:
|
|
||||||
connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_federationapi?sslmode=disable
|
|
||||||
max_open_conns: 10
|
|
||||||
max_idle_conns: 2
|
|
||||||
conn_max_lifetime: -1
|
|
||||||
|
|
||||||
# How many times we will try to resend a failed transaction to a specific server. The
|
|
||||||
# backoff is 2**x seconds, so 1 = 2 seconds, 2 = 4 seconds, 3 = 8 seconds etc.
|
|
||||||
send_max_retries: 16
|
|
||||||
|
|
||||||
# Disable the validation of TLS certificates of remote federated homeservers. Do not
|
|
||||||
# enable this option in production as it presents a security risk!
|
|
||||||
disable_tls_validation: false
|
|
||||||
|
|
||||||
# Use the following proxy server for outbound federation traffic.
|
|
||||||
proxy_outbound:
|
|
||||||
enabled: false
|
|
||||||
protocol: http
|
|
||||||
host: localhost
|
|
||||||
port: 8080
|
|
||||||
|
|
||||||
# Perspective keyservers to use as a backup when direct key fetches fail. This may
|
|
||||||
# be required to satisfy key requests for servers that are no longer online when
|
|
||||||
# joining some rooms.
|
|
||||||
key_perspectives:
|
|
||||||
- server_name: matrix.org
|
|
||||||
keys:
|
|
||||||
- key_id: ed25519:auto
|
|
||||||
public_key: Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw
|
|
||||||
- key_id: ed25519:a_RXGa
|
|
||||||
public_key: l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ
|
|
||||||
|
|
||||||
# This option will control whether Dendrite will prefer to look up keys directly
|
|
||||||
# or whether it should try perspective servers first, using direct fetches as a
|
|
||||||
# last resort.
|
|
||||||
prefer_direct_fetch: false
|
|
||||||
|
|
||||||
# Configuration for the Key Server (for end-to-end encryption).
|
|
||||||
key_server:
|
|
||||||
internal_api:
|
|
||||||
listen: http://0.0.0.0:7779
|
|
||||||
connect: http://key_server:7779
|
|
||||||
database:
|
|
||||||
connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_keyserver?sslmode=disable
|
|
||||||
max_open_conns: 10
|
|
||||||
max_idle_conns: 2
|
|
||||||
conn_max_lifetime: -1
|
|
||||||
|
|
||||||
# Configuration for the Media API.
|
|
||||||
media_api:
|
|
||||||
internal_api:
|
|
||||||
listen: http://0.0.0.0:7774
|
|
||||||
connect: http://media_api:7774
|
|
||||||
external_api:
|
|
||||||
listen: http://0.0.0.0:8074
|
|
||||||
database:
|
|
||||||
connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_mediaapi?sslmode=disable
|
|
||||||
max_open_conns: 10
|
|
||||||
max_idle_conns: 2
|
|
||||||
conn_max_lifetime: -1
|
|
||||||
|
|
||||||
# Storage path for uploaded media. May be relative or absolute.
|
|
||||||
base_path: /var/dendrite/media
|
|
||||||
|
|
||||||
# The maximum allowed file size (in bytes) for media uploads to this homeserver
|
|
||||||
# (0 = unlimited).
|
|
||||||
max_file_size_bytes: 10485760
|
|
||||||
|
|
||||||
# Whether to dynamically generate thumbnails if needed.
|
|
||||||
dynamic_thumbnails: false
|
|
||||||
|
|
||||||
# The maximum number of simultaneous thumbnail generators to run.
|
|
||||||
max_thumbnail_generators: 10
|
|
||||||
|
|
||||||
# A list of thumbnail sizes to be generated for media content.
|
|
||||||
thumbnail_sizes:
|
|
||||||
- width: 32
|
|
||||||
height: 32
|
|
||||||
method: crop
|
|
||||||
- width: 96
|
|
||||||
height: 96
|
|
||||||
method: crop
|
|
||||||
- width: 640
|
|
||||||
height: 480
|
|
||||||
method: scale
|
|
||||||
|
|
||||||
# Configuration for experimental MSC's
|
|
||||||
mscs:
|
|
||||||
# A list of enabled MSC's
|
|
||||||
# Currently valid values are:
|
|
||||||
# - msc2836 (Threading, see https://github.com/matrix-org/matrix-doc/pull/2836)
|
|
||||||
# - msc2946 (Spaces Summary, see https://github.com/matrix-org/matrix-doc/pull/2946)
|
|
||||||
mscs: []
|
|
||||||
database:
|
|
||||||
connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_mscs?sslmode=disable
|
|
||||||
max_open_conns: 5
|
|
||||||
max_idle_conns: 2
|
|
||||||
conn_max_lifetime: -1
|
|
||||||
|
|
||||||
# Configuration for the Room Server.
|
|
||||||
room_server:
|
|
||||||
internal_api:
|
|
||||||
listen: http://0.0.0.0:7770
|
|
||||||
connect: http://room_server:7770
|
|
||||||
database:
|
|
||||||
connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_roomserver?sslmode=disable
|
|
||||||
max_open_conns: 10
|
|
||||||
max_idle_conns: 2
|
|
||||||
conn_max_lifetime: -1
|
|
||||||
|
|
||||||
# Configuration for the Sync API.
|
|
||||||
sync_api:
|
|
||||||
internal_api:
|
|
||||||
listen: http://0.0.0.0:7773
|
|
||||||
connect: http://sync_api:7773
|
|
||||||
external_api:
|
|
||||||
listen: http://0.0.0.0:8073
|
|
||||||
database:
|
|
||||||
connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_syncapi?sslmode=disable
|
|
||||||
max_open_conns: 10
|
|
||||||
max_idle_conns: 2
|
|
||||||
conn_max_lifetime: -1
|
|
||||||
|
|
||||||
# Configuration for the User API.
|
|
||||||
user_api:
|
|
||||||
internal_api:
|
|
||||||
listen: http://0.0.0.0:7781
|
|
||||||
connect: http://user_api:7781
|
|
||||||
account_database:
|
|
||||||
connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_userapi_accounts?sslmode=disable
|
|
||||||
max_open_conns: 10
|
|
||||||
max_idle_conns: 2
|
|
||||||
conn_max_lifetime: -1
|
|
||||||
|
|
||||||
# Configuration for the Push Server API.
|
|
||||||
push_server:
|
|
||||||
internal_api:
|
|
||||||
listen: http://localhost:7782
|
|
||||||
connect: http://localhost:7782
|
|
||||||
database:
|
|
||||||
connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_pushserver?sslmode=disable
|
|
||||||
max_open_conns: 10
|
|
||||||
max_idle_conns: 2
|
|
||||||
conn_max_lifetime: -1
|
|
||||||
|
|
||||||
# Configuration for Opentracing.
|
|
||||||
# See https://github.com/matrix-org/dendrite/tree/master/docs/tracing for information on
|
|
||||||
# how this works and how to set it up.
|
|
||||||
tracing:
|
|
||||||
enabled: false
|
|
||||||
jaeger:
|
|
||||||
serviceName: ""
|
|
||||||
disabled: false
|
|
||||||
rpc_metrics: false
|
|
||||||
tags: []
|
|
||||||
sampler: null
|
|
||||||
reporter: null
|
|
||||||
headers: null
|
|
||||||
baggage_restrictions: null
|
|
||||||
throttler: null
|
|
||||||
|
|
||||||
# Logging configuration, in addition to the standard logging that is sent to
|
|
||||||
# stdout by Dendrite.
|
|
||||||
logging:
|
|
||||||
- type: file
|
|
||||||
level: info
|
|
||||||
params:
|
|
||||||
path: /var/log/dendrite
|
|
|
@ -1,44 +0,0 @@
|
||||||
version: "3.4"
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
hostname: postgres
|
|
||||||
image: postgres:14
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- ./postgres/create_db.sh:/docker-entrypoint-initdb.d/20-create_db.sh
|
|
||||||
# To persist your PostgreSQL databases outside of the Docker image,
|
|
||||||
# to prevent data loss, modify the following ./path_to path:
|
|
||||||
- ./path_to/postgresql:/var/lib/postgresql/data
|
|
||||||
environment:
|
|
||||||
POSTGRES_PASSWORD: itsasecret
|
|
||||||
POSTGRES_USER: dendrite
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U dendrite"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
|
|
||||||
monolith:
|
|
||||||
hostname: monolith
|
|
||||||
image: matrixdotorg/dendrite-monolith:latest
|
|
||||||
command: [
|
|
||||||
"--tls-cert=server.crt",
|
|
||||||
"--tls-key=server.key"
|
|
||||||
]
|
|
||||||
ports:
|
|
||||||
- 8008:8008
|
|
||||||
- 8448:8448
|
|
||||||
volumes:
|
|
||||||
- ./config:/etc/dendrite
|
|
||||||
- ./media:/var/dendrite/media
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
networks:
|
|
||||||
internal:
|
|
||||||
attachable: true
|
|
|
@ -1,143 +0,0 @@
|
||||||
version: "3.4"
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
hostname: postgres
|
|
||||||
image: postgres:14
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- ./postgres/create_db.sh:/docker-entrypoint-initdb.d/20-create_db.sh
|
|
||||||
# To persist your PostgreSQL databases outside of the Docker image,
|
|
||||||
# to prevent data loss, modify the following ./path_to path:
|
|
||||||
- ./path_to/postgresql:/var/lib/postgresql/data
|
|
||||||
environment:
|
|
||||||
POSTGRES_PASSWORD: itsasecret
|
|
||||||
POSTGRES_USER: dendrite
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U dendrite"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
|
|
||||||
jetstream:
|
|
||||||
hostname: jetstream
|
|
||||||
image: nats:latest
|
|
||||||
command: |
|
|
||||||
--jetstream
|
|
||||||
--store_dir /var/lib/nats
|
|
||||||
--cluster_name Dendrite
|
|
||||||
volumes:
|
|
||||||
# To persist your NATS JetStream streams outside of the Docker image,
|
|
||||||
# prevent data loss, modify the following ./path_to path:
|
|
||||||
- ./path_to/nats:/var/lib/nats
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
|
|
||||||
client_api:
|
|
||||||
hostname: client_api
|
|
||||||
image: matrixdotorg/dendrite-polylith:latest
|
|
||||||
command: clientapi
|
|
||||||
volumes:
|
|
||||||
- ./config:/etc/dendrite
|
|
||||||
depends_on:
|
|
||||||
- jetstream
|
|
||||||
- postgres
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
media_api:
|
|
||||||
hostname: media_api
|
|
||||||
image: matrixdotorg/dendrite-polylith:latest
|
|
||||||
command: mediaapi
|
|
||||||
volumes:
|
|
||||||
- ./config:/etc/dendrite
|
|
||||||
- ./media:/var/dendrite/media
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
sync_api:
|
|
||||||
hostname: sync_api
|
|
||||||
image: matrixdotorg/dendrite-polylith:latest
|
|
||||||
command: syncapi
|
|
||||||
volumes:
|
|
||||||
- ./config:/etc/dendrite
|
|
||||||
depends_on:
|
|
||||||
- jetstream
|
|
||||||
- postgres
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
room_server:
|
|
||||||
hostname: room_server
|
|
||||||
image: matrixdotorg/dendrite-polylith:latest
|
|
||||||
command: roomserver
|
|
||||||
volumes:
|
|
||||||
- ./config:/etc/dendrite
|
|
||||||
depends_on:
|
|
||||||
- jetstream
|
|
||||||
- postgres
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
federation_api:
|
|
||||||
hostname: federation_api
|
|
||||||
image: matrixdotorg/dendrite-polylith:latest
|
|
||||||
command: federationapi
|
|
||||||
volumes:
|
|
||||||
- ./config:/etc/dendrite
|
|
||||||
depends_on:
|
|
||||||
- jetstream
|
|
||||||
- postgres
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
key_server:
|
|
||||||
hostname: key_server
|
|
||||||
image: matrixdotorg/dendrite-polylith:latest
|
|
||||||
command: keyserver
|
|
||||||
volumes:
|
|
||||||
- ./config:/etc/dendrite
|
|
||||||
depends_on:
|
|
||||||
- jetstream
|
|
||||||
- postgres
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
user_api:
|
|
||||||
hostname: user_api
|
|
||||||
image: matrixdotorg/dendrite-polylith:latest
|
|
||||||
command: userapi
|
|
||||||
volumes:
|
|
||||||
- ./config:/etc/dendrite
|
|
||||||
depends_on:
|
|
||||||
- jetstream
|
|
||||||
- postgres
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
appservice_api:
|
|
||||||
hostname: appservice_api
|
|
||||||
image: matrixdotorg/dendrite-polylith:latest
|
|
||||||
command: appservice
|
|
||||||
volumes:
|
|
||||||
- ./config:/etc/dendrite
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
depends_on:
|
|
||||||
- jetstream
|
|
||||||
- postgres
|
|
||||||
- room_server
|
|
||||||
- user_api
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
networks:
|
|
||||||
internal:
|
|
||||||
attachable: true
|
|
52
build/docker/docker-compose.yml
Normal file
52
build/docker/docker-compose.yml
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
version: "3.4"
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
hostname: postgres
|
||||||
|
image: postgres:15-alpine
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
# This will create a docker volume to persist the database files in.
|
||||||
|
# If you prefer those files to be outside of docker, you'll need to change this.
|
||||||
|
- dendrite_postgres_data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: itsasecret
|
||||||
|
POSTGRES_USER: dendrite
|
||||||
|
POSTGRES_DATABASE: dendrite
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U dendrite"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
monolith:
|
||||||
|
hostname: monolith
|
||||||
|
image: matrixdotorg/dendrite-monolith:latest
|
||||||
|
ports:
|
||||||
|
- 8008:8008
|
||||||
|
- 8448:8448
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
# The following volumes use docker volumes, change this
|
||||||
|
# if you prefer to have those files outside of docker.
|
||||||
|
- dendrite_media:/var/dendrite/media
|
||||||
|
- dendrite_jetstream:/var/dendrite/jetstream
|
||||||
|
- dendrite_search_index:/var/dendrite/searchindex
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
attachable: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dendrite_postgres_data:
|
||||||
|
dendrite_media:
|
||||||
|
dendrite_jetstream:
|
||||||
|
dendrite_search_index:
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
cd $(git rev-parse --show-toplevel)
|
cd $(git rev-parse --show-toplevel)
|
||||||
|
|
||||||
|
@ -6,5 +6,6 @@ TAG=${1:-latest}
|
||||||
|
|
||||||
echo "Building tag '${TAG}'"
|
echo "Building tag '${TAG}'"
|
||||||
|
|
||||||
docker build -t matrixdotorg/dendrite-monolith:${TAG} -f build/docker/Dockerfile.monolith .
|
docker build . --target monolith -t matrixdotorg/dendrite-monolith:${TAG}
|
||||||
docker build -t matrixdotorg/dendrite-polylith:${TAG} -f build/docker/Dockerfile.polylith .
|
docker build . --target demo-pinecone -t matrixdotorg/dendrite-demo-pinecone:${TAG}
|
||||||
|
docker build . --target demo-yggdrasil -t matrixdotorg/dendrite-demo-yggdrasil:${TAG}
|
|
@ -1,8 +1,7 @@
|
||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
TAG=${1:-latest}
|
TAG=${1:-latest}
|
||||||
|
|
||||||
echo "Pulling tag '${TAG}'"
|
echo "Pulling tag '${TAG}'"
|
||||||
|
|
||||||
docker pull matrixdotorg/dendrite-monolith:${TAG}
|
docker pull matrixdotorg/dendrite-monolith:${TAG}
|
||||||
docker pull matrixdotorg/dendrite-polylith:${TAG}
|
|
|
@ -1,8 +1,7 @@
|
||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
TAG=${1:-latest}
|
TAG=${1:-latest}
|
||||||
|
|
||||||
echo "Pushing tag '${TAG}'"
|
echo "Pushing tag '${TAG}'"
|
||||||
|
|
||||||
docker push matrixdotorg/dendrite-monolith:${TAG}
|
docker push matrixdotorg/dendrite-monolith:${TAG}
|
||||||
docker push matrixdotorg/dendrite-polylith:${TAG}
|
|
|
@ -1,5 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
for db in userapi_accounts mediaapi syncapi roomserver keyserver federationapi appservice mscs; do
|
|
||||||
createdb -U dendrite -O dendrite dendrite_$db
|
|
||||||
done
|
|
2
build/gobind-pinecone/build.sh
Normal file → Executable file
2
build/gobind-pinecone/build.sh
Normal file → Executable file
|
@ -7,7 +7,7 @@ do
|
||||||
case "$option"
|
case "$option"
|
||||||
in
|
in
|
||||||
a) gomobile bind -v -target android -trimpath -ldflags="-s -w" github.com/matrix-org/dendrite/build/gobind-pinecone ;;
|
a) gomobile bind -v -target android -trimpath -ldflags="-s -w" github.com/matrix-org/dendrite/build/gobind-pinecone ;;
|
||||||
i) gomobile bind -v -target ios -trimpath -ldflags="" github.com/matrix-org/dendrite/build/gobind-pinecone ;;
|
i) gomobile bind -v -target ios -trimpath -ldflags="" -o ~/DendriteBindings/Gobind.xcframework . ;;
|
||||||
*) echo "No target specified, specify -a or -i"; exit 1 ;;
|
*) echo "No target specified, specify -a or -i"; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
|
@ -18,45 +18,29 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/tls"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"path/filepath"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/matrix-org/dendrite/appservice"
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/userutil"
|
"github.com/matrix-org/dendrite/clientapi/userutil"
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/conn"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/conduit"
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/rooms"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/monolith"
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/users"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/relay"
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
|
||||||
"github.com/matrix-org/dendrite/federationapi"
|
"github.com/matrix-org/dendrite/federationapi/api"
|
||||||
"github.com/matrix-org/dendrite/internal/httputil"
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
"github.com/matrix-org/dendrite/keyserver"
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
"github.com/matrix-org/dendrite/roomserver"
|
|
||||||
"github.com/matrix-org/dendrite/setup"
|
|
||||||
"github.com/matrix-org/dendrite/setup/base"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
|
||||||
"github.com/matrix-org/dendrite/setup/process"
|
"github.com/matrix-org/dendrite/setup/process"
|
||||||
"github.com/matrix-org/dendrite/userapi"
|
|
||||||
userapiAPI "github.com/matrix-org/dendrite/userapi/api"
|
userapiAPI "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
"github.com/matrix-org/pinecone/types"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/net/http2"
|
|
||||||
"golang.org/x/net/http2/h2c"
|
|
||||||
|
|
||||||
pineconeConnections "github.com/matrix-org/pinecone/connections"
|
|
||||||
pineconeMulticast "github.com/matrix-org/pinecone/multicast"
|
pineconeMulticast "github.com/matrix-org/pinecone/multicast"
|
||||||
pineconeRouter "github.com/matrix-org/pinecone/router"
|
pineconeRouter "github.com/matrix-org/pinecone/router"
|
||||||
pineconeSessions "github.com/matrix-org/pinecone/sessions"
|
|
||||||
"github.com/matrix-org/pinecone/types"
|
|
||||||
|
|
||||||
_ "golang.org/x/mobile/bind"
|
_ "golang.org/x/mobile/bind"
|
||||||
)
|
)
|
||||||
|
@ -65,109 +49,250 @@ const (
|
||||||
PeerTypeRemote = pineconeRouter.PeerTypeRemote
|
PeerTypeRemote = pineconeRouter.PeerTypeRemote
|
||||||
PeerTypeMulticast = pineconeRouter.PeerTypeMulticast
|
PeerTypeMulticast = pineconeRouter.PeerTypeMulticast
|
||||||
PeerTypeBluetooth = pineconeRouter.PeerTypeBluetooth
|
PeerTypeBluetooth = pineconeRouter.PeerTypeBluetooth
|
||||||
|
PeerTypeBonjour = pineconeRouter.PeerTypeBonjour
|
||||||
|
|
||||||
|
MaxFrameSize = types.MaxFrameSize
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Re-export Conduit in this package for bindings.
|
||||||
|
type Conduit struct {
|
||||||
|
conduit.Conduit
|
||||||
|
}
|
||||||
|
|
||||||
type DendriteMonolith struct {
|
type DendriteMonolith struct {
|
||||||
logger logrus.Logger
|
logger logrus.Logger
|
||||||
PineconeRouter *pineconeRouter.Router
|
p2pMonolith monolith.P2PMonolith
|
||||||
PineconeMulticast *pineconeMulticast.Multicast
|
StorageDirectory string
|
||||||
PineconeQUIC *pineconeSessions.Sessions
|
CacheDirectory string
|
||||||
PineconeManager *pineconeConnections.ConnectionManager
|
listener net.Listener
|
||||||
StorageDirectory string
|
}
|
||||||
CacheDirectory string
|
|
||||||
listener net.Listener
|
func (m *DendriteMonolith) PublicKey() string {
|
||||||
httpServer *http.Server
|
return m.p2pMonolith.Router.PublicKey().String()
|
||||||
processContext *process.ProcessContext
|
|
||||||
userAPI userapiAPI.UserInternalAPI
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) BaseURL() string {
|
func (m *DendriteMonolith) BaseURL() string {
|
||||||
return fmt.Sprintf("http://%s", m.listener.Addr().String())
|
return fmt.Sprintf("http://%s", m.p2pMonolith.Addr())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) PeerCount(peertype int) int {
|
func (m *DendriteMonolith) PeerCount(peertype int) int {
|
||||||
return m.PineconeRouter.PeerCount(peertype)
|
return m.p2pMonolith.Router.PeerCount(peertype)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) SessionCount() int {
|
func (m *DendriteMonolith) SessionCount() int {
|
||||||
return len(m.PineconeQUIC.Protocol("matrix").Sessions())
|
return len(m.p2pMonolith.Sessions.Protocol(monolith.SessionProtocol).Sessions())
|
||||||
|
}
|
||||||
|
|
||||||
|
type InterfaceInfo struct {
|
||||||
|
Name string
|
||||||
|
Index int
|
||||||
|
Mtu int
|
||||||
|
Up bool
|
||||||
|
Broadcast bool
|
||||||
|
Loopback bool
|
||||||
|
PointToPoint bool
|
||||||
|
Multicast bool
|
||||||
|
Addrs string
|
||||||
|
}
|
||||||
|
|
||||||
|
type InterfaceRetriever interface {
|
||||||
|
CacheCurrentInterfaces() int
|
||||||
|
GetCachedInterface(index int) *InterfaceInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DendriteMonolith) RegisterNetworkCallback(intfCallback InterfaceRetriever) {
|
||||||
|
callback := func() []pineconeMulticast.InterfaceInfo {
|
||||||
|
count := intfCallback.CacheCurrentInterfaces()
|
||||||
|
intfs := []pineconeMulticast.InterfaceInfo{}
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
iface := intfCallback.GetCachedInterface(i)
|
||||||
|
if iface != nil {
|
||||||
|
intfs = append(intfs, pineconeMulticast.InterfaceInfo{
|
||||||
|
Name: iface.Name,
|
||||||
|
Index: iface.Index,
|
||||||
|
Mtu: iface.Mtu,
|
||||||
|
Up: iface.Up,
|
||||||
|
Broadcast: iface.Broadcast,
|
||||||
|
Loopback: iface.Loopback,
|
||||||
|
PointToPoint: iface.PointToPoint,
|
||||||
|
Multicast: iface.Multicast,
|
||||||
|
Addrs: iface.Addrs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return intfs
|
||||||
|
}
|
||||||
|
m.p2pMonolith.Multicast.RegisterNetworkCallback(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) SetMulticastEnabled(enabled bool) {
|
func (m *DendriteMonolith) SetMulticastEnabled(enabled bool) {
|
||||||
if enabled {
|
if enabled {
|
||||||
m.PineconeMulticast.Start()
|
m.p2pMonolith.Multicast.Start()
|
||||||
} else {
|
} else {
|
||||||
m.PineconeMulticast.Stop()
|
m.p2pMonolith.Multicast.Stop()
|
||||||
m.DisconnectType(int(pineconeRouter.PeerTypeMulticast))
|
m.DisconnectType(int(pineconeRouter.PeerTypeMulticast))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) SetStaticPeer(uri string) {
|
func (m *DendriteMonolith) SetStaticPeer(uri string) {
|
||||||
m.PineconeManager.RemovePeers()
|
m.p2pMonolith.ConnManager.RemovePeers()
|
||||||
m.PineconeManager.AddPeer(strings.TrimSpace(uri))
|
for _, uri := range strings.Split(uri, ",") {
|
||||||
|
m.p2pMonolith.ConnManager.AddPeer(strings.TrimSpace(uri))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServerKeyFromString(nodeID string) (spec.ServerName, error) {
|
||||||
|
var nodeKey spec.ServerName
|
||||||
|
if userID, err := spec.NewUserID(nodeID, false); err == nil {
|
||||||
|
hexKey, decodeErr := hex.DecodeString(string(userID.Domain()))
|
||||||
|
if decodeErr != nil || len(hexKey) != ed25519.PublicKeySize {
|
||||||
|
return "", fmt.Errorf("UserID domain is not a valid ed25519 public key: %v", userID.Domain())
|
||||||
|
} else {
|
||||||
|
nodeKey = userID.Domain()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hexKey, decodeErr := hex.DecodeString(nodeID)
|
||||||
|
if decodeErr != nil || len(hexKey) != ed25519.PublicKeySize {
|
||||||
|
return "", fmt.Errorf("Relay server uri is not a valid ed25519 public key: %v", nodeID)
|
||||||
|
} else {
|
||||||
|
nodeKey = spec.ServerName(nodeID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DendriteMonolith) SetRelayServers(nodeID string, uris string) {
|
||||||
|
relays := []spec.ServerName{}
|
||||||
|
for _, uri := range strings.Split(uris, ",") {
|
||||||
|
uri = strings.TrimSpace(uri)
|
||||||
|
if len(uri) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeKey, err := getServerKeyFromString(uri)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf(err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
relays = append(relays, nodeKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeKey, err := getServerKeyFromString(nodeID)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(nodeKey) == m.PublicKey() {
|
||||||
|
logrus.Infof("Setting own relay servers to: %v", relays)
|
||||||
|
m.p2pMonolith.RelayRetriever.SetRelayServers(relays)
|
||||||
|
} else {
|
||||||
|
relay.UpdateNodeRelayServers(
|
||||||
|
spec.ServerName(nodeKey),
|
||||||
|
relays,
|
||||||
|
m.p2pMonolith.ProcessCtx.Context(),
|
||||||
|
m.p2pMonolith.GetFederationAPI(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DendriteMonolith) GetRelayServers(nodeID string) string {
|
||||||
|
nodeKey, err := getServerKeyFromString(nodeID)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf(err.Error())
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
relaysString := ""
|
||||||
|
if string(nodeKey) == m.PublicKey() {
|
||||||
|
relays := m.p2pMonolith.RelayRetriever.GetRelayServers()
|
||||||
|
|
||||||
|
for i, relay := range relays {
|
||||||
|
if i != 0 {
|
||||||
|
// Append a comma to the previous entry if there is one.
|
||||||
|
relaysString += ","
|
||||||
|
}
|
||||||
|
relaysString += string(relay)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
request := api.P2PQueryRelayServersRequest{Server: spec.ServerName(nodeKey)}
|
||||||
|
response := api.P2PQueryRelayServersResponse{}
|
||||||
|
err := m.p2pMonolith.GetFederationAPI().P2PQueryRelayServers(m.p2pMonolith.ProcessCtx.Context(), &request, &response)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnf("Failed obtaining list of this node's relay servers: %s", err.Error())
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, relay := range response.RelayServers {
|
||||||
|
if i != 0 {
|
||||||
|
// Append a comma to the previous entry if there is one.
|
||||||
|
relaysString += ","
|
||||||
|
}
|
||||||
|
relaysString += string(relay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return relaysString
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DendriteMonolith) RelayingEnabled() bool {
|
||||||
|
return m.p2pMonolith.GetRelayAPI().RelayingEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DendriteMonolith) SetRelayingEnabled(enabled bool) {
|
||||||
|
m.p2pMonolith.GetRelayAPI().SetRelayingEnabled(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) DisconnectType(peertype int) {
|
func (m *DendriteMonolith) DisconnectType(peertype int) {
|
||||||
for _, p := range m.PineconeRouter.Peers() {
|
for _, p := range m.p2pMonolith.Router.Peers() {
|
||||||
if int(peertype) == p.PeerType {
|
if int(peertype) == p.PeerType {
|
||||||
m.PineconeRouter.Disconnect(types.SwitchPortID(p.Port), nil)
|
m.p2pMonolith.Router.Disconnect(types.SwitchPortID(p.Port), nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) DisconnectZone(zone string) {
|
func (m *DendriteMonolith) DisconnectZone(zone string) {
|
||||||
for _, p := range m.PineconeRouter.Peers() {
|
for _, p := range m.p2pMonolith.Router.Peers() {
|
||||||
if zone == p.Zone {
|
if zone == p.Zone {
|
||||||
m.PineconeRouter.Disconnect(types.SwitchPortID(p.Port), nil)
|
m.p2pMonolith.Router.Disconnect(types.SwitchPortID(p.Port), nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) DisconnectPort(port int) {
|
func (m *DendriteMonolith) DisconnectPort(port int) {
|
||||||
m.PineconeRouter.Disconnect(types.SwitchPortID(port), nil)
|
m.p2pMonolith.Router.Disconnect(types.SwitchPortID(port), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) Conduit(zone string, peertype int) (*Conduit, error) {
|
func (m *DendriteMonolith) Conduit(zone string, peertype int) (*Conduit, error) {
|
||||||
l, r := net.Pipe()
|
l, r := net.Pipe()
|
||||||
conduit := &Conduit{conn: r, port: 0}
|
newConduit := Conduit{conduit.NewConduit(r, 0)}
|
||||||
go func() {
|
go func() {
|
||||||
conduit.portMutex.Lock()
|
logrus.Errorf("Attempting authenticated connect")
|
||||||
defer conduit.portMutex.Unlock()
|
var port types.SwitchPortID
|
||||||
loop:
|
var err error
|
||||||
for i := 1; i <= 10; i++ {
|
if port, err = m.p2pMonolith.Router.Connect(
|
||||||
logrus.Errorf("Attempting authenticated connect (attempt %d)", i)
|
l,
|
||||||
var err error
|
pineconeRouter.ConnectionZone(zone),
|
||||||
conduit.port, err = m.PineconeRouter.Connect(
|
pineconeRouter.ConnectionPeerType(peertype),
|
||||||
l,
|
); err != nil {
|
||||||
pineconeRouter.ConnectionZone(zone),
|
logrus.Errorf("Authenticated connect failed: %s", err)
|
||||||
pineconeRouter.ConnectionPeerType(peertype),
|
_ = l.Close()
|
||||||
)
|
_ = r.Close()
|
||||||
switch err {
|
_ = newConduit.Close()
|
||||||
case io.ErrClosedPipe:
|
return
|
||||||
logrus.Errorf("Authenticated connect failed due to closed pipe (attempt %d)", i)
|
|
||||||
return
|
|
||||||
case io.EOF:
|
|
||||||
logrus.Errorf("Authenticated connect failed due to EOF (attempt %d)", i)
|
|
||||||
break loop
|
|
||||||
case nil:
|
|
||||||
logrus.Errorf("Authenticated connect succeeded, connected to port %d (attempt %d)", conduit.port, i)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
logrus.WithError(err).Errorf("Authenticated connect failed (attempt %d)", i)
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ = l.Close()
|
newConduit.SetPort(port)
|
||||||
_ = r.Close()
|
logrus.Infof("Authenticated connect succeeded (port %d)", newConduit.Port())
|
||||||
}()
|
}()
|
||||||
return conduit, nil
|
return &newConduit, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) RegisterUser(localpart, password string) (string, error) {
|
func (m *DendriteMonolith) RegisterUser(localpart, password string) (string, error) {
|
||||||
pubkey := m.PineconeRouter.PublicKey()
|
pubkey := m.p2pMonolith.Router.PublicKey()
|
||||||
userID := userutil.MakeUserID(
|
userID := userutil.MakeUserID(
|
||||||
localpart,
|
localpart,
|
||||||
gomatrixserverlib.ServerName(hex.EncodeToString(pubkey[:])),
|
spec.ServerName(hex.EncodeToString(pubkey[:])),
|
||||||
)
|
)
|
||||||
userReq := &userapiAPI.PerformAccountCreationRequest{
|
userReq := &userapiAPI.PerformAccountCreationRequest{
|
||||||
AccountType: userapiAPI.AccountTypeUser,
|
AccountType: userapiAPI.AccountTypeUser,
|
||||||
|
@ -175,7 +300,7 @@ func (m *DendriteMonolith) RegisterUser(localpart, password string) (string, err
|
||||||
Password: password,
|
Password: password,
|
||||||
}
|
}
|
||||||
userRes := &userapiAPI.PerformAccountCreationResponse{}
|
userRes := &userapiAPI.PerformAccountCreationResponse{}
|
||||||
if err := m.userAPI.PerformAccountCreation(context.Background(), userReq, userRes); err != nil {
|
if err := m.p2pMonolith.GetUserAPI().PerformAccountCreation(context.Background(), userReq, userRes); err != nil {
|
||||||
return userID, fmt.Errorf("userAPI.PerformAccountCreation: %w", err)
|
return userID, fmt.Errorf("userAPI.PerformAccountCreation: %w", err)
|
||||||
}
|
}
|
||||||
return userID, nil
|
return userID, nil
|
||||||
|
@ -193,7 +318,7 @@ func (m *DendriteMonolith) RegisterDevice(localpart, deviceID string) (string, e
|
||||||
AccessToken: hex.EncodeToString(accessTokenBytes[:n]),
|
AccessToken: hex.EncodeToString(accessTokenBytes[:n]),
|
||||||
}
|
}
|
||||||
loginRes := &userapiAPI.PerformDeviceCreationResponse{}
|
loginRes := &userapiAPI.PerformDeviceCreationResponse{}
|
||||||
if err := m.userAPI.PerformDeviceCreation(context.Background(), loginReq, loginRes); err != nil {
|
if err := m.p2pMonolith.GetUserAPI().PerformDeviceCreation(context.Background(), loginReq, loginRes); err != nil {
|
||||||
return "", fmt.Errorf("userAPI.PerformDeviceCreation: %w", err)
|
return "", fmt.Errorf("userAPI.PerformDeviceCreation: %w", err)
|
||||||
}
|
}
|
||||||
if !loginRes.DeviceCreated {
|
if !loginRes.DeviceCreated {
|
||||||
|
@ -202,33 +327,10 @@ func (m *DendriteMonolith) RegisterDevice(localpart, deviceID string) (string, e
|
||||||
return loginRes.Device.AccessToken, nil
|
return loginRes.Device.AccessToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint:gocyclo
|
|
||||||
func (m *DendriteMonolith) Start() {
|
func (m *DendriteMonolith) Start() {
|
||||||
var err error
|
keyfile := filepath.Join(m.StorageDirectory, "p2p.pem")
|
||||||
var sk ed25519.PrivateKey
|
oldKeyfile := filepath.Join(m.StorageDirectory, "p2p.key")
|
||||||
var pk ed25519.PublicKey
|
sk, pk := monolith.GetOrCreateKey(keyfile, oldKeyfile)
|
||||||
keyfile := fmt.Sprintf("%s/p2p.key", m.StorageDirectory)
|
|
||||||
if _, err = os.Stat(keyfile); os.IsNotExist(err) {
|
|
||||||
if pk, sk, err = ed25519.GenerateKey(nil); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if err = ioutil.WriteFile(keyfile, sk, 0644); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
} else if err == nil {
|
|
||||||
if sk, err = ioutil.ReadFile(keyfile); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if len(sk) != ed25519.PrivateKeySize {
|
|
||||||
panic("the private key is not long enough")
|
|
||||||
}
|
|
||||||
pk = sk.Public().(ed25519.PublicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.listener, err = net.Listen("tcp", ":65432")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.logger = logrus.Logger{
|
m.logger = logrus.Logger{
|
||||||
Out: BindLogger{},
|
Out: BindLogger{},
|
||||||
|
@ -236,168 +338,29 @@ func (m *DendriteMonolith) Start() {
|
||||||
m.logger.SetOutput(BindLogger{})
|
m.logger.SetOutput(BindLogger{})
|
||||||
logrus.SetOutput(BindLogger{})
|
logrus.SetOutput(BindLogger{})
|
||||||
|
|
||||||
m.PineconeRouter = pineconeRouter.NewRouter(logrus.WithField("pinecone", "router"), sk, false)
|
m.p2pMonolith = monolith.P2PMonolith{}
|
||||||
m.PineconeQUIC = pineconeSessions.NewSessions(logrus.WithField("pinecone", "sessions"), m.PineconeRouter, []string{"matrix"})
|
m.p2pMonolith.SetupPinecone(sk)
|
||||||
m.PineconeMulticast = pineconeMulticast.NewMulticast(logrus.WithField("pinecone", "multicast"), m.PineconeRouter)
|
|
||||||
m.PineconeManager = pineconeConnections.NewConnectionManager(m.PineconeRouter)
|
|
||||||
|
|
||||||
prefix := hex.EncodeToString(pk)
|
prefix := hex.EncodeToString(pk)
|
||||||
cfg := &config.Dendrite{}
|
cfg := monolith.GenerateDefaultConfig(sk, m.StorageDirectory, m.CacheDirectory, prefix)
|
||||||
cfg.Defaults(true)
|
cfg.Global.ServerName = spec.ServerName(hex.EncodeToString(pk))
|
||||||
cfg.Global.ServerName = gomatrixserverlib.ServerName(hex.EncodeToString(pk))
|
|
||||||
cfg.Global.PrivateKey = sk
|
|
||||||
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
||||||
cfg.Global.JetStream.InMemory = true
|
cfg.Global.JetStream.InMemory = false
|
||||||
cfg.Global.JetStream.StoragePath = config.Path(fmt.Sprintf("%s/%s", m.StorageDirectory, prefix))
|
// NOTE : disabled for now since there is a 64 bit alignment panic on 32 bit systems
|
||||||
cfg.UserAPI.AccountDatabase.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/%s-account.db", m.StorageDirectory, prefix))
|
// This isn't actually fixed: https://github.com/blevesearch/zapx/pull/147
|
||||||
cfg.MediaAPI.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-mediaapi.db", m.StorageDirectory))
|
cfg.SyncAPI.Fulltext.Enabled = false
|
||||||
cfg.SyncAPI.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/%s-syncapi.db", m.StorageDirectory, prefix))
|
|
||||||
cfg.RoomServer.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/%s-roomserver.db", m.StorageDirectory, prefix))
|
|
||||||
cfg.KeyServer.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/%s-keyserver.db", m.StorageDirectory, prefix))
|
|
||||||
cfg.FederationAPI.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/%s-federationsender.db", m.StorageDirectory, prefix))
|
|
||||||
cfg.AppServiceAPI.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/%s-appservice.db", m.StorageDirectory, prefix))
|
|
||||||
cfg.MediaAPI.BasePath = config.Path(fmt.Sprintf("%s/media", m.CacheDirectory))
|
|
||||||
cfg.MediaAPI.AbsBasePath = config.Path(fmt.Sprintf("%s/media", m.CacheDirectory))
|
|
||||||
cfg.MSCs.MSCs = []string{"msc2836", "msc2946"}
|
|
||||||
if err := cfg.Derive(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
base := base.NewBaseDendrite(cfg, "Monolith")
|
processCtx := process.NewProcessContext()
|
||||||
defer base.Close() // nolint: errcheck
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
routers := httputil.NewRouters()
|
||||||
|
|
||||||
accountDB := base.CreateAccountsDB()
|
enableRelaying := false
|
||||||
federation := conn.CreateFederationClient(base, m.PineconeQUIC)
|
enableMetrics := false
|
||||||
|
enableWebsockets := false
|
||||||
serverKeyAPI := &signing.YggdrasilKeys{}
|
m.p2pMonolith.SetupDendrite(processCtx, cfg, cm, routers, 65432, enableRelaying, enableMetrics, enableWebsockets)
|
||||||
keyRing := serverKeyAPI.KeyRing()
|
m.p2pMonolith.StartMonolith()
|
||||||
|
|
||||||
rsAPI := roomserver.NewInternalAPI(base)
|
|
||||||
|
|
||||||
fsAPI := federationapi.NewInternalAPI(
|
|
||||||
base, federation, rsAPI, base.Caches, keyRing, true,
|
|
||||||
)
|
|
||||||
|
|
||||||
keyAPI := keyserver.NewInternalAPI(base, &base.Cfg.KeyServer, fsAPI)
|
|
||||||
m.userAPI = userapi.NewInternalAPI(base, accountDB, &cfg.UserAPI, cfg.Derived.ApplicationServices, keyAPI, rsAPI, base.PushGatewayHTTPClient())
|
|
||||||
keyAPI.SetUserAPI(m.userAPI)
|
|
||||||
|
|
||||||
asAPI := appservice.NewInternalAPI(base, m.userAPI, rsAPI)
|
|
||||||
|
|
||||||
// The underlying roomserver implementation needs to be able to call the fedsender.
|
|
||||||
// This is different to rsAPI which can be the http client which doesn't need this dependency
|
|
||||||
rsAPI.SetFederationAPI(fsAPI, keyRing)
|
|
||||||
|
|
||||||
userProvider := users.NewPineconeUserProvider(m.PineconeRouter, m.PineconeQUIC, m.userAPI, federation)
|
|
||||||
roomProvider := rooms.NewPineconeRoomProvider(m.PineconeRouter, m.PineconeQUIC, fsAPI, federation)
|
|
||||||
|
|
||||||
monolith := setup.Monolith{
|
|
||||||
Config: base.Cfg,
|
|
||||||
AccountDB: accountDB,
|
|
||||||
Client: conn.CreateClient(base, m.PineconeQUIC),
|
|
||||||
FedClient: federation,
|
|
||||||
KeyRing: keyRing,
|
|
||||||
|
|
||||||
AppserviceAPI: asAPI,
|
|
||||||
FederationAPI: fsAPI,
|
|
||||||
RoomserverAPI: rsAPI,
|
|
||||||
UserAPI: m.userAPI,
|
|
||||||
KeyAPI: keyAPI,
|
|
||||||
ExtPublicRoomsProvider: roomProvider,
|
|
||||||
ExtUserDirectoryProvider: userProvider,
|
|
||||||
}
|
|
||||||
monolith.AddAllPublicRoutes(
|
|
||||||
base.ProcessContext,
|
|
||||||
base.PublicClientAPIMux,
|
|
||||||
base.PublicFederationAPIMux,
|
|
||||||
base.PublicKeyAPIMux,
|
|
||||||
base.PublicWellKnownAPIMux,
|
|
||||||
base.PublicMediaAPIMux,
|
|
||||||
base.SynapseAdminMux,
|
|
||||||
)
|
|
||||||
|
|
||||||
httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath()
|
|
||||||
httpRouter.PathPrefix(httputil.InternalPathPrefix).Handler(base.InternalAPIMux)
|
|
||||||
httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(base.PublicClientAPIMux)
|
|
||||||
httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
|
|
||||||
httpRouter.HandleFunc("/pinecone", m.PineconeRouter.ManholeHandler)
|
|
||||||
|
|
||||||
pMux := mux.NewRouter().SkipClean(true).UseEncodedPath()
|
|
||||||
pMux.PathPrefix(users.PublicURL).HandlerFunc(userProvider.FederatedUserProfiles)
|
|
||||||
pMux.PathPrefix(httputil.PublicFederationPathPrefix).Handler(base.PublicFederationAPIMux)
|
|
||||||
pMux.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
|
|
||||||
|
|
||||||
pHTTP := m.PineconeQUIC.Protocol("matrix").HTTP()
|
|
||||||
pHTTP.Mux().Handle(users.PublicURL, pMux)
|
|
||||||
pHTTP.Mux().Handle(httputil.PublicFederationPathPrefix, pMux)
|
|
||||||
pHTTP.Mux().Handle(httputil.PublicMediaPathPrefix, pMux)
|
|
||||||
|
|
||||||
// Build both ends of a HTTP multiplex.
|
|
||||||
h2s := &http2.Server{}
|
|
||||||
m.httpServer = &http.Server{
|
|
||||||
Addr: ":0",
|
|
||||||
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){},
|
|
||||||
ReadTimeout: 10 * time.Second,
|
|
||||||
WriteTimeout: 10 * time.Second,
|
|
||||||
IdleTimeout: 30 * time.Second,
|
|
||||||
BaseContext: func(_ net.Listener) context.Context {
|
|
||||||
return context.Background()
|
|
||||||
},
|
|
||||||
Handler: h2c.NewHandler(pMux, h2s),
|
|
||||||
}
|
|
||||||
|
|
||||||
m.processContext = base.ProcessContext
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
m.logger.Info("Listening on ", cfg.Global.ServerName)
|
|
||||||
m.logger.Fatal(m.httpServer.Serve(m.PineconeQUIC.Protocol("matrix")))
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
logrus.Info("Listening on ", m.listener.Addr())
|
|
||||||
logrus.Fatal(http.Serve(m.listener, httpRouter))
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) Stop() {
|
func (m *DendriteMonolith) Stop() {
|
||||||
m.processContext.ShutdownDendrite()
|
m.p2pMonolith.Stop()
|
||||||
_ = m.listener.Close()
|
|
||||||
m.PineconeMulticast.Stop()
|
|
||||||
_ = m.PineconeQUIC.Close()
|
|
||||||
_ = m.PineconeRouter.Close()
|
|
||||||
m.processContext.WaitForComponentsToFinish()
|
|
||||||
}
|
|
||||||
|
|
||||||
const MaxFrameSize = types.MaxFrameSize
|
|
||||||
|
|
||||||
type Conduit struct {
|
|
||||||
conn net.Conn
|
|
||||||
port types.SwitchPortID
|
|
||||||
portMutex sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conduit) Port() int {
|
|
||||||
c.portMutex.Lock()
|
|
||||||
defer c.portMutex.Unlock()
|
|
||||||
return int(c.port)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conduit) Read(b []byte) (int, error) {
|
|
||||||
return c.conn.Read(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conduit) ReadCopy() ([]byte, error) {
|
|
||||||
var buf [65535 * 2]byte
|
|
||||||
n, err := c.conn.Read(buf[:])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return buf[:n], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conduit) Write(b []byte) (int, error) {
|
|
||||||
return c.conn.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conduit) Close() error {
|
|
||||||
return c.conn.Close()
|
|
||||||
}
|
}
|
||||||
|
|
158
build/gobind-pinecone/monolith_test.go
Normal file
158
build/gobind-pinecone/monolith_test.go
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed 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
|
||||||
|
//
|
||||||
|
// http://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.
|
||||||
|
|
||||||
|
package gobind
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMonolithStarts(t *testing.T) {
|
||||||
|
monolith := DendriteMonolith{
|
||||||
|
StorageDirectory: t.TempDir(),
|
||||||
|
CacheDirectory: t.TempDir(),
|
||||||
|
}
|
||||||
|
monolith.Start()
|
||||||
|
monolith.PublicKey()
|
||||||
|
monolith.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMonolithSetRelayServers(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
nodeID string
|
||||||
|
relays string
|
||||||
|
expectedRelays string
|
||||||
|
expectSelf bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "assorted valid, invalid, empty & self keys",
|
||||||
|
nodeID: "@valid:abcdef123456abcdef123456abcdef123456abcdef123456abcdef123456abcd",
|
||||||
|
relays: "@valid:123456123456abcdef123456abcdef123456abcdef123456abcdef123456abcd,@invalid:notakey,,",
|
||||||
|
expectedRelays: "123456123456abcdef123456abcdef123456abcdef123456abcdef123456abcd",
|
||||||
|
expectSelf: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid node key",
|
||||||
|
nodeID: "@invalid:notakey",
|
||||||
|
relays: "@valid:123456123456abcdef123456abcdef123456abcdef123456abcdef123456abcd,@invalid:notakey,,",
|
||||||
|
expectedRelays: "",
|
||||||
|
expectSelf: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "node is self",
|
||||||
|
nodeID: "self",
|
||||||
|
relays: "@valid:123456123456abcdef123456abcdef123456abcdef123456abcdef123456abcd,@invalid:notakey,,",
|
||||||
|
expectedRelays: "123456123456abcdef123456abcdef123456abcdef123456abcdef123456abcd",
|
||||||
|
expectSelf: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
monolith := DendriteMonolith{
|
||||||
|
StorageDirectory: t.TempDir(),
|
||||||
|
CacheDirectory: t.TempDir(),
|
||||||
|
}
|
||||||
|
monolith.Start()
|
||||||
|
|
||||||
|
inputRelays := tc.relays
|
||||||
|
expectedRelays := tc.expectedRelays
|
||||||
|
if tc.expectSelf {
|
||||||
|
inputRelays += "," + monolith.PublicKey()
|
||||||
|
expectedRelays += "," + monolith.PublicKey()
|
||||||
|
}
|
||||||
|
nodeID := tc.nodeID
|
||||||
|
if nodeID == "self" {
|
||||||
|
nodeID = monolith.PublicKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
monolith.SetRelayServers(nodeID, inputRelays)
|
||||||
|
relays := monolith.GetRelayServers(nodeID)
|
||||||
|
monolith.Stop()
|
||||||
|
|
||||||
|
if !containSameKeys(strings.Split(relays, ","), strings.Split(expectedRelays, ",")) {
|
||||||
|
t.Fatalf("%s: expected %s got %s", tc.name, expectedRelays, relays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func containSameKeys(expected []string, actual []string) bool {
|
||||||
|
if len(expected) != len(actual) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expectedKey := range expected {
|
||||||
|
hasMatch := false
|
||||||
|
for _, actualKey := range actual {
|
||||||
|
if actualKey == expectedKey {
|
||||||
|
hasMatch = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasMatch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseServerKey(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
serverKey string
|
||||||
|
expectedErr bool
|
||||||
|
expectedKey spec.ServerName
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid userid as key",
|
||||||
|
serverKey: "@valid:abcdef123456abcdef123456abcdef123456abcdef123456abcdef123456abcd",
|
||||||
|
expectedErr: false,
|
||||||
|
expectedKey: "abcdef123456abcdef123456abcdef123456abcdef123456abcdef123456abcd",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid key",
|
||||||
|
serverKey: "abcdef123456abcdef123456abcdef123456abcdef123456abcdef123456abcd",
|
||||||
|
expectedErr: false,
|
||||||
|
expectedKey: "abcdef123456abcdef123456abcdef123456abcdef123456abcdef123456abcd",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid userid key",
|
||||||
|
serverKey: "@invalid:notakey",
|
||||||
|
expectedErr: true,
|
||||||
|
expectedKey: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid key",
|
||||||
|
serverKey: "@invalid:notakey",
|
||||||
|
expectedErr: true,
|
||||||
|
expectedKey: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
key, err := getServerKeyFromString(tc.serverKey)
|
||||||
|
if tc.expectedErr && err == nil {
|
||||||
|
t.Fatalf("%s: expected an error", tc.name)
|
||||||
|
} else if !tc.expectedErr && err != nil {
|
||||||
|
t.Fatalf("%s: didn't expect an error: %s", tc.name, err.Error())
|
||||||
|
}
|
||||||
|
if tc.expectedKey != key {
|
||||||
|
t.Fatalf("%s: keys not equal. expected: %s got: %s", tc.name, tc.expectedKey, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,12 +2,17 @@ package gobind
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/getsentry/sentry-go"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/matrix-org/dendrite/appservice"
|
"github.com/matrix-org/dendrite/appservice"
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
|
||||||
|
@ -15,15 +20,20 @@ import (
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/yggrooms"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/yggrooms"
|
||||||
"github.com/matrix-org/dendrite/federationapi"
|
"github.com/matrix-org/dendrite/federationapi"
|
||||||
"github.com/matrix-org/dendrite/federationapi/api"
|
"github.com/matrix-org/dendrite/federationapi/api"
|
||||||
|
"github.com/matrix-org/dendrite/internal"
|
||||||
|
"github.com/matrix-org/dendrite/internal/caching"
|
||||||
"github.com/matrix-org/dendrite/internal/httputil"
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
"github.com/matrix-org/dendrite/keyserver"
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
"github.com/matrix-org/dendrite/roomserver"
|
"github.com/matrix-org/dendrite/roomserver"
|
||||||
"github.com/matrix-org/dendrite/setup"
|
"github.com/matrix-org/dendrite/setup"
|
||||||
"github.com/matrix-org/dendrite/setup/base"
|
basepkg "github.com/matrix-org/dendrite/setup/base"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
"github.com/matrix-org/dendrite/setup/process"
|
"github.com/matrix-org/dendrite/setup/process"
|
||||||
|
"github.com/matrix-org/dendrite/test"
|
||||||
"github.com/matrix-org/dendrite/userapi"
|
"github.com/matrix-org/dendrite/userapi"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
_ "golang.org/x/mobile/bind"
|
_ "golang.org/x/mobile/bind"
|
||||||
|
@ -63,28 +73,70 @@ func (m *DendriteMonolith) DisconnectMulticastPeers() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) Start() {
|
func (m *DendriteMonolith) Start() {
|
||||||
|
var pk ed25519.PublicKey
|
||||||
|
var sk ed25519.PrivateKey
|
||||||
|
|
||||||
m.logger = logrus.Logger{
|
m.logger = logrus.Logger{
|
||||||
Out: BindLogger{},
|
Out: BindLogger{},
|
||||||
}
|
}
|
||||||
m.logger.SetOutput(BindLogger{})
|
m.logger.SetOutput(BindLogger{})
|
||||||
logrus.SetOutput(BindLogger{})
|
logrus.SetOutput(BindLogger{})
|
||||||
|
|
||||||
|
keyfile := filepath.Join(m.StorageDirectory, "p2p.pem")
|
||||||
|
if _, err := os.Stat(keyfile); os.IsNotExist(err) {
|
||||||
|
oldkeyfile := filepath.Join(m.StorageDirectory, "p2p.key")
|
||||||
|
if _, err = os.Stat(oldkeyfile); os.IsNotExist(err) {
|
||||||
|
if err = test.NewMatrixKey(keyfile); err != nil {
|
||||||
|
panic("failed to generate a new PEM key: " + err.Error())
|
||||||
|
}
|
||||||
|
if _, sk, err = config.LoadMatrixKey(keyfile, os.ReadFile); err != nil {
|
||||||
|
panic("failed to load PEM key: " + err.Error())
|
||||||
|
}
|
||||||
|
if len(sk) != ed25519.PrivateKeySize {
|
||||||
|
panic("the private key is not long enough")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if sk, err = os.ReadFile(oldkeyfile); err != nil {
|
||||||
|
panic("failed to read the old private key: " + err.Error())
|
||||||
|
}
|
||||||
|
if len(sk) != ed25519.PrivateKeySize {
|
||||||
|
panic("the private key is not long enough")
|
||||||
|
}
|
||||||
|
if err := test.SaveMatrixKey(keyfile, sk); err != nil {
|
||||||
|
panic("failed to convert the private key to PEM format: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
if _, sk, err = config.LoadMatrixKey(keyfile, os.ReadFile); err != nil {
|
||||||
|
panic("failed to load PEM key: " + err.Error())
|
||||||
|
}
|
||||||
|
if len(sk) != ed25519.PrivateKeySize {
|
||||||
|
panic("the private key is not long enough")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pk = sk.Public().(ed25519.PublicKey)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
m.listener, err = net.Listen("tcp", "localhost:65432")
|
m.listener, err = net.Listen("tcp", "localhost:65432")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ygg, err := yggconn.Setup("dendrite", m.StorageDirectory, "")
|
ygg, err := yggconn.Setup(sk, "dendrite", m.StorageDirectory, "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
m.YggdrasilNode = ygg
|
m.YggdrasilNode = ygg
|
||||||
|
|
||||||
cfg := &config.Dendrite{}
|
cfg := &config.Dendrite{}
|
||||||
cfg.Defaults(true)
|
cfg.Defaults(config.DefaultOpts{
|
||||||
cfg.Global.ServerName = gomatrixserverlib.ServerName(ygg.DerivedServerName())
|
Generate: true,
|
||||||
cfg.Global.PrivateKey = ygg.PrivateKey()
|
SingleDatabase: true,
|
||||||
|
})
|
||||||
|
cfg.Global.ServerName = spec.ServerName(hex.EncodeToString(pk))
|
||||||
|
cfg.Global.PrivateKey = sk
|
||||||
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
||||||
cfg.Global.JetStream.StoragePath = config.Path(fmt.Sprintf("%s/", m.StorageDirectory))
|
cfg.Global.JetStream.StoragePath = config.Path(fmt.Sprintf("%s/", m.StorageDirectory))
|
||||||
cfg.Global.JetStream.InMemory = true
|
cfg.Global.JetStream.InMemory = true
|
||||||
|
@ -94,34 +146,79 @@ func (m *DendriteMonolith) Start() {
|
||||||
cfg.RoomServer.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-roomserver.db", m.StorageDirectory))
|
cfg.RoomServer.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-roomserver.db", m.StorageDirectory))
|
||||||
cfg.KeyServer.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-keyserver.db", m.StorageDirectory))
|
cfg.KeyServer.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-keyserver.db", m.StorageDirectory))
|
||||||
cfg.FederationAPI.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-federationsender.db", m.StorageDirectory))
|
cfg.FederationAPI.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-federationsender.db", m.StorageDirectory))
|
||||||
cfg.AppServiceAPI.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-appservice.db", m.StorageDirectory))
|
|
||||||
cfg.MediaAPI.BasePath = config.Path(fmt.Sprintf("%s/tmp", m.StorageDirectory))
|
cfg.MediaAPI.BasePath = config.Path(fmt.Sprintf("%s/tmp", m.StorageDirectory))
|
||||||
cfg.MediaAPI.AbsBasePath = config.Path(fmt.Sprintf("%s/tmp", m.StorageDirectory))
|
cfg.MediaAPI.AbsBasePath = config.Path(fmt.Sprintf("%s/tmp", m.StorageDirectory))
|
||||||
|
cfg.ClientAPI.RegistrationDisabled = false
|
||||||
|
cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled = true
|
||||||
if err = cfg.Derive(); err != nil {
|
if err = cfg.Derive(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
base := base.NewBaseDendrite(cfg, "Monolith")
|
configErrors := &config.ConfigErrors{}
|
||||||
m.processContext = base.ProcessContext
|
cfg.Verify(configErrors)
|
||||||
defer base.Close() // nolint: errcheck
|
if len(*configErrors) > 0 {
|
||||||
|
for _, err := range *configErrors {
|
||||||
|
logrus.Errorf("Configuration error: %s", err)
|
||||||
|
}
|
||||||
|
logrus.Fatalf("Failed to start due to configuration errors")
|
||||||
|
}
|
||||||
|
|
||||||
accountDB := base.CreateAccountsDB()
|
internal.SetupStdLogging()
|
||||||
federation := ygg.CreateFederationClient(base)
|
internal.SetupHookLogging(cfg.Logging)
|
||||||
|
internal.SetupPprof()
|
||||||
|
|
||||||
|
logrus.Infof("Dendrite version %s", internal.VersionString())
|
||||||
|
|
||||||
|
if !cfg.ClientAPI.RegistrationDisabled && cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled {
|
||||||
|
logrus.Warn("Open registration is enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
closer, err := cfg.SetupTracing()
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Panicf("failed to start opentracing")
|
||||||
|
}
|
||||||
|
defer closer.Close()
|
||||||
|
|
||||||
|
if cfg.Global.Sentry.Enabled {
|
||||||
|
logrus.Info("Setting up Sentry for debugging...")
|
||||||
|
err = sentry.Init(sentry.ClientOptions{
|
||||||
|
Dsn: cfg.Global.Sentry.DSN,
|
||||||
|
Environment: cfg.Global.Sentry.Environment,
|
||||||
|
Debug: true,
|
||||||
|
ServerName: string(cfg.Global.ServerName),
|
||||||
|
Release: "dendrite@" + internal.VersionString(),
|
||||||
|
AttachStacktrace: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Panic("failed to start Sentry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processCtx := process.NewProcessContext()
|
||||||
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
routers := httputil.NewRouters()
|
||||||
|
basepkg.ConfigureAdminEndpoints(processCtx, routers)
|
||||||
|
m.processContext = processCtx
|
||||||
|
defer func() {
|
||||||
|
processCtx.ShutdownDendrite()
|
||||||
|
processCtx.WaitForShutdown()
|
||||||
|
}() // nolint: errcheck
|
||||||
|
|
||||||
|
federation := ygg.CreateFederationClient(cfg)
|
||||||
|
|
||||||
serverKeyAPI := &signing.YggdrasilKeys{}
|
serverKeyAPI := &signing.YggdrasilKeys{}
|
||||||
keyRing := serverKeyAPI.KeyRing()
|
keyRing := serverKeyAPI.KeyRing()
|
||||||
|
|
||||||
rsAPI := roomserver.NewInternalAPI(base)
|
caches := caching.NewRistrettoCache(cfg.Global.Cache.EstimatedMaxSize, cfg.Global.Cache.MaxAge, caching.EnableMetrics)
|
||||||
|
natsInstance := jetstream.NATSInstance{}
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.EnableMetrics)
|
||||||
|
|
||||||
fsAPI := federationapi.NewInternalAPI(
|
fsAPI := federationapi.NewInternalAPI(
|
||||||
base, federation, rsAPI, base.Caches, keyRing, true,
|
processCtx, cfg, cm, &natsInstance, federation, rsAPI, caches, keyRing, true,
|
||||||
)
|
)
|
||||||
|
|
||||||
keyAPI := keyserver.NewInternalAPI(base, &base.Cfg.KeyServer, federation)
|
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, federation, caching.EnableMetrics, fsAPI.IsBlacklistedOrBackingOff)
|
||||||
userAPI := userapi.NewInternalAPI(base, accountDB, &cfg.UserAPI, cfg.Derived.ApplicationServices, keyAPI, rsAPI, base.PushGatewayHTTPClient())
|
|
||||||
keyAPI.SetUserAPI(userAPI)
|
|
||||||
|
|
||||||
asAPI := appservice.NewInternalAPI(base, userAPI, rsAPI)
|
asAPI := appservice.NewInternalAPI(processCtx, cfg, &natsInstance, userAPI, rsAPI)
|
||||||
rsAPI.SetAppserviceAPI(asAPI)
|
rsAPI.SetAppserviceAPI(asAPI)
|
||||||
|
|
||||||
// The underlying roomserver implementation needs to be able to call the fedsender.
|
// The underlying roomserver implementation needs to be able to call the fedsender.
|
||||||
|
@ -129,9 +226,8 @@ func (m *DendriteMonolith) Start() {
|
||||||
rsAPI.SetFederationAPI(fsAPI, keyRing)
|
rsAPI.SetFederationAPI(fsAPI, keyRing)
|
||||||
|
|
||||||
monolith := setup.Monolith{
|
monolith := setup.Monolith{
|
||||||
Config: base.Cfg,
|
Config: cfg,
|
||||||
AccountDB: accountDB,
|
Client: ygg.CreateClient(),
|
||||||
Client: ygg.CreateClient(base),
|
|
||||||
FedClient: federation,
|
FedClient: federation,
|
||||||
KeyRing: keyRing,
|
KeyRing: keyRing,
|
||||||
|
|
||||||
|
@ -139,29 +235,21 @@ func (m *DendriteMonolith) Start() {
|
||||||
FederationAPI: fsAPI,
|
FederationAPI: fsAPI,
|
||||||
RoomserverAPI: rsAPI,
|
RoomserverAPI: rsAPI,
|
||||||
UserAPI: userAPI,
|
UserAPI: userAPI,
|
||||||
KeyAPI: keyAPI,
|
|
||||||
ExtPublicRoomsProvider: yggrooms.NewYggdrasilRoomProvider(
|
ExtPublicRoomsProvider: yggrooms.NewYggdrasilRoomProvider(
|
||||||
ygg, fsAPI, federation,
|
ygg, fsAPI, federation,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
monolith.AddAllPublicRoutes(
|
monolith.AddAllPublicRoutes(processCtx, cfg, routers, cm, &natsInstance, caches, caching.EnableMetrics)
|
||||||
base.ProcessContext,
|
|
||||||
base.PublicClientAPIMux,
|
|
||||||
base.PublicFederationAPIMux,
|
|
||||||
base.PublicKeyAPIMux,
|
|
||||||
base.PublicWellKnownAPIMux,
|
|
||||||
base.PublicMediaAPIMux,
|
|
||||||
base.SynapseAdminMux,
|
|
||||||
)
|
|
||||||
|
|
||||||
httpRouter := mux.NewRouter()
|
httpRouter := mux.NewRouter()
|
||||||
httpRouter.PathPrefix(httputil.InternalPathPrefix).Handler(base.InternalAPIMux)
|
httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(routers.Client)
|
||||||
httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(base.PublicClientAPIMux)
|
httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(routers.Media)
|
||||||
httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
|
httpRouter.PathPrefix(httputil.DendriteAdminPathPrefix).Handler(routers.DendriteAdmin)
|
||||||
|
httpRouter.PathPrefix(httputil.SynapseAdminPathPrefix).Handler(routers.SynapseAdmin)
|
||||||
|
|
||||||
yggRouter := mux.NewRouter()
|
yggRouter := mux.NewRouter()
|
||||||
yggRouter.PathPrefix(httputil.PublicFederationPathPrefix).Handler(base.PublicFederationAPIMux)
|
yggRouter.PathPrefix(httputil.PublicFederationPathPrefix).Handler(routers.Federation)
|
||||||
yggRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
|
yggRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(routers.Media)
|
||||||
|
|
||||||
// Build both ends of a HTTP multiplex.
|
// Build both ends of a HTTP multiplex.
|
||||||
m.httpServer = &http.Server{
|
m.httpServer = &http.Server{
|
||||||
|
@ -178,11 +266,11 @@ func (m *DendriteMonolith) Start() {
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
m.logger.Info("Listening on ", ygg.DerivedServerName())
|
m.logger.Info("Listening on ", ygg.DerivedServerName())
|
||||||
m.logger.Fatal(m.httpServer.Serve(ygg))
|
m.logger.Error(m.httpServer.Serve(ygg))
|
||||||
}()
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
logrus.Info("Listening on ", m.listener.Addr())
|
logrus.Info("Listening on ", m.listener.Addr())
|
||||||
logrus.Fatal(http.Serve(m.listener, httpRouter))
|
logrus.Error(http.Serve(m.listener, httpRouter))
|
||||||
}()
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
logrus.Info("Sending wake-up message to known nodes")
|
logrus.Info("Sending wake-up message to known nodes")
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
FROM golang:1.16-stretch as build
|
#syntax=docker/dockerfile:1.2
|
||||||
|
|
||||||
|
FROM golang:1.20-bullseye as build
|
||||||
RUN apt-get update && apt-get install -y sqlite3
|
RUN apt-get update && apt-get install -y sqlite3
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
|
@ -8,25 +10,27 @@ RUN mkdir /dendrite
|
||||||
|
|
||||||
# Utilise Docker caching when downloading dependencies, this stops us needlessly
|
# Utilise Docker caching when downloading dependencies, this stops us needlessly
|
||||||
# downloading dependencies every time.
|
# downloading dependencies every time.
|
||||||
COPY go.mod .
|
ARG CGO
|
||||||
COPY go.sum .
|
RUN --mount=target=. \
|
||||||
RUN go mod download
|
--mount=type=cache,target=/go/pkg/mod \
|
||||||
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
COPY . .
|
CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-config && \
|
||||||
RUN go build -o /dendrite ./cmd/dendrite-monolith-server
|
CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-keys && \
|
||||||
RUN go build -o /dendrite ./cmd/generate-keys
|
CGO_ENABLED=${CGO} go build -o /dendrite/dendrite ./cmd/dendrite && \
|
||||||
RUN go build -o /dendrite ./cmd/generate-config
|
CGO_ENABLED=${CGO} go build -cover -covermode=atomic -o /dendrite/dendrite-cover -coverpkg "github.com/matrix-org/..." ./cmd/dendrite && \
|
||||||
|
cp build/scripts/complement-cmd.sh /complement-cmd.sh
|
||||||
|
|
||||||
WORKDIR /dendrite
|
WORKDIR /dendrite
|
||||||
RUN ./generate-keys --private-key matrix_key.pem
|
RUN ./generate-keys --private-key matrix_key.pem
|
||||||
|
|
||||||
ENV SERVER_NAME=localhost
|
ENV SERVER_NAME=localhost
|
||||||
ENV API=0
|
ENV API=0
|
||||||
|
ENV COVER=0
|
||||||
EXPOSE 8008 8448
|
EXPOSE 8008 8448
|
||||||
|
|
||||||
# At runtime, generate TLS cert based on the CA now mounted at /ca
|
# At runtime, generate TLS cert based on the CA now mounted at /ca
|
||||||
# At runtime, replace the SERVER_NAME with what we are told
|
# At runtime, replace the SERVER_NAME with what we are told
|
||||||
CMD ./generate-keys --server $SERVER_NAME --tls-cert server.crt --tls-key server.key --tls-authority-cert /complement/ca/ca.crt --tls-authority-key /complement/ca/ca.key && \
|
CMD ./generate-keys -keysize 1024 --server $SERVER_NAME --tls-cert server.crt --tls-key server.key --tls-authority-cert /complement/ca/ca.crt --tls-authority-key /complement/ca/ca.key && \
|
||||||
./generate-config -server $SERVER_NAME --ci > dendrite.yaml && \
|
./generate-config -server $SERVER_NAME --ci > dendrite.yaml && \
|
||||||
cp /complement/ca/ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates && \
|
cp /complement/ca/ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates && \
|
||||||
./dendrite-monolith-server --tls-cert server.crt --tls-key server.key --config dendrite.yaml -api=${API:-0}
|
exec /complement-cmd.sh
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#syntax=docker/dockerfile:1.2
|
||||||
|
|
||||||
# A local development Complement dockerfile, to be used with host mounts
|
# A local development Complement dockerfile, to be used with host mounts
|
||||||
# /cache -> Contains the entire dendrite code at Dockerfile build time. Builds binaries but only keeps the generate-* ones. Pre-compilation saves time.
|
# /cache -> Contains the entire dendrite code at Dockerfile build time. Builds binaries but only keeps the generate-* ones. Pre-compilation saves time.
|
||||||
# /dendrite -> Host-mounted sources
|
# /dendrite -> Host-mounted sources
|
||||||
|
@ -6,48 +8,48 @@
|
||||||
#
|
#
|
||||||
# Use these mounts to make use of this dockerfile:
|
# Use these mounts to make use of this dockerfile:
|
||||||
# COMPLEMENT_HOST_MOUNTS='/your/local/dendrite:/dendrite:ro;/your/go/path:/go:ro'
|
# COMPLEMENT_HOST_MOUNTS='/your/local/dendrite:/dendrite:ro;/your/go/path:/go:ro'
|
||||||
FROM golang:1.16-stretch
|
FROM golang:1.18-stretch
|
||||||
RUN apt-get update && apt-get install -y sqlite3
|
RUN apt-get update && apt-get install -y sqlite3
|
||||||
|
|
||||||
WORKDIR /runtime
|
|
||||||
|
|
||||||
ENV SERVER_NAME=localhost
|
ENV SERVER_NAME=localhost
|
||||||
|
ENV COVER=0
|
||||||
EXPOSE 8008 8448
|
EXPOSE 8008 8448
|
||||||
|
|
||||||
|
WORKDIR /runtime
|
||||||
# This script compiles Dendrite for us.
|
# This script compiles Dendrite for us.
|
||||||
RUN echo '\
|
RUN echo '\
|
||||||
#!/bin/bash -eux \n\
|
#!/bin/bash -eux \n\
|
||||||
if test -f "/runtime/dendrite-monolith-server"; then \n\
|
if test -f "/runtime/dendrite" && test -f "/runtime/dendrite-cover"; then \n\
|
||||||
echo "Skipping compilation; binaries exist" \n\
|
echo "Skipping compilation; binaries exist" \n\
|
||||||
exit 0 \n\
|
exit 0 \n\
|
||||||
fi \n\
|
fi \n\
|
||||||
cd /dendrite \n\
|
cd /dendrite \n\
|
||||||
go build -v -o /runtime /dendrite/cmd/dendrite-monolith-server \n\
|
go build -v -o /runtime /dendrite/cmd/dendrite \n\
|
||||||
' > compile.sh && chmod +x compile.sh
|
go test -c -cover -covermode=atomic -o /runtime/dendrite-cover -coverpkg "github.com/matrix-org/..." /dendrite/cmd/dendrite \n\
|
||||||
|
' > compile.sh && chmod +x compile.sh
|
||||||
|
|
||||||
# This script runs Dendrite for us. Must be run in the /runtime directory.
|
# This script runs Dendrite for us. Must be run in the /runtime directory.
|
||||||
RUN echo '\
|
RUN echo '\
|
||||||
#!/bin/bash -eu \n\
|
#!/bin/bash -eu \n\
|
||||||
./generate-keys --private-key matrix_key.pem \n\
|
./generate-keys --private-key matrix_key.pem \n\
|
||||||
./generate-keys --server $SERVER_NAME --tls-cert server.crt --tls-key server.key --tls-authority-cert /complement/ca/ca.crt --tls-authority-key /complement/ca/ca.key \n\
|
./generate-keys -keysize 1024 --server $SERVER_NAME --tls-cert server.crt --tls-key server.key --tls-authority-cert /complement/ca/ca.crt --tls-authority-key /complement/ca/ca.key \n\
|
||||||
./generate-config -server $SERVER_NAME --ci > dendrite.yaml \n\
|
./generate-config -server $SERVER_NAME --ci > dendrite.yaml \n\
|
||||||
cp /complement/ca/ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates \n\
|
cp /complement/ca/ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates \n\
|
||||||
./dendrite-monolith-server --tls-cert server.crt --tls-key server.key --config dendrite.yaml \n\
|
[ ${COVER} -eq 1 ] && exec ./dendrite-cover --test.coverprofile=integrationcover.log --really-enable-open-registration --tls-cert server.crt --tls-key server.key --config dendrite.yaml \n\
|
||||||
' > run.sh && chmod +x run.sh
|
exec ./dendrite --really-enable-open-registration --tls-cert server.crt --tls-key server.key --config dendrite.yaml \n\
|
||||||
|
' > run.sh && chmod +x run.sh
|
||||||
|
|
||||||
|
|
||||||
WORKDIR /cache
|
WORKDIR /cache
|
||||||
# Pre-download deps; we don't need to do this if the GOPATH is mounted.
|
|
||||||
COPY go.mod .
|
|
||||||
COPY go.sum .
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
# Build the monolith in /cache - we won't actually use this but will rely on build artifacts to speed
|
# Build the monolith in /cache - we won't actually use this but will rely on build artifacts to speed
|
||||||
# up the real compilation. Build the generate-* binaries in the true /runtime locations.
|
# up the real compilation. Build the generate-* binaries in the true /runtime locations.
|
||||||
# If the generate-* source is changed, this dockerfile needs re-running.
|
# If the generate-* source is changed, this dockerfile needs re-running.
|
||||||
COPY . .
|
RUN --mount=target=. \
|
||||||
RUN go build ./cmd/dendrite-monolith-server && go build -o /runtime ./cmd/generate-keys && go build -o /runtime ./cmd/generate-config
|
--mount=type=cache,target=/go/pkg/mod \
|
||||||
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
go build -o /runtime ./cmd/generate-config && \
|
||||||
|
go build -o /runtime ./cmd/generate-keys
|
||||||
|
|
||||||
|
|
||||||
WORKDIR /runtime
|
WORKDIR /runtime
|
||||||
CMD /runtime/compile.sh && /runtime/run.sh
|
CMD /runtime/compile.sh && exec /runtime/run.sh
|
||||||
|
|
|
@ -1,24 +1,26 @@
|
||||||
FROM golang:1.16-stretch as build
|
#syntax=docker/dockerfile:1.2
|
||||||
|
|
||||||
|
FROM golang:1.20-bullseye as build
|
||||||
RUN apt-get update && apt-get install -y postgresql
|
RUN apt-get update && apt-get install -y postgresql
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
# No password when connecting over localhost
|
# No password when connecting over localhost
|
||||||
RUN sed -i "s%127.0.0.1/32 md5%127.0.0.1/32 trust%g" /etc/postgresql/9.6/main/pg_hba.conf && \
|
RUN sed -i "s%127.0.0.1/32 md5%127.0.0.1/32 trust%g" /etc/postgresql/13/main/pg_hba.conf && \
|
||||||
# Bump up max conns for moar concurrency
|
# Bump up max conns for moar concurrency
|
||||||
sed -i 's/max_connections = 100/max_connections = 2000/g' /etc/postgresql/9.6/main/postgresql.conf
|
sed -i 's/max_connections = 100/max_connections = 2000/g' /etc/postgresql/13/main/postgresql.conf
|
||||||
|
|
||||||
# This entry script starts postgres, waits for it to be up then starts dendrite
|
# This entry script starts postgres, waits for it to be up then starts dendrite
|
||||||
RUN echo '\
|
RUN echo '\
|
||||||
#!/bin/bash -eu \n\
|
#!/bin/bash -eu \n\
|
||||||
pg_lsclusters \n\
|
pg_lsclusters \n\
|
||||||
pg_ctlcluster 9.6 main start \n\
|
pg_ctlcluster 13 main start \n\
|
||||||
\n\
|
\n\
|
||||||
until pg_isready \n\
|
until pg_isready \n\
|
||||||
do \n\
|
do \n\
|
||||||
echo "Waiting for postgres"; \n\
|
echo "Waiting for postgres"; \n\
|
||||||
sleep 1; \n\
|
sleep 1; \n\
|
||||||
done \n\
|
done \n\
|
||||||
' > run_postgres.sh && chmod +x run_postgres.sh
|
' > run_postgres.sh && chmod +x run_postgres.sh
|
||||||
|
|
||||||
# we will dump the binaries and config file to this location to ensure any local untracked files
|
# we will dump the binaries and config file to this location to ensure any local untracked files
|
||||||
# that come from the COPY . . file don't contaminate the build
|
# that come from the COPY . . file don't contaminate the build
|
||||||
|
@ -26,29 +28,30 @@ RUN mkdir /dendrite
|
||||||
|
|
||||||
# Utilise Docker caching when downloading dependencies, this stops us needlessly
|
# Utilise Docker caching when downloading dependencies, this stops us needlessly
|
||||||
# downloading dependencies every time.
|
# downloading dependencies every time.
|
||||||
COPY go.mod .
|
ARG CGO
|
||||||
COPY go.sum .
|
RUN --mount=target=. \
|
||||||
RUN go mod download
|
--mount=type=cache,target=/go/pkg/mod \
|
||||||
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
COPY . .
|
CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-config && \
|
||||||
RUN go build -o /dendrite ./cmd/dendrite-monolith-server
|
CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-keys && \
|
||||||
RUN go build -o /dendrite ./cmd/generate-keys
|
CGO_ENABLED=${CGO} go build -o /dendrite/dendrite ./cmd/dendrite && \
|
||||||
RUN go build -o /dendrite ./cmd/generate-config
|
CGO_ENABLED=${CGO} go build -cover -covermode=atomic -o /dendrite/dendrite-cover -coverpkg "github.com/matrix-org/..." ./cmd/dendrite && \
|
||||||
|
cp build/scripts/complement-cmd.sh /complement-cmd.sh
|
||||||
|
|
||||||
WORKDIR /dendrite
|
WORKDIR /dendrite
|
||||||
RUN ./generate-keys --private-key matrix_key.pem
|
RUN ./generate-keys --private-key matrix_key.pem
|
||||||
|
|
||||||
ENV SERVER_NAME=localhost
|
ENV SERVER_NAME=localhost
|
||||||
ENV API=0
|
ENV API=0
|
||||||
|
ENV COVER=0
|
||||||
EXPOSE 8008 8448
|
EXPOSE 8008 8448
|
||||||
|
|
||||||
|
|
||||||
# At runtime, generate TLS cert based on the CA now mounted at /ca
|
# At runtime, generate TLS cert based on the CA now mounted at /ca
|
||||||
# At runtime, replace the SERVER_NAME with what we are told
|
# At runtime, replace the SERVER_NAME with what we are told
|
||||||
CMD /build/run_postgres.sh && ./generate-keys --server $SERVER_NAME --tls-cert server.crt --tls-key server.key --tls-authority-cert /complement/ca/ca.crt --tls-authority-key /complement/ca/ca.key && \
|
CMD /build/run_postgres.sh && ./generate-keys --keysize 1024 --server $SERVER_NAME --tls-cert server.crt --tls-key server.key --tls-authority-cert /complement/ca/ca.crt --tls-authority-key /complement/ca/ca.key && \
|
||||||
./generate-config -server $SERVER_NAME --ci > dendrite.yaml && \
|
./generate-config -server $SERVER_NAME --ci --db postgresql://postgres@localhost/postgres?sslmode=disable > dendrite.yaml && \
|
||||||
# Replace the connection string with a single postgres DB, using user/db = 'postgres' and no password, bump max_conns
|
# Bump max_open_conns up here in the global database config
|
||||||
sed -i "s%connection_string:.*$%connection_string: postgresql://postgres@localhost/postgres?sslmode=disable%g" dendrite.yaml && \
|
sed -i 's/max_open_conns:.*$/max_open_conns: 1990/g' dendrite.yaml && \
|
||||||
sed -i 's/max_open_conns:.*$/max_open_conns: 100/g' dendrite.yaml && \
|
cp /complement/ca/ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates && \
|
||||||
cp /complement/ca/ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates && \
|
exec /complement-cmd.sh
|
||||||
./dendrite-monolith-server --tls-cert server.crt --tls-key server.key --config dendrite.yaml -api=${API:-0}
|
|
|
@ -13,4 +13,4 @@ go build ./cmd/...
|
||||||
./build/scripts/find-lint.sh
|
./build/scripts/find-lint.sh
|
||||||
|
|
||||||
echo "Testing..."
|
echo "Testing..."
|
||||||
go test -v ./...
|
go test --race -v ./...
|
||||||
|
|
21
build/scripts/complement-cmd.sh
Executable file
21
build/scripts/complement-cmd.sh
Executable file
|
@ -0,0 +1,21 @@
|
||||||
|
#!/bin/bash -e
|
||||||
|
|
||||||
|
# This script is intended to be used inside a docker container for Complement
|
||||||
|
|
||||||
|
export GOCOVERDIR=/tmp/covdatafiles
|
||||||
|
mkdir -p "${GOCOVERDIR}"
|
||||||
|
if [[ "${COVER}" -eq 1 ]]; then
|
||||||
|
echo "Running with coverage"
|
||||||
|
exec /dendrite/dendrite-cover \
|
||||||
|
--really-enable-open-registration \
|
||||||
|
--tls-cert server.crt \
|
||||||
|
--tls-key server.key \
|
||||||
|
--config dendrite.yaml
|
||||||
|
else
|
||||||
|
echo "Not running with coverage"
|
||||||
|
exec /dendrite/dendrite \
|
||||||
|
--really-enable-open-registration \
|
||||||
|
--tls-cert server.crt \
|
||||||
|
--tls-key server.key \
|
||||||
|
--config dendrite.yaml
|
||||||
|
fi
|
|
@ -15,5 +15,5 @@ tar -xzf master.tar.gz
|
||||||
|
|
||||||
# Run the tests!
|
# Run the tests!
|
||||||
cd complement-master
|
cd complement-master
|
||||||
COMPLEMENT_BASE_IMAGE=complement-dendrite:latest go test -v -count=1 ./tests
|
COMPLEMENT_BASE_IMAGE=complement-dendrite:latest go test -v -count=1 ./tests ./tests/csapi
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ echo "Installing golangci-lint..."
|
||||||
|
|
||||||
# Make a backup of go.{mod,sum} first
|
# Make a backup of go.{mod,sum} first
|
||||||
cp go.mod go.mod.bak && cp go.sum go.sum.bak
|
cp go.mod go.mod.bak && cp go.sum go.sum.bak
|
||||||
go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.41.1
|
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.45.2
|
||||||
|
|
||||||
# Run linting
|
# Run linting
|
||||||
echo "Looking for lint..."
|
echo "Looking for lint..."
|
||||||
|
@ -33,7 +33,7 @@ echo "Looking for lint..."
|
||||||
# Capture exit code to ensure go.{mod,sum} is restored before exiting
|
# Capture exit code to ensure go.{mod,sum} is restored before exiting
|
||||||
exit_code=0
|
exit_code=0
|
||||||
|
|
||||||
PATH="$PATH:${GOPATH:-~/go}/bin" golangci-lint run $args || exit_code=1
|
PATH="$PATH:$(go env GOPATH)/bin" golangci-lint run $args || exit_code=1
|
||||||
|
|
||||||
# Restore go.{mod,sum}
|
# Restore go.{mod,sum}
|
||||||
mv go.mod.bak go.mod && mv go.sum.bak go.sum
|
mv go.mod.bak go.mod && mv go.sum.bak go.sum
|
||||||
|
|
1475
clientapi/admin_test.go
Normal file
1475
clientapi/admin_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -14,10 +14,18 @@
|
||||||
|
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import "github.com/matrix-org/gomatrixserverlib"
|
import "github.com/matrix-org/gomatrixserverlib/fclient"
|
||||||
|
|
||||||
// ExtraPublicRoomsProvider provides a way to inject extra published rooms into /publicRooms requests.
|
// ExtraPublicRoomsProvider provides a way to inject extra published rooms into /publicRooms requests.
|
||||||
type ExtraPublicRoomsProvider interface {
|
type ExtraPublicRoomsProvider interface {
|
||||||
// Rooms returns the extra rooms. This is called on-demand by clients, so cache appropriately.
|
// Rooms returns the extra rooms. This is called on-demand by clients, so cache appropriately.
|
||||||
Rooms() []gomatrixserverlib.PublicRoom
|
Rooms() []fclient.PublicRoom
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegistrationToken struct {
|
||||||
|
Token *string `json:"token"`
|
||||||
|
UsesAllowed *int32 `json:"uses_allowed"`
|
||||||
|
Pending *int32 `json:"pending"`
|
||||||
|
Completed *int32 `json:"completed"`
|
||||||
|
ExpiryTime *int64 `json:"expiry_time"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -51,14 +51,14 @@ type AccountDatabase interface {
|
||||||
// Note: For an AS user, AS dummy device is returned.
|
// Note: For an AS user, AS dummy device is returned.
|
||||||
// On failure returns an JSON error response which can be sent to the client.
|
// On failure returns an JSON error response which can be sent to the client.
|
||||||
func VerifyUserFromRequest(
|
func VerifyUserFromRequest(
|
||||||
req *http.Request, userAPI api.UserInternalAPI,
|
req *http.Request, userAPI api.QueryAcccessTokenAPI,
|
||||||
) (*api.Device, *util.JSONResponse) {
|
) (*api.Device, *util.JSONResponse) {
|
||||||
// Try to find the Application Service user
|
// Try to find the Application Service user
|
||||||
token, err := ExtractAccessToken(req)
|
token, err := ExtractAccessToken(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: jsonerror.MissingToken(err.Error()),
|
JSON: spec.MissingToken(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var res api.QueryAccessTokenResponse
|
var res api.QueryAccessTokenResponse
|
||||||
|
@ -68,21 +68,23 @@ func VerifyUserFromRequest(
|
||||||
}, &res)
|
}, &res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("userAPI.QueryAccessToken failed")
|
util.GetLogger(req.Context()).WithError(err).Error("userAPI.QueryAccessToken failed")
|
||||||
jsonErr := jsonerror.InternalServerError()
|
return nil, &util.JSONResponse{
|
||||||
return nil, &jsonErr
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if res.Err != "" {
|
if res.Err != "" {
|
||||||
if strings.HasPrefix(strings.ToLower(res.Err), "forbidden:") { // TODO: use actual error and no string comparison
|
if strings.HasPrefix(strings.ToLower(res.Err), "forbidden:") { // TODO: use actual error and no string comparison
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden(res.Err),
|
JSON: spec.Forbidden(res.Err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if res.Device == nil {
|
if res.Device == nil {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: jsonerror.UnknownToken("Unknown token"),
|
JSON: spec.UnknownToken("Unknown token"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res.Device, nil
|
return res.Device, nil
|
||||||
|
|
|
@ -16,6 +16,8 @@ package authtypes
|
||||||
|
|
||||||
// ThreePID represents a third-party identifier
|
// ThreePID represents a third-party identifier
|
||||||
type ThreePID struct {
|
type ThreePID struct {
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
Medium string `json:"medium"`
|
Medium string `json:"medium"`
|
||||||
|
AddedAt int64 `json:"added_at"`
|
||||||
|
ValidatedAt int64 `json:"validated_at"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,16 +15,14 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
uapi "github.com/matrix-org/dendrite/userapi/api"
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,12 +31,17 @@ import (
|
||||||
// called after authorization has completed, with the result of the authorization.
|
// called after authorization has completed, with the result of the authorization.
|
||||||
// If the final return value is non-nil, an error occurred and the cleanup function
|
// If the final return value is non-nil, an error occurred and the cleanup function
|
||||||
// is nil.
|
// is nil.
|
||||||
func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.UserAccountAPI, userAPI UserInternalAPIForLogin, cfg *config.ClientAPI) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
func LoginFromJSONReader(
|
||||||
reqBytes, err := ioutil.ReadAll(r)
|
req *http.Request,
|
||||||
|
useraccountAPI uapi.UserLoginAPI,
|
||||||
|
userAPI UserInternalAPIForLogin,
|
||||||
|
cfg *config.ClientAPI,
|
||||||
|
) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
||||||
|
reqBytes, err := io.ReadAll(req.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := &util.JSONResponse{
|
err := &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("Reading request body failed: " + err.Error()),
|
JSON: spec.BadJSON("Reading request body failed: " + err.Error()),
|
||||||
}
|
}
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -49,7 +52,7 @@ func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.U
|
||||||
if err := json.Unmarshal(reqBytes, &header); err != nil {
|
if err := json.Unmarshal(reqBytes, &header); err != nil {
|
||||||
err := &util.JSONResponse{
|
err := &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("Reading request body failed: " + err.Error()),
|
JSON: spec.BadJSON("Reading request body failed: " + err.Error()),
|
||||||
}
|
}
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -58,23 +61,37 @@ func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.U
|
||||||
switch header.Type {
|
switch header.Type {
|
||||||
case authtypes.LoginTypePassword:
|
case authtypes.LoginTypePassword:
|
||||||
typ = &LoginTypePassword{
|
typ = &LoginTypePassword{
|
||||||
GetAccountByPassword: useraccountAPI.QueryAccountByPassword,
|
UserAPI: useraccountAPI,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
case authtypes.LoginTypeToken:
|
case authtypes.LoginTypeToken:
|
||||||
typ = &LoginTypeToken{
|
typ = &LoginTypeToken{
|
||||||
UserAPI: userAPI,
|
UserAPI: userAPI,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
|
case authtypes.LoginTypeApplicationService:
|
||||||
|
token, err := ExtractAccessToken(req)
|
||||||
|
if err != nil {
|
||||||
|
err := &util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.MissingToken(err.Error()),
|
||||||
|
}
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
typ = &LoginTypeApplicationService{
|
||||||
|
Config: cfg,
|
||||||
|
Token: token,
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
err := util.JSONResponse{
|
err := util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidArgumentValue("unhandled login type: " + header.Type),
|
JSON: spec.InvalidParam("unhandled login type: " + header.Type),
|
||||||
}
|
}
|
||||||
return nil, nil, &err
|
return nil, nil, &err
|
||||||
}
|
}
|
||||||
|
|
||||||
return typ.LoginFromJSON(ctx, reqBytes)
|
return typ.LoginFromJSON(req.Context(), reqBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserInternalAPIForLogin contains the aspects of UserAPI required for logging in.
|
// UserInternalAPIForLogin contains the aspects of UserAPI required for logging in.
|
||||||
|
|
55
clientapi/auth/login_application_service.go
Normal file
55
clientapi/auth/login_application_service.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed 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
|
||||||
|
//
|
||||||
|
// http://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.
|
||||||
|
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/internal"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoginTypeApplicationService describes how to authenticate as an
|
||||||
|
// application service
|
||||||
|
type LoginTypeApplicationService struct {
|
||||||
|
Config *config.ClientAPI
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name implements Type
|
||||||
|
func (t *LoginTypeApplicationService) Name() string {
|
||||||
|
return authtypes.LoginTypeApplicationService
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginFromJSON implements Type
|
||||||
|
func (t *LoginTypeApplicationService) LoginFromJSON(
|
||||||
|
ctx context.Context, reqBytes []byte,
|
||||||
|
) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
||||||
|
var r Login
|
||||||
|
if err := httputil.UnmarshalJSON(reqBytes, &r); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := internal.ValidateApplicationServiceRequest(t.Config, r.Identifier.User, t.Token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup := func(ctx context.Context, j *util.JSONResponse) {}
|
||||||
|
return &r, cleanup, nil
|
||||||
|
}
|
|
@ -17,13 +17,17 @@ package auth
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
"github.com/matrix-org/dendrite/clientapi/userutil"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
uapi "github.com/matrix-org/dendrite/userapi/api"
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,8 +35,9 @@ func TestLoginFromJSONReader(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
tsts := []struct {
|
tsts := []struct {
|
||||||
Name string
|
Name string
|
||||||
Body string
|
Body string
|
||||||
|
Token string
|
||||||
|
|
||||||
WantUsername string
|
WantUsername string
|
||||||
WantDeviceID string
|
WantDeviceID string
|
||||||
|
@ -46,7 +51,7 @@ func TestLoginFromJSONReader(t *testing.T) {
|
||||||
"password": "herpassword",
|
"password": "herpassword",
|
||||||
"device_id": "adevice"
|
"device_id": "adevice"
|
||||||
}`,
|
}`,
|
||||||
WantUsername: "alice",
|
WantUsername: "@alice:example.com",
|
||||||
WantDeviceID: "adevice",
|
WantDeviceID: "adevice",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -60,19 +65,69 @@ func TestLoginFromJSONReader(t *testing.T) {
|
||||||
WantDeviceID: "adevice",
|
WantDeviceID: "adevice",
|
||||||
WantDeletedTokens: []string{"atoken"},
|
WantDeletedTokens: []string{"atoken"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "appServiceWorksUserID",
|
||||||
|
Body: `{
|
||||||
|
"type": "m.login.application_service",
|
||||||
|
"identifier": { "type": "m.id.user", "user": "@alice:example.com" },
|
||||||
|
"device_id": "adevice"
|
||||||
|
}`,
|
||||||
|
Token: "astoken",
|
||||||
|
|
||||||
|
WantUsername: "@alice:example.com",
|
||||||
|
WantDeviceID: "adevice",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "appServiceWorksLocalpart",
|
||||||
|
Body: `{
|
||||||
|
"type": "m.login.application_service",
|
||||||
|
"identifier": { "type": "m.id.user", "user": "alice" },
|
||||||
|
"device_id": "adevice"
|
||||||
|
}`,
|
||||||
|
Token: "astoken",
|
||||||
|
|
||||||
|
WantUsername: "alice",
|
||||||
|
WantDeviceID: "adevice",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tst := range tsts {
|
for _, tst := range tsts {
|
||||||
t.Run(tst.Name, func(t *testing.T) {
|
t.Run(tst.Name, func(t *testing.T) {
|
||||||
var userAPI fakeUserInternalAPI
|
var userAPI fakeUserInternalAPI
|
||||||
cfg := &config.ClientAPI{
|
cfg := &config.ClientAPI{
|
||||||
Matrix: &config.Global{
|
Matrix: &config.Global{
|
||||||
ServerName: serverName,
|
SigningIdentity: fclient.SigningIdentity{
|
||||||
|
ServerName: serverName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Derived: &config.Derived{
|
||||||
|
ApplicationServices: []config.ApplicationService{
|
||||||
|
{
|
||||||
|
ID: "anapplicationservice",
|
||||||
|
ASToken: "astoken",
|
||||||
|
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
||||||
|
"users": {
|
||||||
|
{
|
||||||
|
Exclusive: true,
|
||||||
|
Regex: "@alice:example.com",
|
||||||
|
RegexpObject: regexp.MustCompile("@alice:example.com"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
login, cleanup, err := LoginFromJSONReader(ctx, strings.NewReader(tst.Body), &userAPI, &userAPI, cfg)
|
|
||||||
if err != nil {
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tst.Body))
|
||||||
t.Fatalf("LoginFromJSONReader failed: %+v", err)
|
if tst.Token != "" {
|
||||||
|
req.Header.Add("Authorization", "Bearer "+tst.Token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
login, cleanup, jsonErr := LoginFromJSONReader(req, &userAPI, &userAPI, cfg)
|
||||||
|
if jsonErr != nil {
|
||||||
|
t.Fatalf("LoginFromJSONReader failed: %+v", jsonErr)
|
||||||
|
}
|
||||||
|
|
||||||
cleanup(ctx, &util.JSONResponse{Code: http.StatusOK})
|
cleanup(ctx, &util.JSONResponse{Code: http.StatusOK})
|
||||||
|
|
||||||
if login.Username() != tst.WantUsername {
|
if login.Username() != tst.WantUsername {
|
||||||
|
@ -100,16 +155,17 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
tsts := []struct {
|
tsts := []struct {
|
||||||
Name string
|
Name string
|
||||||
Body string
|
Body string
|
||||||
|
Token string
|
||||||
|
|
||||||
WantErrCode string
|
WantErrCode spec.MatrixErrorCode
|
||||||
}{
|
}{
|
||||||
{Name: "empty", WantErrCode: "M_BAD_JSON"},
|
{Name: "empty", WantErrCode: spec.ErrorBadJSON},
|
||||||
{
|
{
|
||||||
Name: "badUnmarshal",
|
Name: "badUnmarshal",
|
||||||
Body: `badsyntaxJSON`,
|
Body: `badsyntaxJSON`,
|
||||||
WantErrCode: "M_BAD_JSON",
|
WantErrCode: spec.ErrorBadJSON,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "badPassword",
|
Name: "badPassword",
|
||||||
|
@ -119,7 +175,7 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
||||||
"password": "invalidpassword",
|
"password": "invalidpassword",
|
||||||
"device_id": "adevice"
|
"device_id": "adevice"
|
||||||
}`,
|
}`,
|
||||||
WantErrCode: "M_FORBIDDEN",
|
WantErrCode: spec.ErrorForbidden,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "badToken",
|
Name: "badToken",
|
||||||
|
@ -128,7 +184,7 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
||||||
"token": "invalidtoken",
|
"token": "invalidtoken",
|
||||||
"device_id": "adevice"
|
"device_id": "adevice"
|
||||||
}`,
|
}`,
|
||||||
WantErrCode: "M_FORBIDDEN",
|
WantErrCode: spec.ErrorForbidden,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "badType",
|
Name: "badType",
|
||||||
|
@ -136,7 +192,46 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
||||||
"type": "m.login.invalid",
|
"type": "m.login.invalid",
|
||||||
"device_id": "adevice"
|
"device_id": "adevice"
|
||||||
}`,
|
}`,
|
||||||
WantErrCode: "M_INVALID_ARGUMENT_VALUE",
|
WantErrCode: spec.ErrorInvalidParam,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "noASToken",
|
||||||
|
Body: `{
|
||||||
|
"type": "m.login.application_service",
|
||||||
|
"identifier": { "type": "m.id.user", "user": "@alice:example.com" },
|
||||||
|
"device_id": "adevice"
|
||||||
|
}`,
|
||||||
|
WantErrCode: "M_MISSING_TOKEN",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "badASToken",
|
||||||
|
Token: "badastoken",
|
||||||
|
Body: `{
|
||||||
|
"type": "m.login.application_service",
|
||||||
|
"identifier": { "type": "m.id.user", "user": "@alice:example.com" },
|
||||||
|
"device_id": "adevice"
|
||||||
|
}`,
|
||||||
|
WantErrCode: "M_UNKNOWN_TOKEN",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "badASNamespace",
|
||||||
|
Token: "astoken",
|
||||||
|
Body: `{
|
||||||
|
"type": "m.login.application_service",
|
||||||
|
"identifier": { "type": "m.id.user", "user": "@bob:example.com" },
|
||||||
|
"device_id": "adevice"
|
||||||
|
}`,
|
||||||
|
WantErrCode: "M_EXCLUSIVE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "badASUserID",
|
||||||
|
Token: "astoken",
|
||||||
|
Body: `{
|
||||||
|
"type": "m.login.application_service",
|
||||||
|
"identifier": { "type": "m.id.user", "user": "@alice:wrong.example.com" },
|
||||||
|
"device_id": "adevice"
|
||||||
|
}`,
|
||||||
|
WantErrCode: "M_INVALID_USERNAME",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tst := range tsts {
|
for _, tst := range tsts {
|
||||||
|
@ -144,14 +239,38 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
||||||
var userAPI fakeUserInternalAPI
|
var userAPI fakeUserInternalAPI
|
||||||
cfg := &config.ClientAPI{
|
cfg := &config.ClientAPI{
|
||||||
Matrix: &config.Global{
|
Matrix: &config.Global{
|
||||||
ServerName: serverName,
|
SigningIdentity: fclient.SigningIdentity{
|
||||||
|
ServerName: serverName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Derived: &config.Derived{
|
||||||
|
ApplicationServices: []config.ApplicationService{
|
||||||
|
{
|
||||||
|
ID: "anapplicationservice",
|
||||||
|
ASToken: "astoken",
|
||||||
|
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
||||||
|
"users": {
|
||||||
|
{
|
||||||
|
Exclusive: true,
|
||||||
|
Regex: "@alice:example.com",
|
||||||
|
RegexpObject: regexp.MustCompile("@alice:example.com"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
_, cleanup, errRes := LoginFromJSONReader(ctx, strings.NewReader(tst.Body), &userAPI, &userAPI, cfg)
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tst.Body))
|
||||||
|
if tst.Token != "" {
|
||||||
|
req.Header.Add("Authorization", "Bearer "+tst.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, cleanup, errRes := LoginFromJSONReader(req, &userAPI, &userAPI, cfg)
|
||||||
if errRes == nil {
|
if errRes == nil {
|
||||||
cleanup(ctx, nil)
|
cleanup(ctx, nil)
|
||||||
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
|
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
|
||||||
} else if merr, ok := errRes.JSON.(*jsonerror.MatrixError); ok && merr.ErrCode != tst.WantErrCode {
|
} else if merr, ok := errRes.JSON.(spec.MatrixError); ok && merr.ErrCode != tst.WantErrCode {
|
||||||
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
|
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -160,7 +279,6 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
||||||
|
|
||||||
type fakeUserInternalAPI struct {
|
type fakeUserInternalAPI struct {
|
||||||
UserInternalAPIForLogin
|
UserInternalAPIForLogin
|
||||||
uapi.UserAccountAPI
|
|
||||||
DeletedTokens []string
|
DeletedTokens []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,7 +288,15 @@ func (ua *fakeUserInternalAPI) QueryAccountByPassword(ctx context.Context, req *
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
res.Exists = true
|
res.Exists = true
|
||||||
res.Account = &uapi.Account{}
|
res.Account = &uapi.Account{UserID: userutil.MakeUserID(req.Localpart, req.ServerName)}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ua *fakeUserInternalAPI) QueryAccountByLocalpart(ctx context.Context, req *uapi.QueryAccountByLocalpartRequest, res *uapi.QueryAccountByLocalpartResponse) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ua *fakeUserInternalAPI) PerformAccountCreation(ctx context.Context, req *uapi.PerformAccountCreationRequest, res *uapi.PerformAccountCreationResponse) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,6 +305,10 @@ func (ua *fakeUserInternalAPI) PerformLoginTokenDeletion(ctx context.Context, re
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ua *fakeUserInternalAPI) PerformLoginTokenCreation(ctx context.Context, req *uapi.PerformLoginTokenCreationRequest, res *uapi.PerformLoginTokenCreationResponse) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (*fakeUserInternalAPI) QueryLoginToken(ctx context.Context, req *uapi.QueryLoginTokenRequest, res *uapi.QueryLoginTokenResponse) error {
|
func (*fakeUserInternalAPI) QueryLoginToken(ctx context.Context, req *uapi.QueryLoginTokenRequest, res *uapi.QueryLoginTokenResponse) error {
|
||||||
if req.Token == "invalidtoken" {
|
if req.Token == "invalidtoken" {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -20,9 +20,9 @@ import (
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
uapi "github.com/matrix-org/dendrite/userapi/api"
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -48,13 +48,15 @@ func (t *LoginTypeToken) LoginFromJSON(ctx context.Context, reqBytes []byte) (*L
|
||||||
var res uapi.QueryLoginTokenResponse
|
var res uapi.QueryLoginTokenResponse
|
||||||
if err := t.UserAPI.QueryLoginToken(ctx, &uapi.QueryLoginTokenRequest{Token: r.Token}, &res); err != nil {
|
if err := t.UserAPI.QueryLoginToken(ctx, &uapi.QueryLoginTokenRequest{Token: r.Token}, &res); err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("UserAPI.QueryLoginToken failed")
|
util.GetLogger(ctx).WithError(err).Error("UserAPI.QueryLoginToken failed")
|
||||||
jsonErr := jsonerror.InternalServerError()
|
return nil, nil, &util.JSONResponse{
|
||||||
return nil, nil, &jsonErr
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if res.Data == nil {
|
if res.Data == nil {
|
||||||
return nil, nil, &util.JSONResponse{
|
return nil, nil, &util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("invalid login token"),
|
JSON: spec.Forbidden("invalid login token"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,20 +16,21 @@ package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"github.com/go-ldap/ldap/v3"
|
||||||
|
"github.com/google/uuid"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/userutil"
|
"github.com/matrix-org/dendrite/clientapi/userutil"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GetAccountByPassword func(ctx context.Context, req *api.QueryAccountByPasswordRequest, res *api.QueryAccountByPasswordResponse) error
|
|
||||||
|
|
||||||
type PasswordRequest struct {
|
type PasswordRequest struct {
|
||||||
Login
|
Login
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
@ -37,8 +38,8 @@ type PasswordRequest struct {
|
||||||
|
|
||||||
// LoginTypePassword implements https://matrix.org/docs/spec/client_server/r0.6.1#password-based
|
// LoginTypePassword implements https://matrix.org/docs/spec/client_server/r0.6.1#password-based
|
||||||
type LoginTypePassword struct {
|
type LoginTypePassword struct {
|
||||||
GetAccountByPassword GetAccountByPassword
|
Config *config.ClientAPI
|
||||||
Config *config.ClientAPI
|
UserAPI api.UserLoginAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *LoginTypePassword) Name() string {
|
func (t *LoginTypePassword) Name() string {
|
||||||
|
@ -59,51 +60,227 @@ func (t *LoginTypePassword) LoginFromJSON(ctx context.Context, reqBytes []byte)
|
||||||
return login, func(context.Context, *util.JSONResponse) {}, nil
|
return login, func(context.Context, *util.JSONResponse) {}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *LoginTypePassword) Login(ctx context.Context, req interface{}) (*Login, *util.JSONResponse) {
|
func (t *LoginTypePassword) Login(ctx context.Context, request *PasswordRequest) (*Login, *util.JSONResponse) {
|
||||||
r := req.(*PasswordRequest)
|
fullUsername := request.Username()
|
||||||
username := strings.ToLower(r.Username())
|
if fullUsername == "" {
|
||||||
if username == "" {
|
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: jsonerror.BadJSON("A username must be supplied."),
|
JSON: spec.BadJSON("A username must be supplied."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
localpart, err := userutil.ParseUsernameParam(username, &t.Config.Matrix.ServerName)
|
if len(request.Password) == 0 {
|
||||||
|
return nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: spec.BadJSON("A password must be supplied."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
username, domain, err := userutil.ParseUsernameParam(fullUsername, t.Config.Matrix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: jsonerror.InvalidUsername(err.Error()),
|
JSON: spec.InvalidUsername(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Squash username to all lowercase letters
|
if !t.Config.Matrix.IsLocalServerName(domain) {
|
||||||
|
return nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: spec.InvalidUsername("The server name is not known."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var account *api.Account
|
||||||
|
if t.Config.Ldap.Enabled {
|
||||||
|
isAdmin, err := t.authenticateLdap(username, request.Password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
acc, err := t.getOrCreateAccount(ctx, username, domain, isAdmin)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
account = acc
|
||||||
|
} else {
|
||||||
|
acc, err := t.authenticateDb(ctx, username, domain, request.Password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
account = acc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the user, so login.Username() can do the right thing
|
||||||
|
request.Identifier.User = account.UserID
|
||||||
|
request.User = account.UserID
|
||||||
|
return &request.Login, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *LoginTypePassword) authenticateDb(ctx context.Context, username string, domain spec.ServerName, password string) (*api.Account, *util.JSONResponse) {
|
||||||
res := &api.QueryAccountByPasswordResponse{}
|
res := &api.QueryAccountByPasswordResponse{}
|
||||||
err = t.GetAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{Localpart: strings.ToLower(localpart), PlaintextPassword: r.Password}, res)
|
err := t.UserAPI.QueryAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{
|
||||||
|
Localpart: strings.ToLower(username),
|
||||||
|
ServerName: domain,
|
||||||
|
PlaintextPassword: password,
|
||||||
|
}, res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.Unknown("unable to fetch account by password"),
|
JSON: spec.Unknown("Unable to fetch account by password."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !res.Exists {
|
if !res.Exists {
|
||||||
err = t.GetAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{
|
err = t.UserAPI.QueryAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{
|
||||||
Localpart: localpart,
|
Localpart: username,
|
||||||
PlaintextPassword: r.Password,
|
ServerName: domain,
|
||||||
|
PlaintextPassword: password,
|
||||||
}, res)
|
}, res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.Unknown("unable to fetch account by password"),
|
JSON: spec.Unknown("Unable to fetch account by password."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Technically we could tell them if the user does not exist by checking if err == sql.ErrNoRows
|
|
||||||
// but that would leak the existence of the user.
|
|
||||||
if !res.Exists {
|
if !res.Exists {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("The username or password was incorrect or the account does not exist."),
|
JSON: spec.Forbidden("The username or password was incorrect or the account does not exist."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &r.Login, nil
|
return res.Account, nil
|
||||||
|
}
|
||||||
|
func (t *LoginTypePassword) authenticateLdap(username, password string) (bool, *util.JSONResponse) {
|
||||||
|
var conn *ldap.Conn
|
||||||
|
conn, err := ldap.DialURL(t.Config.Ldap.Uri)
|
||||||
|
if err != nil {
|
||||||
|
return false, &util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("unable to connect to ldap: " + err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
if t.Config.Ldap.AdminBindEnabled {
|
||||||
|
err = conn.Bind(t.Config.Ldap.AdminBindDn, t.Config.Ldap.AdminBindPassword)
|
||||||
|
if err != nil {
|
||||||
|
return false, &util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("unable to bind to ldap: " + err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filter := strings.ReplaceAll(t.Config.Ldap.SearchFilter, "{username}", username)
|
||||||
|
searchRequest := ldap.NewSearchRequest(
|
||||||
|
t.Config.Ldap.BaseDn, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
|
||||||
|
0, 0, false, filter, []string{t.Config.Ldap.SearchAttribute}, nil,
|
||||||
|
)
|
||||||
|
result, err := conn.Search(searchRequest)
|
||||||
|
if err != nil {
|
||||||
|
return false, &util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("unable to bind to search ldap: " + err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(result.Entries) > 1 {
|
||||||
|
return false, &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: spec.BadJSON("'user' must be duplicated."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(result.Entries) < 1 {
|
||||||
|
return false, &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: spec.BadJSON("'user' not found."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userDN := result.Entries[0].DN
|
||||||
|
err = conn.Bind(userDN, password)
|
||||||
|
if err != nil {
|
||||||
|
return false, &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: spec.InvalidUsername(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bindDn := strings.ReplaceAll(t.Config.Ldap.UserBindDn, "{username}", username)
|
||||||
|
err = conn.Bind(bindDn, password)
|
||||||
|
if err != nil {
|
||||||
|
return false, &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: spec.InvalidUsername(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdmin, err := t.isLdapAdmin(conn, username)
|
||||||
|
if err != nil {
|
||||||
|
return false, &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: spec.InvalidUsername(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isAdmin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *LoginTypePassword) isLdapAdmin(conn *ldap.Conn, username string) (bool, error) {
|
||||||
|
searchRequest := ldap.NewSearchRequest(
|
||||||
|
t.Config.Ldap.AdminGroupDn,
|
||||||
|
ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false,
|
||||||
|
strings.ReplaceAll(t.Config.Ldap.AdminGroupFilter, "{username}", username),
|
||||||
|
[]string{t.Config.Ldap.AdminGroupAttribute},
|
||||||
|
nil)
|
||||||
|
|
||||||
|
sr, err := conn.Search(searchRequest)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sr.Entries) < 1 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *LoginTypePassword) getOrCreateAccount(ctx context.Context, username string, domain spec.ServerName, admin bool) (*api.Account, *util.JSONResponse) {
|
||||||
|
var existing api.QueryAccountByLocalpartResponse
|
||||||
|
err := t.UserAPI.QueryAccountByLocalpart(ctx, &api.QueryAccountByLocalpartRequest{
|
||||||
|
Localpart: username,
|
||||||
|
ServerName: domain,
|
||||||
|
}, &existing)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return existing.Account, nil
|
||||||
|
}
|
||||||
|
if err != sql.ErrNoRows {
|
||||||
|
return nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: spec.InvalidUsername(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accountType := api.AccountTypeUser
|
||||||
|
if admin {
|
||||||
|
accountType = api.AccountTypeAdmin
|
||||||
|
}
|
||||||
|
var created api.PerformAccountCreationResponse
|
||||||
|
err = t.UserAPI.PerformAccountCreation(ctx, &api.PerformAccountCreationRequest{
|
||||||
|
AppServiceID: "ldap",
|
||||||
|
Localpart: username,
|
||||||
|
Password: uuid.New().String(),
|
||||||
|
AccountType: accountType,
|
||||||
|
OnConflict: api.ConflictAbort,
|
||||||
|
}, &created)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(*api.ErrorConflict); ok {
|
||||||
|
return nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.UserInUse("Desired user ID is already taken."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("failed to create account: " + err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return created.Account, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,10 +18,11 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
|
@ -54,7 +55,7 @@ type LoginCleanupFunc func(context.Context, *util.JSONResponse)
|
||||||
// https://matrix.org/docs/spec/client_server/r0.6.1#identifier-types
|
// https://matrix.org/docs/spec/client_server/r0.6.1#identifier-types
|
||||||
type LoginIdentifier struct {
|
type LoginIdentifier struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
// when type = m.id.user
|
// when type = m.id.user or m.id.application_service
|
||||||
User string `json:"user"`
|
User string `json:"user"`
|
||||||
// when type = m.id.thirdparty
|
// when type = m.id.thirdparty
|
||||||
Medium string `json:"medium"`
|
Medium string `json:"medium"`
|
||||||
|
@ -102,21 +103,20 @@ type userInteractiveFlow struct {
|
||||||
// the user already has a valid access token, but we want to double-check
|
// the user already has a valid access token, but we want to double-check
|
||||||
// that it isn't stolen by re-authenticating them.
|
// that it isn't stolen by re-authenticating them.
|
||||||
type UserInteractive struct {
|
type UserInteractive struct {
|
||||||
Completed []string
|
sync.RWMutex
|
||||||
Flows []userInteractiveFlow
|
Flows []userInteractiveFlow
|
||||||
// Map of login type to implementation
|
// Map of login type to implementation
|
||||||
Types map[string]Type
|
Types map[string]Type
|
||||||
// Map of session ID to completed login types, will need to be extended in future
|
// Map of session ID to completed login types, will need to be extended in future
|
||||||
Sessions map[string][]string
|
Sessions map[string][]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserInteractive(userAccountAPI api.UserAccountAPI, cfg *config.ClientAPI) *UserInteractive {
|
func NewUserInteractive(userAccountAPI api.UserLoginAPI, cfg *config.ClientAPI) *UserInteractive {
|
||||||
typePassword := &LoginTypePassword{
|
typePassword := &LoginTypePassword{
|
||||||
GetAccountByPassword: userAccountAPI.QueryAccountByPassword,
|
UserAPI: userAccountAPI,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
return &UserInteractive{
|
return &UserInteractive{
|
||||||
Completed: []string{},
|
|
||||||
Flows: []userInteractiveFlow{
|
Flows: []userInteractiveFlow{
|
||||||
{
|
{
|
||||||
Stages: []string{typePassword.Name()},
|
Stages: []string{typePassword.Name()},
|
||||||
|
@ -130,6 +130,8 @@ func NewUserInteractive(userAccountAPI api.UserAccountAPI, cfg *config.ClientAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UserInteractive) IsSingleStageFlow(authType string) bool {
|
func (u *UserInteractive) IsSingleStageFlow(authType string) bool {
|
||||||
|
u.RLock()
|
||||||
|
defer u.RUnlock()
|
||||||
for _, f := range u.Flows {
|
for _, f := range u.Flows {
|
||||||
if len(f.Stages) == 1 && f.Stages[0] == authType {
|
if len(f.Stages) == 1 && f.Stages[0] == authType {
|
||||||
return true
|
return true
|
||||||
|
@ -139,9 +141,10 @@ func (u *UserInteractive) IsSingleStageFlow(authType string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UserInteractive) AddCompletedStage(sessionID, authType string) {
|
func (u *UserInteractive) AddCompletedStage(sessionID, authType string) {
|
||||||
|
u.Lock()
|
||||||
// TODO: Handle multi-stage flows
|
// TODO: Handle multi-stage flows
|
||||||
u.Completed = append(u.Completed, authType)
|
|
||||||
delete(u.Sessions, sessionID)
|
delete(u.Sessions, sessionID)
|
||||||
|
u.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
type Challenge struct {
|
type Challenge struct {
|
||||||
|
@ -153,12 +156,17 @@ type Challenge struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Challenge returns an HTTP 401 with the supported flows for authenticating
|
// Challenge returns an HTTP 401 with the supported flows for authenticating
|
||||||
func (u *UserInteractive) Challenge(sessionID string) *util.JSONResponse {
|
func (u *UserInteractive) challenge(sessionID string) *util.JSONResponse {
|
||||||
|
u.RLock()
|
||||||
|
completed := u.Sessions[sessionID]
|
||||||
|
flows := u.Flows
|
||||||
|
u.RUnlock()
|
||||||
|
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: 401,
|
Code: 401,
|
||||||
JSON: Challenge{
|
JSON: Challenge{
|
||||||
Completed: u.Completed,
|
Completed: completed,
|
||||||
Flows: u.Flows,
|
Flows: flows,
|
||||||
Session: sessionID,
|
Session: sessionID,
|
||||||
Params: make(map[string]interface{}),
|
Params: make(map[string]interface{}),
|
||||||
},
|
},
|
||||||
|
@ -170,11 +178,15 @@ func (u *UserInteractive) NewSession() *util.JSONResponse {
|
||||||
sessionID, err := GenerateAccessToken()
|
sessionID, err := GenerateAccessToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Error("failed to generate session ID")
|
logrus.WithError(err).Error("failed to generate session ID")
|
||||||
res := jsonerror.InternalServerError()
|
return &util.JSONResponse{
|
||||||
return &res
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
u.Lock()
|
||||||
u.Sessions[sessionID] = []string{}
|
u.Sessions[sessionID] = []string{}
|
||||||
return u.Challenge(sessionID)
|
u.Unlock()
|
||||||
|
return u.challenge(sessionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResponseWithChallenge mixes together a JSON body (e.g an error with errcode/message) with the
|
// ResponseWithChallenge mixes together a JSON body (e.g an error with errcode/message) with the
|
||||||
|
@ -183,15 +195,19 @@ func (u *UserInteractive) ResponseWithChallenge(sessionID string, response inter
|
||||||
mixedObjects := make(map[string]interface{})
|
mixedObjects := make(map[string]interface{})
|
||||||
b, err := json.Marshal(response)
|
b, err := json.Marshal(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ise := jsonerror.InternalServerError()
|
return &util.JSONResponse{
|
||||||
return &ise
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ = json.Unmarshal(b, &mixedObjects)
|
_ = json.Unmarshal(b, &mixedObjects)
|
||||||
challenge := u.Challenge(sessionID)
|
challenge := u.challenge(sessionID)
|
||||||
b, err = json.Marshal(challenge.JSON)
|
b, err = json.Marshal(challenge.JSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ise := jsonerror.InternalServerError()
|
return &util.JSONResponse{
|
||||||
return &ise
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ = json.Unmarshal(b, &mixedObjects)
|
_ = json.Unmarshal(b, &mixedObjects)
|
||||||
|
|
||||||
|
@ -216,22 +232,31 @@ func (u *UserInteractive) Verify(ctx context.Context, bodyBytes []byte, device *
|
||||||
|
|
||||||
// extract the type so we know which login type to use
|
// extract the type so we know which login type to use
|
||||||
authType := gjson.GetBytes(bodyBytes, "auth.type").Str
|
authType := gjson.GetBytes(bodyBytes, "auth.type").Str
|
||||||
|
|
||||||
|
u.RLock()
|
||||||
loginType, ok := u.Types[authType]
|
loginType, ok := u.Types[authType]
|
||||||
|
u.RUnlock()
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("Unknown auth.type: " + authType),
|
JSON: spec.BadJSON("Unknown auth.type: " + authType),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// retrieve the session
|
// retrieve the session
|
||||||
sessionID := gjson.GetBytes(bodyBytes, "auth.session").Str
|
sessionID := gjson.GetBytes(bodyBytes, "auth.session").Str
|
||||||
if _, ok = u.Sessions[sessionID]; !ok {
|
|
||||||
|
u.RLock()
|
||||||
|
_, ok = u.Sessions[sessionID]
|
||||||
|
u.RUnlock()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
// if the login type is part of a single stage flow then allow them to omit the session ID
|
// if the login type is part of a single stage flow then allow them to omit the session ID
|
||||||
if !u.IsSingleStageFlow(authType) {
|
if !u.IsSingleStageFlow(authType) {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.Unknown("The auth.session is missing or unknown."),
|
JSON: spec.Unknown("The auth.session is missing or unknown."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,13 +8,14 @@ import (
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
serverName = gomatrixserverlib.ServerName("example.com")
|
serverName = spec.ServerName("example.com")
|
||||||
// space separated localpart+password -> account
|
// space separated localpart+password -> account
|
||||||
lookup = make(map[string]*api.Account)
|
lookup = make(map[string]*api.Account)
|
||||||
device = &api.Device{
|
device = &api.Device{
|
||||||
|
@ -24,9 +25,7 @@ var (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeAccountDatabase struct {
|
type fakeAccountDatabase struct{}
|
||||||
api.UserAccountAPI
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *fakeAccountDatabase) PerformPasswordUpdate(ctx context.Context, req *api.PerformPasswordUpdateRequest, res *api.PerformPasswordUpdateResponse) error {
|
func (d *fakeAccountDatabase) PerformPasswordUpdate(ctx context.Context, req *api.PerformPasswordUpdateRequest, res *api.PerformPasswordUpdateResponse) error {
|
||||||
return nil
|
return nil
|
||||||
|
@ -46,10 +45,20 @@ func (d *fakeAccountDatabase) QueryAccountByPassword(ctx context.Context, req *a
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *fakeAccountDatabase) QueryAccountByLocalpart(ctx context.Context, req *api.QueryAccountByLocalpartRequest, res *api.QueryAccountByLocalpartResponse) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *fakeAccountDatabase) PerformAccountCreation(ctx context.Context, req *api.PerformAccountCreationRequest, res *api.PerformAccountCreationResponse) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func setup() *UserInteractive {
|
func setup() *UserInteractive {
|
||||||
cfg := &config.ClientAPI{
|
cfg := &config.ClientAPI{
|
||||||
Matrix: &config.Global{
|
Matrix: &config.Global{
|
||||||
ServerName: serverName,
|
SigningIdentity: fclient.SigningIdentity{
|
||||||
|
ServerName: serverName,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return NewUserInteractive(&fakeAccountDatabase{}, cfg)
|
return NewUserInteractive(&fakeAccountDatabase{}, cfg)
|
||||||
|
@ -189,3 +198,38 @@ func TestUserInteractivePasswordBadLogin(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUserInteractive_AddCompletedStage(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
sessionID string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "first user",
|
||||||
|
sessionID: util.RandomString(8),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "second user",
|
||||||
|
sessionID: util.RandomString(8),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "third user",
|
||||||
|
sessionID: util.RandomString(8),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
u := setup()
|
||||||
|
ctx := context.Background()
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, resp := u.Verify(ctx, []byte("{}"), nil)
|
||||||
|
challenge, ok := resp.JSON.(Challenge)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected a Challenge, got %T", resp.JSON)
|
||||||
|
}
|
||||||
|
if len(challenge.Completed) > 0 {
|
||||||
|
t.Fatalf("expected 0 completed stages, got %d", len(challenge.Completed))
|
||||||
|
}
|
||||||
|
u.AddCompletedStage(tt.sessionID, "")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -15,56 +15,54 @@
|
||||||
package clientapi
|
package clientapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gorilla/mux"
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/dendrite/setup/process"
|
||||||
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||||
|
|
||||||
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||||
"github.com/matrix-org/dendrite/clientapi/api"
|
"github.com/matrix-org/dendrite/clientapi/api"
|
||||||
"github.com/matrix-org/dendrite/clientapi/producers"
|
"github.com/matrix-org/dendrite/clientapi/producers"
|
||||||
"github.com/matrix-org/dendrite/clientapi/routing"
|
"github.com/matrix-org/dendrite/clientapi/routing"
|
||||||
federationAPI "github.com/matrix-org/dendrite/federationapi/api"
|
federationAPI "github.com/matrix-org/dendrite/federationapi/api"
|
||||||
"github.com/matrix-org/dendrite/internal/transactions"
|
"github.com/matrix-org/dendrite/internal/transactions"
|
||||||
keyserverAPI "github.com/matrix-org/dendrite/keyserver/api"
|
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
"github.com/matrix-org/dendrite/setup/process"
|
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddPublicRoutes sets up and registers HTTP handlers for the ClientAPI component.
|
// AddPublicRoutes sets up and registers HTTP handlers for the ClientAPI component.
|
||||||
func AddPublicRoutes(
|
func AddPublicRoutes(
|
||||||
process *process.ProcessContext,
|
processContext *process.ProcessContext,
|
||||||
router *mux.Router,
|
routers httputil.Routers,
|
||||||
synapseAdminRouter *mux.Router,
|
cfg *config.Dendrite,
|
||||||
cfg *config.ClientAPI,
|
natsInstance *jetstream.NATSInstance,
|
||||||
federation *gomatrixserverlib.FederationClient,
|
federation fclient.FederationClient,
|
||||||
rsAPI roomserverAPI.RoomserverInternalAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
asAPI appserviceAPI.AppServiceQueryAPI,
|
asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
transactionsCache *transactions.Cache,
|
transactionsCache *transactions.Cache,
|
||||||
fsAPI federationAPI.FederationInternalAPI,
|
fsAPI federationAPI.ClientFederationAPI,
|
||||||
userAPI userapi.UserInternalAPI,
|
userAPI userapi.ClientUserAPI,
|
||||||
userDirectoryProvider userapi.UserDirectoryProvider,
|
userDirectoryProvider userapi.QuerySearchProfilesAPI,
|
||||||
keyAPI keyserverAPI.KeyInternalAPI,
|
extRoomsProvider api.ExtraPublicRoomsProvider, enableMetrics bool,
|
||||||
extRoomsProvider api.ExtraPublicRoomsProvider,
|
|
||||||
mscCfg *config.MSCs,
|
|
||||||
) {
|
) {
|
||||||
js, natsClient := jetstream.Prepare(process, &cfg.Matrix.JetStream)
|
js, natsClient := natsInstance.Prepare(processContext, &cfg.Global.JetStream)
|
||||||
|
|
||||||
syncProducer := &producers.SyncAPIProducer{
|
syncProducer := &producers.SyncAPIProducer{
|
||||||
JetStream: js,
|
JetStream: js,
|
||||||
TopicClientData: cfg.Matrix.JetStream.Prefixed(jetstream.OutputClientData),
|
TopicReceiptEvent: cfg.Global.JetStream.Prefixed(jetstream.OutputReceiptEvent),
|
||||||
TopicReceiptEvent: cfg.Matrix.JetStream.Prefixed(jetstream.OutputReceiptEvent),
|
TopicSendToDeviceEvent: cfg.Global.JetStream.Prefixed(jetstream.OutputSendToDeviceEvent),
|
||||||
TopicSendToDeviceEvent: cfg.Matrix.JetStream.Prefixed(jetstream.OutputSendToDeviceEvent),
|
TopicTypingEvent: cfg.Global.JetStream.Prefixed(jetstream.OutputTypingEvent),
|
||||||
TopicTypingEvent: cfg.Matrix.JetStream.Prefixed(jetstream.OutputTypingEvent),
|
TopicPresenceEvent: cfg.Global.JetStream.Prefixed(jetstream.OutputPresenceEvent),
|
||||||
TopicPresenceEvent: cfg.Matrix.JetStream.Prefixed(jetstream.OutputPresenceEvent),
|
|
||||||
UserAPI: userAPI,
|
UserAPI: userAPI,
|
||||||
ServerName: cfg.Matrix.ServerName,
|
ServerName: cfg.Global.ServerName,
|
||||||
}
|
}
|
||||||
|
|
||||||
routing.Setup(
|
routing.Setup(
|
||||||
router, synapseAdminRouter, cfg, rsAPI, asAPI,
|
routers,
|
||||||
|
cfg, rsAPI, asAPI,
|
||||||
userAPI, userDirectoryProvider, federation,
|
userAPI, userDirectoryProvider, federation,
|
||||||
syncProducer, transactionsCache, fsAPI, keyAPI,
|
syncProducer, transactionsCache, fsAPI,
|
||||||
extRoomsProvider, mscCfg, natsClient,
|
extRoomsProvider, natsClient, enableMetrics,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
2437
clientapi/clientapi_test.go
Normal file
2437
clientapi/clientapi_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -16,11 +16,11 @@ package httputil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,11 +29,13 @@ import (
|
||||||
func UnmarshalJSONRequest(req *http.Request, iface interface{}) *util.JSONResponse {
|
func UnmarshalJSONRequest(req *http.Request, iface interface{}) *util.JSONResponse {
|
||||||
// encoding/json allows invalid utf-8, matrix does not
|
// encoding/json allows invalid utf-8, matrix does not
|
||||||
// https://matrix.org/docs/spec/client_server/r0.6.1#api-standards
|
// https://matrix.org/docs/spec/client_server/r0.6.1#api-standards
|
||||||
body, err := ioutil.ReadAll(req.Body)
|
body, err := io.ReadAll(req.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("ioutil.ReadAll failed")
|
util.GetLogger(req.Context()).WithError(err).Error("io.ReadAll failed")
|
||||||
resp := jsonerror.InternalServerError()
|
return &util.JSONResponse{
|
||||||
return &resp
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return UnmarshalJSON(body, iface)
|
return UnmarshalJSON(body, iface)
|
||||||
|
@ -43,7 +45,7 @@ func UnmarshalJSON(body []byte, iface interface{}) *util.JSONResponse {
|
||||||
if !utf8.Valid(body) {
|
if !utf8.Valid(body) {
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.NotJSON("Body contains invalid UTF-8"),
|
JSON: spec.NotJSON("Body contains invalid UTF-8"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +55,7 @@ func UnmarshalJSON(body []byte, iface interface{}) *util.JSONResponse {
|
||||||
// valid JSON with incorrect types for values.
|
// valid JSON with incorrect types for values.
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("The request body could not be decoded into valid JSON. " + err.Error()),
|
JSON: spec.BadJSON("The request body could not be decoded into valid JSON. " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -1,209 +0,0 @@
|
||||||
// Copyright 2017 Vector Creations Ltd
|
|
||||||
//
|
|
||||||
// Licensed 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
|
|
||||||
//
|
|
||||||
// http://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.
|
|
||||||
|
|
||||||
package jsonerror
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"github.com/matrix-org/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MatrixError represents the "standard error response" in Matrix.
|
|
||||||
// http://matrix.org/docs/spec/client_server/r0.2.0.html#api-standards
|
|
||||||
type MatrixError struct {
|
|
||||||
ErrCode string `json:"errcode"`
|
|
||||||
Err string `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e MatrixError) Error() string {
|
|
||||||
return fmt.Sprintf("%s: %s", e.ErrCode, e.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InternalServerError returns a 500 Internal Server Error in a matrix-compliant
|
|
||||||
// format.
|
|
||||||
func InternalServerError() util.JSONResponse {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: Unknown("Internal Server Error"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unknown is an unexpected error
|
|
||||||
func Unknown(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_UNKNOWN", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forbidden is an error when the client tries to access a resource
|
|
||||||
// they are not allowed to access.
|
|
||||||
func Forbidden(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_FORBIDDEN", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BadJSON is an error when the client supplies malformed JSON.
|
|
||||||
func BadJSON(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_BAD_JSON", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BadAlias is an error when the client supplies a bad alias.
|
|
||||||
func BadAlias(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_BAD_ALIAS", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotJSON is an error when the client supplies something that is not JSON
|
|
||||||
// to a JSON endpoint.
|
|
||||||
func NotJSON(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_NOT_JSON", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotFound is an error when the client tries to access an unknown resource.
|
|
||||||
func NotFound(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_NOT_FOUND", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MissingArgument is an error when the client tries to access a resource
|
|
||||||
// without providing an argument that is required.
|
|
||||||
func MissingArgument(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_MISSING_ARGUMENT", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InvalidArgumentValue is an error when the client tries to provide an
|
|
||||||
// invalid value for a valid argument
|
|
||||||
func InvalidArgumentValue(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_INVALID_ARGUMENT_VALUE", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MissingToken is an error when the client tries to access a resource which
|
|
||||||
// requires authentication without supplying credentials.
|
|
||||||
func MissingToken(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_MISSING_TOKEN", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnknownToken is an error when the client tries to access a resource which
|
|
||||||
// requires authentication and supplies an unrecognised token
|
|
||||||
func UnknownToken(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_UNKNOWN_TOKEN", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WeakPassword is an error which is returned when the client tries to register
|
|
||||||
// using a weak password. http://matrix.org/docs/spec/client_server/r0.2.0.html#password-based
|
|
||||||
func WeakPassword(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_WEAK_PASSWORD", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InvalidUsername is an error returned when the client tries to register an
|
|
||||||
// invalid username
|
|
||||||
func InvalidUsername(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_INVALID_USERNAME", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserInUse is an error returned when the client tries to register an
|
|
||||||
// username that already exists
|
|
||||||
func UserInUse(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_USER_IN_USE", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoomInUse is an error returned when the client tries to make a room
|
|
||||||
// that already exists
|
|
||||||
func RoomInUse(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_ROOM_IN_USE", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ASExclusive is an error returned when an application service tries to
|
|
||||||
// register an username that is outside of its registered namespace, or if a
|
|
||||||
// user attempts to register a username or room alias within an exclusive
|
|
||||||
// namespace.
|
|
||||||
func ASExclusive(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_EXCLUSIVE", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GuestAccessForbidden is an error which is returned when the client is
|
|
||||||
// forbidden from accessing a resource as a guest.
|
|
||||||
func GuestAccessForbidden(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_GUEST_ACCESS_FORBIDDEN", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InvalidSignature is an error which is returned when the client tries
|
|
||||||
// to upload invalid signatures.
|
|
||||||
func InvalidSignature(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_INVALID_SIGNATURE", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InvalidParam is an error that is returned when a parameter was invalid,
|
|
||||||
// traditionally with cross-signing.
|
|
||||||
func InvalidParam(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_INVALID_PARAM", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MissingParam is an error that is returned when a parameter was incorrect,
|
|
||||||
// traditionally with cross-signing.
|
|
||||||
func MissingParam(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_MISSING_PARAM", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LeaveServerNoticeError is an error returned when trying to reject an invite
|
|
||||||
// for a server notice room.
|
|
||||||
func LeaveServerNoticeError() *MatrixError {
|
|
||||||
return &MatrixError{
|
|
||||||
ErrCode: "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM",
|
|
||||||
Err: "You cannot reject this invite",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type IncompatibleRoomVersionError struct {
|
|
||||||
RoomVersion string `json:"room_version"`
|
|
||||||
Error string `json:"error"`
|
|
||||||
Code string `json:"errcode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncompatibleRoomVersion is an error which is returned when the client
|
|
||||||
// requests a room with a version that is unsupported.
|
|
||||||
func IncompatibleRoomVersion(roomVersion gomatrixserverlib.RoomVersion) *IncompatibleRoomVersionError {
|
|
||||||
return &IncompatibleRoomVersionError{
|
|
||||||
Code: "M_INCOMPATIBLE_ROOM_VERSION",
|
|
||||||
RoomVersion: string(roomVersion),
|
|
||||||
Error: "Your homeserver does not support the features required to join this room",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnsupportedRoomVersion is an error which is returned when the client
|
|
||||||
// requests a room with a version that is unsupported.
|
|
||||||
func UnsupportedRoomVersion(msg string) *MatrixError {
|
|
||||||
return &MatrixError{"M_UNSUPPORTED_ROOM_VERSION", msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LimitExceededError is a rate-limiting error.
|
|
||||||
type LimitExceededError struct {
|
|
||||||
MatrixError
|
|
||||||
RetryAfterMS int64 `json:"retry_after_ms,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// LimitExceeded is an error when the client tries to send events too quickly.
|
|
||||||
func LimitExceeded(msg string, retryAfterMS int64) *LimitExceededError {
|
|
||||||
return &LimitExceededError{
|
|
||||||
MatrixError: MatrixError{"M_LIMIT_EXCEEDED", msg},
|
|
||||||
RetryAfterMS: retryAfterMS,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotTrusted is an error which is returned when the client asks the server to
|
|
||||||
// proxy a request (e.g. 3PID association) to a server that isn't trusted
|
|
||||||
func NotTrusted(serverName string) *MatrixError {
|
|
||||||
return &MatrixError{
|
|
||||||
ErrCode: "M_SERVER_NOT_TRUSTED",
|
|
||||||
Err: fmt.Sprintf("Untrusted server '%s'", serverName),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
// Copyright 2017 Vector Creations Ltd
|
|
||||||
//
|
|
||||||
// Licensed 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
|
|
||||||
//
|
|
||||||
// http://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.
|
|
||||||
|
|
||||||
package jsonerror
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLimitExceeded(t *testing.T) {
|
|
||||||
e := LimitExceeded("too fast", 5000)
|
|
||||||
jsonBytes, err := json.Marshal(&e)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("TestLimitExceeded: Failed to marshal LimitExceeded error. %s", err.Error())
|
|
||||||
}
|
|
||||||
want := `{"errcode":"M_LIMIT_EXCEEDED","error":"too fast","retry_after_ms":5000}`
|
|
||||||
if string(jsonBytes) != want {
|
|
||||||
t.Errorf("TestLimitExceeded: want %s, got %s", want, string(jsonBytes))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestForbidden(t *testing.T) {
|
|
||||||
e := Forbidden("you shall not pass")
|
|
||||||
jsonBytes, err := json.Marshal(&e)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("TestForbidden: Failed to marshal Forbidden error. %s", err.Error())
|
|
||||||
}
|
|
||||||
want := `{"errcode":"M_FORBIDDEN","error":"you shall not pass"}`
|
|
||||||
if string(jsonBytes) != want {
|
|
||||||
t.Errorf("TestForbidden: want %s, got %s", want, string(jsonBytes))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,63 +17,34 @@ package producers
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
"github.com/matrix-org/dendrite/syncapi/types"
|
"github.com/matrix-org/dendrite/syncapi/types"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"github.com/nats-io/nats.go"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SyncAPIProducer produces events for the sync API server to consume
|
// SyncAPIProducer produces events for the sync API server to consume
|
||||||
type SyncAPIProducer struct {
|
type SyncAPIProducer struct {
|
||||||
TopicClientData string
|
|
||||||
TopicReceiptEvent string
|
TopicReceiptEvent string
|
||||||
TopicSendToDeviceEvent string
|
TopicSendToDeviceEvent string
|
||||||
TopicTypingEvent string
|
TopicTypingEvent string
|
||||||
TopicPresenceEvent string
|
TopicPresenceEvent string
|
||||||
JetStream nats.JetStreamContext
|
JetStream nats.JetStreamContext
|
||||||
ServerName gomatrixserverlib.ServerName
|
ServerName spec.ServerName
|
||||||
UserAPI userapi.UserInternalAPI
|
UserAPI userapi.ClientUserAPI
|
||||||
}
|
|
||||||
|
|
||||||
// SendData sends account data to the sync API server
|
|
||||||
func (p *SyncAPIProducer) SendData(userID string, roomID string, dataType string, readMarker *eventutil.ReadMarkerJSON, ignoredUsers *types.IgnoredUsers) error {
|
|
||||||
m := &nats.Msg{
|
|
||||||
Subject: p.TopicClientData,
|
|
||||||
Header: nats.Header{},
|
|
||||||
}
|
|
||||||
m.Header.Set(jetstream.UserID, userID)
|
|
||||||
|
|
||||||
data := eventutil.AccountData{
|
|
||||||
RoomID: roomID,
|
|
||||||
Type: dataType,
|
|
||||||
ReadMarker: readMarker,
|
|
||||||
IgnoredUsers: ignoredUsers,
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
m.Data, err = json.Marshal(data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"user_id": userID,
|
|
||||||
"room_id": roomID,
|
|
||||||
"data_type": dataType,
|
|
||||||
}).Tracef("Producing to topic '%s'", p.TopicClientData)
|
|
||||||
|
|
||||||
_, err = p.JetStream.PublishMsg(m)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SyncAPIProducer) SendReceipt(
|
func (p *SyncAPIProducer) SendReceipt(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID, roomID, eventID, receiptType string, timestamp gomatrixserverlib.Timestamp,
|
userID, roomID, eventID, receiptType string, timestamp spec.Timestamp,
|
||||||
) error {
|
) error {
|
||||||
m := &nats.Msg{
|
m := &nats.Msg{
|
||||||
Subject: p.TopicReceiptEvent,
|
Subject: p.TopicReceiptEvent,
|
||||||
|
@ -83,7 +54,7 @@ func (p *SyncAPIProducer) SendReceipt(
|
||||||
m.Header.Set(jetstream.RoomID, roomID)
|
m.Header.Set(jetstream.RoomID, roomID)
|
||||||
m.Header.Set(jetstream.EventID, eventID)
|
m.Header.Set(jetstream.EventID, eventID)
|
||||||
m.Header.Set("type", receiptType)
|
m.Header.Set("type", receiptType)
|
||||||
m.Header.Set("timestamp", strconv.Itoa(int(timestamp)))
|
m.Header.Set("timestamp", fmt.Sprintf("%d", timestamp))
|
||||||
|
|
||||||
log.WithFields(log.Fields{}).Tracef("Producing to topic '%s'", p.TopicReceiptEvent)
|
log.WithFields(log.Fields{}).Tracef("Producing to topic '%s'", p.TopicReceiptEvent)
|
||||||
_, err := p.JetStream.PublishMsg(m, nats.Context(ctx))
|
_, err := p.JetStream.PublishMsg(m, nats.Context(ctx))
|
||||||
|
@ -92,7 +63,7 @@ func (p *SyncAPIProducer) SendReceipt(
|
||||||
|
|
||||||
func (p *SyncAPIProducer) SendToDevice(
|
func (p *SyncAPIProducer) SendToDevice(
|
||||||
ctx context.Context, sender, userID, deviceID, eventType string,
|
ctx context.Context, sender, userID, deviceID, eventType string,
|
||||||
message interface{},
|
message json.RawMessage,
|
||||||
) error {
|
) error {
|
||||||
devices := []string{}
|
devices := []string{}
|
||||||
_, domain, err := gomatrixserverlib.SplitID('@', userID)
|
_, domain, err := gomatrixserverlib.SplitID('@', userID)
|
||||||
|
@ -120,24 +91,19 @@ func (p *SyncAPIProducer) SendToDevice(
|
||||||
devices = append(devices, deviceID)
|
devices = append(devices, deviceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
js, err := json.Marshal(message)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"num_devices": len(devices),
|
"num_devices": len(devices),
|
||||||
"type": eventType,
|
"type": eventType,
|
||||||
}).Tracef("Producing to topic '%s'", p.TopicSendToDeviceEvent)
|
}).Tracef("Producing to topic '%s'", p.TopicSendToDeviceEvent)
|
||||||
for _, device := range devices {
|
for i, device := range devices {
|
||||||
ote := &types.OutputSendToDeviceEvent{
|
ote := &types.OutputSendToDeviceEvent{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
DeviceID: device,
|
DeviceID: device,
|
||||||
SendToDeviceEvent: gomatrixserverlib.SendToDeviceEvent{
|
SendToDeviceEvent: gomatrixserverlib.SendToDeviceEvent{
|
||||||
Sender: sender,
|
Sender: sender,
|
||||||
Type: eventType,
|
Type: eventType,
|
||||||
Content: js,
|
Content: message,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,15 +112,17 @@ func (p *SyncAPIProducer) SendToDevice(
|
||||||
log.WithError(err).Error("sendToDevice failed json.Marshal")
|
log.WithError(err).Error("sendToDevice failed json.Marshal")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
m := &nats.Msg{
|
m := nats.NewMsg(p.TopicSendToDeviceEvent)
|
||||||
Subject: p.TopicSendToDeviceEvent,
|
m.Data = eventJSON
|
||||||
Data: eventJSON,
|
|
||||||
Header: nats.Header{},
|
|
||||||
}
|
|
||||||
m.Header.Set("sender", sender)
|
m.Header.Set("sender", sender)
|
||||||
m.Header.Set(jetstream.UserID, userID)
|
m.Header.Set(jetstream.UserID, userID)
|
||||||
|
|
||||||
if _, err = p.JetStream.PublishMsg(m, nats.Context(ctx)); err != nil {
|
if _, err = p.JetStream.PublishMsg(m, nats.Context(ctx)); err != nil {
|
||||||
log.WithError(err).Error("sendToDevice failed t.Producer.SendMessage")
|
if i < len(devices)-1 {
|
||||||
|
log.WithError(err).Warn("sendToDevice failed to PublishMsg, trying further devices")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.WithError(err).Error("sendToDevice failed to PublishMsg for all devices")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -187,7 +155,7 @@ func (p *SyncAPIProducer) SendPresence(
|
||||||
m.Header.Set("status_msg", *statusMsg)
|
m.Header.Set("status_msg", *statusMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Header.Set("last_active_ts", strconv.Itoa(int(gomatrixserverlib.AsTimestamp(time.Now()))))
|
m.Header.Set("last_active_ts", strconv.Itoa(int(spec.AsTimestamp(time.Now()))))
|
||||||
|
|
||||||
_, err := p.JetStream.PublishMsg(m, nats.Context(ctx))
|
_, err := p.JetStream.PublishMsg(m, nats.Context(ctx))
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -17,29 +17,28 @@ package routing
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/producers"
|
"github.com/matrix-org/dendrite/clientapi/producers"
|
||||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/syncapi/types"
|
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetAccountData implements GET /user/{userId}/[rooms/{roomid}/]account_data/{type}
|
// GetAccountData implements GET /user/{userId}/[rooms/{roomid}/]account_data/{type}
|
||||||
func GetAccountData(
|
func GetAccountData(
|
||||||
req *http.Request, userAPI api.UserInternalAPI, device *api.Device,
|
req *http.Request, userAPI api.ClientUserAPI, device *api.Device,
|
||||||
userID string, roomID string, dataType string,
|
userID string, roomID string, dataType string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
if userID != device.UserID {
|
if userID != device.UserID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("userID does not match the current user"),
|
JSON: spec.Forbidden("userID does not match the current user"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,19 +69,19 @@ func GetAccountData(
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: jsonerror.NotFound("data not found"),
|
JSON: spec.NotFound("data not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveAccountData implements PUT /user/{userId}/[rooms/{roomId}/]account_data/{type}
|
// SaveAccountData implements PUT /user/{userId}/[rooms/{roomId}/]account_data/{type}
|
||||||
func SaveAccountData(
|
func SaveAccountData(
|
||||||
req *http.Request, userAPI api.UserInternalAPI, device *api.Device,
|
req *http.Request, userAPI api.ClientUserAPI, device *api.Device,
|
||||||
userID string, roomID string, dataType string, syncProducer *producers.SyncAPIProducer,
|
userID string, roomID string, dataType string, syncProducer *producers.SyncAPIProducer,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
if userID != device.UserID {
|
if userID != device.UserID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("userID does not match the current user"),
|
JSON: spec.Forbidden("userID does not match the current user"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,27 +90,30 @@ func SaveAccountData(
|
||||||
if req.Body == http.NoBody {
|
if req.Body == http.NoBody {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.NotJSON("Content not JSON"),
|
JSON: spec.NotJSON("Content not JSON"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataType == "m.fully_read" || dataType == "m.push_rules" {
|
if dataType == "m.fully_read" || dataType == "m.push_rules" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden(fmt.Sprintf("Unable to modify %q using this API", dataType)),
|
JSON: spec.Forbidden(fmt.Sprintf("Unable to modify %q using this API", dataType)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(req.Body)
|
body, err := io.ReadAll(req.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("ioutil.ReadAll failed")
|
util.GetLogger(req.Context()).WithError(err).Error("io.ReadAll failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !json.Valid(body) {
|
if !json.Valid(body) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("Bad JSON content"),
|
JSON: spec.BadJSON("Bad JSON content"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,18 +129,6 @@ func SaveAccountData(
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var ignoredUsers *types.IgnoredUsers
|
|
||||||
if dataType == "m.ignored_user_list" {
|
|
||||||
ignoredUsers = &types.IgnoredUsers{}
|
|
||||||
_ = json.Unmarshal(body, ignoredUsers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: user API should do this since it's account data
|
|
||||||
if err := syncProducer.SendData(userID, roomID, dataType, nil, ignoredUsers); err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("syncProducer.SendData failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: struct{}{},
|
JSON: struct{}{},
|
||||||
|
@ -152,11 +142,19 @@ type fullyReadEvent struct {
|
||||||
// SaveReadMarker implements POST /rooms/{roomId}/read_markers
|
// SaveReadMarker implements POST /rooms/{roomId}/read_markers
|
||||||
func SaveReadMarker(
|
func SaveReadMarker(
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
userAPI api.UserInternalAPI, rsAPI roomserverAPI.RoomserverInternalAPI,
|
userAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
syncProducer *producers.SyncAPIProducer, device *api.Device, roomID string,
|
syncProducer *producers.SyncAPIProducer, device *api.Device, roomID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
|
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("userID for this device is invalid"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Verify that the user is a member of this room
|
// Verify that the user is a member of this room
|
||||||
resErr := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
resErr := checkMemberInRoom(req.Context(), rsAPI, *deviceUserID, roomID)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
@ -167,38 +165,34 @@ func SaveReadMarker(
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.FullyRead == "" {
|
if r.FullyRead != "" {
|
||||||
return util.JSONResponse{
|
data, err := json.Marshal(fullyReadEvent{EventID: r.FullyRead})
|
||||||
Code: http.StatusBadRequest,
|
if err != nil {
|
||||||
JSON: jsonerror.BadJSON("Missing m.fully_read mandatory field"),
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dataReq := api.InputAccountDataRequest{
|
||||||
|
UserID: device.UserID,
|
||||||
|
DataType: "m.fully_read",
|
||||||
|
RoomID: roomID,
|
||||||
|
AccountData: data,
|
||||||
|
}
|
||||||
|
dataRes := api.InputAccountDataResponse{}
|
||||||
|
if err := userAPI.InputAccountData(req.Context(), &dataReq, &dataRes); err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("userAPI.InputAccountData failed")
|
||||||
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(fullyReadEvent{EventID: r.FullyRead})
|
// Handle the read receipts that may be included in the read marker.
|
||||||
if err != nil {
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
|
|
||||||
dataReq := api.InputAccountDataRequest{
|
|
||||||
UserID: device.UserID,
|
|
||||||
DataType: "m.fully_read",
|
|
||||||
RoomID: roomID,
|
|
||||||
AccountData: data,
|
|
||||||
}
|
|
||||||
dataRes := api.InputAccountDataResponse{}
|
|
||||||
if err := userAPI.InputAccountData(req.Context(), &dataReq, &dataRes); err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("userAPI.InputAccountData failed")
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := syncProducer.SendData(device.UserID, roomID, "m.fully_read", &r, nil); err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("syncProducer.SendData failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle the read receipt that may be included in the read marker
|
|
||||||
if r.Read != "" {
|
if r.Read != "" {
|
||||||
return SetReceipt(req, syncProducer, device, roomID, "m.read", r.Read)
|
return SetReceipt(req, userAPI, syncProducer, device, roomID, "m.read", r.Read)
|
||||||
|
}
|
||||||
|
if r.ReadPrivate != "" {
|
||||||
|
return SetReceipt(req, userAPI, syncProducer, device, roomID, "m.read.private", r.ReadPrivate)
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
587
clientapi/routing/admin.go
Normal file
587
clientapi/routing/admin.go
Normal file
|
@ -0,0 +1,587 @@
|
||||||
|
package routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/matrix-org/dendrite/internal"
|
||||||
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/exp/constraints"
|
||||||
|
|
||||||
|
clientapi "github.com/matrix-org/dendrite/clientapi/api"
|
||||||
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
var validRegistrationTokenRegex = regexp.MustCompile("^[[:ascii:][:digit:]_]*$")
|
||||||
|
|
||||||
|
func AdminCreateNewRegistrationToken(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
|
if !cfg.RegistrationRequiresToken {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("Registration via tokens is not enabled on this homeserver"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request := struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
UsesAllowed *int32 `json:"uses_allowed,omitempty"`
|
||||||
|
ExpiryTime *int64 `json:"expiry_time,omitempty"`
|
||||||
|
Length int32 `json:"length"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON(fmt.Sprintf("Failed to decode request body: %s", err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
token := request.Token
|
||||||
|
usesAllowed := request.UsesAllowed
|
||||||
|
expiryTime := request.ExpiryTime
|
||||||
|
length := request.Length
|
||||||
|
|
||||||
|
if len(token) == 0 {
|
||||||
|
if length == 0 {
|
||||||
|
// length not provided in request. Assign default value of 16.
|
||||||
|
length = 16
|
||||||
|
}
|
||||||
|
// token not present in request body. Hence, generate a random token.
|
||||||
|
if length <= 0 || length > 64 {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("length must be greater than zero and not greater than 64"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
token = util.RandomString(int(length))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(token) > 64 {
|
||||||
|
//Token present in request body, but is too long.
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("token must not be longer than 64"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isTokenValid := validRegistrationTokenRegex.Match([]byte(token))
|
||||||
|
if !isTokenValid {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("token must consist only of characters matched by the regex [A-Za-z0-9-_]"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// At this point, we have a valid token, either through request body or through random generation.
|
||||||
|
if usesAllowed != nil && *usesAllowed < 0 {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("uses_allowed must be a non-negative integer or null"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if expiryTime != nil && spec.Timestamp(*expiryTime).Time().Before(time.Now()) {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("expiry_time must not be in the past"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pending := int32(0)
|
||||||
|
completed := int32(0)
|
||||||
|
// If usesAllowed or expiryTime is 0, it means they are not present in the request. NULL (indicating unlimited uses / no expiration will be persisted in DB)
|
||||||
|
registrationToken := &clientapi.RegistrationToken{
|
||||||
|
Token: &token,
|
||||||
|
UsesAllowed: usesAllowed,
|
||||||
|
Pending: &pending,
|
||||||
|
Completed: &completed,
|
||||||
|
ExpiryTime: expiryTime,
|
||||||
|
}
|
||||||
|
created, err := userAPI.PerformAdminCreateRegistrationToken(req.Context(), registrationToken)
|
||||||
|
if !created {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusConflict,
|
||||||
|
JSON: map[string]string{
|
||||||
|
"error": fmt.Sprintf("token: %s already exists", token),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: map[string]interface{}{
|
||||||
|
"token": token,
|
||||||
|
"uses_allowed": getReturnValue(usesAllowed),
|
||||||
|
"pending": pending,
|
||||||
|
"completed": completed,
|
||||||
|
"expiry_time": getReturnValue(expiryTime),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getReturnValue[t constraints.Integer](in *t) any {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return *in
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminListRegistrationTokens(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
|
queryParams := req.URL.Query()
|
||||||
|
returnAll := true
|
||||||
|
valid := true
|
||||||
|
validQuery, ok := queryParams["valid"]
|
||||||
|
if ok {
|
||||||
|
returnAll = false
|
||||||
|
validValue, err := strconv.ParseBool(validQuery[0])
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("invalid 'valid' query parameter"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
valid = validValue
|
||||||
|
}
|
||||||
|
tokens, err := userAPI.PerformAdminListRegistrationTokens(req.Context(), returnAll, valid)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.ErrorUnknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: map[string]interface{}{
|
||||||
|
"registration_tokens": tokens,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminGetRegistrationToken(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
tokenText := vars["token"]
|
||||||
|
token, err := userAPI.PerformAdminGetRegistrationToken(req.Context(), tokenText)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: spec.NotFound(fmt.Sprintf("token: %s not found", tokenText)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminDeleteRegistrationToken(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
tokenText := vars["token"]
|
||||||
|
err = userAPI.PerformAdminDeleteRegistrationToken(req.Context(), tokenText)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: map[string]interface{}{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminUpdateRegistrationToken(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
tokenText := vars["token"]
|
||||||
|
request := make(map[string]*int64)
|
||||||
|
if err = json.NewDecoder(req.Body).Decode(&request); err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON(fmt.Sprintf("Failed to decode request body: %s", err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newAttributes := make(map[string]interface{})
|
||||||
|
usesAllowed, ok := request["uses_allowed"]
|
||||||
|
if ok {
|
||||||
|
// Only add usesAllowed to newAtrributes if it is present and valid
|
||||||
|
if usesAllowed != nil && *usesAllowed < 0 {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("uses_allowed must be a non-negative integer or null"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newAttributes["usesAllowed"] = usesAllowed
|
||||||
|
}
|
||||||
|
expiryTime, ok := request["expiry_time"]
|
||||||
|
if ok {
|
||||||
|
// Only add expiryTime to newAtrributes if it is present and valid
|
||||||
|
if expiryTime != nil && spec.Timestamp(*expiryTime).Time().Before(time.Now()) {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("expiry_time must not be in the past"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newAttributes["expiryTime"] = expiryTime
|
||||||
|
}
|
||||||
|
if len(newAttributes) == 0 {
|
||||||
|
// No attributes to update. Return existing token
|
||||||
|
return AdminGetRegistrationToken(req, cfg, userAPI)
|
||||||
|
}
|
||||||
|
updatedToken, err := userAPI.PerformAdminUpdateRegistrationToken(req.Context(), tokenText, newAttributes)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: spec.NotFound(fmt.Sprintf("token: %s not found", tokenText)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: *updatedToken,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminEvacuateRoom(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
affected, err := rsAPI.PerformAdminEvacuateRoom(req.Context(), vars["roomID"])
|
||||||
|
switch err.(type) {
|
||||||
|
case nil:
|
||||||
|
case eventutil.ErrRoomNoExists:
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: spec.NotFound(err.Error()),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
logrus.WithError(err).WithField("roomID", vars["roomID"]).Error("Failed to evacuate room")
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: map[string]interface{}{
|
||||||
|
"affected": affected,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminEvacuateUser(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
affected, err := rsAPI.PerformAdminEvacuateUser(req.Context(), vars["userID"])
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).WithField("userID", vars["userID"]).Error("Failed to evacuate user")
|
||||||
|
return util.MessageResponse(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: map[string]interface{}{
|
||||||
|
"affected": affected,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminPurgeRoom(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = rsAPI.PerformAdminPurgeRoom(context.Background(), vars["roomID"]); err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: struct{}{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminResetPassword(req *http.Request, cfg *config.ClientAPI, device *api.Device, userAPI api.ClientUserAPI) util.JSONResponse {
|
||||||
|
if req.Body == nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.Unknown("Missing request body"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
var localpart string
|
||||||
|
userID := vars["userID"]
|
||||||
|
localpart, serverName, err := cfg.Matrix.SplitLocalID('@', userID)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.InvalidParam(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accAvailableResp := &api.QueryAccountAvailabilityResponse{}
|
||||||
|
if err = userAPI.QueryAccountAvailability(req.Context(), &api.QueryAccountAvailabilityRequest{
|
||||||
|
Localpart: localpart,
|
||||||
|
ServerName: serverName,
|
||||||
|
}, accAvailableResp); err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if accAvailableResp.Available {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: spec.Unknown("User does not exist"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request := struct {
|
||||||
|
Password string `json:"password"`
|
||||||
|
LogoutDevices bool `json:"logout_devices"`
|
||||||
|
}{}
|
||||||
|
if err = json.NewDecoder(req.Body).Decode(&request); err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.Unknown("Failed to decode request body: " + err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if request.Password == "" {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.MissingParam("Expecting non-empty password."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = internal.ValidatePassword(request.Password); err != nil {
|
||||||
|
return *internal.PasswordResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateReq := &api.PerformPasswordUpdateRequest{
|
||||||
|
Localpart: localpart,
|
||||||
|
ServerName: serverName,
|
||||||
|
Password: request.Password,
|
||||||
|
LogoutDevices: request.LogoutDevices,
|
||||||
|
}
|
||||||
|
updateRes := &api.PerformPasswordUpdateResponse{}
|
||||||
|
if err := userAPI.PerformPasswordUpdate(req.Context(), updateReq, updateRes); err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.Unknown("Failed to perform password update: " + err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
JSON: struct {
|
||||||
|
Updated bool `json:"password_updated"`
|
||||||
|
}{
|
||||||
|
Updated: updateRes.PasswordUpdated,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminReindex(req *http.Request, cfg *config.ClientAPI, device *api.Device, natsClient *nats.Conn) util.JSONResponse {
|
||||||
|
_, err := natsClient.RequestMsg(nats.NewMsg(cfg.Matrix.JetStream.Prefixed(jetstream.InputFulltextReindex)), time.Second*10)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Error("failed to publish nats message")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
JSON: struct{}{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminMarkAsStale(req *http.Request, cfg *config.ClientAPI, keyAPI api.ClientKeyAPI) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
userID := vars["userID"]
|
||||||
|
|
||||||
|
_, domain, err := gomatrixserverlib.SplitID('@', userID)
|
||||||
|
if err != nil {
|
||||||
|
return util.MessageResponse(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
if cfg.Matrix.IsLocalServerName(domain) {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.InvalidParam("Can not mark local device list as stale"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = keyAPI.PerformMarkAsStaleIfNeeded(req.Context(), &api.PerformMarkAsStaleRequest{
|
||||||
|
UserID: userID,
|
||||||
|
Domain: domain,
|
||||||
|
}, &struct{}{})
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown(fmt.Sprintf("Failed to mark device list as stale: %s", err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
JSON: struct{}{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminDownloadState(req *http.Request, device *api.Device, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
roomID, ok := vars["roomID"]
|
||||||
|
if !ok {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.MissingParam("Expecting room ID."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serverName, ok := vars["serverName"]
|
||||||
|
if !ok {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.MissingParam("Expecting remote server name."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = rsAPI.PerformAdminDownloadState(req.Context(), roomID, device.UserID, spec.ServerName(serverName)); err != nil {
|
||||||
|
if errors.Is(err, eventutil.ErrRoomNoExists{}) {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: spec.NotFound(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logrus.WithError(err).WithFields(logrus.Fields{
|
||||||
|
"userID": device.UserID,
|
||||||
|
"serverName": serverName,
|
||||||
|
"roomID": roomID,
|
||||||
|
}).Error("failed to download state")
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: struct{}{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEventReports returns reported events for a given user/room.
|
||||||
|
func GetEventReports(
|
||||||
|
req *http.Request,
|
||||||
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
|
from, limit uint64,
|
||||||
|
backwards bool,
|
||||||
|
userID, roomID string,
|
||||||
|
) util.JSONResponse {
|
||||||
|
|
||||||
|
eventReports, count, err := rsAPI.QueryAdminEventReports(req.Context(), from, limit, backwards, userID, roomID)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Error("failed to query event reports")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := map[string]any{
|
||||||
|
"event_reports": eventReports,
|
||||||
|
"total": count,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a next_token if there are still reports
|
||||||
|
if int64(from+limit) < count {
|
||||||
|
resp["next_token"] = int(from) + len(eventReports)
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
JSON: resp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEventReport(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, reportID string) util.JSONResponse {
|
||||||
|
parsedReportID, err := strconv.ParseUint(reportID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
// Given this is an admin endpoint, let them know what didn't work.
|
||||||
|
JSON: spec.InvalidParam(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
report, err := rsAPI.QueryAdminEventReport(req.Context(), parsedReportID)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
JSON: report,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteEventReport(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, reportID string) util.JSONResponse {
|
||||||
|
parsedReportID, err := strconv.ParseUint(reportID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
// Given this is an admin endpoint, let them know what didn't work.
|
||||||
|
JSON: spec.InvalidParam(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rsAPI.PerformAdminDeleteEventReport(req.Context(), parsedReportID)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
JSON: struct{}{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUint64OrDefault(input string, defaultValue uint64) uint64 {
|
||||||
|
v, err := strconv.ParseUint(input, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
|
@ -17,8 +17,8 @@ package routing
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
@ -44,14 +44,14 @@ type connectionInfo struct {
|
||||||
|
|
||||||
// GetAdminWhois implements GET /admin/whois/{userId}
|
// GetAdminWhois implements GET /admin/whois/{userId}
|
||||||
func GetAdminWhois(
|
func GetAdminWhois(
|
||||||
req *http.Request, userAPI api.UserInternalAPI, device *api.Device,
|
req *http.Request, userAPI api.ClientUserAPI, device *api.Device,
|
||||||
userID string,
|
userID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
allowed := device.AccountType == api.AccountTypeAdmin || userID == device.UserID
|
allowed := device.AccountType == api.AccountTypeAdmin || userID == device.UserID
|
||||||
if !allowed {
|
if !allowed {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("userID does not match the current user"),
|
JSON: spec.Forbidden("userID does not match the current user"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +61,10 @@ func GetAdminWhois(
|
||||||
}, &queryRes)
|
}, &queryRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("GetAdminWhois failed to query user devices")
|
util.GetLogger(req.Context()).WithError(err).Error("GetAdminWhois failed to query user devices")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
devices := make(map[string]deviceInfo)
|
devices := make(map[string]deviceInfo)
|
||||||
|
|
|
@ -15,23 +15,23 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetAliases implements GET /_matrix/client/r0/rooms/{roomId}/aliases
|
// GetAliases implements GET /_matrix/client/r0/rooms/{roomId}/aliases
|
||||||
func GetAliases(
|
func GetAliases(
|
||||||
req *http.Request, rsAPI api.RoomserverInternalAPI, device *userapi.Device, roomID string,
|
req *http.Request, rsAPI api.ClientRoomserverAPI, device *userapi.Device, roomID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
stateTuple := gomatrixserverlib.StateKeyTuple{
|
stateTuple := gomatrixserverlib.StateKeyTuple{
|
||||||
EventType: gomatrixserverlib.MRoomHistoryVisibility,
|
EventType: spec.MRoomHistoryVisibility,
|
||||||
StateKey: "",
|
StateKey: "",
|
||||||
}
|
}
|
||||||
stateReq := &api.QueryCurrentStateRequest{
|
stateReq := &api.QueryCurrentStateRequest{
|
||||||
|
@ -44,29 +44,40 @@ func GetAliases(
|
||||||
return util.ErrorResponse(fmt.Errorf("rsAPI.QueryCurrentState: %w", err))
|
return util.ErrorResponse(fmt.Errorf("rsAPI.QueryCurrentState: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
visibility := "invite"
|
visibility := gomatrixserverlib.HistoryVisibilityInvited
|
||||||
if historyVisEvent, ok := stateRes.StateEvents[stateTuple]; ok {
|
if historyVisEvent, ok := stateRes.StateEvents[stateTuple]; ok {
|
||||||
var err error
|
var err error
|
||||||
visibility, err = historyVisEvent.HistoryVisibility()
|
var content gomatrixserverlib.HistoryVisibilityContent
|
||||||
if err != nil {
|
if err = json.Unmarshal(historyVisEvent.Content(), &content); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("historyVisEvent.HistoryVisibility failed")
|
util.GetLogger(req.Context()).WithError(err).Error("historyVisEvent.HistoryVisibility failed")
|
||||||
return util.ErrorResponse(fmt.Errorf("historyVisEvent.HistoryVisibility: %w", err))
|
return util.ErrorResponse(fmt.Errorf("historyVisEvent.HistoryVisibility: %w", err))
|
||||||
}
|
}
|
||||||
|
visibility = content.HistoryVisibility
|
||||||
}
|
}
|
||||||
if visibility != gomatrixserverlib.WorldReadable {
|
if visibility != spec.WorldReadable {
|
||||||
|
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("userID doesn't have power level to change visibility"),
|
||||||
|
}
|
||||||
|
}
|
||||||
queryReq := api.QueryMembershipForUserRequest{
|
queryReq := api.QueryMembershipForUserRequest{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
UserID: device.UserID,
|
UserID: *deviceUserID,
|
||||||
}
|
}
|
||||||
var queryRes api.QueryMembershipForUserResponse
|
var queryRes api.QueryMembershipForUserResponse
|
||||||
if err := rsAPI.QueryMembershipForUser(req.Context(), &queryReq, &queryRes); err != nil {
|
if err := rsAPI.QueryMembershipForUser(req.Context(), &queryReq, &queryRes); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.QueryMembershipsForRoom failed")
|
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.QueryMembershipsForRoom failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !queryRes.IsInRoom {
|
if !queryRes.IsInRoom {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("You aren't a member of this room."),
|
JSON: spec.Forbidden("You aren't a member of this room."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,11 +15,11 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
@ -31,8 +31,7 @@ const recaptchaTemplate = `
|
||||||
<title>Authentication</title>
|
<title>Authentication</title>
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1,
|
<meta name='viewport' content='width=device-width, initial-scale=1,
|
||||||
user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'>
|
user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'>
|
||||||
<script src="https://www.google.com/recaptcha/api.js"
|
<script src="{{.apiJsUrl}}" async defer></script>
|
||||||
async defer></script>
|
|
||||||
<script src="//code.jquery.com/jquery-1.11.2.min.js"></script>
|
<script src="//code.jquery.com/jquery-1.11.2.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
function captchaDone() {
|
function captchaDone() {
|
||||||
|
@ -51,8 +50,8 @@ function captchaDone() {
|
||||||
Please verify that you're not a robot.
|
Please verify that you're not a robot.
|
||||||
</p>
|
</p>
|
||||||
<input type="hidden" name="session" value="{{.session}}" />
|
<input type="hidden" name="session" value="{{.session}}" />
|
||||||
<div class="g-recaptcha"
|
<div class="{{.sitekeyClass}}"
|
||||||
data-sitekey="{{.siteKey}}"
|
data-sitekey="{{.sitekey}}"
|
||||||
data-callback="captchaDone">
|
data-callback="captchaDone">
|
||||||
</div>
|
</div>
|
||||||
<noscript>
|
<noscript>
|
||||||
|
@ -102,21 +101,38 @@ func serveTemplate(w http.ResponseWriter, templateHTML string, data map[string]s
|
||||||
func AuthFallback(
|
func AuthFallback(
|
||||||
w http.ResponseWriter, req *http.Request, authType string,
|
w http.ResponseWriter, req *http.Request, authType string,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
) *util.JSONResponse {
|
) {
|
||||||
sessionID := req.URL.Query().Get("session")
|
// We currently only support "m.login.recaptcha", so fail early if that's not requested
|
||||||
|
if authType == authtypes.LoginTypeRecaptcha {
|
||||||
|
if !cfg.RecaptchaEnabled {
|
||||||
|
writeHTTPMessage(w, req,
|
||||||
|
"Recaptcha login is disabled on this Homeserver",
|
||||||
|
http.StatusBadRequest,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
writeHTTPMessage(w, req, fmt.Sprintf("Unknown authtype %q", authType), http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionID := req.URL.Query().Get("session")
|
||||||
if sessionID == "" {
|
if sessionID == "" {
|
||||||
return writeHTTPMessage(w, req,
|
writeHTTPMessage(w, req,
|
||||||
"Session ID not provided",
|
"Session ID not provided",
|
||||||
http.StatusBadRequest,
|
http.StatusBadRequest,
|
||||||
)
|
)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
serveRecaptcha := func() {
|
serveRecaptcha := func() {
|
||||||
data := map[string]string{
|
data := map[string]string{
|
||||||
"myUrl": req.URL.String(),
|
"myUrl": req.URL.String(),
|
||||||
"session": sessionID,
|
"session": sessionID,
|
||||||
"siteKey": cfg.RecaptchaPublicKey,
|
"apiJsUrl": cfg.RecaptchaApiJsUrl,
|
||||||
|
"sitekey": cfg.RecaptchaPublicKey,
|
||||||
|
"sitekeyClass": cfg.RecaptchaSitekeyClass,
|
||||||
|
"formField": cfg.RecaptchaFormField,
|
||||||
}
|
}
|
||||||
serveTemplate(w, recaptchaTemplate, data)
|
serveTemplate(w, recaptchaTemplate, data)
|
||||||
}
|
}
|
||||||
|
@ -128,70 +144,44 @@ func AuthFallback(
|
||||||
|
|
||||||
if req.Method == http.MethodGet {
|
if req.Method == http.MethodGet {
|
||||||
// Handle Recaptcha
|
// Handle Recaptcha
|
||||||
if authType == authtypes.LoginTypeRecaptcha {
|
serveRecaptcha()
|
||||||
if err := checkRecaptchaEnabled(cfg, w, req); err != nil {
|
return
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
serveRecaptcha()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &util.JSONResponse{
|
|
||||||
Code: http.StatusNotFound,
|
|
||||||
JSON: jsonerror.NotFound("Unknown auth stage type"),
|
|
||||||
}
|
|
||||||
} else if req.Method == http.MethodPost {
|
} else if req.Method == http.MethodPost {
|
||||||
// Handle Recaptcha
|
// Handle Recaptcha
|
||||||
if authType == authtypes.LoginTypeRecaptcha {
|
clientIP := req.RemoteAddr
|
||||||
if err := checkRecaptchaEnabled(cfg, w, req); err != nil {
|
err := req.ParseForm()
|
||||||
return err
|
if err != nil {
|
||||||
}
|
util.GetLogger(req.Context()).WithError(err).Error("req.ParseForm failed")
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
clientIP := req.RemoteAddr
|
serveRecaptcha()
|
||||||
err := req.ParseForm()
|
return
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("req.ParseForm failed")
|
|
||||||
res := jsonerror.InternalServerError()
|
|
||||||
return &res
|
|
||||||
}
|
|
||||||
|
|
||||||
response := req.Form.Get("g-recaptcha-response")
|
|
||||||
if err := validateRecaptcha(cfg, response, clientIP); err != nil {
|
|
||||||
util.GetLogger(req.Context()).Error(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success. Add recaptcha as a completed login flow
|
|
||||||
sessions.addCompletedSessionStage(sessionID, authtypes.LoginTypeRecaptcha)
|
|
||||||
|
|
||||||
serveSuccess()
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &util.JSONResponse{
|
response := req.Form.Get(cfg.RecaptchaFormField)
|
||||||
Code: http.StatusNotFound,
|
err = validateRecaptcha(cfg, response, clientIP)
|
||||||
JSON: jsonerror.NotFound("Unknown auth stage type"),
|
switch err {
|
||||||
|
case ErrMissingResponse:
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
serveRecaptcha() // serve the initial page again, instead of nothing
|
||||||
|
return
|
||||||
|
case ErrInvalidCaptcha:
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
serveRecaptcha()
|
||||||
|
return
|
||||||
|
case nil:
|
||||||
|
default: // something else failed
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("failed to validate recaptcha")
|
||||||
|
serveRecaptcha()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return &util.JSONResponse{
|
|
||||||
Code: http.StatusMethodNotAllowed,
|
|
||||||
JSON: jsonerror.NotFound("Bad method"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkRecaptchaEnabled creates an error response if recaptcha is not usable on homeserver.
|
// Success. Add recaptcha as a completed login flow
|
||||||
func checkRecaptchaEnabled(
|
sessions.addCompletedSessionStage(sessionID, authtypes.LoginTypeRecaptcha)
|
||||||
cfg *config.ClientAPI,
|
|
||||||
w http.ResponseWriter,
|
serveSuccess()
|
||||||
req *http.Request,
|
return
|
||||||
) *util.JSONResponse {
|
|
||||||
if !cfg.RecaptchaEnabled {
|
|
||||||
return writeHTTPMessage(w, req,
|
|
||||||
"Recaptcha login is disabled on this Homeserver",
|
|
||||||
http.StatusBadRequest,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return nil
|
writeHTTPMessage(w, req, "Bad method", http.StatusMethodNotAllowed)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeHTTPMessage writes the given header and message to the HTTP response writer.
|
// writeHTTPMessage writes the given header and message to the HTTP response writer.
|
||||||
|
@ -199,13 +189,10 @@ func checkRecaptchaEnabled(
|
||||||
func writeHTTPMessage(
|
func writeHTTPMessage(
|
||||||
w http.ResponseWriter, req *http.Request,
|
w http.ResponseWriter, req *http.Request,
|
||||||
message string, header int,
|
message string, header int,
|
||||||
) *util.JSONResponse {
|
) {
|
||||||
w.WriteHeader(header)
|
w.WriteHeader(header)
|
||||||
_, err := w.Write([]byte(message))
|
_, err := w.Write([]byte(message))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("w.Write failed")
|
util.GetLogger(req.Context()).WithError(err).Error("w.Write failed")
|
||||||
res := jsonerror.InternalServerError()
|
|
||||||
return &res
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
147
clientapi/routing/auth_fallback_test.go
Normal file
147
clientapi/routing/auth_fallback_test.go
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
package routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_AuthFallback(t *testing.T) {
|
||||||
|
cfg := config.Dendrite{}
|
||||||
|
cfg.Defaults(config.DefaultOpts{Generate: true, SingleDatabase: true})
|
||||||
|
for _, useHCaptcha := range []bool{false, true} {
|
||||||
|
for _, recaptchaEnabled := range []bool{false, true} {
|
||||||
|
for _, wantErr := range []bool{false, true} {
|
||||||
|
t.Run(fmt.Sprintf("useHCaptcha(%v) - recaptchaEnabled(%v) - wantErr(%v)", useHCaptcha, recaptchaEnabled, wantErr), func(t *testing.T) {
|
||||||
|
// Set the defaults for each test
|
||||||
|
cfg.ClientAPI.Defaults(config.DefaultOpts{Generate: true, SingleDatabase: true})
|
||||||
|
cfg.ClientAPI.RecaptchaEnabled = recaptchaEnabled
|
||||||
|
cfg.ClientAPI.RecaptchaPublicKey = "pub"
|
||||||
|
cfg.ClientAPI.RecaptchaPrivateKey = "priv"
|
||||||
|
if useHCaptcha {
|
||||||
|
cfg.ClientAPI.RecaptchaSiteVerifyAPI = "https://hcaptcha.com/siteverify"
|
||||||
|
cfg.ClientAPI.RecaptchaApiJsUrl = "https://js.hcaptcha.com/1/api.js"
|
||||||
|
cfg.ClientAPI.RecaptchaFormField = "h-captcha-response"
|
||||||
|
cfg.ClientAPI.RecaptchaSitekeyClass = "h-captcha"
|
||||||
|
}
|
||||||
|
cfgErrs := &config.ConfigErrors{}
|
||||||
|
cfg.ClientAPI.Verify(cfgErrs)
|
||||||
|
if len(*cfgErrs) > 0 {
|
||||||
|
t.Fatalf("(hCaptcha=%v) unexpected config errors: %s", useHCaptcha, cfgErrs.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/?session=1337", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
|
||||||
|
if !recaptchaEnabled {
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("unexpected response code: %d, want %d", rec.Code, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
if rec.Body.String() != "Recaptcha login is disabled on this Homeserver" {
|
||||||
|
t.Fatalf("unexpected response body: %s", rec.Body.String())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !strings.Contains(rec.Body.String(), cfg.ClientAPI.RecaptchaSitekeyClass) {
|
||||||
|
t.Fatalf("body does not contain %s: %s", cfg.ClientAPI.RecaptchaSitekeyClass, rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if wantErr {
|
||||||
|
_, _ = w.Write([]byte(`{"success":false}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = w.Write([]byte(`{"success":true}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close() // nolint: errcheck
|
||||||
|
|
||||||
|
cfg.ClientAPI.RecaptchaSiteVerifyAPI = srv.URL
|
||||||
|
|
||||||
|
// check the result after sending the captcha
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/?session=1337", nil)
|
||||||
|
req.Form = url.Values{}
|
||||||
|
req.Form.Add(cfg.ClientAPI.RecaptchaFormField, "someRandomValue")
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
|
||||||
|
if recaptchaEnabled {
|
||||||
|
if !wantErr {
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("unexpected response code: %d, want %d", rec.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
if rec.Body.String() != successTemplate {
|
||||||
|
t.Fatalf("unexpected response: %s, want %s", rec.Body.String(), successTemplate)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if rec.Code != http.StatusUnauthorized {
|
||||||
|
t.Fatalf("unexpected response code: %d, want %d", rec.Code, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
wantString := "Authentication"
|
||||||
|
if !strings.Contains(rec.Body.String(), wantString) {
|
||||||
|
t.Fatalf("expected response to contain '%s', but didn't: %s", wantString, rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("unexpected response code: %d, want %d", rec.Code, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
if rec.Body.String() != "Recaptcha login is disabled on this Homeserver" {
|
||||||
|
t.Fatalf("unexpected response: %s, want %s", rec.Body.String(), "successTemplate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("unknown fallbacks are handled correctly", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/?session=1337", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
AuthFallback(rec, req, "DoesNotExist", &cfg.ClientAPI)
|
||||||
|
if rec.Code != http.StatusNotImplemented {
|
||||||
|
t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusNotImplemented)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unknown methods are handled correctly", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/?session=1337", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
|
||||||
|
if rec.Code != http.StatusMethodNotAllowed {
|
||||||
|
t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing session parameter is handled correctly", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing session parameter is handled correctly", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing 'response' is handled correctly", func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/?session=1337", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -17,26 +17,22 @@ package routing
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver/version"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetCapabilities returns information about the server's supported feature set
|
// GetCapabilities returns information about the server's supported feature set
|
||||||
// and other relevant capabilities to an authenticated user.
|
// and other relevant capabilities to an authenticated user.
|
||||||
func GetCapabilities(
|
func GetCapabilities(rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
||||||
req *http.Request, rsAPI roomserverAPI.RoomserverInternalAPI,
|
versionsMap := map[gomatrixserverlib.RoomVersion]string{}
|
||||||
) util.JSONResponse {
|
for v, desc := range version.SupportedRoomVersions() {
|
||||||
roomVersionsQueryReq := roomserverAPI.QueryRoomVersionCapabilitiesRequest{}
|
if desc.Stable() {
|
||||||
roomVersionsQueryRes := roomserverAPI.QueryRoomVersionCapabilitiesResponse{}
|
versionsMap[v] = "stable"
|
||||||
if err := rsAPI.QueryRoomVersionCapabilities(
|
} else {
|
||||||
req.Context(),
|
versionsMap[v] = "unstable"
|
||||||
&roomVersionsQueryReq,
|
}
|
||||||
&roomVersionsQueryRes,
|
|
||||||
); err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("queryAPI.QueryRoomVersionCapabilities failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response := map[string]interface{}{
|
response := map[string]interface{}{
|
||||||
|
@ -44,7 +40,10 @@ func GetCapabilities(
|
||||||
"m.change_password": map[string]bool{
|
"m.change_password": map[string]bool{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
},
|
},
|
||||||
"m.room_versions": roomVersionsQueryRes,
|
"m.room_versions": map[string]interface{}{
|
||||||
|
"default": rsAPI.DefaultRoomVersion(),
|
||||||
|
"available": versionsMap,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,10 +26,9 @@ import (
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
roomserverVersion "github.com/matrix-org/dendrite/roomserver/version"
|
roomserverVersion "github.com/matrix-org/dendrite/roomserver/version"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
|
@ -38,32 +37,19 @@ import (
|
||||||
|
|
||||||
// https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
|
// https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
|
||||||
type createRoomRequest struct {
|
type createRoomRequest struct {
|
||||||
Invite []string `json:"invite"`
|
Invite []string `json:"invite"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Visibility string `json:"visibility"`
|
Visibility string `json:"visibility"`
|
||||||
Topic string `json:"topic"`
|
Topic string `json:"topic"`
|
||||||
Preset string `json:"preset"`
|
Preset string `json:"preset"`
|
||||||
CreationContent json.RawMessage `json:"creation_content"`
|
CreationContent json.RawMessage `json:"creation_content"`
|
||||||
InitialState []fledglingEvent `json:"initial_state"`
|
InitialState []gomatrixserverlib.FledglingEvent `json:"initial_state"`
|
||||||
RoomAliasName string `json:"room_alias_name"`
|
RoomAliasName string `json:"room_alias_name"`
|
||||||
GuestCanJoin bool `json:"guest_can_join"`
|
RoomVersion gomatrixserverlib.RoomVersion `json:"room_version"`
|
||||||
RoomVersion gomatrixserverlib.RoomVersion `json:"room_version"`
|
PowerLevelContentOverride json.RawMessage `json:"power_level_content_override"`
|
||||||
PowerLevelContentOverride json.RawMessage `json:"power_level_content_override"`
|
IsDirect bool `json:"is_direct"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
presetPrivateChat = "private_chat"
|
|
||||||
presetTrustedPrivateChat = "trusted_private_chat"
|
|
||||||
presetPublicChat = "public_chat"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
historyVisibilityShared = "shared"
|
|
||||||
// TODO: These should be implemented once history visibility is implemented
|
|
||||||
// historyVisibilityWorldReadable = "world_readable"
|
|
||||||
// historyVisibilityInvited = "invited"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (r createRoomRequest) Validate() *util.JSONResponse {
|
func (r createRoomRequest) Validate() *util.JSONResponse {
|
||||||
whitespace := "\t\n\x0b\x0c\r " // https://docs.python.org/2/library/string.html#string.whitespace
|
whitespace := "\t\n\x0b\x0c\r " // https://docs.python.org/2/library/string.html#string.whitespace
|
||||||
// https://github.com/matrix-org/synapse/blob/v0.19.2/synapse/handlers/room.py#L81
|
// https://github.com/matrix-org/synapse/blob/v0.19.2/synapse/handlers/room.py#L81
|
||||||
|
@ -71,28 +57,23 @@ func (r createRoomRequest) Validate() *util.JSONResponse {
|
||||||
if strings.ContainsAny(r.RoomAliasName, whitespace+":") {
|
if strings.ContainsAny(r.RoomAliasName, whitespace+":") {
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("room_alias_name cannot contain whitespace or ':'"),
|
JSON: spec.BadJSON("room_alias_name cannot contain whitespace or ':'"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, userID := range r.Invite {
|
for _, userID := range r.Invite {
|
||||||
// TODO: We should put user ID parsing code into gomatrixserverlib and use that instead
|
if _, err := spec.NewUserID(userID, true); err != nil {
|
||||||
// (see https://github.com/matrix-org/gomatrixserverlib/blob/3394e7c7003312043208aa73727d2256eea3d1f6/eventcontent.go#L347 )
|
|
||||||
// It should be a struct (with pointers into a single string to avoid copying) and
|
|
||||||
// we should update all refs to use UserID types rather than strings.
|
|
||||||
// https://github.com/matrix-org/synapse/blob/v0.19.2/synapse/types.py#L92
|
|
||||||
if _, _, err := gomatrixserverlib.SplitID('@', userID); err != nil {
|
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("user id must be in the form @localpart:domain"),
|
JSON: spec.BadJSON("user id must be in the form @localpart:domain"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch r.Preset {
|
switch r.Preset {
|
||||||
case presetPrivateChat, presetTrustedPrivateChat, presetPublicChat, "":
|
case spec.PresetPrivateChat, spec.PresetTrustedPrivateChat, spec.PresetPublicChat, "":
|
||||||
default:
|
default:
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("preset must be any of 'private_chat', 'trusted_private_chat', 'public_chat'"),
|
JSON: spec.BadJSON("preset must be any of 'private_chat', 'trusted_private_chat', 'public_chat'"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +85,7 @@ func (r createRoomRequest) Validate() *util.JSONResponse {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("malformed creation_content"),
|
JSON: spec.BadJSON("malformed creation_content"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +94,7 @@ func (r createRoomRequest) Validate() *util.JSONResponse {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("malformed creation_content"),
|
JSON: spec.BadJSON("malformed creation_content"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,448 +107,131 @@ type createRoomResponse struct {
|
||||||
RoomAlias string `json:"room_alias,omitempty"` // in synapse not spec
|
RoomAlias string `json:"room_alias,omitempty"` // in synapse not spec
|
||||||
}
|
}
|
||||||
|
|
||||||
// fledglingEvent is a helper representation of an event used when creating many events in succession.
|
|
||||||
type fledglingEvent struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
StateKey string `json:"state_key"`
|
|
||||||
Content interface{} `json:"content"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateRoom implements /createRoom
|
// CreateRoom implements /createRoom
|
||||||
func CreateRoom(
|
func CreateRoom(
|
||||||
req *http.Request, device *api.Device,
|
req *http.Request, device *api.Device,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
profileAPI api.UserProfileAPI, rsAPI roomserverAPI.RoomserverInternalAPI,
|
profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
asAPI appserviceAPI.AppServiceQueryAPI,
|
asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
var r createRoomRequest
|
var createRequest createRoomRequest
|
||||||
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
resErr := httputil.UnmarshalJSONRequest(req, &createRequest)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
if resErr = r.Validate(); resErr != nil {
|
if resErr = createRequest.Validate(); resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
evTime, err := httputil.ParseTSParam(req)
|
evTime, err := httputil.ParseTSParam(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidArgumentValue(err.Error()),
|
JSON: spec.InvalidParam(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return createRoom(req.Context(), r, device, cfg, profileAPI, rsAPI, asAPI, evTime)
|
return createRoom(req.Context(), createRequest, device, cfg, profileAPI, rsAPI, asAPI, evTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// createRoom implements /createRoom
|
// createRoom implements /createRoom
|
||||||
// nolint: gocyclo
|
|
||||||
func createRoom(
|
func createRoom(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
r createRoomRequest, device *api.Device,
|
createRequest createRoomRequest, device *api.Device,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
profileAPI api.UserProfileAPI, rsAPI roomserverAPI.RoomserverInternalAPI,
|
profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
asAPI appserviceAPI.AppServiceQueryAPI,
|
asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
evTime time.Time,
|
evTime time.Time,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
// TODO (#267): Check room ID doesn't clash with an existing one, and we
|
userID, err := spec.NewUserID(device.UserID, true)
|
||||||
// probably shouldn't be using pseudo-random strings, maybe GUIDs?
|
if err != nil {
|
||||||
roomID := fmt.Sprintf("!%s:%s", util.RandomString(16), cfg.Matrix.ServerName)
|
util.GetLogger(ctx).WithError(err).Error("invalid userID")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !cfg.Matrix.IsLocalServerName(userID.Domain()) {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden(fmt.Sprintf("User domain %q not configured locally", userID.Domain())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger := util.GetLogger(ctx)
|
logger := util.GetLogger(ctx)
|
||||||
userID := device.UserID
|
|
||||||
|
// TODO: Check room ID doesn't clash with an existing one, and we
|
||||||
|
// probably shouldn't be using pseudo-random strings, maybe GUIDs?
|
||||||
|
roomID, err := spec.NewRoomID(fmt.Sprintf("!%s:%s", util.RandomString(16), userID.Domain()))
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("invalid roomID")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clobber keys: creator, room_version
|
// Clobber keys: creator, room_version
|
||||||
|
|
||||||
roomVersion := roomserverVersion.DefaultRoomVersion()
|
roomVersion := rsAPI.DefaultRoomVersion()
|
||||||
if r.RoomVersion != "" {
|
if createRequest.RoomVersion != "" {
|
||||||
candidateVersion := gomatrixserverlib.RoomVersion(r.RoomVersion)
|
candidateVersion := gomatrixserverlib.RoomVersion(createRequest.RoomVersion)
|
||||||
_, roomVersionError := roomserverVersion.SupportedRoomVersion(candidateVersion)
|
_, roomVersionError := roomserverVersion.SupportedRoomVersion(candidateVersion)
|
||||||
if roomVersionError != nil {
|
if roomVersionError != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.UnsupportedRoomVersion(roomVersionError.Error()),
|
JSON: spec.UnsupportedRoomVersion(roomVersionError.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
roomVersion = candidateVersion
|
roomVersion = candidateVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: visibility/presets/raw initial state
|
|
||||||
// TODO: Create room alias association
|
|
||||||
// Make sure this doesn't fall into an application service's namespace though!
|
|
||||||
|
|
||||||
logger.WithFields(log.Fields{
|
logger.WithFields(log.Fields{
|
||||||
"userID": userID,
|
"userID": userID.String(),
|
||||||
"roomID": roomID,
|
"roomID": roomID.String(),
|
||||||
"roomVersion": roomVersion,
|
"roomVersion": roomVersion,
|
||||||
}).Info("Creating new room")
|
}).Info("Creating new room")
|
||||||
|
|
||||||
profile, err := appserviceAPI.RetrieveUserProfile(ctx, userID, asAPI, profileAPI)
|
profile, err := appserviceAPI.RetrieveUserProfile(ctx, userID.String(), asAPI, profileAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("appserviceAPI.RetrieveUserProfile failed")
|
util.GetLogger(ctx).WithError(err).Error("appserviceAPI.RetrieveUserProfile failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
}
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
createContent := map[string]interface{}{}
|
|
||||||
if len(r.CreationContent) > 0 {
|
|
||||||
if err = json.Unmarshal(r.CreationContent, &createContent); err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("json.Unmarshal for creation_content failed")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.BadJSON("invalid create content"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
createContent["creator"] = userID
|
|
||||||
createContent["room_version"] = roomVersion
|
|
||||||
powerLevelContent := eventutil.InitialPowerLevelsContent(userID)
|
|
||||||
joinRuleContent := gomatrixserverlib.JoinRuleContent{
|
|
||||||
JoinRule: gomatrixserverlib.Invite,
|
|
||||||
}
|
|
||||||
historyVisibilityContent := gomatrixserverlib.HistoryVisibilityContent{
|
|
||||||
HistoryVisibility: historyVisibilityShared,
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.PowerLevelContentOverride != nil {
|
|
||||||
// Merge powerLevelContentOverride fields by unmarshalling it atop the defaults
|
|
||||||
err = json.Unmarshal(r.PowerLevelContentOverride, &powerLevelContent)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("json.Unmarshal for power_level_content_override failed")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.BadJSON("malformed power_level_content_override"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r.Preset {
|
userDisplayName := profile.DisplayName
|
||||||
case presetPrivateChat:
|
userAvatarURL := profile.AvatarURL
|
||||||
joinRuleContent.JoinRule = gomatrixserverlib.Invite
|
|
||||||
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
|
keyID := cfg.Matrix.KeyID
|
||||||
case presetTrustedPrivateChat:
|
privateKey := cfg.Matrix.PrivateKey
|
||||||
joinRuleContent.JoinRule = gomatrixserverlib.Invite
|
|
||||||
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
|
req := roomserverAPI.PerformCreateRoomRequest{
|
||||||
// TODO If trusted_private_chat, all invitees are given the same power level as the room creator.
|
InvitedUsers: createRequest.Invite,
|
||||||
case presetPublicChat:
|
RoomName: createRequest.Name,
|
||||||
joinRuleContent.JoinRule = gomatrixserverlib.Public
|
Visibility: createRequest.Visibility,
|
||||||
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
|
Topic: createRequest.Topic,
|
||||||
|
StatePreset: createRequest.Preset,
|
||||||
|
CreationContent: createRequest.CreationContent,
|
||||||
|
InitialState: createRequest.InitialState,
|
||||||
|
RoomAliasName: createRequest.RoomAliasName,
|
||||||
|
RoomVersion: roomVersion,
|
||||||
|
PowerLevelContentOverride: createRequest.PowerLevelContentOverride,
|
||||||
|
IsDirect: createRequest.IsDirect,
|
||||||
|
|
||||||
|
UserDisplayName: userDisplayName,
|
||||||
|
UserAvatarURL: userAvatarURL,
|
||||||
|
KeyID: keyID,
|
||||||
|
PrivateKey: privateKey,
|
||||||
|
EventTime: evTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
createEvent := fledglingEvent{
|
roomAlias, createRes := rsAPI.PerformCreateRoom(ctx, *userID, *roomID, &req)
|
||||||
Type: gomatrixserverlib.MRoomCreate,
|
if createRes != nil {
|
||||||
Content: createContent,
|
return *createRes
|
||||||
}
|
|
||||||
powerLevelEvent := fledglingEvent{
|
|
||||||
Type: gomatrixserverlib.MRoomPowerLevels,
|
|
||||||
Content: powerLevelContent,
|
|
||||||
}
|
|
||||||
joinRuleEvent := fledglingEvent{
|
|
||||||
Type: gomatrixserverlib.MRoomJoinRules,
|
|
||||||
Content: joinRuleContent,
|
|
||||||
}
|
|
||||||
historyVisibilityEvent := fledglingEvent{
|
|
||||||
Type: gomatrixserverlib.MRoomHistoryVisibility,
|
|
||||||
Content: historyVisibilityContent,
|
|
||||||
}
|
|
||||||
membershipEvent := fledglingEvent{
|
|
||||||
Type: gomatrixserverlib.MRoomMember,
|
|
||||||
StateKey: userID,
|
|
||||||
Content: gomatrixserverlib.MemberContent{
|
|
||||||
Membership: gomatrixserverlib.Join,
|
|
||||||
DisplayName: profile.DisplayName,
|
|
||||||
AvatarURL: profile.AvatarURL,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var nameEvent *fledglingEvent
|
|
||||||
var topicEvent *fledglingEvent
|
|
||||||
var guestAccessEvent *fledglingEvent
|
|
||||||
var aliasEvent *fledglingEvent
|
|
||||||
|
|
||||||
if r.Name != "" {
|
|
||||||
nameEvent = &fledglingEvent{
|
|
||||||
Type: gomatrixserverlib.MRoomName,
|
|
||||||
Content: eventutil.NameContent{
|
|
||||||
Name: r.Name,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Topic != "" {
|
|
||||||
topicEvent = &fledglingEvent{
|
|
||||||
Type: gomatrixserverlib.MRoomTopic,
|
|
||||||
Content: eventutil.TopicContent{
|
|
||||||
Topic: r.Topic,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.GuestCanJoin {
|
|
||||||
guestAccessEvent = &fledglingEvent{
|
|
||||||
Type: gomatrixserverlib.MRoomGuestAccess,
|
|
||||||
Content: eventutil.GuestAccessContent{
|
|
||||||
GuestAccess: "can_join",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var roomAlias string
|
|
||||||
if r.RoomAliasName != "" {
|
|
||||||
roomAlias = fmt.Sprintf("#%s:%s", r.RoomAliasName, cfg.Matrix.ServerName)
|
|
||||||
// check it's free TODO: This races but is better than nothing
|
|
||||||
hasAliasReq := roomserverAPI.GetRoomIDForAliasRequest{
|
|
||||||
Alias: roomAlias,
|
|
||||||
IncludeAppservices: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
var aliasResp roomserverAPI.GetRoomIDForAliasResponse
|
|
||||||
err = rsAPI.GetRoomIDForAlias(ctx, &hasAliasReq, &aliasResp)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("aliasAPI.GetRoomIDForAlias failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
if aliasResp.RoomID != "" {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.RoomInUse("Room ID already exists."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
aliasEvent = &fledglingEvent{
|
|
||||||
Type: gomatrixserverlib.MRoomCanonicalAlias,
|
|
||||||
Content: eventutil.CanonicalAlias{
|
|
||||||
Alias: roomAlias,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var initialStateEvents []fledglingEvent
|
|
||||||
for i := range r.InitialState {
|
|
||||||
if r.InitialState[i].StateKey != "" {
|
|
||||||
initialStateEvents = append(initialStateEvents, r.InitialState[i])
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r.InitialState[i].Type {
|
|
||||||
case gomatrixserverlib.MRoomCreate:
|
|
||||||
continue
|
|
||||||
|
|
||||||
case gomatrixserverlib.MRoomPowerLevels:
|
|
||||||
powerLevelEvent = r.InitialState[i]
|
|
||||||
|
|
||||||
case gomatrixserverlib.MRoomJoinRules:
|
|
||||||
joinRuleEvent = r.InitialState[i]
|
|
||||||
|
|
||||||
case gomatrixserverlib.MRoomHistoryVisibility:
|
|
||||||
historyVisibilityEvent = r.InitialState[i]
|
|
||||||
|
|
||||||
case gomatrixserverlib.MRoomGuestAccess:
|
|
||||||
guestAccessEvent = &r.InitialState[i]
|
|
||||||
|
|
||||||
case gomatrixserverlib.MRoomName:
|
|
||||||
nameEvent = &r.InitialState[i]
|
|
||||||
|
|
||||||
case gomatrixserverlib.MRoomTopic:
|
|
||||||
topicEvent = &r.InitialState[i]
|
|
||||||
|
|
||||||
default:
|
|
||||||
initialStateEvents = append(initialStateEvents, r.InitialState[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// send events into the room in order of:
|
|
||||||
// 1- m.room.create
|
|
||||||
// 2- room creator join member
|
|
||||||
// 3- m.room.power_levels
|
|
||||||
// 4- m.room.join_rules
|
|
||||||
// 5- m.room.history_visibility
|
|
||||||
// 6- m.room.canonical_alias (opt)
|
|
||||||
// 7- m.room.guest_access (opt)
|
|
||||||
// 8- other initial state items
|
|
||||||
// 9- m.room.name (opt)
|
|
||||||
// 10- m.room.topic (opt)
|
|
||||||
// 11- invite events (opt) - with is_direct flag if applicable TODO
|
|
||||||
// 12- 3pid invite events (opt) TODO
|
|
||||||
// This differs from Synapse slightly. Synapse would vary the ordering of 3-7
|
|
||||||
// depending on if those events were in "initial_state" or not. This made it
|
|
||||||
// harder to reason about, hence sticking to a strict static ordering.
|
|
||||||
// TODO: Synapse has txn/token ID on each event. Do we need to do this here?
|
|
||||||
eventsToMake := []fledglingEvent{
|
|
||||||
createEvent, membershipEvent, powerLevelEvent, joinRuleEvent, historyVisibilityEvent,
|
|
||||||
}
|
|
||||||
if guestAccessEvent != nil {
|
|
||||||
eventsToMake = append(eventsToMake, *guestAccessEvent)
|
|
||||||
}
|
|
||||||
eventsToMake = append(eventsToMake, initialStateEvents...)
|
|
||||||
if nameEvent != nil {
|
|
||||||
eventsToMake = append(eventsToMake, *nameEvent)
|
|
||||||
}
|
|
||||||
if topicEvent != nil {
|
|
||||||
eventsToMake = append(eventsToMake, *topicEvent)
|
|
||||||
}
|
|
||||||
if aliasEvent != nil {
|
|
||||||
// TODO: bit of a chicken and egg problem here as the alias doesn't exist and cannot until we have made the room.
|
|
||||||
// This means we might fail creating the alias but say the canonical alias is something that doesn't exist.
|
|
||||||
eventsToMake = append(eventsToMake, *aliasEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: invite events
|
|
||||||
// TODO: 3pid invite events
|
|
||||||
|
|
||||||
var builtEvents []*gomatrixserverlib.HeaderedEvent
|
|
||||||
authEvents := gomatrixserverlib.NewAuthEvents(nil)
|
|
||||||
for i, e := range eventsToMake {
|
|
||||||
depth := i + 1 // depth starts at 1
|
|
||||||
|
|
||||||
builder := gomatrixserverlib.EventBuilder{
|
|
||||||
Sender: userID,
|
|
||||||
RoomID: roomID,
|
|
||||||
Type: e.Type,
|
|
||||||
StateKey: &e.StateKey,
|
|
||||||
Depth: int64(depth),
|
|
||||||
}
|
|
||||||
err = builder.SetContent(e.Content)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("builder.SetContent failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
if i > 0 {
|
|
||||||
builder.PrevEvents = []gomatrixserverlib.EventReference{builtEvents[i-1].EventReference()}
|
|
||||||
}
|
|
||||||
var ev *gomatrixserverlib.Event
|
|
||||||
ev, err = buildEvent(&builder, &authEvents, cfg, evTime, roomVersion)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("buildEvent failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = gomatrixserverlib.Allowed(ev, &authEvents); err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("gomatrixserverlib.Allowed failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the event to the list of auth events
|
|
||||||
builtEvents = append(builtEvents, ev.Headered(roomVersion))
|
|
||||||
err = authEvents.AddEvent(ev)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("authEvents.AddEvent failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inputs := make([]roomserverAPI.InputRoomEvent, 0, len(builtEvents))
|
|
||||||
for _, event := range builtEvents {
|
|
||||||
inputs = append(inputs, roomserverAPI.InputRoomEvent{
|
|
||||||
Kind: roomserverAPI.KindNew,
|
|
||||||
Event: event,
|
|
||||||
Origin: cfg.Matrix.ServerName,
|
|
||||||
SendAsServer: roomserverAPI.DoNotSendToOtherServers,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err = roomserverAPI.SendInputRoomEvents(ctx, rsAPI, inputs, false); err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("roomserverAPI.SendInputRoomEvents failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(#269): Reserve room alias while we create the room. This stops us
|
|
||||||
// from creating the room but still failing due to the alias having already
|
|
||||||
// been taken.
|
|
||||||
if roomAlias != "" {
|
|
||||||
aliasReq := roomserverAPI.SetRoomAliasRequest{
|
|
||||||
Alias: roomAlias,
|
|
||||||
RoomID: roomID,
|
|
||||||
UserID: userID,
|
|
||||||
}
|
|
||||||
|
|
||||||
var aliasResp roomserverAPI.SetRoomAliasResponse
|
|
||||||
err = rsAPI.SetRoomAlias(ctx, &aliasReq, &aliasResp)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("aliasAPI.SetRoomAlias failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
|
|
||||||
if aliasResp.AliasExists {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.RoomInUse("Room alias already exists."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this is a direct message then we should invite the participants.
|
|
||||||
if len(r.Invite) > 0 {
|
|
||||||
// Build some stripped state for the invite.
|
|
||||||
var globalStrippedState []gomatrixserverlib.InviteV2StrippedState
|
|
||||||
for _, event := range builtEvents {
|
|
||||||
switch event.Type() {
|
|
||||||
case gomatrixserverlib.MRoomName:
|
|
||||||
fallthrough
|
|
||||||
case gomatrixserverlib.MRoomCanonicalAlias:
|
|
||||||
fallthrough
|
|
||||||
case gomatrixserverlib.MRoomEncryption:
|
|
||||||
fallthrough
|
|
||||||
case gomatrixserverlib.MRoomMember:
|
|
||||||
fallthrough
|
|
||||||
case gomatrixserverlib.MRoomJoinRules:
|
|
||||||
ev := event.Event
|
|
||||||
globalStrippedState = append(
|
|
||||||
globalStrippedState,
|
|
||||||
gomatrixserverlib.NewInviteV2StrippedState(ev),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process the invites.
|
|
||||||
for _, invitee := range r.Invite {
|
|
||||||
// Build the invite event.
|
|
||||||
inviteEvent, err := buildMembershipEvent(
|
|
||||||
ctx, invitee, "", profileAPI, device, gomatrixserverlib.Invite,
|
|
||||||
roomID, true, cfg, evTime, rsAPI, asAPI,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
inviteStrippedState := append(
|
|
||||||
globalStrippedState,
|
|
||||||
gomatrixserverlib.NewInviteV2StrippedState(inviteEvent.Event),
|
|
||||||
)
|
|
||||||
// Send the invite event to the roomserver.
|
|
||||||
err = roomserverAPI.SendInvite(
|
|
||||||
ctx,
|
|
||||||
rsAPI,
|
|
||||||
inviteEvent.Headered(roomVersion),
|
|
||||||
inviteStrippedState, // invite room state
|
|
||||||
cfg.Matrix.ServerName, // send as server
|
|
||||||
nil, // transaction ID
|
|
||||||
)
|
|
||||||
switch e := err.(type) {
|
|
||||||
case *roomserverAPI.PerformError:
|
|
||||||
return e.JSONResponse()
|
|
||||||
case nil:
|
|
||||||
default:
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("roomserverAPI.SendInvite failed")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: jsonerror.InternalServerError(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Visibility == "public" {
|
|
||||||
// expose this room in the published room list
|
|
||||||
var pubRes roomserverAPI.PerformPublishResponse
|
|
||||||
rsAPI.PerformPublish(ctx, &roomserverAPI.PerformPublishRequest{
|
|
||||||
RoomID: roomID,
|
|
||||||
Visibility: "public",
|
|
||||||
}, &pubRes)
|
|
||||||
if pubRes.Error != nil {
|
|
||||||
// treat as non-fatal since the room is already made by this point
|
|
||||||
util.GetLogger(ctx).WithError(pubRes.Error).Error("failed to visibility:public")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response := createRoomResponse{
|
response := createRoomResponse{
|
||||||
RoomID: roomID,
|
RoomID: roomID.String(),
|
||||||
RoomAlias: roomAlias,
|
RoomAlias: roomAlias,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -576,30 +240,3 @@ func createRoom(
|
||||||
JSON: response,
|
JSON: response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildEvent fills out auth_events for the builder then builds the event
|
|
||||||
func buildEvent(
|
|
||||||
builder *gomatrixserverlib.EventBuilder,
|
|
||||||
provider gomatrixserverlib.AuthEventProvider,
|
|
||||||
cfg *config.ClientAPI,
|
|
||||||
evTime time.Time,
|
|
||||||
roomVersion gomatrixserverlib.RoomVersion,
|
|
||||||
) (*gomatrixserverlib.Event, error) {
|
|
||||||
eventsNeeded, err := gomatrixserverlib.StateNeededForEventBuilder(builder)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
refs, err := eventsNeeded.AuthEventReferences(provider)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
builder.AuthEvents = refs
|
|
||||||
event, err := builder.Build(
|
|
||||||
evTime, cfg.Matrix.ServerName, cfg.Matrix.KeyID,
|
|
||||||
cfg.Matrix.PrivateKey, roomVersion,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot build event %s : Builder failed to build. %w", builder.Type, err)
|
|
||||||
}
|
|
||||||
return event, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth"
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,16 +15,16 @@ import (
|
||||||
func Deactivate(
|
func Deactivate(
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
userInteractiveAuth *auth.UserInteractive,
|
userInteractiveAuth *auth.UserInteractive,
|
||||||
accountAPI api.UserAccountAPI,
|
accountAPI api.ClientUserAPI,
|
||||||
deviceAPI *api.Device,
|
deviceAPI *api.Device,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
ctx := req.Context()
|
ctx := req.Context()
|
||||||
defer req.Body.Close() // nolint:errcheck
|
defer req.Body.Close() // nolint:errcheck
|
||||||
bodyBytes, err := ioutil.ReadAll(req.Body)
|
bodyBytes, err := io.ReadAll(req.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("The request body could not be read: " + err.Error()),
|
JSON: spec.BadJSON("The request body could not be read: " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,19 +33,26 @@ func Deactivate(
|
||||||
return *errRes
|
return *errRes
|
||||||
}
|
}
|
||||||
|
|
||||||
localpart, _, err := gomatrixserverlib.SplitID('@', login.Username())
|
localpart, serverName, err := gomatrixserverlib.SplitID('@', login.Username())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var res api.PerformAccountDeactivationResponse
|
var res api.PerformAccountDeactivationResponse
|
||||||
err = accountAPI.PerformAccountDeactivation(ctx, &api.PerformAccountDeactivationRequest{
|
err = accountAPI.PerformAccountDeactivation(ctx, &api.PerformAccountDeactivationRequest{
|
||||||
Localpart: localpart,
|
Localpart: localpart,
|
||||||
|
ServerName: serverName,
|
||||||
}, &res)
|
}, &res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformAccountDeactivation failed")
|
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformAccountDeactivation failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
|
@ -15,15 +15,16 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"encoding/json"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth"
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
@ -50,7 +51,7 @@ type devicesDeleteJSON struct {
|
||||||
|
|
||||||
// GetDeviceByID handles /devices/{deviceID}
|
// GetDeviceByID handles /devices/{deviceID}
|
||||||
func GetDeviceByID(
|
func GetDeviceByID(
|
||||||
req *http.Request, userAPI api.UserInternalAPI, device *api.Device,
|
req *http.Request, userAPI api.ClientUserAPI, device *api.Device,
|
||||||
deviceID string,
|
deviceID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
var queryRes api.QueryDevicesResponse
|
var queryRes api.QueryDevicesResponse
|
||||||
|
@ -59,7 +60,10 @@ func GetDeviceByID(
|
||||||
}, &queryRes)
|
}, &queryRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("QueryDevices failed")
|
util.GetLogger(req.Context()).WithError(err).Error("QueryDevices failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var targetDevice *api.Device
|
var targetDevice *api.Device
|
||||||
for _, device := range queryRes.Devices {
|
for _, device := range queryRes.Devices {
|
||||||
|
@ -71,7 +75,7 @@ func GetDeviceByID(
|
||||||
if targetDevice == nil {
|
if targetDevice == nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: jsonerror.NotFound("Unknown device"),
|
JSON: spec.NotFound("Unknown device"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +92,7 @@ func GetDeviceByID(
|
||||||
|
|
||||||
// GetDevicesByLocalpart handles /devices
|
// GetDevicesByLocalpart handles /devices
|
||||||
func GetDevicesByLocalpart(
|
func GetDevicesByLocalpart(
|
||||||
req *http.Request, userAPI api.UserInternalAPI, device *api.Device,
|
req *http.Request, userAPI api.ClientUserAPI, device *api.Device,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
var queryRes api.QueryDevicesResponse
|
var queryRes api.QueryDevicesResponse
|
||||||
err := userAPI.QueryDevices(req.Context(), &api.QueryDevicesRequest{
|
err := userAPI.QueryDevices(req.Context(), &api.QueryDevicesRequest{
|
||||||
|
@ -96,7 +100,10 @@ func GetDevicesByLocalpart(
|
||||||
}, &queryRes)
|
}, &queryRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("QueryDevices failed")
|
util.GetLogger(req.Context()).WithError(err).Error("QueryDevices failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res := devicesJSON{}
|
res := devicesJSON{}
|
||||||
|
@ -118,7 +125,7 @@ func GetDevicesByLocalpart(
|
||||||
|
|
||||||
// UpdateDeviceByID handles PUT on /devices/{deviceID}
|
// UpdateDeviceByID handles PUT on /devices/{deviceID}
|
||||||
func UpdateDeviceByID(
|
func UpdateDeviceByID(
|
||||||
req *http.Request, userAPI api.UserInternalAPI, device *api.Device,
|
req *http.Request, userAPI api.ClientUserAPI, device *api.Device,
|
||||||
deviceID string,
|
deviceID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
|
|
||||||
|
@ -138,18 +145,15 @@ func UpdateDeviceByID(
|
||||||
}, &performRes)
|
}, &performRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("PerformDeviceUpdate failed")
|
util.GetLogger(req.Context()).WithError(err).Error("PerformDeviceUpdate failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !performRes.DeviceExists {
|
if !performRes.DeviceExists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: jsonerror.Forbidden("device does not exist"),
|
JSON: spec.Forbidden("device does not exist"),
|
||||||
}
|
|
||||||
}
|
|
||||||
if performRes.Forbidden {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: jsonerror.Forbidden("device not owned by current user"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,7 +165,7 @@ func UpdateDeviceByID(
|
||||||
|
|
||||||
// DeleteDeviceById handles DELETE requests to /devices/{deviceId}
|
// DeleteDeviceById handles DELETE requests to /devices/{deviceId}
|
||||||
func DeleteDeviceById(
|
func DeleteDeviceById(
|
||||||
req *http.Request, userInteractiveAuth *auth.UserInteractive, userAPI api.UserInternalAPI, device *api.Device,
|
req *http.Request, userInteractiveAuth *auth.UserInteractive, userAPI api.ClientUserAPI, device *api.Device,
|
||||||
deviceID string,
|
deviceID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
var (
|
var (
|
||||||
|
@ -175,11 +179,11 @@ func DeleteDeviceById(
|
||||||
}()
|
}()
|
||||||
ctx := req.Context()
|
ctx := req.Context()
|
||||||
defer req.Body.Close() // nolint:errcheck
|
defer req.Body.Close() // nolint:errcheck
|
||||||
bodyBytes, err := ioutil.ReadAll(req.Body)
|
bodyBytes, err := io.ReadAll(req.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("The request body could not be read: " + err.Error()),
|
JSON: spec.BadJSON("The request body could not be read: " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,7 +193,7 @@ func DeleteDeviceById(
|
||||||
if dev != deviceID {
|
if dev != deviceID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("session & device mismatch"),
|
JSON: spec.Forbidden("session and device mismatch"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,7 +215,10 @@ func DeleteDeviceById(
|
||||||
localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
|
localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
util.GetLogger(ctx).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure that the access token being used matches the login creds used for user interactive auth, else
|
// make sure that the access token being used matches the login creds used for user interactive auth, else
|
||||||
|
@ -219,7 +226,7 @@ func DeleteDeviceById(
|
||||||
if login.Username() != localpart && login.Username() != device.UserID {
|
if login.Username() != localpart && login.Username() != device.UserID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 403,
|
Code: 403,
|
||||||
JSON: jsonerror.Forbidden("Cannot delete another user's device"),
|
JSON: spec.Forbidden("Cannot delete another user's device"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,7 +236,10 @@ func DeleteDeviceById(
|
||||||
DeviceIDs: []string{deviceID},
|
DeviceIDs: []string{deviceID},
|
||||||
}, &res); err != nil {
|
}, &res); err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformDeviceDeletion failed")
|
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformDeviceDeletion failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteOK = true
|
deleteOK = true
|
||||||
|
@ -242,16 +252,40 @@ func DeleteDeviceById(
|
||||||
|
|
||||||
// DeleteDevices handles POST requests to /delete_devices
|
// DeleteDevices handles POST requests to /delete_devices
|
||||||
func DeleteDevices(
|
func DeleteDevices(
|
||||||
req *http.Request, userAPI api.UserInternalAPI, device *api.Device,
|
req *http.Request, userInteractiveAuth *auth.UserInteractive, userAPI api.ClientUserAPI, device *api.Device,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
ctx := req.Context()
|
ctx := req.Context()
|
||||||
payload := devicesDeleteJSON{}
|
|
||||||
|
|
||||||
if resErr := httputil.UnmarshalJSONRequest(req, &payload); resErr != nil {
|
bodyBytes, err := io.ReadAll(req.Body)
|
||||||
return *resErr
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("The request body could not be read: " + err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer req.Body.Close() // nolint:errcheck
|
||||||
|
|
||||||
|
// initiate UIA
|
||||||
|
login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes, device)
|
||||||
|
if errRes != nil {
|
||||||
|
return *errRes
|
||||||
}
|
}
|
||||||
|
|
||||||
defer req.Body.Close() // nolint: errcheck
|
if login.Username() != device.UserID {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("unable to delete devices for other user"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := devicesDeleteJSON{}
|
||||||
|
if err = json.Unmarshal(bodyBytes, &payload); err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("unable to unmarshal device deletion request")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var res api.PerformDeviceDeletionResponse
|
var res api.PerformDeviceDeletionResponse
|
||||||
if err := userAPI.PerformDeviceDeletion(ctx, &api.PerformDeviceDeletionRequest{
|
if err := userAPI.PerformDeviceDeletion(ctx, &api.PerformDeviceDeletionRequest{
|
||||||
|
@ -259,7 +293,10 @@ func DeleteDevices(
|
||||||
DeviceIDs: payload.Devices,
|
DeviceIDs: payload.Devices,
|
||||||
}, &res); err != nil {
|
}, &res); err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformDeviceDeletion failed")
|
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformDeviceDeletion failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
|
@ -18,14 +18,16 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
federationAPI "github.com/matrix-org/dendrite/federationapi/api"
|
federationAPI "github.com/matrix-org/dendrite/federationapi/api"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"github.com/matrix-org/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type roomDirectoryResponse struct {
|
type roomDirectoryResponse struct {
|
||||||
|
@ -33,7 +35,7 @@ type roomDirectoryResponse struct {
|
||||||
Servers []string `json:"servers"`
|
Servers []string `json:"servers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *roomDirectoryResponse) fillServers(servers []gomatrixserverlib.ServerName) {
|
func (r *roomDirectoryResponse) fillServers(servers []spec.ServerName) {
|
||||||
r.Servers = make([]string, len(servers))
|
r.Servers = make([]string, len(servers))
|
||||||
for i, s := range servers {
|
for i, s := range servers {
|
||||||
r.Servers[i] = string(s)
|
r.Servers[i] = string(s)
|
||||||
|
@ -44,16 +46,16 @@ func (r *roomDirectoryResponse) fillServers(servers []gomatrixserverlib.ServerNa
|
||||||
func DirectoryRoom(
|
func DirectoryRoom(
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
roomAlias string,
|
roomAlias string,
|
||||||
federation *gomatrixserverlib.FederationClient,
|
federation fclient.FederationClient,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
rsAPI roomserverAPI.RoomserverInternalAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
fedSenderAPI federationAPI.FederationInternalAPI,
|
fedSenderAPI federationAPI.ClientFederationAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
_, domain, err := gomatrixserverlib.SplitID('#', roomAlias)
|
_, domain, err := gomatrixserverlib.SplitID('#', roomAlias)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("Room alias must be in the form '#localpart:domain'"),
|
JSON: spec.InvalidParam("Room alias must be in the form '#localpart:domain'"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +69,10 @@ func DirectoryRoom(
|
||||||
queryRes := &roomserverAPI.GetRoomIDForAliasResponse{}
|
queryRes := &roomserverAPI.GetRoomIDForAliasResponse{}
|
||||||
if err = rsAPI.GetRoomIDForAlias(req.Context(), queryReq, queryRes); err != nil {
|
if err = rsAPI.GetRoomIDForAlias(req.Context(), queryReq, queryRes); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.GetRoomIDForAlias failed")
|
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.GetRoomIDForAlias failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.RoomID = queryRes.RoomID
|
res.RoomID = queryRes.RoomID
|
||||||
|
@ -75,13 +80,16 @@ func DirectoryRoom(
|
||||||
if res.RoomID == "" {
|
if res.RoomID == "" {
|
||||||
// If we don't know it locally, do a federation query.
|
// If we don't know it locally, do a federation query.
|
||||||
// But don't send the query to ourselves.
|
// But don't send the query to ourselves.
|
||||||
if domain != cfg.Matrix.ServerName {
|
if !cfg.Matrix.IsLocalServerName(domain) {
|
||||||
fedRes, fedErr := federation.LookupRoomAlias(req.Context(), domain, roomAlias)
|
fedRes, fedErr := federation.LookupRoomAlias(req.Context(), cfg.Matrix.ServerName, domain, roomAlias)
|
||||||
if fedErr != nil {
|
if fedErr != nil {
|
||||||
// TODO: Return 502 if the remote server errored.
|
// TODO: Return 502 if the remote server errored.
|
||||||
// TODO: Return 504 if the remote server timed out.
|
// TODO: Return 504 if the remote server timed out.
|
||||||
util.GetLogger(req.Context()).WithError(fedErr).Error("federation.LookupRoomAlias failed")
|
util.GetLogger(req.Context()).WithError(fedErr).Error("federation.LookupRoomAlias failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
res.RoomID = fedRes.RoomID
|
res.RoomID = fedRes.RoomID
|
||||||
res.fillServers(fedRes.Servers)
|
res.fillServers(fedRes.Servers)
|
||||||
|
@ -90,7 +98,7 @@ func DirectoryRoom(
|
||||||
if res.RoomID == "" {
|
if res.RoomID == "" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: jsonerror.NotFound(
|
JSON: spec.NotFound(
|
||||||
fmt.Sprintf("Room alias %s not found", roomAlias),
|
fmt.Sprintf("Room alias %s not found", roomAlias),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
@ -100,7 +108,10 @@ func DirectoryRoom(
|
||||||
var joinedHostsRes federationAPI.QueryJoinedHostServerNamesInRoomResponse
|
var joinedHostsRes federationAPI.QueryJoinedHostServerNamesInRoomResponse
|
||||||
if err = fedSenderAPI.QueryJoinedHostServerNamesInRoom(req.Context(), &joinedHostsReq, &joinedHostsRes); err != nil {
|
if err = fedSenderAPI.QueryJoinedHostServerNamesInRoom(req.Context(), &joinedHostsReq, &joinedHostsRes); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("fedSenderAPI.QueryJoinedHostServerNamesInRoom failed")
|
util.GetLogger(req.Context()).WithError(err).Error("fedSenderAPI.QueryJoinedHostServerNamesInRoom failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
res.fillServers(joinedHostsRes.ServerNames)
|
res.fillServers(joinedHostsRes.ServerNames)
|
||||||
}
|
}
|
||||||
|
@ -117,20 +128,20 @@ func SetLocalAlias(
|
||||||
device *userapi.Device,
|
device *userapi.Device,
|
||||||
alias string,
|
alias string,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
rsAPI roomserverAPI.RoomserverInternalAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
_, domain, err := gomatrixserverlib.SplitID('#', alias)
|
_, domain, err := gomatrixserverlib.SplitID('#', alias)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("Room alias must be in the form '#localpart:domain'"),
|
JSON: spec.InvalidParam("Room alias must be in the form '#localpart:domain'"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if domain != cfg.Matrix.ServerName {
|
if !cfg.Matrix.IsLocalServerName(domain) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("Alias must be on local homeserver"),
|
JSON: spec.Forbidden("Alias must be on local homeserver"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,7 +154,7 @@ func SetLocalAlias(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("User ID must be in the form '@localpart:domain'"),
|
JSON: spec.BadJSON("User ID must be in the form '@localpart:domain'"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, appservice := range cfg.Derived.ApplicationServices {
|
for _, appservice := range cfg.Derived.ApplicationServices {
|
||||||
|
@ -155,7 +166,7 @@ func SetLocalAlias(
|
||||||
if namespace.Exclusive && namespace.RegexpObject.MatchString(alias) {
|
if namespace.Exclusive && namespace.RegexpObject.MatchString(alias) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.ASExclusive("Alias is reserved by an application service"),
|
JSON: spec.ASExclusive("Alias is reserved by an application service"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -170,21 +181,50 @@ func SetLocalAlias(
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
queryReq := roomserverAPI.SetRoomAliasRequest{
|
roomID, err := spec.NewRoomID(r.RoomID)
|
||||||
UserID: device.UserID,
|
if err != nil {
|
||||||
RoomID: r.RoomID,
|
return util.JSONResponse{
|
||||||
Alias: alias,
|
Code: http.StatusBadRequest,
|
||||||
}
|
JSON: spec.InvalidParam("invalid room ID"),
|
||||||
var queryRes roomserverAPI.SetRoomAliasResponse
|
}
|
||||||
if err := rsAPI.SetRoomAlias(req.Context(), &queryReq, &queryRes); err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("aliasAPI.SetRoomAlias failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if queryRes.AliasExists {
|
userID, err := spec.NewUserID(device.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
senderID, err := rsAPI.QuerySenderIDForUser(req.Context(), *roomID, *userID)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("QuerySenderIDForUser failed")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}
|
||||||
|
} else if senderID == nil {
|
||||||
|
util.GetLogger(req.Context()).WithField("roomID", *roomID).WithField("userID", *userID).Error("Sender ID not found")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aliasAlreadyExists, err := rsAPI.SetRoomAlias(req.Context(), *senderID, *roomID, alias)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("aliasAPI.SetRoomAlias failed")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if aliasAlreadyExists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusConflict,
|
Code: http.StatusConflict,
|
||||||
JSON: jsonerror.Unknown("The alias " + alias + " already exists."),
|
JSON: spec.Unknown("The alias " + alias + " already exists."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,29 +239,93 @@ func RemoveLocalAlias(
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
device *userapi.Device,
|
device *userapi.Device,
|
||||||
alias string,
|
alias string,
|
||||||
rsAPI roomserverAPI.RoomserverInternalAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
queryReq := roomserverAPI.RemoveRoomAliasRequest{
|
userID, err := spec.NewUserID(device.UserID, true)
|
||||||
Alias: alias,
|
if err != nil {
|
||||||
UserID: device.UserID,
|
|
||||||
}
|
|
||||||
var queryRes roomserverAPI.RemoveRoomAliasResponse
|
|
||||||
if err := rsAPI.RemoveRoomAlias(req.Context(), &queryReq, &queryRes); err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("aliasAPI.RemoveRoomAlias failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !queryRes.Found {
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: jsonerror.NotFound("The alias does not exist."),
|
JSON: spec.InternalServerError{Err: "UserID for device is invalid"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !queryRes.Removed {
|
roomIDReq := roomserverAPI.GetRoomIDForAliasRequest{Alias: alias}
|
||||||
|
roomIDRes := roomserverAPI.GetRoomIDForAliasResponse{}
|
||||||
|
err = rsAPI.GetRoomIDForAlias(req.Context(), &roomIDReq, &roomIDRes)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: spec.NotFound("The alias does not exist."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validRoomID, err := spec.NewRoomID(roomIDRes.RoomID)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: spec.NotFound("The alias does not exist."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This seems like the kind of auth check that should be done in the roomserver, but
|
||||||
|
// if this check fails (user is not in the room), then there will be no SenderID for the user
|
||||||
|
// for pseudo-ID rooms - it will just return "". However, we can't use lack of a sender ID
|
||||||
|
// as meaning they are not in the room, since lacking a sender ID could be caused by other bugs.
|
||||||
|
// TODO: maybe have QuerySenderIDForUser return richer errors?
|
||||||
|
var queryResp roomserverAPI.QueryMembershipForUserResponse
|
||||||
|
err = rsAPI.QueryMembershipForUser(req.Context(), &roomserverAPI.QueryMembershipForUserRequest{
|
||||||
|
RoomID: validRoomID.String(),
|
||||||
|
UserID: *userID,
|
||||||
|
}, &queryResp)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("roomserverAPI.QueryMembershipForUser failed")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !queryResp.IsInRoom {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("You do not have permission to remove this alias."),
|
JSON: spec.Forbidden("You do not have permission to remove this alias."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceSenderID, err := rsAPI.QuerySenderIDForUser(req.Context(), *validRoomID, *userID)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: spec.NotFound("The alias does not exist."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: how to handle this case? missing user/room keys seem to be a whole new class of errors
|
||||||
|
if deviceSenderID == nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aliasFound, aliasRemoved, err := rsAPI.RemoveRoomAlias(req.Context(), *deviceSenderID, alias)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("aliasAPI.RemoveRoomAlias failed")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !aliasFound {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: spec.NotFound("The alias does not exist."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !aliasRemoved {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("You do not have permission to remove this alias."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,7 +341,7 @@ type roomVisibility struct {
|
||||||
|
|
||||||
// GetVisibility implements GET /directory/list/room/{roomID}
|
// GetVisibility implements GET /directory/list/room/{roomID}
|
||||||
func GetVisibility(
|
func GetVisibility(
|
||||||
req *http.Request, rsAPI roomserverAPI.RoomserverInternalAPI,
|
req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
roomID string,
|
roomID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
var res roomserverAPI.QueryPublishedRoomsResponse
|
var res roomserverAPI.QueryPublishedRoomsResponse
|
||||||
|
@ -246,12 +350,15 @@ func GetVisibility(
|
||||||
}, &res)
|
}, &res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("QueryPublishedRooms failed")
|
util.GetLogger(req.Context()).WithError(err).Error("QueryPublishedRooms failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var v roomVisibility
|
var v roomVisibility
|
||||||
if len(res.RoomIDs) == 1 {
|
if len(res.RoomIDs) == 1 {
|
||||||
v.Visibility = gomatrixserverlib.Public
|
v.Visibility = spec.Public
|
||||||
} else {
|
} else {
|
||||||
v.Visibility = "private"
|
v.Visibility = "private"
|
||||||
}
|
}
|
||||||
|
@ -265,10 +372,33 @@ func GetVisibility(
|
||||||
// SetVisibility implements PUT /directory/list/room/{roomID}
|
// SetVisibility implements PUT /directory/list/room/{roomID}
|
||||||
// TODO: Allow admin users to edit the room visibility
|
// TODO: Allow admin users to edit the room visibility
|
||||||
func SetVisibility(
|
func SetVisibility(
|
||||||
req *http.Request, rsAPI roomserverAPI.RoomserverInternalAPI, dev *userapi.Device,
|
req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, dev *userapi.Device,
|
||||||
roomID string,
|
roomID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
resErr := checkMemberInRoom(req.Context(), rsAPI, dev.UserID, roomID)
|
deviceUserID, err := spec.NewUserID(dev.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("userID for this device is invalid"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
validRoomID, err := spec.NewRoomID(roomID)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("roomID is invalid")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("RoomID is invalid"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
senderID, err := rsAPI.QuerySenderIDForUser(req.Context(), *validRoomID, *deviceUserID)
|
||||||
|
if err != nil || senderID == nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.Unknown("failed to find senderID for this user"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resErr := checkMemberInRoom(req.Context(), rsAPI, *deviceUserID, roomID)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
@ -276,23 +406,26 @@ func SetVisibility(
|
||||||
queryEventsReq := roomserverAPI.QueryLatestEventsAndStateRequest{
|
queryEventsReq := roomserverAPI.QueryLatestEventsAndStateRequest{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
StateToFetch: []gomatrixserverlib.StateKeyTuple{{
|
StateToFetch: []gomatrixserverlib.StateKeyTuple{{
|
||||||
EventType: gomatrixserverlib.MRoomPowerLevels,
|
EventType: spec.MRoomPowerLevels,
|
||||||
StateKey: "",
|
StateKey: "",
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
var queryEventsRes roomserverAPI.QueryLatestEventsAndStateResponse
|
var queryEventsRes roomserverAPI.QueryLatestEventsAndStateResponse
|
||||||
err := rsAPI.QueryLatestEventsAndState(req.Context(), &queryEventsReq, &queryEventsRes)
|
err = rsAPI.QueryLatestEventsAndState(req.Context(), &queryEventsReq, &queryEventsRes)
|
||||||
if err != nil || len(queryEventsRes.StateEvents) == 0 {
|
if err != nil || len(queryEventsRes.StateEvents) == 0 {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("could not query events from room")
|
util.GetLogger(req.Context()).WithError(err).Error("could not query events from room")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTSPEC: Check if the user's power is greater than power required to change m.room.canonical_alias event
|
// NOTSPEC: Check if the user's power is greater than power required to change m.room.canonical_alias event
|
||||||
power, _ := gomatrixserverlib.NewPowerLevelContentFromEvent(queryEventsRes.StateEvents[0].Event)
|
power, _ := gomatrixserverlib.NewPowerLevelContentFromEvent(queryEventsRes.StateEvents[0].PDU)
|
||||||
if power.UserLevel(dev.UserID) < power.EventLevel(gomatrixserverlib.MRoomCanonicalAlias, true) {
|
if power.UserLevel(*senderID) < power.EventLevel(spec.MRoomCanonicalAlias, true) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("userID doesn't have power level to change visibility"),
|
JSON: spec.Forbidden("userID doesn't have power level to change visibility"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,14 +434,54 @@ func SetVisibility(
|
||||||
return *reqErr
|
return *reqErr
|
||||||
}
|
}
|
||||||
|
|
||||||
var publishRes roomserverAPI.PerformPublishResponse
|
if err = rsAPI.PerformPublish(req.Context(), &roomserverAPI.PerformPublishRequest{
|
||||||
rsAPI.PerformPublish(req.Context(), &roomserverAPI.PerformPublishRequest{
|
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
Visibility: v.Visibility,
|
Visibility: v.Visibility,
|
||||||
}, &publishRes)
|
}); err != nil {
|
||||||
if publishRes.Error != nil {
|
util.GetLogger(req.Context()).WithError(err).Error("failed to publish room")
|
||||||
util.GetLogger(req.Context()).WithError(publishRes.Error).Error("PerformPublish failed")
|
return util.JSONResponse{
|
||||||
return publishRes.Error.JSONResponse()
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
JSON: struct{}{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetVisibilityAS(
|
||||||
|
req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, dev *userapi.Device,
|
||||||
|
networkID, roomID string,
|
||||||
|
) util.JSONResponse {
|
||||||
|
if dev.AccountType != userapi.AccountTypeAppService {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: spec.Forbidden("Only appservice may use this endpoint"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var v roomVisibility
|
||||||
|
|
||||||
|
// If the method is delete, we simply mark the visibility as private
|
||||||
|
if req.Method == http.MethodDelete {
|
||||||
|
v.Visibility = "private"
|
||||||
|
} else {
|
||||||
|
if reqErr := httputil.UnmarshalJSONRequest(req, &v); reqErr != nil {
|
||||||
|
return *reqErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := rsAPI.PerformPublish(req.Context(), &roomserverAPI.PerformPublishRequest{
|
||||||
|
RoomID: roomID,
|
||||||
|
Visibility: v.Visibility,
|
||||||
|
NetworkID: networkID,
|
||||||
|
AppserviceID: dev.AppserviceID,
|
||||||
|
}); err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("failed to publish room")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
|
@ -23,36 +23,40 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/api"
|
"github.com/matrix-org/dendrite/clientapi/api"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"github.com/matrix-org/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cacheMu sync.Mutex
|
cacheMu sync.Mutex
|
||||||
publicRoomsCache []gomatrixserverlib.PublicRoom
|
publicRoomsCache []fclient.PublicRoom
|
||||||
)
|
)
|
||||||
|
|
||||||
type PublicRoomReq struct {
|
type PublicRoomReq struct {
|
||||||
Since string `json:"since,omitempty"`
|
Since string `json:"since,omitempty"`
|
||||||
Limit int16 `json:"limit,omitempty"`
|
Limit int64 `json:"limit,omitempty"`
|
||||||
Filter filter `json:"filter,omitempty"`
|
Filter filter `json:"filter,omitempty"`
|
||||||
Server string `json:"server,omitempty"`
|
Server string `json:"server,omitempty"`
|
||||||
|
IncludeAllNetworks bool `json:"include_all_networks,omitempty"`
|
||||||
|
NetworkID string `json:"third_party_instance_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type filter struct {
|
type filter struct {
|
||||||
SearchTerms string `json:"generic_search_term,omitempty"`
|
SearchTerms string `json:"generic_search_term,omitempty"`
|
||||||
|
RoomTypes []string `json:"room_types,omitempty"` // TODO: Implement filter on this
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPostPublicRooms implements GET and POST /publicRooms
|
// GetPostPublicRooms implements GET and POST /publicRooms
|
||||||
func GetPostPublicRooms(
|
func GetPostPublicRooms(
|
||||||
req *http.Request, rsAPI roomserverAPI.RoomserverInternalAPI,
|
req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
extRoomsProvider api.ExtraPublicRoomsProvider,
|
extRoomsProvider api.ExtraPublicRoomsProvider,
|
||||||
federation *gomatrixserverlib.FederationClient,
|
federation fclient.FederationClient,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
var request PublicRoomReq
|
var request PublicRoomReq
|
||||||
|
@ -60,18 +64,27 @@ func GetPostPublicRooms(
|
||||||
return *fillErr
|
return *fillErr
|
||||||
}
|
}
|
||||||
|
|
||||||
serverName := gomatrixserverlib.ServerName(request.Server)
|
if request.IncludeAllNetworks && request.NetworkID != "" {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.InvalidParam("include_all_networks and third_party_instance_id can not be used together"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if serverName != "" && serverName != cfg.Matrix.ServerName {
|
serverName := spec.ServerName(request.Server)
|
||||||
|
if serverName != "" && !cfg.Matrix.IsLocalServerName(serverName) {
|
||||||
res, err := federation.GetPublicRoomsFiltered(
|
res, err := federation.GetPublicRoomsFiltered(
|
||||||
req.Context(), serverName,
|
req.Context(), cfg.Matrix.ServerName, serverName,
|
||||||
int(request.Limit), request.Since,
|
int(request.Limit), request.Since,
|
||||||
request.Filter.SearchTerms, false,
|
request.Filter.SearchTerms, false,
|
||||||
"",
|
"",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("failed to get public rooms")
|
util.GetLogger(req.Context()).WithError(err).Error("failed to get public rooms")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
@ -82,7 +95,10 @@ func GetPostPublicRooms(
|
||||||
response, err := publicRooms(req.Context(), request, rsAPI, extRoomsProvider)
|
response, err := publicRooms(req.Context(), request, rsAPI, extRoomsProvider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Errorf("failed to work out public rooms")
|
util.GetLogger(req.Context()).WithError(err).Errorf("failed to work out public rooms")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
@ -91,13 +107,13 @@ func GetPostPublicRooms(
|
||||||
}
|
}
|
||||||
|
|
||||||
func publicRooms(
|
func publicRooms(
|
||||||
ctx context.Context, request PublicRoomReq, rsAPI roomserverAPI.RoomserverInternalAPI, extRoomsProvider api.ExtraPublicRoomsProvider,
|
ctx context.Context, request PublicRoomReq, rsAPI roomserverAPI.ClientRoomserverAPI, extRoomsProvider api.ExtraPublicRoomsProvider,
|
||||||
) (*gomatrixserverlib.RespPublicRooms, error) {
|
) (*fclient.RespPublicRooms, error) {
|
||||||
|
|
||||||
response := gomatrixserverlib.RespPublicRooms{
|
response := fclient.RespPublicRooms{
|
||||||
Chunk: []gomatrixserverlib.PublicRoom{},
|
Chunk: []fclient.PublicRoom{},
|
||||||
}
|
}
|
||||||
var limit int16
|
var limit int64
|
||||||
var offset int64
|
var offset int64
|
||||||
limit = request.Limit
|
limit = request.Limit
|
||||||
if limit == 0 {
|
if limit == 0 {
|
||||||
|
@ -112,9 +128,9 @@ func publicRooms(
|
||||||
}
|
}
|
||||||
err = nil
|
err = nil
|
||||||
|
|
||||||
var rooms []gomatrixserverlib.PublicRoom
|
var rooms []fclient.PublicRoom
|
||||||
if request.Since == "" {
|
if request.Since == "" {
|
||||||
rooms = refreshPublicRoomCache(ctx, rsAPI, extRoomsProvider)
|
rooms = refreshPublicRoomCache(ctx, rsAPI, extRoomsProvider, request)
|
||||||
} else {
|
} else {
|
||||||
rooms = getPublicRoomsFromCache()
|
rooms = getPublicRoomsFromCache()
|
||||||
}
|
}
|
||||||
|
@ -136,14 +152,14 @@ func publicRooms(
|
||||||
return &response, err
|
return &response, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterRooms(rooms []gomatrixserverlib.PublicRoom, searchTerm string) []gomatrixserverlib.PublicRoom {
|
func filterRooms(rooms []fclient.PublicRoom, searchTerm string) []fclient.PublicRoom {
|
||||||
if searchTerm == "" {
|
if searchTerm == "" {
|
||||||
return rooms
|
return rooms
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizedTerm := strings.ToLower(searchTerm)
|
normalizedTerm := strings.ToLower(searchTerm)
|
||||||
|
|
||||||
result := make([]gomatrixserverlib.PublicRoom, 0)
|
result := make([]fclient.PublicRoom, 0)
|
||||||
for _, room := range rooms {
|
for _, room := range rooms {
|
||||||
if strings.Contains(strings.ToLower(room.Name), normalizedTerm) ||
|
if strings.Contains(strings.ToLower(room.Name), normalizedTerm) ||
|
||||||
strings.Contains(strings.ToLower(room.Topic), normalizedTerm) ||
|
strings.Contains(strings.ToLower(room.Topic), normalizedTerm) ||
|
||||||
|
@ -162,7 +178,7 @@ func fillPublicRoomsReq(httpReq *http.Request, request *PublicRoomReq) *util.JSO
|
||||||
if httpReq.Method != "GET" && httpReq.Method != "POST" {
|
if httpReq.Method != "GET" && httpReq.Method != "POST" {
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusMethodNotAllowed,
|
Code: http.StatusMethodNotAllowed,
|
||||||
JSON: jsonerror.NotFound("Bad method"),
|
JSON: spec.NotFound("Bad method"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if httpReq.Method == "GET" {
|
if httpReq.Method == "GET" {
|
||||||
|
@ -173,10 +189,10 @@ func fillPublicRoomsReq(httpReq *http.Request, request *PublicRoomReq) *util.JSO
|
||||||
util.GetLogger(httpReq.Context()).WithError(err).Error("strconv.Atoi failed")
|
util.GetLogger(httpReq.Context()).WithError(err).Error("strconv.Atoi failed")
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: 400,
|
Code: 400,
|
||||||
JSON: jsonerror.BadJSON("limit param is not a number"),
|
JSON: spec.BadJSON("limit param is not a number"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
request.Limit = int16(limit)
|
request.Limit = int64(limit)
|
||||||
request.Since = httpReq.FormValue("since")
|
request.Since = httpReq.FormValue("since")
|
||||||
request.Server = httpReq.FormValue("server")
|
request.Server = httpReq.FormValue("server")
|
||||||
} else {
|
} else {
|
||||||
|
@ -196,15 +212,15 @@ func fillPublicRoomsReq(httpReq *http.Request, request *PublicRoomReq) *util.JSO
|
||||||
|
|
||||||
// sliceInto returns a subslice of `slice` which honours the since/limit values given.
|
// sliceInto returns a subslice of `slice` which honours the since/limit values given.
|
||||||
//
|
//
|
||||||
// 0 1 2 3 4 5 6 index
|
// 0 1 2 3 4 5 6 index
|
||||||
// [A, B, C, D, E, F, G] slice
|
// [A, B, C, D, E, F, G] slice
|
||||||
//
|
//
|
||||||
// limit=3 => A,B,C (prev='', next='3')
|
// limit=3 => A,B,C (prev='', next='3')
|
||||||
// limit=3&since=3 => D,E,F (prev='0', next='6')
|
// limit=3&since=3 => D,E,F (prev='0', next='6')
|
||||||
// limit=3&since=6 => G (prev='3', next='')
|
// limit=3&since=6 => G (prev='3', next='')
|
||||||
//
|
//
|
||||||
// A value of '-1' for prev/next indicates no position.
|
// A value of '-1' for prev/next indicates no position.
|
||||||
func sliceInto(slice []gomatrixserverlib.PublicRoom, since int64, limit int16) (subset []gomatrixserverlib.PublicRoom, prev, next int) {
|
func sliceInto(slice []fclient.PublicRoom, since int64, limit int64) (subset []fclient.PublicRoom, prev, next int) {
|
||||||
prev = -1
|
prev = -1
|
||||||
next = -1
|
next = -1
|
||||||
|
|
||||||
|
@ -229,17 +245,27 @@ func sliceInto(slice []gomatrixserverlib.PublicRoom, since int64, limit int16) (
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshPublicRoomCache(
|
func refreshPublicRoomCache(
|
||||||
ctx context.Context, rsAPI roomserverAPI.RoomserverInternalAPI, extRoomsProvider api.ExtraPublicRoomsProvider,
|
ctx context.Context, rsAPI roomserverAPI.ClientRoomserverAPI, extRoomsProvider api.ExtraPublicRoomsProvider,
|
||||||
) []gomatrixserverlib.PublicRoom {
|
request PublicRoomReq,
|
||||||
|
) []fclient.PublicRoom {
|
||||||
cacheMu.Lock()
|
cacheMu.Lock()
|
||||||
defer cacheMu.Unlock()
|
defer cacheMu.Unlock()
|
||||||
var extraRooms []gomatrixserverlib.PublicRoom
|
var extraRooms []fclient.PublicRoom
|
||||||
if extRoomsProvider != nil {
|
if extRoomsProvider != nil {
|
||||||
extraRooms = extRoomsProvider.Rooms()
|
extraRooms = extRoomsProvider.Rooms()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: this is only here to make Sytest happy, for now.
|
||||||
|
ns := strings.Split(request.NetworkID, "|")
|
||||||
|
if len(ns) == 2 {
|
||||||
|
request.NetworkID = ns[1]
|
||||||
|
}
|
||||||
|
|
||||||
var queryRes roomserverAPI.QueryPublishedRoomsResponse
|
var queryRes roomserverAPI.QueryPublishedRoomsResponse
|
||||||
err := rsAPI.QueryPublishedRooms(ctx, &roomserverAPI.QueryPublishedRoomsRequest{}, &queryRes)
|
err := rsAPI.QueryPublishedRooms(ctx, &roomserverAPI.QueryPublishedRoomsRequest{
|
||||||
|
NetworkID: request.NetworkID,
|
||||||
|
IncludeAllNetworks: request.IncludeAllNetworks,
|
||||||
|
}, &queryRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("QueryPublishedRooms failed")
|
util.GetLogger(ctx).WithError(err).Error("QueryPublishedRooms failed")
|
||||||
return publicRoomsCache
|
return publicRoomsCache
|
||||||
|
@ -249,7 +275,7 @@ func refreshPublicRoomCache(
|
||||||
util.GetLogger(ctx).WithError(err).Error("PopulatePublicRooms failed")
|
util.GetLogger(ctx).WithError(err).Error("PopulatePublicRooms failed")
|
||||||
return publicRoomsCache
|
return publicRoomsCache
|
||||||
}
|
}
|
||||||
publicRoomsCache = []gomatrixserverlib.PublicRoom{}
|
publicRoomsCache = []fclient.PublicRoom{}
|
||||||
publicRoomsCache = append(publicRoomsCache, pubRooms...)
|
publicRoomsCache = append(publicRoomsCache, pubRooms...)
|
||||||
publicRoomsCache = append(publicRoomsCache, extraRooms...)
|
publicRoomsCache = append(publicRoomsCache, extraRooms...)
|
||||||
publicRoomsCache = dedupeAndShuffle(publicRoomsCache)
|
publicRoomsCache = dedupeAndShuffle(publicRoomsCache)
|
||||||
|
@ -261,16 +287,16 @@ func refreshPublicRoomCache(
|
||||||
return publicRoomsCache
|
return publicRoomsCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPublicRoomsFromCache() []gomatrixserverlib.PublicRoom {
|
func getPublicRoomsFromCache() []fclient.PublicRoom {
|
||||||
cacheMu.Lock()
|
cacheMu.Lock()
|
||||||
defer cacheMu.Unlock()
|
defer cacheMu.Unlock()
|
||||||
return publicRoomsCache
|
return publicRoomsCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func dedupeAndShuffle(in []gomatrixserverlib.PublicRoom) []gomatrixserverlib.PublicRoom {
|
func dedupeAndShuffle(in []fclient.PublicRoom) []fclient.PublicRoom {
|
||||||
// de-duplicate rooms with the same room ID. We can join the room via any of these aliases as we know these servers
|
// de-duplicate rooms with the same room ID. We can join the room via any of these aliases as we know these servers
|
||||||
// are alive and well, so we arbitrarily pick one (purposefully shuffling them to spread the load a bit)
|
// are alive and well, so we arbitrarily pick one (purposefully shuffling them to spread the load a bit)
|
||||||
var publicRooms []gomatrixserverlib.PublicRoom
|
var publicRooms []fclient.PublicRoom
|
||||||
haveRoomIDs := make(map[string]bool)
|
haveRoomIDs := make(map[string]bool)
|
||||||
rand.Shuffle(len(in), func(i, j int) {
|
rand.Shuffle(len(in), func(i, j int) {
|
||||||
in[i], in[j] = in[j], in[i]
|
in[i], in[j] = in[j], in[i]
|
||||||
|
|
|
@ -4,25 +4,25 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
func pubRoom(name string) gomatrixserverlib.PublicRoom {
|
func pubRoom(name string) fclient.PublicRoom {
|
||||||
return gomatrixserverlib.PublicRoom{
|
return fclient.PublicRoom{
|
||||||
Name: name,
|
Name: name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSliceInto(t *testing.T) {
|
func TestSliceInto(t *testing.T) {
|
||||||
slice := []gomatrixserverlib.PublicRoom{
|
slice := []fclient.PublicRoom{
|
||||||
pubRoom("a"), pubRoom("b"), pubRoom("c"), pubRoom("d"), pubRoom("e"), pubRoom("f"), pubRoom("g"),
|
pubRoom("a"), pubRoom("b"), pubRoom("c"), pubRoom("d"), pubRoom("e"), pubRoom("f"), pubRoom("g"),
|
||||||
}
|
}
|
||||||
limit := int16(3)
|
limit := int64(3)
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
since int64
|
since int64
|
||||||
wantPrev int
|
wantPrev int
|
||||||
wantNext int
|
wantNext int
|
||||||
wantSubset []gomatrixserverlib.PublicRoom
|
wantSubset []fclient.PublicRoom
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
since: 0,
|
since: 0,
|
||||||
|
|
|
@ -1,141 +0,0 @@
|
||||||
// Copyright 2019 Alex Chen
|
|
||||||
//
|
|
||||||
// Licensed 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
|
|
||||||
//
|
|
||||||
// http://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.
|
|
||||||
|
|
||||||
package routing
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"github.com/matrix-org/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
type getEventRequest struct {
|
|
||||||
req *http.Request
|
|
||||||
device *userapi.Device
|
|
||||||
roomID string
|
|
||||||
eventID string
|
|
||||||
cfg *config.ClientAPI
|
|
||||||
federation *gomatrixserverlib.FederationClient
|
|
||||||
requestedEvent *gomatrixserverlib.Event
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEvent implements GET /_matrix/client/r0/rooms/{roomId}/event/{eventId}
|
|
||||||
// https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-rooms-roomid-event-eventid
|
|
||||||
func GetEvent(
|
|
||||||
req *http.Request,
|
|
||||||
device *userapi.Device,
|
|
||||||
roomID string,
|
|
||||||
eventID string,
|
|
||||||
cfg *config.ClientAPI,
|
|
||||||
rsAPI api.RoomserverInternalAPI,
|
|
||||||
federation *gomatrixserverlib.FederationClient,
|
|
||||||
) util.JSONResponse {
|
|
||||||
eventsReq := api.QueryEventsByIDRequest{
|
|
||||||
EventIDs: []string{eventID},
|
|
||||||
}
|
|
||||||
var eventsResp api.QueryEventsByIDResponse
|
|
||||||
err := rsAPI.QueryEventsByID(req.Context(), &eventsReq, &eventsResp)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("queryAPI.QueryEventsByID failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(eventsResp.Events) == 0 {
|
|
||||||
// Event not found locally
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusNotFound,
|
|
||||||
JSON: jsonerror.NotFound("The event was not found or you do not have permission to read this event"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requestedEvent := eventsResp.Events[0].Event
|
|
||||||
|
|
||||||
r := getEventRequest{
|
|
||||||
req: req,
|
|
||||||
device: device,
|
|
||||||
roomID: roomID,
|
|
||||||
eventID: eventID,
|
|
||||||
cfg: cfg,
|
|
||||||
federation: federation,
|
|
||||||
requestedEvent: requestedEvent,
|
|
||||||
}
|
|
||||||
|
|
||||||
stateReq := api.QueryStateAfterEventsRequest{
|
|
||||||
RoomID: r.requestedEvent.RoomID(),
|
|
||||||
PrevEventIDs: r.requestedEvent.PrevEventIDs(),
|
|
||||||
StateToFetch: []gomatrixserverlib.StateKeyTuple{{
|
|
||||||
EventType: gomatrixserverlib.MRoomMember,
|
|
||||||
StateKey: device.UserID,
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
var stateResp api.QueryStateAfterEventsResponse
|
|
||||||
if err := rsAPI.QueryStateAfterEvents(req.Context(), &stateReq, &stateResp); err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("queryAPI.QueryStateAfterEvents failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !stateResp.RoomExists {
|
|
||||||
util.GetLogger(req.Context()).Errorf("Expected to find room for event %s but failed", r.requestedEvent.EventID())
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !stateResp.PrevEventsExist {
|
|
||||||
// Missing some events locally; stateResp.StateEvents unavailable.
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusNotFound,
|
|
||||||
JSON: jsonerror.NotFound("The event was not found or you do not have permission to read this event"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var appService *config.ApplicationService
|
|
||||||
if device.AppserviceID != "" {
|
|
||||||
for _, as := range cfg.Derived.ApplicationServices {
|
|
||||||
if as.ID == device.AppserviceID {
|
|
||||||
appService = &as
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, stateEvent := range stateResp.StateEvents {
|
|
||||||
if appService != nil {
|
|
||||||
if !appService.IsInterestedInUserID(*stateEvent.StateKey()) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else if !stateEvent.StateKeyEquals(device.UserID) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
membership, err := stateEvent.Membership()
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("stateEvent.Membership failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
if membership == gomatrixserverlib.Join {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
JSON: gomatrixserverlib.ToClientEvent(r.requestedEvent, gomatrixserverlib.FormatAll),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusNotFound,
|
|
||||||
JSON: jsonerror.NotFound("The event was not found or you do not have permission to read this event"),
|
|
||||||
}
|
|
||||||
}
|
|
68
clientapi/routing/joined_rooms.go
Normal file
68
clientapi/routing/joined_rooms.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed 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
|
||||||
|
//
|
||||||
|
// http://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.
|
||||||
|
|
||||||
|
package routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
)
|
||||||
|
|
||||||
|
type getJoinedRoomsResponse struct {
|
||||||
|
JoinedRooms []string `json:"joined_rooms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetJoinedRooms(
|
||||||
|
req *http.Request,
|
||||||
|
device *userapi.Device,
|
||||||
|
rsAPI api.ClientRoomserverAPI,
|
||||||
|
) util.JSONResponse {
|
||||||
|
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("Invalid device user ID")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rooms, err := rsAPI.QueryRoomsForUser(req.Context(), *deviceUserID, "join")
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("QueryRoomsForUser failed")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("internal server error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var roomIDStrs []string
|
||||||
|
if rooms == nil {
|
||||||
|
roomIDStrs = []string{}
|
||||||
|
} else {
|
||||||
|
roomIDStrs = make([]string, len(rooms))
|
||||||
|
for i, roomID := range rooms {
|
||||||
|
roomIDStrs[i] = roomID.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
JSON: getJoinedRoomsResponse{roomIDStrs},
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,31 +15,34 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrix"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func JoinRoomByIDOrAlias(
|
func JoinRoomByIDOrAlias(
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
device *api.Device,
|
device *api.Device,
|
||||||
rsAPI roomserverAPI.RoomserverInternalAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
profileAPI api.UserProfileAPI,
|
profileAPI api.ClientUserAPI,
|
||||||
roomIDOrAlias string,
|
roomIDOrAlias string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
// Prepare to ask the roomserver to perform the room join.
|
// Prepare to ask the roomserver to perform the room join.
|
||||||
joinReq := roomserverAPI.PerformJoinRequest{
|
joinReq := roomserverAPI.PerformJoinRequest{
|
||||||
RoomIDOrAlias: roomIDOrAlias,
|
RoomIDOrAlias: roomIDOrAlias,
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
|
IsGuest: device.AccountType == api.AccountTypeGuest,
|
||||||
Content: map[string]interface{}{},
|
Content: map[string]interface{}{},
|
||||||
}
|
}
|
||||||
joinRes := roomserverAPI.PerformJoinResponse{}
|
|
||||||
|
|
||||||
// Check to see if any ?server_name= query parameters were
|
// Check to see if any ?server_name= query parameters were
|
||||||
// given in the request.
|
// given in the request.
|
||||||
|
@ -47,7 +50,7 @@ func JoinRoomByIDOrAlias(
|
||||||
for _, serverName := range serverNames {
|
for _, serverName := range serverNames {
|
||||||
joinReq.ServerNames = append(
|
joinReq.ServerNames = append(
|
||||||
joinReq.ServerNames,
|
joinReq.ServerNames,
|
||||||
gomatrixserverlib.ServerName(serverName),
|
spec.ServerName(serverName),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,50 +63,84 @@ func JoinRoomByIDOrAlias(
|
||||||
// Work out our localpart for the client profile request.
|
// Work out our localpart for the client profile request.
|
||||||
|
|
||||||
// Request our profile content to populate the request content with.
|
// Request our profile content to populate the request content with.
|
||||||
res := &api.QueryProfileResponse{}
|
profile, err := profileAPI.QueryProfile(req.Context(), device.UserID)
|
||||||
err := profileAPI.QueryProfile(req.Context(), &api.QueryProfileRequest{UserID: device.UserID}, res)
|
|
||||||
if err != nil || !res.UserExists {
|
|
||||||
if !res.UserExists {
|
|
||||||
util.GetLogger(req.Context()).Error("Unable to query user profile, no profile found.")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: jsonerror.Unknown("Unable to query user profile, no profile found."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("UserProfileAPI.QueryProfile failed")
|
switch err {
|
||||||
} else {
|
case nil:
|
||||||
joinReq.Content["displayname"] = res.DisplayName
|
joinReq.Content["displayname"] = profile.DisplayName
|
||||||
joinReq.Content["avatar_url"] = res.AvatarURL
|
joinReq.Content["avatar_url"] = profile.AvatarURL
|
||||||
|
case appserviceAPI.ErrProfileNotExists:
|
||||||
|
util.GetLogger(req.Context()).Error("Unable to query user profile, no profile found.")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.Unknown("Unable to query user profile, no profile found."),
|
||||||
|
}
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ask the roomserver to perform the join.
|
// Ask the roomserver to perform the join.
|
||||||
done := make(chan util.JSONResponse, 1)
|
done := make(chan util.JSONResponse, 1)
|
||||||
go func() {
|
go func() {
|
||||||
defer close(done)
|
defer close(done)
|
||||||
rsAPI.PerformJoin(req.Context(), &joinReq, &joinRes)
|
roomID, _, err := rsAPI.PerformJoin(req.Context(), &joinReq)
|
||||||
if joinRes.Error != nil {
|
var response util.JSONResponse
|
||||||
done <- joinRes.Error.JSONResponse()
|
|
||||||
} else {
|
switch e := err.(type) {
|
||||||
done <- util.JSONResponse{
|
case nil: // success case
|
||||||
|
response = util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
// TODO: Put the response struct somewhere internal.
|
// TODO: Put the response struct somewhere internal.
|
||||||
JSON: struct {
|
JSON: struct {
|
||||||
RoomID string `json:"room_id"`
|
RoomID string `json:"room_id"`
|
||||||
}{joinRes.RoomID},
|
}{roomID},
|
||||||
|
}
|
||||||
|
case roomserverAPI.ErrInvalidID:
|
||||||
|
response = util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.Unknown(e.Error()),
|
||||||
|
}
|
||||||
|
case roomserverAPI.ErrNotAllowed:
|
||||||
|
jsonErr := spec.Forbidden(e.Error())
|
||||||
|
if device.AccountType == api.AccountTypeGuest {
|
||||||
|
jsonErr = spec.GuestAccessForbidden(e.Error())
|
||||||
|
}
|
||||||
|
response = util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: jsonErr,
|
||||||
|
}
|
||||||
|
case *gomatrix.HTTPError: // this ensures we proxy responses over federation to the client
|
||||||
|
response = util.JSONResponse{
|
||||||
|
Code: e.Code,
|
||||||
|
JSON: json.RawMessage(e.Message),
|
||||||
|
}
|
||||||
|
case eventutil.ErrRoomNoExists:
|
||||||
|
response = util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: spec.NotFound(e.Error()),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
response = util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
done <- response
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Wait either for the join to finish, or for us to hit a reasonable
|
// Wait either for the join to finish, or for us to hit a reasonable
|
||||||
// timeout, at which point we'll just return a 200 to placate clients.
|
// timeout, at which point we'll just return a 200 to placate clients.
|
||||||
|
timer := time.NewTimer(time.Second * 20)
|
||||||
select {
|
select {
|
||||||
case <-time.After(time.Second * 20):
|
case <-timer.C:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusAccepted,
|
Code: http.StatusAccepted,
|
||||||
JSON: jsonerror.Unknown("The room join will continue in the background."),
|
JSON: spec.Unknown("The room join will continue in the background."),
|
||||||
}
|
}
|
||||||
case result := <-done:
|
case result := <-done:
|
||||||
|
// Stop and drain the timer
|
||||||
|
if !timer.Stop() {
|
||||||
|
<-timer.C
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
166
clientapi/routing/joinroom_test.go
Normal file
166
clientapi/routing/joinroom_test.go
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
package routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/federationapi/statistics"
|
||||||
|
"github.com/matrix-org/dendrite/internal/caching"
|
||||||
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/appservice"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver"
|
||||||
|
"github.com/matrix-org/dendrite/test"
|
||||||
|
"github.com/matrix-org/dendrite/test/testrig"
|
||||||
|
"github.com/matrix-org/dendrite/userapi"
|
||||||
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testIsBlacklistedOrBackingOff = func(s spec.ServerName) (*statistics.ServerStatistics, error) {
|
||||||
|
return &statistics.ServerStatistics{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJoinRoomByIDOrAlias(t *testing.T) {
|
||||||
|
alice := test.NewUser(t)
|
||||||
|
bob := test.NewUser(t)
|
||||||
|
charlie := test.NewUser(t, test.WithAccountType(uapi.AccountTypeGuest))
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
||||||
|
cfg, processCtx, close := testrig.CreateConfig(t, dbType)
|
||||||
|
defer close()
|
||||||
|
|
||||||
|
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||||
|
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
||||||
|
natsInstance := jetstream.NATSInstance{}
|
||||||
|
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
|
||||||
|
rsAPI.SetFederationAPI(nil, nil) // creates the rs.Inputer etc
|
||||||
|
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
|
||||||
|
asAPI := appservice.NewInternalAPI(processCtx, cfg, &natsInstance, userAPI, rsAPI)
|
||||||
|
|
||||||
|
// Create the users in the userapi
|
||||||
|
for _, u := range []*test.User{alice, bob, charlie} {
|
||||||
|
localpart, serverName, _ := gomatrixserverlib.SplitID('@', u.ID)
|
||||||
|
userRes := &uapi.PerformAccountCreationResponse{}
|
||||||
|
if err := userAPI.PerformAccountCreation(ctx, &uapi.PerformAccountCreationRequest{
|
||||||
|
AccountType: u.AccountType,
|
||||||
|
Localpart: localpart,
|
||||||
|
ServerName: serverName,
|
||||||
|
Password: "someRandomPassword",
|
||||||
|
}, userRes); err != nil {
|
||||||
|
t.Errorf("failed to create account: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
aliceDev := &uapi.Device{UserID: alice.ID}
|
||||||
|
bobDev := &uapi.Device{UserID: bob.ID}
|
||||||
|
charlieDev := &uapi.Device{UserID: charlie.ID, AccountType: uapi.AccountTypeGuest}
|
||||||
|
|
||||||
|
// create a room with disabled guest access and invite Bob
|
||||||
|
resp := createRoom(ctx, createRoomRequest{
|
||||||
|
Name: "testing",
|
||||||
|
IsDirect: true,
|
||||||
|
Topic: "testing",
|
||||||
|
Visibility: "public",
|
||||||
|
Preset: spec.PresetPublicChat,
|
||||||
|
RoomAliasName: "alias",
|
||||||
|
Invite: []string{bob.ID},
|
||||||
|
}, aliceDev, &cfg.ClientAPI, userAPI, rsAPI, asAPI, time.Now())
|
||||||
|
crResp, ok := resp.JSON.(createRoomResponse)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("response is not a createRoomResponse: %+v", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a room with guest access enabled and invite Charlie
|
||||||
|
resp = createRoom(ctx, createRoomRequest{
|
||||||
|
Name: "testing",
|
||||||
|
IsDirect: true,
|
||||||
|
Topic: "testing",
|
||||||
|
Visibility: "public",
|
||||||
|
Preset: spec.PresetPublicChat,
|
||||||
|
Invite: []string{charlie.ID},
|
||||||
|
}, aliceDev, &cfg.ClientAPI, userAPI, rsAPI, asAPI, time.Now())
|
||||||
|
crRespWithGuestAccess, ok := resp.JSON.(createRoomResponse)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("response is not a createRoomResponse: %+v", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dummy request
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
req, err := http.NewRequest(http.MethodPost, "/?server_name=test", body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
device *uapi.Device
|
||||||
|
roomID string
|
||||||
|
wantHTTP200 bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "User can join successfully by alias",
|
||||||
|
device: bobDev,
|
||||||
|
roomID: crResp.RoomAlias,
|
||||||
|
wantHTTP200: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "User can join successfully by roomID",
|
||||||
|
device: bobDev,
|
||||||
|
roomID: crResp.RoomID,
|
||||||
|
wantHTTP200: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "join is forbidden if user is guest",
|
||||||
|
device: charlieDev,
|
||||||
|
roomID: crResp.RoomID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "room does not exist",
|
||||||
|
device: aliceDev,
|
||||||
|
roomID: "!doesnotexist:test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user from different server",
|
||||||
|
device: &uapi.Device{UserID: "@wrong:server"},
|
||||||
|
roomID: crResp.RoomAlias,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user doesn't exist locally",
|
||||||
|
device: &uapi.Device{UserID: "@doesnotexist:test"},
|
||||||
|
roomID: crResp.RoomAlias,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid room ID",
|
||||||
|
device: aliceDev,
|
||||||
|
roomID: "invalidRoomID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "roomAlias does not exist",
|
||||||
|
device: aliceDev,
|
||||||
|
roomID: "#doesnotexist:test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "room with guest_access event",
|
||||||
|
device: charlieDev,
|
||||||
|
roomID: crRespWithGuestAccess.RoomID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
joinResp := JoinRoomByIDOrAlias(req, tc.device, rsAPI, userAPI, tc.roomID)
|
||||||
|
if tc.wantHTTP200 && !joinResp.Is2xx() {
|
||||||
|
t.Fatalf("expected join room to succeed, but didn't: %+v", joinResp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -20,8 +20,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -55,53 +55,50 @@ type keyBackupSessionResponse struct {
|
||||||
|
|
||||||
// Create a new key backup. Request must contain a `keyBackupVersion`. Returns a `keyBackupVersionCreateResponse`.
|
// Create a new key backup. Request must contain a `keyBackupVersion`. Returns a `keyBackupVersionCreateResponse`.
|
||||||
// Implements POST /_matrix/client/r0/room_keys/version
|
// Implements POST /_matrix/client/r0/room_keys/version
|
||||||
func CreateKeyBackupVersion(req *http.Request, userAPI userapi.UserInternalAPI, device *userapi.Device) util.JSONResponse {
|
func CreateKeyBackupVersion(req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device) util.JSONResponse {
|
||||||
var kb keyBackupVersion
|
var kb keyBackupVersion
|
||||||
resErr := httputil.UnmarshalJSONRequest(req, &kb)
|
resErr := httputil.UnmarshalJSONRequest(req, &kb)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
var performKeyBackupResp userapi.PerformKeyBackupResponse
|
if len(kb.AuthData) == 0 {
|
||||||
if err := userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: spec.BadJSON("missing auth_data"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
version, err := userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
Version: "",
|
Version: "",
|
||||||
AuthData: kb.AuthData,
|
AuthData: kb.AuthData,
|
||||||
Algorithm: kb.Algorithm,
|
Algorithm: kb.Algorithm,
|
||||||
}, &performKeyBackupResp); err != nil {
|
})
|
||||||
return jsonerror.InternalServerError()
|
if err != nil {
|
||||||
}
|
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %w", err))
|
||||||
if performKeyBackupResp.Error != "" {
|
|
||||||
if performKeyBackupResp.BadInput {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 400,
|
|
||||||
JSON: jsonerror.InvalidArgumentValue(performKeyBackupResp.Error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %s", performKeyBackupResp.Error))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
JSON: keyBackupVersionCreateResponse{
|
JSON: keyBackupVersionCreateResponse{
|
||||||
Version: performKeyBackupResp.Version,
|
Version: version,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeyBackupVersion returns the key backup version specified. If `version` is empty, the latest `keyBackupVersionResponse` is returned.
|
// KeyBackupVersion returns the key backup version specified. If `version` is empty, the latest `keyBackupVersionResponse` is returned.
|
||||||
// Implements GET /_matrix/client/r0/room_keys/version and GET /_matrix/client/r0/room_keys/version/{version}
|
// Implements GET /_matrix/client/r0/room_keys/version and GET /_matrix/client/r0/room_keys/version/{version}
|
||||||
func KeyBackupVersion(req *http.Request, userAPI userapi.UserInternalAPI, device *userapi.Device, version string) util.JSONResponse {
|
func KeyBackupVersion(req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version string) util.JSONResponse {
|
||||||
var queryResp userapi.QueryKeyBackupResponse
|
queryResp, err := userAPI.QueryKeyBackup(req.Context(), &userapi.QueryKeyBackupRequest{
|
||||||
userAPI.QueryKeyBackup(req.Context(), &userapi.QueryKeyBackupRequest{
|
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
Version: version,
|
Version: version,
|
||||||
}, &queryResp)
|
})
|
||||||
if queryResp.Error != "" {
|
if err != nil {
|
||||||
return util.ErrorResponse(fmt.Errorf("QueryKeyBackup: %s", queryResp.Error))
|
return util.ErrorResponse(fmt.Errorf("QueryKeyBackup: %s", err))
|
||||||
}
|
}
|
||||||
if !queryResp.Exists {
|
if !queryResp.Exists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 404,
|
Code: 404,
|
||||||
JSON: jsonerror.NotFound("version not found"),
|
JSON: spec.NotFound("version not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -118,37 +115,35 @@ func KeyBackupVersion(req *http.Request, userAPI userapi.UserInternalAPI, device
|
||||||
|
|
||||||
// Modify the auth data of a key backup. Version must not be empty. Request must contain a `keyBackupVersion`
|
// Modify the auth data of a key backup. Version must not be empty. Request must contain a `keyBackupVersion`
|
||||||
// Implements PUT /_matrix/client/r0/room_keys/version/{version}
|
// Implements PUT /_matrix/client/r0/room_keys/version/{version}
|
||||||
func ModifyKeyBackupVersionAuthData(req *http.Request, userAPI userapi.UserInternalAPI, device *userapi.Device, version string) util.JSONResponse {
|
func ModifyKeyBackupVersionAuthData(req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version string) util.JSONResponse {
|
||||||
var kb keyBackupVersion
|
var kb keyBackupVersion
|
||||||
resErr := httputil.UnmarshalJSONRequest(req, &kb)
|
resErr := httputil.UnmarshalJSONRequest(req, &kb)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
var performKeyBackupResp userapi.PerformKeyBackupResponse
|
performKeyBackupResp, err := userAPI.UpdateBackupKeyAuthData(req.Context(), &userapi.PerformKeyBackupRequest{
|
||||||
if err := userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{
|
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
Version: version,
|
Version: version,
|
||||||
AuthData: kb.AuthData,
|
AuthData: kb.AuthData,
|
||||||
Algorithm: kb.Algorithm,
|
Algorithm: kb.Algorithm,
|
||||||
}, &performKeyBackupResp); err != nil {
|
})
|
||||||
return jsonerror.InternalServerError()
|
switch e := err.(type) {
|
||||||
}
|
case spec.ErrRoomKeysVersion:
|
||||||
if performKeyBackupResp.Error != "" {
|
return util.JSONResponse{
|
||||||
if performKeyBackupResp.BadInput {
|
Code: http.StatusForbidden,
|
||||||
return util.JSONResponse{
|
JSON: e,
|
||||||
Code: 400,
|
|
||||||
JSON: jsonerror.InvalidArgumentValue(performKeyBackupResp.Error),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %s", performKeyBackupResp.Error))
|
case nil:
|
||||||
|
default:
|
||||||
|
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %w", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !performKeyBackupResp.Exists {
|
if !performKeyBackupResp.Exists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 404,
|
Code: 404,
|
||||||
JSON: jsonerror.NotFound("backup version not found"),
|
JSON: spec.NotFound("backup version not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Unclear what the 200 body should be
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
JSON: keyBackupVersionCreateResponse{
|
JSON: keyBackupVersionCreateResponse{
|
||||||
|
@ -159,64 +154,47 @@ func ModifyKeyBackupVersionAuthData(req *http.Request, userAPI userapi.UserInter
|
||||||
|
|
||||||
// Delete a version of key backup. Version must not be empty. If the key backup was previously deleted, will return 200 OK.
|
// Delete a version of key backup. Version must not be empty. If the key backup was previously deleted, will return 200 OK.
|
||||||
// Implements DELETE /_matrix/client/r0/room_keys/version/{version}
|
// Implements DELETE /_matrix/client/r0/room_keys/version/{version}
|
||||||
func DeleteKeyBackupVersion(req *http.Request, userAPI userapi.UserInternalAPI, device *userapi.Device, version string) util.JSONResponse {
|
func DeleteKeyBackupVersion(req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version string) util.JSONResponse {
|
||||||
var performKeyBackupResp userapi.PerformKeyBackupResponse
|
exists, err := userAPI.DeleteKeyBackup(req.Context(), device.UserID, version)
|
||||||
if err := userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{
|
if err != nil {
|
||||||
UserID: device.UserID,
|
return util.ErrorResponse(fmt.Errorf("DeleteKeyBackup: %s", err))
|
||||||
Version: version,
|
|
||||||
DeleteBackup: true,
|
|
||||||
}, &performKeyBackupResp); err != nil {
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
}
|
||||||
if performKeyBackupResp.Error != "" {
|
if !exists {
|
||||||
if performKeyBackupResp.BadInput {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 400,
|
|
||||||
JSON: jsonerror.InvalidArgumentValue(performKeyBackupResp.Error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %s", performKeyBackupResp.Error))
|
|
||||||
}
|
|
||||||
if !performKeyBackupResp.Exists {
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 404,
|
Code: 404,
|
||||||
JSON: jsonerror.NotFound("backup version not found"),
|
JSON: spec.NotFound("backup version not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Unclear what the 200 body should be
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
JSON: keyBackupVersionCreateResponse{
|
JSON: struct{}{},
|
||||||
Version: performKeyBackupResp.Version,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload a bunch of session keys for a given `version`.
|
// Upload a bunch of session keys for a given `version`.
|
||||||
func UploadBackupKeys(
|
func UploadBackupKeys(
|
||||||
req *http.Request, userAPI userapi.UserInternalAPI, device *userapi.Device, version string, keys *keyBackupSessionRequest,
|
req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version string, keys *keyBackupSessionRequest,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
var performKeyBackupResp userapi.PerformKeyBackupResponse
|
performKeyBackupResp, err := userAPI.UpdateBackupKeyAuthData(req.Context(), &userapi.PerformKeyBackupRequest{
|
||||||
if err := userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{
|
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
Version: version,
|
Version: version,
|
||||||
Keys: *keys,
|
Keys: *keys,
|
||||||
}, &performKeyBackupResp); err != nil && performKeyBackupResp.Error == "" {
|
})
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
switch e := err.(type) {
|
||||||
if performKeyBackupResp.Error != "" {
|
case spec.ErrRoomKeysVersion:
|
||||||
if performKeyBackupResp.BadInput {
|
return util.JSONResponse{
|
||||||
return util.JSONResponse{
|
Code: http.StatusForbidden,
|
||||||
Code: 400,
|
JSON: e,
|
||||||
JSON: jsonerror.InvalidArgumentValue(performKeyBackupResp.Error),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %s", performKeyBackupResp.Error))
|
case nil:
|
||||||
|
default:
|
||||||
|
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %w", e))
|
||||||
}
|
}
|
||||||
if !performKeyBackupResp.Exists {
|
if !performKeyBackupResp.Exists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 404,
|
Code: 404,
|
||||||
JSON: jsonerror.NotFound("backup version not found"),
|
JSON: spec.NotFound("backup version not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -230,23 +208,22 @@ func UploadBackupKeys(
|
||||||
|
|
||||||
// Get keys from a given backup version. Response returned varies depending on if roomID and sessionID are set.
|
// Get keys from a given backup version. Response returned varies depending on if roomID and sessionID are set.
|
||||||
func GetBackupKeys(
|
func GetBackupKeys(
|
||||||
req *http.Request, userAPI userapi.UserInternalAPI, device *userapi.Device, version, roomID, sessionID string,
|
req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version, roomID, sessionID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
var queryResp userapi.QueryKeyBackupResponse
|
queryResp, err := userAPI.QueryKeyBackup(req.Context(), &userapi.QueryKeyBackupRequest{
|
||||||
userAPI.QueryKeyBackup(req.Context(), &userapi.QueryKeyBackupRequest{
|
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
Version: version,
|
Version: version,
|
||||||
ReturnKeys: true,
|
ReturnKeys: true,
|
||||||
KeysForRoomID: roomID,
|
KeysForRoomID: roomID,
|
||||||
KeysForSessionID: sessionID,
|
KeysForSessionID: sessionID,
|
||||||
}, &queryResp)
|
})
|
||||||
if queryResp.Error != "" {
|
if err != nil {
|
||||||
return util.ErrorResponse(fmt.Errorf("QueryKeyBackup: %s", queryResp.Error))
|
return util.ErrorResponse(fmt.Errorf("QueryKeyBackup: %w", err))
|
||||||
}
|
}
|
||||||
if !queryResp.Exists {
|
if !queryResp.Exists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 404,
|
Code: 404,
|
||||||
JSON: jsonerror.NotFound("version not found"),
|
JSON: spec.NotFound("version not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if sessionID != "" {
|
if sessionID != "" {
|
||||||
|
@ -263,17 +240,20 @@ func GetBackupKeys(
|
||||||
}
|
}
|
||||||
} else if roomID != "" {
|
} else if roomID != "" {
|
||||||
roomData, ok := queryResp.Keys[roomID]
|
roomData, ok := queryResp.Keys[roomID]
|
||||||
if ok {
|
if !ok {
|
||||||
// wrap response in "sessions"
|
// If no keys are found, then an object with an empty sessions property will be returned
|
||||||
return util.JSONResponse{
|
roomData = make(map[string]userapi.KeyBackupSession)
|
||||||
Code: 200,
|
|
||||||
JSON: struct {
|
|
||||||
Sessions map[string]userapi.KeyBackupSession `json:"sessions"`
|
|
||||||
}{
|
|
||||||
Sessions: roomData,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// wrap response in "sessions"
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: struct {
|
||||||
|
Sessions map[string]userapi.KeyBackupSession `json:"sessions"`
|
||||||
|
}{
|
||||||
|
Sessions: roomData,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// response is the same as the upload request
|
// response is the same as the upload request
|
||||||
var resp keyBackupSessionRequest
|
var resp keyBackupSessionRequest
|
||||||
|
@ -294,6 +274,6 @@ func GetBackupKeys(
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 404,
|
Code: 404,
|
||||||
JSON: jsonerror.NotFound("keys not found"),
|
JSON: spec.NotFound("keys not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,10 +20,9 @@ import (
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth"
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/keyserver/api"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,9 +32,9 @@ type crossSigningRequest struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func UploadCrossSigningDeviceKeys(
|
func UploadCrossSigningDeviceKeys(
|
||||||
req *http.Request, userInteractiveAuth *auth.UserInteractive,
|
req *http.Request,
|
||||||
keyserverAPI api.KeyInternalAPI, device *userapi.Device,
|
keyserverAPI api.ClientKeyAPI, device *api.Device,
|
||||||
accountAPI userapi.UserAccountAPI, cfg *config.ClientAPI,
|
accountAPI api.ClientUserAPI, cfg *config.ClientAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
uploadReq := &crossSigningRequest{}
|
uploadReq := &crossSigningRequest{}
|
||||||
uploadRes := &api.PerformUploadDeviceKeysResponse{}
|
uploadRes := &api.PerformUploadDeviceKeysResponse{}
|
||||||
|
@ -63,8 +62,8 @@ func UploadCrossSigningDeviceKeys(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
typePassword := auth.LoginTypePassword{
|
typePassword := auth.LoginTypePassword{
|
||||||
GetAccountByPassword: accountAPI.QueryAccountByPassword,
|
UserAPI: accountAPI,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
if _, authErr := typePassword.Login(req.Context(), &uploadReq.Auth.PasswordRequest); authErr != nil {
|
if _, authErr := typePassword.Login(req.Context(), &uploadReq.Auth.PasswordRequest); authErr != nil {
|
||||||
return *authErr
|
return *authErr
|
||||||
|
@ -79,22 +78,22 @@ func UploadCrossSigningDeviceKeys(
|
||||||
case err.IsInvalidSignature:
|
case err.IsInvalidSignature:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidSignature(err.Error()),
|
JSON: spec.InvalidSignature(err.Error()),
|
||||||
}
|
}
|
||||||
case err.IsMissingParam:
|
case err.IsMissingParam:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.MissingParam(err.Error()),
|
JSON: spec.MissingParam(err.Error()),
|
||||||
}
|
}
|
||||||
case err.IsInvalidParam:
|
case err.IsInvalidParam:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidParam(err.Error()),
|
JSON: spec.InvalidParam(err.Error()),
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.Unknown(err.Error()),
|
JSON: spec.Unknown(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,7 +104,7 @@ func UploadCrossSigningDeviceKeys(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func UploadCrossSigningDeviceSignatures(req *http.Request, keyserverAPI api.KeyInternalAPI, device *userapi.Device) util.JSONResponse {
|
func UploadCrossSigningDeviceSignatures(req *http.Request, keyserverAPI api.ClientKeyAPI, device *api.Device) util.JSONResponse {
|
||||||
uploadReq := &api.PerformUploadDeviceSignaturesRequest{}
|
uploadReq := &api.PerformUploadDeviceSignaturesRequest{}
|
||||||
uploadRes := &api.PerformUploadDeviceSignaturesResponse{}
|
uploadRes := &api.PerformUploadDeviceSignaturesResponse{}
|
||||||
|
|
||||||
|
@ -121,22 +120,22 @@ func UploadCrossSigningDeviceSignatures(req *http.Request, keyserverAPI api.KeyI
|
||||||
case err.IsInvalidSignature:
|
case err.IsInvalidSignature:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidSignature(err.Error()),
|
JSON: spec.InvalidSignature(err.Error()),
|
||||||
}
|
}
|
||||||
case err.IsMissingParam:
|
case err.IsMissingParam:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.MissingParam(err.Error()),
|
JSON: spec.MissingParam(err.Error()),
|
||||||
}
|
}
|
||||||
case err.IsInvalidParam:
|
case err.IsInvalidParam:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.InvalidParam(err.Error()),
|
JSON: spec.InvalidParam(err.Error()),
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.Unknown(err.Error()),
|
JSON: spec.Unknown(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,11 +19,11 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
||||||
"github.com/matrix-org/dendrite/keyserver/api"
|
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
type uploadKeysRequest struct {
|
type uploadKeysRequest struct {
|
||||||
|
@ -31,7 +31,7 @@ type uploadKeysRequest struct {
|
||||||
OneTimeKeys map[string]json.RawMessage `json:"one_time_keys"`
|
OneTimeKeys map[string]json.RawMessage `json:"one_time_keys"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func UploadKeys(req *http.Request, keyAPI api.KeyInternalAPI, device *userapi.Device) util.JSONResponse {
|
func UploadKeys(req *http.Request, keyAPI api.ClientKeyAPI, device *api.Device) util.JSONResponse {
|
||||||
var r uploadKeysRequest
|
var r uploadKeysRequest
|
||||||
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
|
@ -62,10 +62,15 @@ func UploadKeys(req *http.Request, keyAPI api.KeyInternalAPI, device *userapi.De
|
||||||
}
|
}
|
||||||
|
|
||||||
var uploadRes api.PerformUploadKeysResponse
|
var uploadRes api.PerformUploadKeysResponse
|
||||||
keyAPI.PerformUploadKeys(req.Context(), uploadReq, &uploadRes)
|
if err := keyAPI.PerformUploadKeys(req.Context(), uploadReq, &uploadRes); err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
if uploadRes.Error != nil {
|
if uploadRes.Error != nil {
|
||||||
util.GetLogger(req.Context()).WithError(uploadRes.Error).Error("Failed to PerformUploadKeys")
|
util.GetLogger(req.Context()).WithError(uploadRes.Error).Error("Failed to PerformUploadKeys")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(uploadRes.KeyErrors) > 0 {
|
if len(uploadRes.KeyErrors) > 0 {
|
||||||
util.GetLogger(req.Context()).WithField("key_errors", uploadRes.KeyErrors).Error("Failed to upload one or more keys")
|
util.GetLogger(req.Context()).WithField("key_errors", uploadRes.KeyErrors).Error("Failed to upload one or more keys")
|
||||||
|
@ -75,7 +80,6 @@ func UploadKeys(req *http.Request, keyAPI api.KeyInternalAPI, device *userapi.De
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keyCount := make(map[string]int)
|
keyCount := make(map[string]int)
|
||||||
// we only return key counts when the client uploads OTKs
|
|
||||||
if len(uploadRes.OneTimeKeyCounts) > 0 {
|
if len(uploadRes.OneTimeKeyCounts) > 0 {
|
||||||
keyCount = uploadRes.OneTimeKeyCounts[0].KeyCount
|
keyCount = uploadRes.OneTimeKeyCounts[0].KeyCount
|
||||||
}
|
}
|
||||||
|
@ -89,7 +93,6 @@ func UploadKeys(req *http.Request, keyAPI api.KeyInternalAPI, device *userapi.De
|
||||||
|
|
||||||
type queryKeysRequest struct {
|
type queryKeysRequest struct {
|
||||||
Timeout int `json:"timeout"`
|
Timeout int `json:"timeout"`
|
||||||
Token string `json:"token"`
|
|
||||||
DeviceKeys map[string][]string `json:"device_keys"`
|
DeviceKeys map[string][]string `json:"device_keys"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,10 +100,14 @@ func (r *queryKeysRequest) GetTimeout() time.Duration {
|
||||||
if r.Timeout == 0 {
|
if r.Timeout == 0 {
|
||||||
return 10 * time.Second
|
return 10 * time.Second
|
||||||
}
|
}
|
||||||
return time.Duration(r.Timeout) * time.Millisecond
|
timeout := time.Duration(r.Timeout) * time.Millisecond
|
||||||
|
if timeout > time.Second*20 {
|
||||||
|
timeout = time.Second * 20
|
||||||
|
}
|
||||||
|
return timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
func QueryKeys(req *http.Request, keyAPI api.KeyInternalAPI, device *userapi.Device) util.JSONResponse {
|
func QueryKeys(req *http.Request, keyAPI api.ClientKeyAPI, device *api.Device) util.JSONResponse {
|
||||||
var r queryKeysRequest
|
var r queryKeysRequest
|
||||||
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
|
@ -111,7 +118,6 @@ func QueryKeys(req *http.Request, keyAPI api.KeyInternalAPI, device *userapi.Dev
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
UserToDevices: r.DeviceKeys,
|
UserToDevices: r.DeviceKeys,
|
||||||
Timeout: r.GetTimeout(),
|
Timeout: r.GetTimeout(),
|
||||||
// TODO: Token?
|
|
||||||
}, &queryRes)
|
}, &queryRes)
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
|
@ -138,7 +144,7 @@ func (r *claimKeysRequest) GetTimeout() time.Duration {
|
||||||
return time.Duration(r.TimeoutMS) * time.Millisecond
|
return time.Duration(r.TimeoutMS) * time.Millisecond
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClaimKeys(req *http.Request, keyAPI api.KeyInternalAPI) util.JSONResponse {
|
func ClaimKeys(req *http.Request, keyAPI api.ClientKeyAPI) util.JSONResponse {
|
||||||
var r claimKeysRequest
|
var r claimKeysRequest
|
||||||
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
|
@ -151,7 +157,10 @@ func ClaimKeys(req *http.Request, keyAPI api.KeyInternalAPI) util.JSONResponse {
|
||||||
}, &claimRes)
|
}, &claimRes)
|
||||||
if claimRes.Error != nil {
|
if claimRes.Error != nil {
|
||||||
util.GetLogger(req.Context()).WithError(claimRes.Error).Error("failed to PerformClaimKeys")
|
util.GetLogger(req.Context()).WithError(claimRes.Error).Error("failed to PerformClaimKeys")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: spec.InternalServerError{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue