mirror of
https://github.com/element-hq/synapse.git
synced 2025-12-13 01:50:46 +00:00
Compare commits
797 Commits
erikj/init
...
v0.10.0-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25b32b63ae | ||
|
|
e330c802e4 | ||
|
|
c9cb354b58 | ||
|
|
5a9e0c3682 | ||
|
|
e85c7873dc | ||
|
|
efdaa5dd55 | ||
|
|
da51acf0e7 | ||
|
|
f4d552589e | ||
|
|
90fde4b8d7 | ||
|
|
0de2aad061 | ||
|
|
3f6f74686a | ||
|
|
82145912c3 | ||
|
|
59891a294f | ||
|
|
3e1029fe80 | ||
|
|
af7c1397d1 | ||
|
|
bfb66773a4 | ||
|
|
747535f20f | ||
|
|
133d90abfb | ||
|
|
1d1c303b9b | ||
|
|
d33f31d741 | ||
|
|
f63208a1c0 | ||
|
|
37403ab06c | ||
|
|
8b52fe48b5 | ||
|
|
d9088c923f | ||
|
|
86cef6a91b | ||
|
|
571ac105e6 | ||
|
|
51c53369a3 | ||
|
|
61f36d9939 | ||
|
|
f8f3d72e2b | ||
|
|
457970c724 | ||
|
|
1bd1a43073 | ||
|
|
0f6a25f670 | ||
|
|
b9490e8cbb | ||
|
|
5dbd102470 | ||
|
|
fd5ad0f00e | ||
|
|
42f12ad92f | ||
|
|
aa3c9c7bd0 | ||
|
|
1f7642efa9 | ||
|
|
3e9ee62db0 | ||
|
|
b578c822e3 | ||
|
|
3befc9ccc3 | ||
|
|
d5c31e01f2 | ||
|
|
cb8201ba12 | ||
|
|
c141d47a28 | ||
|
|
61cd03466f | ||
|
|
f764f92647 | ||
|
|
ca0d28ef34 | ||
|
|
8a951540f6 | ||
|
|
482648123f | ||
|
|
fd88ea19c0 | ||
|
|
bb9611bd46 | ||
|
|
9b63def388 | ||
|
|
23b21e5215 | ||
|
|
9d720223f2 | ||
|
|
099ce4bc38 | ||
|
|
22346a0ee7 | ||
|
|
cbd053bb8f | ||
|
|
be27d81808 | ||
|
|
4cf302de5b | ||
|
|
c50ad14bae | ||
|
|
a0b8e5f2fe | ||
|
|
aadb2238c9 | ||
|
|
09d23b6209 | ||
|
|
daa01842f8 | ||
|
|
d7272f8d9d | ||
|
|
78fa346b07 | ||
|
|
a45ec7c651 | ||
|
|
40da1f200d | ||
|
|
abc6986a24 | ||
|
|
e624cdec64 | ||
|
|
c3dd2ecd5e | ||
|
|
38a965b816 | ||
|
|
a82938416d | ||
|
|
0bfdaf1f4f | ||
|
|
a5cbd20001 | ||
|
|
128ed32e6b | ||
|
|
ee59af9ac0 | ||
|
|
f704c10f29 | ||
|
|
6e7d36a72c | ||
|
|
d3da63f766 | ||
|
|
8199475ce0 | ||
|
|
0d4abf7777 | ||
|
|
e55291ce5e | ||
|
|
8e254862f4 | ||
|
|
85d0bc3bdc | ||
|
|
cfc503681f | ||
|
|
dc2a105fca | ||
|
|
83eb627b5a | ||
|
|
776ee6d92b | ||
|
|
f72ed6c6a3 | ||
|
|
8899df13bf | ||
|
|
8f4165628b | ||
|
|
d3d582bc1c | ||
|
|
4d8e1e1f9e | ||
|
|
afef6f5d16 | ||
|
|
2d97e65558 | ||
|
|
1a9510bb84 | ||
|
|
47abebfd6d | ||
|
|
f9d4da7f45 | ||
|
|
30883d8409 | ||
|
|
891dfd90bd | ||
|
|
68b255c5a1 | ||
|
|
95b0f5449d | ||
|
|
129ee4e149 | ||
|
|
c5966b2a97 | ||
|
|
0cceb2ac92 | ||
|
|
d6bcc68ea7 | ||
|
|
b16cd18a86 | ||
|
|
9f7f228ec2 | ||
|
|
3d77e56c12 | ||
|
|
d884047d34 | ||
|
|
2bb2c02571 | ||
|
|
3d1cdda762 | ||
|
|
57877b01d7 | ||
|
|
5db5677969 | ||
|
|
7e77a82c5f | ||
|
|
7eb4d626ba | ||
|
|
06750140f6 | ||
|
|
adbd720fab | ||
|
|
8b7ce2945b | ||
|
|
a6c27de1aa | ||
|
|
c044aca1fd | ||
|
|
ba5d34a832 | ||
|
|
6a191d62ed | ||
|
|
21ac8be5f7 | ||
|
|
3e4e367f09 | ||
|
|
0fbed2a8fa | ||
|
|
998a72d4d9 | ||
|
|
c10ac7806e | ||
|
|
101ee3fd00 | ||
|
|
df361d08f7 | ||
|
|
7b0e797080 | ||
|
|
2eb91e6694 | ||
|
|
cfa62007a3 | ||
|
|
5ce903e2f7 | ||
|
|
a7eeb34c64 | ||
|
|
f7e2f981ea | ||
|
|
bcc1d34d35 | ||
|
|
f4122c64b5 | ||
|
|
415c2f0549 | ||
|
|
f43041aacd | ||
|
|
73605f8070 | ||
|
|
de3b7b55d6 | ||
|
|
d46208c12c | ||
|
|
4f11a5b2b5 | ||
|
|
7bbaab9432 | ||
|
|
7b49236b37 | ||
|
|
fdb724cb70 | ||
|
|
7e3d1c7d92 | ||
|
|
d7451e0f22 | ||
|
|
4807616e16 | ||
|
|
275f7c987c | ||
|
|
b24d7ebd6e | ||
|
|
2df8dd9b37 | ||
|
|
a23a760b3f | ||
|
|
7568fe880d | ||
|
|
4ff0228c25 | ||
|
|
dcd5983fe4 | ||
|
|
45610305ea | ||
|
|
88e03da39f | ||
|
|
9dba813234 | ||
|
|
53a817518b | ||
|
|
6eaa116867 | ||
|
|
4762c276cb | ||
|
|
dc8399ee00 | ||
|
|
1b994a97dd | ||
|
|
10b874067b | ||
|
|
017b798e4f | ||
|
|
2c019eea11 | ||
|
|
bb0a475c30 | ||
|
|
dcefac3b06 | ||
|
|
559c51debc | ||
|
|
6f274f7e13 | ||
|
|
7ce71f2ffc | ||
|
|
8c3a62b5c7 | ||
|
|
86eaaa885b | ||
|
|
e0b6e49466 | ||
|
|
2cd6cb9f65 | ||
|
|
aa88582e00 | ||
|
|
5119e416e8 | ||
|
|
8f04b6fa7a | ||
|
|
7dec0b2bee | ||
|
|
06218ab125 | ||
|
|
2352974aab | ||
|
|
9c5385b53a | ||
|
|
ffab798a38 | ||
|
|
62126c996c | ||
|
|
3213ff630c | ||
|
|
20addfa358 | ||
|
|
9eb5b23d3a | ||
|
|
0211890134 | ||
|
|
ffdb8c3828 | ||
|
|
e69b669083 | ||
|
|
0db40d3e93 | ||
|
|
e3c8e2c13c | ||
|
|
efe60d5e8c | ||
|
|
b2c7bd4b09 | ||
|
|
b3768ec10a | ||
|
|
b8e386db59 | ||
|
|
fe994e728f | ||
|
|
1d08bf7c17 | ||
|
|
63b1eaf32c | ||
|
|
b811c98574 | ||
|
|
433314cc34 | ||
|
|
8049c9a71e | ||
|
|
f596ff402e | ||
|
|
2efb93af52 | ||
|
|
953dbd28a7 | ||
|
|
7eea3e356f | ||
|
|
3e1b77efc2 | ||
|
|
b52b4a84ec | ||
|
|
1e62a3d3a9 | ||
|
|
a89559d797 | ||
|
|
07507643cb | ||
|
|
185ac7ee6c | ||
|
|
a0dea6eaed | ||
|
|
c67ba143fa | ||
|
|
e7768e77f5 | ||
|
|
883aabe423 | ||
|
|
07ad03d5df | ||
|
|
e124128542 | ||
|
|
c77048e12f | ||
|
|
2e35a733cc | ||
|
|
413a4c289b | ||
|
|
4d6cb8814e | ||
|
|
7148aaf5d0 | ||
|
|
28d07a02e4 | ||
|
|
2c963054f8 | ||
|
|
11b0a34074 | ||
|
|
c772dffc9f | ||
|
|
a4d62ba36a | ||
|
|
c472d6107e | ||
|
|
39e21ea51c | ||
|
|
2da3b1e60b | ||
|
|
62c010283d | ||
|
|
459085184c | ||
|
|
2b4f47db9c | ||
|
|
33d83f3615 | ||
|
|
ff7c2e41de | ||
|
|
6886bba988 | ||
|
|
103e1c2431 | ||
|
|
4e2e67fd50 | ||
|
|
a56eccbbfc | ||
|
|
20c0324e9c | ||
|
|
cf7a40b08a | ||
|
|
90dbd71c13 | ||
|
|
3b5823c74d | ||
|
|
bde97b988a | ||
|
|
53d1174aa9 | ||
|
|
ddef5ea126 | ||
|
|
b6ee0585bd | ||
|
|
4f973eb657 | ||
|
|
4cab2cfa34 | ||
|
|
b6d4a4c6d8 | ||
|
|
d155b318d2 | ||
|
|
a2ed7f437c | ||
|
|
c456d17daf | ||
|
|
09489499e7 | ||
|
|
4da05fa0ae | ||
|
|
8cedf3ce95 | ||
|
|
baa55fb69e | ||
|
|
002a44ac1a | ||
|
|
62b4b72fe4 | ||
|
|
b49a30a972 | ||
|
|
4624d6035e | ||
|
|
d5cc794598 | ||
|
|
5989637f37 | ||
|
|
016c089f13 | ||
|
|
e5991af629 | ||
|
|
17bb9a7eb9 | ||
|
|
a5ea22d468 | ||
|
|
7e3b14fe78 | ||
|
|
532fcc997a | ||
|
|
0b3389bcd2 | ||
|
|
0d7f0febf4 | ||
|
|
b7cb37b189 | ||
|
|
a01097d60b | ||
|
|
f3049d0b81 | ||
|
|
a887efa07a | ||
|
|
9158ad1abb | ||
|
|
b5f0d73ea3 | ||
|
|
ed88720952 | ||
|
|
f0979afdb0 | ||
|
|
bf0d59ed30 | ||
|
|
c2d08ca62a | ||
|
|
4019b48aaa | ||
|
|
294dbd712f | ||
|
|
1af188209a | ||
|
|
8cd34dfe95 | ||
|
|
d2caa5351a | ||
|
|
fb8d2862c1 | ||
|
|
8ad2d2d1cb | ||
|
|
f26a3df1bf | ||
|
|
19fa3731ae | ||
|
|
465acb0c6a | ||
|
|
64afbe6ccd | ||
|
|
04192ee05b | ||
|
|
8fb79eeea4 | ||
|
|
ce9e2f84ad | ||
|
|
304343f4d7 | ||
|
|
af812b68dd | ||
|
|
d85ce8d89b | ||
|
|
f53bae0c19 | ||
|
|
77c5db5977 | ||
|
|
81682d0f82 | ||
|
|
87311d1b8c | ||
|
|
f0dd6d4cbd | ||
|
|
ca041d5526 | ||
|
|
716e426933 | ||
|
|
e8b2f6f8a1 | ||
|
|
dfc74c30c9 | ||
|
|
28ef344077 | ||
|
|
2ef182ee93 | ||
|
|
b5770f8947 | ||
|
|
a7dcbfe430 | ||
|
|
1a3255b507 | ||
|
|
fb47c3cfbe | ||
|
|
65e69dec8b | ||
|
|
c3e2600c67 | ||
|
|
400894616d | ||
|
|
c0a975cc2e | ||
|
|
12b83f1a0d | ||
|
|
00ab882ed6 | ||
|
|
41938afed8 | ||
|
|
1a60545626 | ||
|
|
ac78e60de6 | ||
|
|
bd1236c0ee | ||
|
|
ddf7979531 | ||
|
|
0862fed2a8 | ||
|
|
80a61330ee | ||
|
|
67362a9a03 | ||
|
|
480d720388 | ||
|
|
901f56fa63 | ||
|
|
9beaedd164 | ||
|
|
2124f668db | ||
|
|
11374a77ef | ||
|
|
f0dd568e16 | ||
|
|
b5f55a1d85 | ||
|
|
5130d80d79 | ||
|
|
6825eef955 | ||
|
|
6924852592 | ||
|
|
f043b14bc0 | ||
|
|
9c72011fd7 | ||
|
|
2f556e0c55 | ||
|
|
7fa1363fb0 | ||
|
|
275dab6b55 | ||
|
|
a68abc79fd | ||
|
|
653533a3da | ||
|
|
9bf61ef97b | ||
|
|
0e58d19163 | ||
|
|
18968efa0a | ||
|
|
eb928c9f52 | ||
|
|
9d112f4440 | ||
|
|
ad460a8315 | ||
|
|
bf628cf6dd | ||
|
|
9e5a353663 | ||
|
|
6f6ebd216d | ||
|
|
73513ececc | ||
|
|
1f24c2e589 | ||
|
|
22049ea700 | ||
|
|
050ebccf30 | ||
|
|
d88e20cdb9 | ||
|
|
eceb554a2f | ||
|
|
b849a64f8d | ||
|
|
0460406298 | ||
|
|
9a3cd1c00d | ||
|
|
fb7def3344 | ||
|
|
f13890ddce | ||
|
|
aaa749d366 | ||
|
|
bc42ca121f | ||
|
|
cee69441d3 | ||
|
|
b5209c5744 | ||
|
|
66da8f60d0 | ||
|
|
f0583f65e1 | ||
|
|
44c9102e7a | ||
|
|
6a7cf6b41f | ||
|
|
2acee97c2b | ||
|
|
7f7ec84d6f | ||
|
|
cebde85b94 | ||
|
|
f00f8346f1 | ||
|
|
83f119a84a | ||
|
|
9d0326baa6 | ||
|
|
186f61a3ac | ||
|
|
fe9bac3749 | ||
|
|
6c01ceb8d0 | ||
|
|
2eda996a63 | ||
|
|
4706f3964d | ||
|
|
4df76b0a5d | ||
|
|
a005b7269a | ||
|
|
261ccd7f5f | ||
|
|
942e39e87c | ||
|
|
9c5fc81c2d | ||
|
|
fd2c07bfed | ||
|
|
405f8c4796 | ||
|
|
c42ed47660 | ||
|
|
1a87f5f26c | ||
|
|
a3dc31cab9 | ||
|
|
4dd47236e7 | ||
|
|
295b400d57 | ||
|
|
716cf144ec | ||
|
|
1e365e88bd | ||
|
|
2d41dc0069 | ||
|
|
f7f07dc517 | ||
|
|
b8690dd840 | ||
|
|
378a0f7a79 | ||
|
|
da84946de4 | ||
|
|
63a7b3ad1e | ||
|
|
5730b20c6d | ||
|
|
8047fd2434 | ||
|
|
3bbd0d0e09 | ||
|
|
9dda396baa | ||
|
|
13ed3b9985 | ||
|
|
bd2cf9d4bf | ||
|
|
d4902a7ad0 | ||
|
|
55bf90b9e4 | ||
|
|
53f0bf85d7 | ||
|
|
1c3d844e73 | ||
|
|
0d7d9c37b6 | ||
|
|
d8866d7277 | ||
|
|
2ef2f6d593 | ||
|
|
3483b78d1a | ||
|
|
d3ded420b1 | ||
|
|
22716774d5 | ||
|
|
5044e6c544 | ||
|
|
09e23334de | ||
|
|
02410e9239 | ||
|
|
e552b78d50 | ||
|
|
fde0da6f19 | ||
|
|
3f04a08a0c | ||
|
|
4bbfbf898e | ||
|
|
6e17463228 | ||
|
|
522f285f9b | ||
|
|
b8d49be5a1 | ||
|
|
90abdaf3bc | ||
|
|
b579a8ea18 | ||
|
|
53ef3a0bfe | ||
|
|
d70c847b4f | ||
|
|
d15f166093 | ||
|
|
ca580ef862 | ||
|
|
45bac68064 | ||
|
|
784aaa53df | ||
|
|
8355b4d074 | ||
|
|
a7b65bdedf | ||
|
|
d94590ed48 | ||
|
|
afbd3b2fc4 | ||
|
|
79e37a7ecb | ||
|
|
0f118e55db | ||
|
|
2f54522d44 | ||
|
|
dd74436ffd | ||
|
|
11f51e6ded | ||
|
|
086df80790 | ||
|
|
291e942332 | ||
|
|
31ade3b3e9 | ||
|
|
36b3b75b21 | ||
|
|
6d1dea337b | ||
|
|
99eb1172b0 | ||
|
|
6cb3212fc2 | ||
|
|
554c63ca60 | ||
|
|
fff7905409 | ||
|
|
00dd207f60 | ||
|
|
e417469af2 | ||
|
|
cb7dac3a5d | ||
|
|
2651fd5e24 | ||
|
|
764856777c | ||
|
|
e7b25a649c | ||
|
|
804b732aab | ||
|
|
45fffe8cbe | ||
|
|
9ba3c1ede4 | ||
|
|
a0bebeda8b | ||
|
|
27e093cbc1 | ||
|
|
d9f60e8dc8 | ||
|
|
0e42dfbe22 | ||
|
|
5ebd33302f | ||
|
|
17167898c8 | ||
|
|
6eadbfbea0 | ||
|
|
1a9a9abcc7 | ||
|
|
74b7de83ec | ||
|
|
36317f3dad | ||
|
|
052ac0c8d0 | ||
|
|
49a2c10279 | ||
|
|
5d53c14342 | ||
|
|
4752a990c8 | ||
|
|
106a3051b8 | ||
|
|
284f55a7fb | ||
|
|
1ce1509989 | ||
|
|
8bb85c8c5a | ||
|
|
c8135f808b | ||
|
|
b21d015c55 | ||
|
|
e70e8e053e | ||
|
|
1b446a5d85 | ||
|
|
59a0682f3e | ||
|
|
b6adfc59f5 | ||
|
|
254aa3c986 | ||
|
|
f43544eecc | ||
|
|
a04cde613e | ||
|
|
4429e720ae | ||
|
|
ee49098843 | ||
|
|
51f5d36f4f | ||
|
|
f8c2cd129d | ||
|
|
f6d1183fc5 | ||
|
|
2043527b9b | ||
|
|
53447e9cd3 | ||
|
|
d61ce3f670 | ||
|
|
a910984b58 | ||
|
|
e309b1045d | ||
|
|
0180bfe4aa | ||
|
|
1f3d1d85a9 | ||
|
|
39a3340f73 | ||
|
|
ae3bff3491 | ||
|
|
dc085ddf8c | ||
|
|
73d23c6ae8 | ||
|
|
6189d8e54d | ||
|
|
115ef3ddac | ||
|
|
4fb858d90a | ||
|
|
88f1ea36ce | ||
|
|
c2633907c5 | ||
|
|
ebfdd2eb5b | ||
|
|
a551c5dad7 | ||
|
|
27e4b45c06 | ||
|
|
ac5f2bf9db | ||
|
|
80a167b1f0 | ||
|
|
7ae8afb7ef | ||
|
|
9118a92862 | ||
|
|
8eca5bd50a | ||
|
|
e01b825cc9 | ||
|
|
ab45e12d31 | ||
|
|
f407cbd2f1 | ||
|
|
227f8ef031 | ||
|
|
2bc60c55af | ||
|
|
20814fabdd | ||
|
|
9084cdd70f | ||
|
|
5b731178b2 | ||
|
|
3a653515ec | ||
|
|
aa729349dd | ||
|
|
5b1631a4a9 | ||
|
|
291cba284b | ||
|
|
253f76a0a5 | ||
|
|
6837c5edab | ||
|
|
7223129916 | ||
|
|
5ae4a84211 | ||
|
|
118a760719 | ||
|
|
19505e0392 | ||
|
|
df431b127b | ||
|
|
882ac83d8d | ||
|
|
d3e09f12d0 | ||
|
|
677be13ffc | ||
|
|
350b88656a | ||
|
|
9de94d5a4d | ||
|
|
2b7120e233 | ||
|
|
8b256a7296 | ||
|
|
62ccc6d95f | ||
|
|
01858bcbf2 | ||
|
|
2aeee2a905 | ||
|
|
5e7883ec19 | ||
|
|
c6a03c46e6 | ||
|
|
e4c65b338d | ||
|
|
99914ec9f8 | ||
|
|
ef910a0358 | ||
|
|
591c4bf223 | ||
|
|
e1150cac4b | ||
|
|
165eb2dbe6 | ||
|
|
880fb46de0 | ||
|
|
9396723995 | ||
|
|
65878a2319 | ||
|
|
ad31fa3040 | ||
|
|
4d1b6f4ad1 | ||
|
|
0b0033c40b | ||
|
|
755def8083 | ||
|
|
1e90715a3d | ||
|
|
131bdf9bb1 | ||
|
|
10f1bdb9a2 | ||
|
|
d5cea26d45 | ||
|
|
c71176858b | ||
|
|
f8bd4de87d | ||
|
|
c3b37abdfd | ||
|
|
6c74fd62a0 | ||
|
|
9ff7f66a2b | ||
|
|
70f272f71c | ||
|
|
8763dd80ef | ||
|
|
807229f2f2 | ||
|
|
acb12cc811 | ||
|
|
d62dee7eae | ||
|
|
0f29cfabc3 | ||
|
|
e275a9c0d9 | ||
|
|
aa32bd38e4 | ||
|
|
372d4c6d7b | ||
|
|
575ec91d82 | ||
|
|
10be983f2c | ||
|
|
415b158ce2 | ||
|
|
de01438a57 | ||
|
|
a2c4f3f150 | ||
|
|
0a4330cd5d | ||
|
|
47ec693e29 | ||
|
|
1d566edb81 | ||
|
|
6e1ad283cf | ||
|
|
ef3d8754f5 | ||
|
|
142934084a | ||
|
|
96c5b9f87c | ||
|
|
7cd6a6f6cf | ||
|
|
c5d1b4986b | ||
|
|
7f4105a5c9 | ||
|
|
f4d58deba1 | ||
|
|
386b7330d2 | ||
|
|
0ad1c67234 | ||
|
|
7d6a1dae31 | ||
|
|
656223fbd3 | ||
|
|
67800f7626 | ||
|
|
2f7f8e1c2b | ||
|
|
4770cec7bc | ||
|
|
e1e9f0c5b2 | ||
|
|
ab78a8926e | ||
|
|
f6f902d459 | ||
|
|
cdb3757942 | ||
|
|
92e1c8983d | ||
|
|
0c894e1ebd | ||
|
|
084c365c3a | ||
|
|
c37a6e151f | ||
|
|
36ea26c5c0 | ||
|
|
7c549dd557 | ||
|
|
899d4675dd | ||
|
|
243c56e725 | ||
|
|
3edd2d5c93 | ||
|
|
47fb089eb5 | ||
|
|
4f1d984e56 | ||
|
|
5e0c533672 | ||
|
|
968b01a91a | ||
|
|
4071f29653 | ||
|
|
f1b83d88a3 | ||
|
|
a988361aea | ||
|
|
8888982db3 | ||
|
|
9af432257d | ||
|
|
cf706cc6ef | ||
|
|
5971d240d4 | ||
|
|
ca4f458787 | ||
|
|
df6db5c802 | ||
|
|
6edff11a88 | ||
|
|
63878c0379 | ||
|
|
02590c3e1d | ||
|
|
619a21812b | ||
|
|
fec4485e28 | ||
|
|
409bcc76bd | ||
|
|
cffe6057fb | ||
|
|
80fd2b574c | ||
|
|
e122685978 | ||
|
|
54ef09f860 | ||
|
|
d7b3ac46f8 | ||
|
|
4429e4bf24 | ||
|
|
ec07dba29e | ||
|
|
c167cbc9fd | ||
|
|
a6fb2aa2a5 | ||
|
|
1fce36b111 | ||
|
|
8b28209c60 | ||
|
|
30c72d377e | ||
|
|
e4eddf9b36 | ||
|
|
c1779a79bc | ||
|
|
74850d7f75 | ||
|
|
07a1223156 | ||
|
|
0d31ad5101 | ||
|
|
a0dfffb33c | ||
|
|
6e5ac4a28f | ||
|
|
8022b27fc2 | ||
|
|
95dedb866f | ||
|
|
78672a9fd5 | ||
|
|
da6a7bbdde | ||
|
|
2551b6645d | ||
|
|
5e4ba463b7 | ||
|
|
51da995806 | ||
|
|
5002056b16 | ||
|
|
5c75adff95 | ||
|
|
367382b575 | ||
|
|
4df11b5039 | ||
|
|
84e6b4001f | ||
|
|
17653a5dfe | ||
|
|
e269c511f6 | ||
|
|
5e3b254dc8 | ||
|
|
d244fa9741 | ||
|
|
e89ca34e0e | ||
|
|
79b7154454 | ||
|
|
4ef556f650 | ||
|
|
b036596b75 | ||
|
|
cd525c0f5a | ||
|
|
3c224f4d0e | ||
|
|
d38862a080 | ||
|
|
2640d6718d | ||
|
|
de87541862 | ||
|
|
22d2f498fa | ||
|
|
d79ffa1898 | ||
|
|
2236ef6c92 | ||
|
|
da1aa07db5 | ||
|
|
4ac1941592 | ||
|
|
476899295f | ||
|
|
fca28d243e | ||
|
|
37feb4031f | ||
|
|
0cd1401f8d | ||
|
|
724bb1e7d9 | ||
|
|
1c7912751e | ||
|
|
9d36eb4eab | ||
|
|
b0f71db3ff | ||
|
|
84e1cacea4 | ||
|
|
6538d445e8 | ||
|
|
52f98f8a5b | ||
|
|
22a7ba8b22 | ||
|
|
3a42f32134 | ||
|
|
4fa0f53521 | ||
|
|
326121aec4 | ||
|
|
9a9386226a | ||
|
|
126d562576 | ||
|
|
f08c33e834 | ||
|
|
45543028bb | ||
|
|
f683b5de47 | ||
|
|
db0dca2f6f | ||
|
|
89c0cd4acc | ||
|
|
6101ce427a | ||
|
|
5fe26a9b5c | ||
|
|
35698484a5 | ||
|
|
63562f6d5a | ||
|
|
a151693a3b | ||
|
|
ac29318b84 | ||
|
|
dfa98f911b | ||
|
|
4605953b0f | ||
|
|
ef8e8ebd91 | ||
|
|
3188e94ac4 | ||
|
|
97a64f3ebe | ||
|
|
b850c9fa04 | ||
|
|
4a7a4a5b6c | ||
|
|
771fc05d30 | ||
|
|
938939fd89 | ||
|
|
028a570e17 | ||
|
|
0e4393652f | ||
|
|
b994fb2b96 | ||
|
|
f10fd8a470 | ||
|
|
3c11c9c122 | ||
|
|
673375fe2d | ||
|
|
3c92231094 | ||
|
|
119e5d7702 | ||
|
|
271ee604f8 | ||
|
|
04c01882fc | ||
|
|
f4664a6cbd | ||
|
|
ecb26beda5 | ||
|
|
0c4ac271ca | ||
|
|
0cf7e480b4 | ||
|
|
ed2584050f | ||
|
|
977338a7af | ||
|
|
31049c4d72 | ||
|
|
deb0237166 | ||
|
|
e45b05647e | ||
|
|
3d5a955e08 | ||
|
|
d18f37e026 | ||
|
|
9951542393 | ||
|
|
041b6cba61 | ||
|
|
63075118a5 | ||
|
|
531d7955fd | ||
|
|
bfa4a7f8b0 | ||
|
|
d0fece8d3c | ||
|
|
bdcd7693c8 | ||
|
|
43c2e8deae | ||
|
|
1692dc019d | ||
|
|
a9aea68fd5 | ||
|
|
261d809a47 | ||
|
|
d9cc5de9e5 | ||
|
|
b8940cd902 | ||
|
|
1942382246 | ||
|
|
eb9bd2d949 | ||
|
|
2d386d7038 | ||
|
|
4ac2823b3c | ||
|
|
22c7c5eb8f | ||
|
|
42c12c04f6 | ||
|
|
adb5b76ff5 | ||
|
|
3bcdf3664c | ||
|
|
9eeb03c0dd | ||
|
|
32937f3ea0 | ||
|
|
7b50769eb9 | ||
|
|
7693f24792 | ||
|
|
46a65c282f | ||
|
|
92b20713d7 | ||
|
|
da4ed08739 | ||
|
|
9060dc6b59 | ||
|
|
1fae1b3166 | ||
|
|
80b4119279 | ||
|
|
4011cf1c42 | ||
|
|
50c87b8eed | ||
|
|
345995fcde | ||
|
|
62cebee8ee | ||
|
|
95cbfee8ae | ||
|
|
4ad8350607 | ||
|
|
6ea9cf58be | ||
|
|
c95480963e | ||
|
|
069296dbb0 | ||
|
|
2d4d2bbae4 | ||
|
|
2f1348f339 | ||
|
|
74aaacf82a | ||
|
|
c28f1d16f0 | ||
|
|
265f30bd3f | ||
|
|
c9e62927f2 | ||
|
|
1aa11cf7ce | ||
|
|
6b69ddd17a | ||
|
|
d624e2a638 |
10
AUTHORS.rst
10
AUTHORS.rst
@@ -35,3 +35,13 @@ Turned to Dust <dwinslow86 at gmail.com>
|
|||||||
|
|
||||||
Brabo <brabo at riseup.net>
|
Brabo <brabo at riseup.net>
|
||||||
* Installation instruction fixes
|
* Installation instruction fixes
|
||||||
|
|
||||||
|
Ivan Shapovalov <intelfx100 at gmail.com>
|
||||||
|
* contrib/systemd: a sample systemd unit file and a logger configuration
|
||||||
|
|
||||||
|
Eric Myhre <hash at exultant.us>
|
||||||
|
* Fix bug where ``media_store_path`` config option was ignored by v0 content
|
||||||
|
repository API.
|
||||||
|
|
||||||
|
Muthu Subramanian <muthu.subramanian.karunanidhi at ericsson.com>
|
||||||
|
* Add SAML2 support for registration and logins.
|
||||||
|
|||||||
248
CHANGES.rst
248
CHANGES.rst
@@ -1,9 +1,249 @@
|
|||||||
Changes in synapse vX
|
Changes in synapse v0.10.0-rc4 (2015-08-27)
|
||||||
=====================
|
===========================================
|
||||||
|
|
||||||
* Changed config option from ``disable_registration`` to
|
* Allow UTF-8 filenames for upload. (PR #259)
|
||||||
``enable_registration``. Old option will be ignored.
|
|
||||||
|
|
||||||
|
Changes in synapse v0.10.0-rc3 (2015-08-25)
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
* Add ``--keys-directory`` config option to specify where files such as
|
||||||
|
certs and signing keys should be stored in, when using ``--generate-config``
|
||||||
|
or ``--generate-keys``. (PR #250)
|
||||||
|
* Allow ``--config-path`` to specify a directory, causing synapse to use all
|
||||||
|
\*.yaml files in the directory as config files. (PR #249)
|
||||||
|
* Add ``web_client_location`` config option to specify static files to be
|
||||||
|
hosted by synapse under ``/_matrix/client``. (PR #245)
|
||||||
|
* Add helper utility to synapse to read and parse the config files and extract
|
||||||
|
the value of a given key. For example::
|
||||||
|
|
||||||
|
$ python -m synapse.config read server_name -c homeserver.yaml
|
||||||
|
localhost
|
||||||
|
|
||||||
|
(PR #246)
|
||||||
|
|
||||||
|
|
||||||
|
Changes in synapse v0.10.0-rc2 (2015-08-24)
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
* Fix bug where we incorrectly populated the ``event_forward_extremities``
|
||||||
|
table, resulting in problems joining large remote rooms (e.g.
|
||||||
|
``#matrix:matrix.org``)
|
||||||
|
* Reduce the number of times we wake up pushers by not listening for presence
|
||||||
|
or typing events, reducing the CPU cost of each pusher.
|
||||||
|
|
||||||
|
|
||||||
|
Changes in synapse v0.10.0-rc1 (2015-08-21)
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
Also see v0.9.4-rc1 changelog, which has been amalgamated into this release.
|
||||||
|
|
||||||
|
General:
|
||||||
|
|
||||||
|
* Upgrade to Twisted 15 (PR #173)
|
||||||
|
* Add support for serving and fetching encryption keys over federation.
|
||||||
|
(PR #208)
|
||||||
|
* Add support for logging in with email address (PR #234)
|
||||||
|
* Add support for new ``m.room.canonical_alias`` event. (PR #233)
|
||||||
|
* Change synapse to treat user IDs case insensitively during registration and
|
||||||
|
login. (If two users already exist with case insensitive matching user ids,
|
||||||
|
synapse will continue to require them to specify their user ids exactly.)
|
||||||
|
* Error if a user tries to register with an email already in use. (PR #211)
|
||||||
|
* Add extra and improve existing caches (PR #212, #219, #226, #228)
|
||||||
|
* Batch various storage request (PR #226, #228)
|
||||||
|
* Fix bug where we didn't correctly log the entity that triggered the request
|
||||||
|
if the request came in via an application service (PR #230)
|
||||||
|
* Fix bug where we needlessly regenerated the full list of rooms an AS is
|
||||||
|
interested in. (PR #232)
|
||||||
|
* Add support for AS's to use v2_alpha registration API (PR #210)
|
||||||
|
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
|
||||||
|
* Add ``--generate-keys`` that will generate any missing cert and key files in
|
||||||
|
the configuration files. This is equivalent to running ``--generate-config``
|
||||||
|
on an existing configuration file. (PR #220)
|
||||||
|
* ``--generate-config`` now no longer requires a ``--server-name`` parameter
|
||||||
|
when used on existing configuration files. (PR #220)
|
||||||
|
* Add ``--print-pidfile`` flag that controls the printing of the pid to stdout
|
||||||
|
of the demonised process. (PR #213)
|
||||||
|
|
||||||
|
Media Repository:
|
||||||
|
|
||||||
|
* Fix bug where we picked a lower resolution image than requested. (PR #205)
|
||||||
|
* Add support for specifying if a the media repository should dynamically
|
||||||
|
thumbnail images or not. (PR #206)
|
||||||
|
|
||||||
|
Metrics:
|
||||||
|
|
||||||
|
* Add statistics from the reactor to the metrics API. (PR #224, #225)
|
||||||
|
|
||||||
|
Demo Homeservers:
|
||||||
|
|
||||||
|
* Fix starting the demo homeservers without rate-limiting enabled. (PR #182)
|
||||||
|
* Fix enabling registration on demo homeservers (PR #223)
|
||||||
|
|
||||||
|
|
||||||
|
Changes in synapse v0.9.4-rc1 (2015-07-21)
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
General:
|
||||||
|
|
||||||
|
* Add basic implementation of receipts. (SPEC-99)
|
||||||
|
* Add support for configuration presets in room creation API. (PR #203)
|
||||||
|
* Add auth event that limits the visibility of history for new users.
|
||||||
|
(SPEC-134)
|
||||||
|
* Add SAML2 login/registration support. (PR #201. Thanks Muthu Subramanian!)
|
||||||
|
* Add client side key management APIs for end to end encryption. (PR #198)
|
||||||
|
* Change power level semantics so that you cannot kick, ban or change power
|
||||||
|
levels of users that have equal or greater power level than you. (SYN-192)
|
||||||
|
* Improve performance by bulk inserting events where possible. (PR #193)
|
||||||
|
* Improve performance by bulk verifying signatures where possible. (PR #194)
|
||||||
|
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
|
||||||
|
* Add support for including TLS certificate chains.
|
||||||
|
|
||||||
|
Media Repository:
|
||||||
|
|
||||||
|
* Add Content-Disposition headers to content repository responses. (SYN-150)
|
||||||
|
|
||||||
|
|
||||||
|
Changes in synapse v0.9.3 (2015-07-01)
|
||||||
|
======================================
|
||||||
|
|
||||||
|
No changes from v0.9.3 Release Candidate 1.
|
||||||
|
|
||||||
|
Changes in synapse v0.9.3-rc1 (2015-06-23)
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
General:
|
||||||
|
|
||||||
|
* Fix a memory leak in the notifier. (SYN-412)
|
||||||
|
* Improve performance of room initial sync. (SYN-418)
|
||||||
|
* General improvements to logging.
|
||||||
|
* Remove ``access_token`` query params from ``INFO`` level logging.
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
|
||||||
|
* Add support for specifying and configuring multiple listeners. (SYN-389)
|
||||||
|
|
||||||
|
Application services:
|
||||||
|
|
||||||
|
* Fix bug where synapse failed to send user queries to application services.
|
||||||
|
|
||||||
|
Changes in synapse v0.9.2-r2 (2015-06-15)
|
||||||
|
=========================================
|
||||||
|
|
||||||
|
Fix packaging so that schema delta python files get included in the package.
|
||||||
|
|
||||||
|
Changes in synapse v0.9.2 (2015-06-12)
|
||||||
|
======================================
|
||||||
|
|
||||||
|
General:
|
||||||
|
|
||||||
|
* Use ultrajson for json (de)serialisation when a canonical encoding is not
|
||||||
|
required. Ultrajson is significantly faster than simplejson in certain
|
||||||
|
circumstances.
|
||||||
|
* Use connection pools for outgoing HTTP connections.
|
||||||
|
* Process thumbnails on separate threads.
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
|
||||||
|
* Add option, ``gzip_responses``, to disable HTTP response compression.
|
||||||
|
|
||||||
|
Federation:
|
||||||
|
|
||||||
|
* Improve resilience of backfill by ensuring we fetch any missing auth events.
|
||||||
|
* Improve performance of backfill and joining remote rooms by removing
|
||||||
|
unnecessary computations. This included handling events we'd previously
|
||||||
|
handled as well as attempting to compute the current state for outliers.
|
||||||
|
|
||||||
|
|
||||||
|
Changes in synapse v0.9.1 (2015-05-26)
|
||||||
|
======================================
|
||||||
|
|
||||||
|
General:
|
||||||
|
|
||||||
|
* Add support for backfilling when a client paginates. This allows servers to
|
||||||
|
request history for a room from remote servers when a client tries to
|
||||||
|
paginate history the server does not have - SYN-36
|
||||||
|
* Fix bug where you couldn't disable non-default pushrules - SYN-378
|
||||||
|
* Fix ``register_new_user`` script - SYN-359
|
||||||
|
* Improve performance of fetching events from the database, this improves both
|
||||||
|
initialSync and sending of events.
|
||||||
|
* Improve performance of event streams, allowing synapse to handle more
|
||||||
|
simultaneous connected clients.
|
||||||
|
|
||||||
|
Federation:
|
||||||
|
|
||||||
|
* Fix bug with existing backfill implementation where it returned the wrong
|
||||||
|
selection of events in some circumstances.
|
||||||
|
* Improve performance of joining remote rooms.
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
|
||||||
|
* Add support for changing the bind host of the metrics listener via the
|
||||||
|
``metrics_bind_host`` option.
|
||||||
|
|
||||||
|
|
||||||
|
Changes in synapse v0.9.0-r5 (2015-05-21)
|
||||||
|
=========================================
|
||||||
|
|
||||||
|
* Add more database caches to reduce amount of work done for each pusher. This
|
||||||
|
radically reduces CPU usage when multiple pushers are set up in the same room.
|
||||||
|
|
||||||
|
Changes in synapse v0.9.0 (2015-05-07)
|
||||||
|
======================================
|
||||||
|
|
||||||
|
General:
|
||||||
|
|
||||||
|
* Add support for using a PostgreSQL database instead of SQLite. See
|
||||||
|
`docs/postgres.rst`_ for details.
|
||||||
|
* Add password change and reset APIs. See `Registration`_ in the spec.
|
||||||
|
* Fix memory leak due to not releasing stale notifiers - SYN-339.
|
||||||
|
* Fix race in caches that occasionally caused some presence updates to be
|
||||||
|
dropped - SYN-369.
|
||||||
|
* Check server name has not changed on restart.
|
||||||
|
* Add a sample systemd unit file and a logger configuration in
|
||||||
|
contrib/systemd. Contributed Ivan Shapovalov.
|
||||||
|
|
||||||
|
Federation:
|
||||||
|
|
||||||
|
* Add key distribution mechanisms for fetching public keys of unavailable
|
||||||
|
remote home servers. See `Retrieving Server Keys`_ in the spec.
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
|
||||||
|
* Add support for multiple config files.
|
||||||
|
* Add support for dictionaries in config files.
|
||||||
|
* Remove support for specifying config options on the command line, except
|
||||||
|
for:
|
||||||
|
|
||||||
|
* ``--daemonize`` - Daemonize the home server.
|
||||||
|
* ``--manhole`` - Turn on the twisted telnet manhole service on the given
|
||||||
|
port.
|
||||||
|
* ``--database-path`` - The path to a sqlite database to use.
|
||||||
|
* ``--verbose`` - The verbosity level.
|
||||||
|
* ``--log-file`` - File to log to.
|
||||||
|
* ``--log-config`` - Python logging config file.
|
||||||
|
* ``--enable-registration`` - Enable registration for new users.
|
||||||
|
|
||||||
|
Application services:
|
||||||
|
|
||||||
|
* Reliably retry sending of events from Synapse to application services, as per
|
||||||
|
`Application Services`_ spec.
|
||||||
|
* Application services can no longer register via the ``/register`` API,
|
||||||
|
instead their configuration should be saved to a file and listed in the
|
||||||
|
synapse ``app_service_config_files`` config option. The AS configuration file
|
||||||
|
has the same format as the old ``/register`` request.
|
||||||
|
See `docs/application_services.rst`_ for more information.
|
||||||
|
|
||||||
|
.. _`docs/postgres.rst`: docs/postgres.rst
|
||||||
|
.. _`docs/application_services.rst`: docs/application_services.rst
|
||||||
|
.. _`Registration`: https://github.com/matrix-org/matrix-doc/blob/master/specification/10_client_server_api.rst#registration
|
||||||
|
.. _`Retrieving Server Keys`: https://github.com/matrix-org/matrix-doc/blob/6f2698/specification/30_server_server_api.rst#retrieving-server-keys
|
||||||
|
.. _`Application Services`: https://github.com/matrix-org/matrix-doc/blob/0c6bd9/specification/25_application_service_api.rst#home-server---application-service-api
|
||||||
|
|
||||||
Changes in synapse v0.8.1 (2015-03-18)
|
Changes in synapse v0.8.1 (2015-03-18)
|
||||||
======================================
|
======================================
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ include *.rst
|
|||||||
include demo/README
|
include demo/README
|
||||||
|
|
||||||
recursive-include synapse/storage/schema *.sql
|
recursive-include synapse/storage/schema *.sql
|
||||||
|
recursive-include synapse/storage/schema *.py
|
||||||
|
|
||||||
recursive-include demo *.dh
|
recursive-include demo *.dh
|
||||||
recursive-include demo *.py
|
recursive-include demo *.py
|
||||||
|
|||||||
183
README.rst
183
README.rst
@@ -7,7 +7,7 @@ Matrix is an ambitious new ecosystem for open federated Instant Messaging and
|
|||||||
VoIP. The basics you need to know to get up and running are:
|
VoIP. The basics you need to know to get up and running are:
|
||||||
|
|
||||||
- Everything in Matrix happens in a room. Rooms are distributed and do not
|
- Everything in Matrix happens in a room. Rooms are distributed and do not
|
||||||
exist on any single server. Rooms can be located using convenience aliases
|
exist on any single server. Rooms can be located using convenience aliases
|
||||||
like ``#matrix:matrix.org`` or ``#test:localhost:8448``.
|
like ``#matrix:matrix.org`` or ``#test:localhost:8448``.
|
||||||
|
|
||||||
- Matrix user IDs look like ``@matthew:matrix.org`` (although in the future
|
- Matrix user IDs look like ``@matthew:matrix.org`` (although in the future
|
||||||
@@ -23,7 +23,7 @@ The overall architecture is::
|
|||||||
accessed by the web client at http://matrix.org/beta or via an IRC bridge at
|
accessed by the web client at http://matrix.org/beta or via an IRC bridge at
|
||||||
irc://irc.freenode.net/matrix.
|
irc://irc.freenode.net/matrix.
|
||||||
|
|
||||||
Synapse is currently in rapid development, but as of version 0.5 we believe it
|
Synapse is currently in rapid development, but as of version 0.5 we believe it
|
||||||
is sufficiently stable to be run as an internet-facing service for real usage!
|
is sufficiently stable to be run as an internet-facing service for real usage!
|
||||||
|
|
||||||
About Matrix
|
About Matrix
|
||||||
@@ -101,36 +101,40 @@ header files for python C extensions.
|
|||||||
|
|
||||||
Installing prerequisites on Ubuntu or Debian::
|
Installing prerequisites on Ubuntu or Debian::
|
||||||
|
|
||||||
$ sudo apt-get install build-essential python2.7-dev libffi-dev \
|
sudo apt-get install build-essential python2.7-dev libffi-dev \
|
||||||
python-pip python-setuptools sqlite3 \
|
python-pip python-setuptools sqlite3 \
|
||||||
libssl-dev python-virtualenv libjpeg-dev
|
libssl-dev python-virtualenv libjpeg-dev
|
||||||
|
|
||||||
Installing prerequisites on ArchLinux::
|
Installing prerequisites on ArchLinux::
|
||||||
|
|
||||||
$ sudo pacman -S base-devel python2 python-pip \
|
sudo pacman -S base-devel python2 python-pip \
|
||||||
python-setuptools python-virtualenv sqlite3
|
python-setuptools python-virtualenv sqlite3
|
||||||
|
|
||||||
Installing prerequisites on Mac OS X::
|
Installing prerequisites on Mac OS X::
|
||||||
|
|
||||||
$ xcode-select --install
|
xcode-select --install
|
||||||
$ sudo pip install virtualenv
|
sudo easy_install pip
|
||||||
|
sudo pip install virtualenv
|
||||||
|
|
||||||
To install the synapse homeserver run::
|
To install the synapse homeserver run::
|
||||||
|
|
||||||
$ virtualenv ~/.synapse
|
virtualenv -p python2.7 ~/.synapse
|
||||||
$ source ~/.synapse/bin/activate
|
source ~/.synapse/bin/activate
|
||||||
$ pip install --process-dependency-links https://github.com/matrix-org/synapse/tarball/master
|
pip install --process-dependency-links https://github.com/matrix-org/synapse/tarball/master
|
||||||
|
|
||||||
This installs synapse, along with the libraries it uses, into a virtual
|
This installs synapse, along with the libraries it uses, into a virtual
|
||||||
environment under ``~/.synapse``.
|
environment under ``~/.synapse``. Feel free to pick a different directory
|
||||||
|
if you prefer.
|
||||||
|
|
||||||
|
In case of problems, please see the _Troubleshooting section below.
|
||||||
|
|
||||||
Alternatively, Silvio Fricke has contributed a Dockerfile to automate the
|
Alternatively, Silvio Fricke has contributed a Dockerfile to automate the
|
||||||
above in Docker at https://registry.hub.docker.com/u/silviof/docker-matrix/.
|
above in Docker at https://registry.hub.docker.com/u/silviof/docker-matrix/.
|
||||||
|
|
||||||
To set up your homeserver, run (in your virtualenv, as before)::
|
To set up your homeserver, run (in your virtualenv, as before)::
|
||||||
|
|
||||||
$ cd ~/.synapse
|
cd ~/.synapse
|
||||||
$ python -m synapse.app.homeserver \
|
python -m synapse.app.homeserver \
|
||||||
--server-name machine.my.domain.name \
|
--server-name machine.my.domain.name \
|
||||||
--config-path homeserver.yaml \
|
--config-path homeserver.yaml \
|
||||||
--generate-config
|
--generate-config
|
||||||
@@ -170,13 +174,13 @@ traditionally used for convenience and simplicity.
|
|||||||
|
|
||||||
The advantages of Postgres include:
|
The advantages of Postgres include:
|
||||||
|
|
||||||
* significant performance improvements due to the superior threading and
|
* significant performance improvements due to the superior threading and
|
||||||
caching model, smarter query optimiser
|
caching model, smarter query optimiser
|
||||||
* allowing the DB to be run on separate hardware
|
* allowing the DB to be run on separate hardware
|
||||||
* allowing basic active/backup high-availability with a "hot spare" synapse
|
* allowing basic active/backup high-availability with a "hot spare" synapse
|
||||||
pointing at the same DB master, as well as enabling DB replication in
|
pointing at the same DB master, as well as enabling DB replication in
|
||||||
synapse itself.
|
synapse itself.
|
||||||
|
|
||||||
The only disadvantage is that the code is relatively new as of April 2015 and
|
The only disadvantage is that the code is relatively new as of April 2015 and
|
||||||
may have a few regressions relative to SQLite.
|
may have a few regressions relative to SQLite.
|
||||||
|
|
||||||
@@ -186,12 +190,12 @@ For information on how to install and use PostgreSQL, please see
|
|||||||
Running Synapse
|
Running Synapse
|
||||||
===============
|
===============
|
||||||
|
|
||||||
To actually run your new homeserver, pick a working directory for Synapse to run
|
To actually run your new homeserver, pick a working directory for Synapse to
|
||||||
(e.g. ``~/.synapse``), and::
|
run (e.g. ``~/.synapse``), and::
|
||||||
|
|
||||||
$ cd ~/.synapse
|
cd ~/.synapse
|
||||||
$ source ./bin/activate
|
source ./bin/activate
|
||||||
$ synctl start
|
synctl start
|
||||||
|
|
||||||
Platform Specific Instructions
|
Platform Specific Instructions
|
||||||
==============================
|
==============================
|
||||||
@@ -209,48 +213,50 @@ defaults to python 3, but synapse currently assumes python 2.7 by default:
|
|||||||
|
|
||||||
pip may be outdated (6.0.7-1 and needs to be upgraded to 6.0.8-1 )::
|
pip may be outdated (6.0.7-1 and needs to be upgraded to 6.0.8-1 )::
|
||||||
|
|
||||||
$ sudo pip2.7 install --upgrade pip
|
sudo pip2.7 install --upgrade pip
|
||||||
|
|
||||||
You also may need to explicitly specify python 2.7 again during the install
|
You also may need to explicitly specify python 2.7 again during the install
|
||||||
request::
|
request::
|
||||||
|
|
||||||
$ pip2.7 install --process-dependency-links \
|
pip2.7 install --process-dependency-links \
|
||||||
https://github.com/matrix-org/synapse/tarball/master
|
https://github.com/matrix-org/synapse/tarball/master
|
||||||
|
|
||||||
If you encounter an error with lib bcrypt causing an Wrong ELF Class:
|
If you encounter an error with lib bcrypt causing an Wrong ELF Class:
|
||||||
ELFCLASS32 (x64 Systems), you may need to reinstall py-bcrypt to correctly
|
ELFCLASS32 (x64 Systems), you may need to reinstall py-bcrypt to correctly
|
||||||
compile it under the right architecture. (This should not be needed if
|
compile it under the right architecture. (This should not be needed if
|
||||||
installing under virtualenv)::
|
installing under virtualenv)::
|
||||||
|
|
||||||
$ sudo pip2.7 uninstall py-bcrypt
|
sudo pip2.7 uninstall py-bcrypt
|
||||||
$ sudo pip2.7 install py-bcrypt
|
sudo pip2.7 install py-bcrypt
|
||||||
|
|
||||||
During setup of Synapse you need to call python2.7 directly again::
|
During setup of Synapse you need to call python2.7 directly again::
|
||||||
|
|
||||||
$ cd ~/.synapse
|
cd ~/.synapse
|
||||||
$ python2.7 -m synapse.app.homeserver \
|
python2.7 -m synapse.app.homeserver \
|
||||||
--server-name machine.my.domain.name \
|
--server-name machine.my.domain.name \
|
||||||
--config-path homeserver.yaml \
|
--config-path homeserver.yaml \
|
||||||
--generate-config
|
--generate-config
|
||||||
|
|
||||||
...substituting your host and domain name as appropriate.
|
...substituting your host and domain name as appropriate.
|
||||||
|
|
||||||
Windows Install
|
Windows Install
|
||||||
---------------
|
---------------
|
||||||
Synapse can be installed on Cygwin. It requires the following Cygwin packages:
|
Synapse can be installed on Cygwin. It requires the following Cygwin packages:
|
||||||
|
|
||||||
- gcc
|
- gcc
|
||||||
- git
|
- git
|
||||||
- libffi-devel
|
- libffi-devel
|
||||||
- openssl (and openssl-devel, python-openssl)
|
- openssl (and openssl-devel, python-openssl)
|
||||||
- python
|
- python
|
||||||
- python-setuptools
|
- python-setuptools
|
||||||
|
|
||||||
The content repository requires additional packages and will be unable to process
|
The content repository requires additional packages and will be unable to process
|
||||||
uploads without them:
|
uploads without them:
|
||||||
- libjpeg8
|
|
||||||
- libjpeg8-devel
|
- libjpeg8
|
||||||
- zlib
|
- libjpeg8-devel
|
||||||
|
- zlib
|
||||||
|
|
||||||
If you choose to install Synapse without these packages, you will need to reinstall
|
If you choose to install Synapse without these packages, you will need to reinstall
|
||||||
``pillow`` for changes to be applied, e.g. ``pip uninstall pillow`` ``pip install
|
``pillow`` for changes to be applied, e.g. ``pip uninstall pillow`` ``pip install
|
||||||
pillow --user``
|
pillow --user``
|
||||||
@@ -272,33 +278,33 @@ Troubleshooting
|
|||||||
Troubleshooting Installation
|
Troubleshooting Installation
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
Synapse requires pip 1.7 or later, so if your OS provides too old a version and
|
Synapse requires pip 1.7 or later, so if your OS provides too old a version and
|
||||||
you get errors about ``error: no such option: --process-dependency-links`` you
|
you get errors about ``error: no such option: --process-dependency-links`` you
|
||||||
may need to manually upgrade it::
|
may need to manually upgrade it::
|
||||||
|
|
||||||
$ sudo pip install --upgrade pip
|
sudo pip install --upgrade pip
|
||||||
|
|
||||||
If pip crashes mid-installation for reason (e.g. lost terminal), pip may
|
If pip crashes mid-installation for reason (e.g. lost terminal), pip may
|
||||||
refuse to run until you remove the temporary installation directory it
|
refuse to run until you remove the temporary installation directory it
|
||||||
created. To reset the installation::
|
created. To reset the installation::
|
||||||
|
|
||||||
$ rm -rf /tmp/pip_install_matrix
|
rm -rf /tmp/pip_install_matrix
|
||||||
|
|
||||||
pip seems to leak *lots* of memory during installation. For instance, a Linux
|
pip seems to leak *lots* of memory during installation. For instance, a Linux
|
||||||
host with 512MB of RAM may run out of memory whilst installing Twisted. If this
|
host with 512MB of RAM may run out of memory whilst installing Twisted. If this
|
||||||
happens, you will have to individually install the dependencies which are
|
happens, you will have to individually install the dependencies which are
|
||||||
failing, e.g.::
|
failing, e.g.::
|
||||||
|
|
||||||
$ pip install twisted
|
pip install twisted
|
||||||
|
|
||||||
On OSX, if you encounter clang: error: unknown argument: '-mno-fused-madd' you
|
On OS X, if you encounter clang: error: unknown argument: '-mno-fused-madd' you
|
||||||
will need to export CFLAGS=-Qunused-arguments.
|
will need to export CFLAGS=-Qunused-arguments.
|
||||||
|
|
||||||
Troubleshooting Running
|
Troubleshooting Running
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
If synapse fails with ``missing "sodium.h"`` crypto errors, you may need
|
If synapse fails with ``missing "sodium.h"`` crypto errors, you may need
|
||||||
to manually upgrade PyNaCL, as synapse uses NaCl (http://nacl.cr.yp.to/) for
|
to manually upgrade PyNaCL, as synapse uses NaCl (http://nacl.cr.yp.to/) for
|
||||||
encryption and digital signatures.
|
encryption and digital signatures.
|
||||||
Unfortunately PyNACL currently has a few issues
|
Unfortunately PyNACL currently has a few issues
|
||||||
(https://github.com/pyca/pynacl/issues/53) and
|
(https://github.com/pyca/pynacl/issues/53) and
|
||||||
@@ -307,10 +313,11 @@ correctly, causing all tests to fail with errors about missing "sodium.h". To
|
|||||||
fix try re-installing from PyPI or directly from
|
fix try re-installing from PyPI or directly from
|
||||||
(https://github.com/pyca/pynacl)::
|
(https://github.com/pyca/pynacl)::
|
||||||
|
|
||||||
$ # Install from PyPI
|
# Install from PyPI
|
||||||
$ pip install --user --upgrade --force pynacl
|
pip install --user --upgrade --force pynacl
|
||||||
$ # Install from github
|
|
||||||
$ pip install --user https://github.com/pyca/pynacl/tarball/master
|
# Install from github
|
||||||
|
pip install --user https://github.com/pyca/pynacl/tarball/master
|
||||||
|
|
||||||
ArchLinux
|
ArchLinux
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
@@ -318,8 +325,8 @@ ArchLinux
|
|||||||
If running `$ synctl start` fails with 'returned non-zero exit status 1',
|
If running `$ synctl start` fails with 'returned non-zero exit status 1',
|
||||||
you will need to explicitly call Python2.7 - either running as::
|
you will need to explicitly call Python2.7 - either running as::
|
||||||
|
|
||||||
$ python2.7 -m synapse.app.homeserver --daemonize -c homeserver.yaml --pid-file homeserver.pid
|
python2.7 -m synapse.app.homeserver --daemonize -c homeserver.yaml
|
||||||
|
|
||||||
...or by editing synctl with the correct python executable.
|
...or by editing synctl with the correct python executable.
|
||||||
|
|
||||||
Synapse Development
|
Synapse Development
|
||||||
@@ -328,16 +335,16 @@ Synapse Development
|
|||||||
To check out a synapse for development, clone the git repo into a working
|
To check out a synapse for development, clone the git repo into a working
|
||||||
directory of your choice::
|
directory of your choice::
|
||||||
|
|
||||||
$ git clone https://github.com/matrix-org/synapse.git
|
git clone https://github.com/matrix-org/synapse.git
|
||||||
$ cd synapse
|
cd synapse
|
||||||
|
|
||||||
Synapse has a number of external dependencies, that are easiest
|
Synapse has a number of external dependencies, that are easiest
|
||||||
to install using pip and a virtualenv::
|
to install using pip and a virtualenv::
|
||||||
|
|
||||||
$ virtualenv env
|
virtualenv env
|
||||||
$ source env/bin/activate
|
source env/bin/activate
|
||||||
$ python synapse/python_dependencies.py | xargs -n1 pip install
|
python synapse/python_dependencies.py | xargs -n1 pip install
|
||||||
$ pip install setuptools_trial mock
|
pip install setuptools_trial mock
|
||||||
|
|
||||||
This will run a process of downloading and installing all the needed
|
This will run a process of downloading and installing all the needed
|
||||||
dependencies into a virtual env.
|
dependencies into a virtual env.
|
||||||
@@ -345,7 +352,7 @@ dependencies into a virtual env.
|
|||||||
Once this is done, you may wish to run Synapse's unit tests, to
|
Once this is done, you may wish to run Synapse's unit tests, to
|
||||||
check that everything is installed as it should be::
|
check that everything is installed as it should be::
|
||||||
|
|
||||||
$ python setup.py test
|
python setup.py test
|
||||||
|
|
||||||
This should end with a 'PASSED' result::
|
This should end with a 'PASSED' result::
|
||||||
|
|
||||||
@@ -357,14 +364,11 @@ This should end with a 'PASSED' result::
|
|||||||
Upgrading an existing Synapse
|
Upgrading an existing Synapse
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
IMPORTANT: Before upgrading an existing synapse to a new version, please
|
The instructions for upgrading synapse are in `UPGRADE.rst`_.
|
||||||
refer to UPGRADE.rst for any additional instructions.
|
Please check these instructions as upgrading may require extra steps for some
|
||||||
|
versions of synapse.
|
||||||
Otherwise, simply re-install the new codebase over the current one - e.g.
|
|
||||||
by ``pip install --process-dependency-links
|
|
||||||
https://github.com/matrix-org/synapse/tarball/master``
|
|
||||||
if using pip, or by ``git pull`` if running off a git working copy.
|
|
||||||
|
|
||||||
|
.. _UPGRADE.rst: UPGRADE.rst
|
||||||
|
|
||||||
Setting up Federation
|
Setting up Federation
|
||||||
=====================
|
=====================
|
||||||
@@ -386,11 +390,11 @@ IDs:
|
|||||||
For the first form, simply pass the required hostname (of the machine) as the
|
For the first form, simply pass the required hostname (of the machine) as the
|
||||||
--server-name parameter::
|
--server-name parameter::
|
||||||
|
|
||||||
$ python -m synapse.app.homeserver \
|
python -m synapse.app.homeserver \
|
||||||
--server-name machine.my.domain.name \
|
--server-name machine.my.domain.name \
|
||||||
--config-path homeserver.yaml \
|
--config-path homeserver.yaml \
|
||||||
--generate-config
|
--generate-config
|
||||||
$ python -m synapse.app.homeserver --config-path homeserver.yaml
|
python -m synapse.app.homeserver --config-path homeserver.yaml
|
||||||
|
|
||||||
Alternatively, you can run ``synctl start`` to guide you through the process.
|
Alternatively, you can run ``synctl start`` to guide you through the process.
|
||||||
|
|
||||||
@@ -407,12 +411,11 @@ record would then look something like::
|
|||||||
At this point, you should then run the homeserver with the hostname of this
|
At this point, you should then run the homeserver with the hostname of this
|
||||||
SRV record, as that is the name other machines will expect it to have::
|
SRV record, as that is the name other machines will expect it to have::
|
||||||
|
|
||||||
$ python -m synapse.app.homeserver \
|
python -m synapse.app.homeserver \
|
||||||
--server-name YOURDOMAIN \
|
--server-name YOURDOMAIN \
|
||||||
--bind-port 8448 \
|
|
||||||
--config-path homeserver.yaml \
|
--config-path homeserver.yaml \
|
||||||
--generate-config
|
--generate-config
|
||||||
$ python -m synapse.app.homeserver --config-path homeserver.yaml
|
python -m synapse.app.homeserver --config-path homeserver.yaml
|
||||||
|
|
||||||
|
|
||||||
You may additionally want to pass one or more "-v" options, in order to
|
You may additionally want to pass one or more "-v" options, in order to
|
||||||
@@ -426,8 +429,8 @@ private federation (``localhost:8080``, ``localhost:8081`` and
|
|||||||
``localhost:8082``) which you can then access through the webclient running at
|
``localhost:8082``) which you can then access through the webclient running at
|
||||||
http://localhost:8080. Simply run::
|
http://localhost:8080. Simply run::
|
||||||
|
|
||||||
$ demo/start.sh
|
demo/start.sh
|
||||||
|
|
||||||
This is mainly useful just for development purposes.
|
This is mainly useful just for development purposes.
|
||||||
|
|
||||||
Running The Demo Web Client
|
Running The Demo Web Client
|
||||||
@@ -490,7 +493,7 @@ time.
|
|||||||
Where's the spec?!
|
Where's the spec?!
|
||||||
==================
|
==================
|
||||||
|
|
||||||
The source of the matrix spec lives at https://github.com/matrix-org/matrix-doc.
|
The source of the matrix spec lives at https://github.com/matrix-org/matrix-doc.
|
||||||
A recent HTML snapshot of this lives at http://matrix.org/docs/spec
|
A recent HTML snapshot of this lives at http://matrix.org/docs/spec
|
||||||
|
|
||||||
|
|
||||||
@@ -500,10 +503,10 @@ Building Internal API Documentation
|
|||||||
Before building internal API documentation install sphinx and
|
Before building internal API documentation install sphinx and
|
||||||
sphinxcontrib-napoleon::
|
sphinxcontrib-napoleon::
|
||||||
|
|
||||||
$ pip install sphinx
|
pip install sphinx
|
||||||
$ pip install sphinxcontrib-napoleon
|
pip install sphinxcontrib-napoleon
|
||||||
|
|
||||||
Building internal API documentation::
|
Building internal API documentation::
|
||||||
|
|
||||||
$ python setup.py build_sphinx
|
python setup.py build_sphinx
|
||||||
|
|
||||||
|
|||||||
35
UPGRADE.rst
35
UPGRADE.rst
@@ -1,4 +1,37 @@
|
|||||||
Upgrading to v0.x.x
|
Upgrading Synapse
|
||||||
|
=================
|
||||||
|
|
||||||
|
Before upgrading check if any special steps are required to upgrade from the
|
||||||
|
what you currently have installed to current version of synapse. The extra
|
||||||
|
instructions that may be required are listed later in this document.
|
||||||
|
|
||||||
|
If synapse was installed in a virtualenv then active that virtualenv before
|
||||||
|
upgrading. If synapse is installed in a virtualenv in ``~/.synapse/`` then run:
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
source ~/.synapse/bin/activate
|
||||||
|
|
||||||
|
If synapse was installed using pip then upgrade to the latest version by
|
||||||
|
running:
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
pip install --upgrade --process-dependency-links https://github.com/matrix-org/synapse/tarball/master
|
||||||
|
|
||||||
|
If synapse was installed using git then upgrade to the latest version by
|
||||||
|
running:
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
# Pull the latest version of the master branch.
|
||||||
|
git pull
|
||||||
|
# Update the versions of synapse's python dependencies.
|
||||||
|
python synapse/python_dependencies.py | xargs -n1 pip install
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Upgrading to v0.9.0
|
||||||
===================
|
===================
|
||||||
|
|
||||||
Application services have had a breaking API change in this version.
|
Application services have had a breaking API change in this version.
|
||||||
|
|||||||
@@ -21,3 +21,5 @@ handlers:
|
|||||||
root:
|
root:
|
||||||
level: INFO
|
level: INFO
|
||||||
handlers: [journal]
|
handlers: [journal]
|
||||||
|
|
||||||
|
disable_existing_loggers: False
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ if [ -f $PID_FILE ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
find "$DIR" -name "*.log" -delete
|
for port in 8080 8081 8082; do
|
||||||
find "$DIR" -name "*.db" -delete
|
rm -rf $DIR/$port
|
||||||
|
rm -rf $DIR/media_store.$port
|
||||||
|
done
|
||||||
|
|
||||||
rm -rf $DIR/etc
|
rm -rf $DIR/etc
|
||||||
|
|||||||
@@ -8,38 +8,41 @@ cd "$DIR/.."
|
|||||||
|
|
||||||
mkdir -p demo/etc
|
mkdir -p demo/etc
|
||||||
|
|
||||||
# Check the --no-rate-limit param
|
export PYTHONPATH=$(readlink -f $(pwd))
|
||||||
PARAMS=""
|
|
||||||
if [ $# -eq 1 ]; then
|
|
||||||
if [ $1 = "--no-rate-limit" ]; then
|
echo $PYTHONPATH
|
||||||
PARAMS="--rc-messages-per-second 1000 --rc-message-burst-count 1000"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
for port in 8080 8081 8082; do
|
for port in 8080 8081 8082; do
|
||||||
echo "Starting server on port $port... "
|
echo "Starting server on port $port... "
|
||||||
|
|
||||||
https_port=$((port + 400))
|
https_port=$((port + 400))
|
||||||
|
mkdir -p demo/$port
|
||||||
|
pushd demo/$port
|
||||||
|
|
||||||
|
#rm $DIR/etc/$port.config
|
||||||
python -m synapse.app.homeserver \
|
python -m synapse.app.homeserver \
|
||||||
--generate-config \
|
--generate-config \
|
||||||
--config-path "demo/etc/$port.config" \
|
|
||||||
-p "$https_port" \
|
|
||||||
--unsecure-port "$port" \
|
|
||||||
-H "localhost:$https_port" \
|
-H "localhost:$https_port" \
|
||||||
-f "$DIR/$port.log" \
|
--config-path "$DIR/etc/$port.config" \
|
||||||
-d "$DIR/$port.db" \
|
|
||||||
-D --pid-file "$DIR/$port.pid" \
|
# Check script parameters
|
||||||
--manhole $((port + 1000)) \
|
if [ $# -eq 1 ]; then
|
||||||
--tls-dh-params-path "demo/demo.tls.dh" \
|
if [ $1 = "--no-rate-limit" ]; then
|
||||||
--media-store-path "demo/media_store.$port" \
|
# Set high limits in config file to disable rate limiting
|
||||||
$PARAMS $SYNAPSE_PARAMS \
|
perl -p -i -e 's/rc_messages_per_second.*/rc_messages_per_second: 1000/g' $DIR/etc/$port.config
|
||||||
--enable-registration
|
perl -p -i -e 's/rc_message_burst_count.*/rc_message_burst_count: 1000/g' $DIR/etc/$port.config
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
perl -p -i -e 's/^enable_registration:.*/enable_registration: true/g' $DIR/etc/$port.config
|
||||||
|
|
||||||
python -m synapse.app.homeserver \
|
python -m synapse.app.homeserver \
|
||||||
--config-path "demo/etc/$port.config" \
|
--config-path "$DIR/etc/$port.config" \
|
||||||
|
-D \
|
||||||
-vv \
|
-vv \
|
||||||
|
|
||||||
|
popd
|
||||||
done
|
done
|
||||||
|
|
||||||
cd "$CWD"
|
cd "$CWD"
|
||||||
|
|||||||
36
docs/application_services.rst
Normal file
36
docs/application_services.rst
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
Registering an Application Service
|
||||||
|
==================================
|
||||||
|
|
||||||
|
The registration of new application services depends on the homeserver used.
|
||||||
|
In synapse, you need to create a new configuration file for your AS and add it
|
||||||
|
to the list specified under the ``app_service_config_files`` config
|
||||||
|
option in your synapse config.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
app_service_config_files:
|
||||||
|
- /home/matrix/.synapse/<your-AS>.yaml
|
||||||
|
|
||||||
|
|
||||||
|
The format of the AS configuration file is as follows:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
url: <base url of AS>
|
||||||
|
as_token: <token AS will add to requests to HS>
|
||||||
|
hs_token: <token HS will add to requests to AS>
|
||||||
|
sender_localpart: <localpart of AS user>
|
||||||
|
namespaces:
|
||||||
|
users: # List of users we're interested in
|
||||||
|
- exclusive: <bool>
|
||||||
|
regex: <regex>
|
||||||
|
- ...
|
||||||
|
aliases: [] # List of aliases we're interested in
|
||||||
|
rooms: [] # List of room ids we're interested in
|
||||||
|
|
||||||
|
See the spec_ for further details on how application services work.
|
||||||
|
|
||||||
|
.. _spec: https://github.com/matrix-org/matrix-doc/blob/master/specification/25_application_service_api.rst#application-service-api
|
||||||
|
|
||||||
@@ -34,19 +34,15 @@ Synapse config
|
|||||||
When you are ready to start using PostgreSQL, add the following line to your
|
When you are ready to start using PostgreSQL, add the following line to your
|
||||||
config file::
|
config file::
|
||||||
|
|
||||||
database_config: <db_config_file>
|
database:
|
||||||
|
name: psycopg2
|
||||||
Where ``<db_config_file>`` is the file name that points to a yaml file of the
|
args:
|
||||||
following form::
|
user: <user>
|
||||||
|
password: <pass>
|
||||||
name: psycopg2
|
database: <db>
|
||||||
args:
|
host: <host>
|
||||||
user: <user>
|
cp_min: 5
|
||||||
password: <pass>
|
cp_max: 10
|
||||||
database: <db>
|
|
||||||
host: <host>
|
|
||||||
cp_min: 5
|
|
||||||
cp_max: 10
|
|
||||||
|
|
||||||
All key, values in ``args`` are passed to the ``psycopg2.connect(..)``
|
All key, values in ``args`` are passed to the ``psycopg2.connect(..)``
|
||||||
function, except keys beginning with ``cp_``, which are consumed by the twisted
|
function, except keys beginning with ``cp_``, which are consumed by the twisted
|
||||||
@@ -86,13 +82,13 @@ complete, restart synapse. For instance::
|
|||||||
cp homeserver.db homeserver.db.snapshot
|
cp homeserver.db homeserver.db.snapshot
|
||||||
./synctl start
|
./synctl start
|
||||||
|
|
||||||
Assuming your database config file (as described in the section *Synapse
|
Assuming your new config file (as described in the section *Synapse config*)
|
||||||
config*) is named ``database_config.yaml`` and the SQLite snapshot is at
|
is named ``homeserver-postgres.yaml`` and the SQLite snapshot is at
|
||||||
``homeserver.db.snapshot`` then simply run::
|
``homeserver.db.snapshot`` then simply run::
|
||||||
|
|
||||||
python scripts/port_from_sqlite_to_postgres.py \
|
python scripts/port_from_sqlite_to_postgres.py \
|
||||||
--sqlite-database homeserver.db.snapshot \
|
--sqlite-database homeserver.db.snapshot \
|
||||||
--postgres-config database_config.yaml
|
--postgres-config homeserver-postgres.yaml
|
||||||
|
|
||||||
The flag ``--curses`` displays a coloured curses progress UI.
|
The flag ``--curses`` displays a coloured curses progress UI.
|
||||||
|
|
||||||
|
|||||||
116
scripts-dev/convert_server_keys.py
Normal file
116
scripts-dev/convert_server_keys.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import psycopg2
|
||||||
|
import yaml
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import hashlib
|
||||||
|
from syutil.base64util import encode_base64
|
||||||
|
from syutil.crypto.signing_key import read_signing_keys
|
||||||
|
from syutil.crypto.jsonsign import sign_json
|
||||||
|
from syutil.jsonutil import encode_canonical_json
|
||||||
|
|
||||||
|
|
||||||
|
def select_v1_keys(connection):
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute("SELECT server_name, key_id, verify_key FROM server_signature_keys")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
cursor.close()
|
||||||
|
results = {}
|
||||||
|
for server_name, key_id, verify_key in rows:
|
||||||
|
results.setdefault(server_name, {})[key_id] = encode_base64(verify_key)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def select_v1_certs(connection):
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute("SELECT server_name, tls_certificate FROM server_tls_certificates")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
cursor.close()
|
||||||
|
results = {}
|
||||||
|
for server_name, tls_certificate in rows:
|
||||||
|
results[server_name] = tls_certificate
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def select_v2_json(connection):
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute("SELECT server_name, key_id, key_json FROM server_keys_json")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
cursor.close()
|
||||||
|
results = {}
|
||||||
|
for server_name, key_id, key_json in rows:
|
||||||
|
results.setdefault(server_name, {})[key_id] = json.loads(str(key_json).decode("utf-8"))
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def convert_v1_to_v2(server_name, valid_until, keys, certificate):
|
||||||
|
return {
|
||||||
|
"old_verify_keys": {},
|
||||||
|
"server_name": server_name,
|
||||||
|
"verify_keys": {
|
||||||
|
key_id: {"key": key}
|
||||||
|
for key_id, key in keys.items()
|
||||||
|
},
|
||||||
|
"valid_until_ts": valid_until,
|
||||||
|
"tls_fingerprints": [fingerprint(certificate)],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def fingerprint(certificate):
|
||||||
|
finger = hashlib.sha256(certificate)
|
||||||
|
return {"sha256": encode_base64(finger.digest())}
|
||||||
|
|
||||||
|
|
||||||
|
def rows_v2(server, json):
|
||||||
|
valid_until = json["valid_until_ts"]
|
||||||
|
key_json = encode_canonical_json(json)
|
||||||
|
for key_id in json["verify_keys"]:
|
||||||
|
yield (server, key_id, "-", valid_until, valid_until, buffer(key_json))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
config = yaml.load(open(sys.argv[1]))
|
||||||
|
valid_until = int(time.time() / (3600 * 24)) * 1000 * 3600 * 24
|
||||||
|
|
||||||
|
server_name = config["server_name"]
|
||||||
|
signing_key = read_signing_keys(open(config["signing_key_path"]))[0]
|
||||||
|
|
||||||
|
database = config["database"]
|
||||||
|
assert database["name"] == "psycopg2", "Can only convert for postgresql"
|
||||||
|
args = database["args"]
|
||||||
|
args.pop("cp_max")
|
||||||
|
args.pop("cp_min")
|
||||||
|
connection = psycopg2.connect(**args)
|
||||||
|
keys = select_v1_keys(connection)
|
||||||
|
certificates = select_v1_certs(connection)
|
||||||
|
json = select_v2_json(connection)
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
for server in keys:
|
||||||
|
if not server in json:
|
||||||
|
v2_json = convert_v1_to_v2(
|
||||||
|
server, valid_until, keys[server], certificates[server]
|
||||||
|
)
|
||||||
|
v2_json = sign_json(v2_json, server_name, signing_key)
|
||||||
|
result[server] = v2_json
|
||||||
|
|
||||||
|
yaml.safe_dump(result, sys.stdout, default_flow_style=False)
|
||||||
|
|
||||||
|
rows = list(
|
||||||
|
row for server, json in result.items()
|
||||||
|
for row in rows_v2(server, json)
|
||||||
|
)
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.executemany(
|
||||||
|
"INSERT INTO server_keys_json ("
|
||||||
|
" server_name, key_id, from_server,"
|
||||||
|
" ts_added_ms, ts_valid_until_ms, key_json"
|
||||||
|
") VALUES (%s, %s, %s, %s, %s, %s)",
|
||||||
|
rows
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
10
scripts/port_from_sqlite_to_postgres.py
Normal file → Executable file
10
scripts/port_from_sqlite_to_postgres.py
Normal file → Executable file
@@ -1,3 +1,4 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015 OpenMarket Ltd
|
# Copyright 2015 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
@@ -105,7 +106,7 @@ class Store(object):
|
|||||||
try:
|
try:
|
||||||
txn = conn.cursor()
|
txn = conn.cursor()
|
||||||
return func(
|
return func(
|
||||||
LoggingTransaction(txn, desc, self.database_engine),
|
LoggingTransaction(txn, desc, self.database_engine, []),
|
||||||
*args, **kwargs
|
*args, **kwargs
|
||||||
)
|
)
|
||||||
except self.database_engine.module.DatabaseError as e:
|
except self.database_engine.module.DatabaseError as e:
|
||||||
@@ -377,9 +378,7 @@ class Porter(object):
|
|||||||
|
|
||||||
for i, row in enumerate(rows):
|
for i, row in enumerate(rows):
|
||||||
rows[i] = tuple(
|
rows[i] = tuple(
|
||||||
self.postgres_store.database_engine.encode_parameter(
|
conv(j, col)
|
||||||
conv(j, col)
|
|
||||||
)
|
|
||||||
for j, col in enumerate(row)
|
for j, col in enumerate(row)
|
||||||
if j > 0
|
if j > 0
|
||||||
)
|
)
|
||||||
@@ -724,6 +723,9 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
postgres_config = yaml.safe_load(args.postgres_config)
|
postgres_config = yaml.safe_load(args.postgres_config)
|
||||||
|
|
||||||
|
if "database" in postgres_config:
|
||||||
|
postgres_config = postgres_config["database"]
|
||||||
|
|
||||||
if "name" not in postgres_config:
|
if "name" not in postgres_config:
|
||||||
sys.stderr.write("Malformed database config: no 'name'")
|
sys.stderr.write("Malformed database config: no 'name'")
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|||||||
@@ -33,9 +33,10 @@ def request_registration(user, password, server_location, shared_secret):
|
|||||||
).hexdigest()
|
).hexdigest()
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"username": user,
|
"user": user,
|
||||||
"password": password,
|
"password": password,
|
||||||
"mac": mac,
|
"mac": mac,
|
||||||
|
"type": "org.matrix.login.shared_secret",
|
||||||
}
|
}
|
||||||
|
|
||||||
server_location = server_location.rstrip("/")
|
server_location = server_location.rstrip("/")
|
||||||
@@ -43,7 +44,7 @@ def request_registration(user, password, server_location, shared_secret):
|
|||||||
print "Sending registration request..."
|
print "Sending registration request..."
|
||||||
|
|
||||||
req = urllib2.Request(
|
req = urllib2.Request(
|
||||||
"%s/_matrix/client/v2_alpha/register" % (server_location,),
|
"%s/_matrix/client/api/v1/register" % (server_location,),
|
||||||
data=json.dumps(data),
|
data=json.dumps(data),
|
||||||
headers={'Content-Type': 'application/json'}
|
headers={'Content-Type': 'application/json'}
|
||||||
)
|
)
|
||||||
2
scripts/upgrade_db_to_v0.6.0.py
Normal file → Executable file
2
scripts/upgrade_db_to_v0.6.0.py
Normal file → Executable file
@@ -1,4 +1,4 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
from synapse.storage import SCHEMA_VERSION, read_schema
|
from synapse.storage import SCHEMA_VERSION, read_schema
|
||||||
from synapse.storage._base import SQLBaseStore
|
from synapse.storage._base import SQLBaseStore
|
||||||
from synapse.storage.signatures import SignatureStore
|
from synapse.storage.signatures import SignatureStore
|
||||||
|
|||||||
@@ -16,3 +16,6 @@ ignore =
|
|||||||
docs/*
|
docs/*
|
||||||
pylint.cfg
|
pylint.cfg
|
||||||
tox.ini
|
tox.ini
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
max-line-length = 90
|
||||||
|
|||||||
5
setup.py
5
setup.py
@@ -14,6 +14,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import glob
|
||||||
import os
|
import os
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ setup(
|
|||||||
description="Reference Synapse Home Server",
|
description="Reference Synapse Home Server",
|
||||||
install_requires=dependencies['requirements'](include_conditional=True).keys(),
|
install_requires=dependencies['requirements'](include_conditional=True).keys(),
|
||||||
setup_requires=[
|
setup_requires=[
|
||||||
"Twisted==14.0.2", # Here to override setuptools_trial's dependency on Twisted>=2.4.0
|
"Twisted>=15.1.0", # Here to override setuptools_trial's dependency on Twisted>=2.4.0
|
||||||
"setuptools_trial",
|
"setuptools_trial",
|
||||||
"mock"
|
"mock"
|
||||||
],
|
],
|
||||||
@@ -55,5 +56,5 @@ setup(
|
|||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
scripts=["synctl", "register_new_matrix_user"],
|
scripts=["synctl"] + glob.glob("scripts/*"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,4 +16,4 @@
|
|||||||
""" This is a reference implementation of a Matrix home server.
|
""" This is a reference implementation of a Matrix home server.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.8.1-r4"
|
__version__ = "0.10.0-rc4"
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from twisted.internet import defer
|
|||||||
from synapse.api.constants import EventTypes, Membership, JoinRules
|
from synapse.api.constants import EventTypes, Membership, JoinRules
|
||||||
from synapse.api.errors import AuthError, Codes, SynapseError
|
from synapse.api.errors import AuthError, Codes, SynapseError
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
from synapse.util.async import run_on_reactor
|
|
||||||
from synapse.types import UserID, ClientInfo
|
from synapse.types import UserID, ClientInfo
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -30,7 +29,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
AuthEventTypes = (
|
AuthEventTypes = (
|
||||||
EventTypes.Create, EventTypes.Member, EventTypes.PowerLevels,
|
EventTypes.Create, EventTypes.Member, EventTypes.PowerLevels,
|
||||||
EventTypes.JoinRules,
|
EventTypes.JoinRules, EventTypes.RoomHistoryVisibility,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -45,6 +44,11 @@ class Auth(object):
|
|||||||
def check(self, event, auth_events):
|
def check(self, event, auth_events):
|
||||||
""" Checks if this event is correctly authed.
|
""" Checks if this event is correctly authed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: the event being checked.
|
||||||
|
auth_events (dict: event-key -> event): the existing room state.
|
||||||
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if the auth checks pass.
|
True if the auth checks pass.
|
||||||
"""
|
"""
|
||||||
@@ -65,7 +69,10 @@ class Auth(object):
|
|||||||
if event.type == EventTypes.Aliases:
|
if event.type == EventTypes.Aliases:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logger.debug("Auth events: %s", auth_events)
|
logger.debug(
|
||||||
|
"Auth events: %s",
|
||||||
|
[a.event_id for a in auth_events.values()]
|
||||||
|
)
|
||||||
|
|
||||||
if event.type == EventTypes.Member:
|
if event.type == EventTypes.Member:
|
||||||
allowed = self.is_membership_change_allowed(
|
allowed = self.is_membership_change_allowed(
|
||||||
@@ -185,6 +192,9 @@ class Auth(object):
|
|||||||
join_rule = JoinRules.INVITE
|
join_rule = JoinRules.INVITE
|
||||||
|
|
||||||
user_level = self._get_user_power_level(event.user_id, auth_events)
|
user_level = self._get_user_power_level(event.user_id, auth_events)
|
||||||
|
target_level = self._get_user_power_level(
|
||||||
|
target_user_id, auth_events
|
||||||
|
)
|
||||||
|
|
||||||
# FIXME (erikj): What should we do here as the default?
|
# FIXME (erikj): What should we do here as the default?
|
||||||
ban_level = self._get_named_level(auth_events, "ban", 50)
|
ban_level = self._get_named_level(auth_events, "ban", 50)
|
||||||
@@ -256,12 +266,12 @@ class Auth(object):
|
|||||||
elif target_user_id != event.user_id:
|
elif target_user_id != event.user_id:
|
||||||
kick_level = self._get_named_level(auth_events, "kick", 50)
|
kick_level = self._get_named_level(auth_events, "kick", 50)
|
||||||
|
|
||||||
if user_level < kick_level:
|
if user_level < kick_level or user_level <= target_level:
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
403, "You cannot kick user %s." % target_user_id
|
403, "You cannot kick user %s." % target_user_id
|
||||||
)
|
)
|
||||||
elif Membership.BAN == membership:
|
elif Membership.BAN == membership:
|
||||||
if user_level < ban_level:
|
if user_level < ban_level or user_level <= target_level:
|
||||||
raise AuthError(403, "You don't have permission to ban")
|
raise AuthError(403, "You don't have permission to ban")
|
||||||
else:
|
else:
|
||||||
raise AuthError(500, "Unknown membership %s" % membership)
|
raise AuthError(500, "Unknown membership %s" % membership)
|
||||||
@@ -314,7 +324,7 @@ class Auth(object):
|
|||||||
Returns:
|
Returns:
|
||||||
tuple : of UserID and device string:
|
tuple : of UserID and device string:
|
||||||
User ID object of the user making the request
|
User ID object of the user making the request
|
||||||
Client ID object of the client instance the user is using
|
ClientInfo object of the client instance the user is using
|
||||||
Raises:
|
Raises:
|
||||||
AuthError if no user by that token exists or the token is invalid.
|
AuthError if no user by that token exists or the token is invalid.
|
||||||
"""
|
"""
|
||||||
@@ -342,12 +352,14 @@ class Auth(object):
|
|||||||
if not user_id:
|
if not user_id:
|
||||||
raise KeyError
|
raise KeyError
|
||||||
|
|
||||||
|
request.authenticated_entity = user_id
|
||||||
|
|
||||||
defer.returnValue(
|
defer.returnValue(
|
||||||
(UserID.from_string(user_id), ClientInfo("", ""))
|
(UserID.from_string(user_id), ClientInfo("", ""))
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass # normal users won't have this query parameter set
|
pass # normal users won't have the user_id query parameter set.
|
||||||
|
|
||||||
user_info = yield self.get_user_by_token(access_token)
|
user_info = yield self.get_user_by_token(access_token)
|
||||||
user = user_info["user"]
|
user = user_info["user"]
|
||||||
@@ -360,7 +372,7 @@ class Auth(object):
|
|||||||
default=[""]
|
default=[""]
|
||||||
)[0]
|
)[0]
|
||||||
if user and access_token and ip_addr:
|
if user and access_token and ip_addr:
|
||||||
yield self.store.insert_client_ip(
|
self.store.insert_client_ip(
|
||||||
user=user,
|
user=user,
|
||||||
access_token=access_token,
|
access_token=access_token,
|
||||||
device_id=user_info["device_id"],
|
device_id=user_info["device_id"],
|
||||||
@@ -368,6 +380,8 @@ class Auth(object):
|
|||||||
user_agent=user_agent
|
user_agent=user_agent
|
||||||
)
|
)
|
||||||
|
|
||||||
|
request.authenticated_entity = user.to_string()
|
||||||
|
|
||||||
defer.returnValue((user, ClientInfo(device_id, token_id)))
|
defer.returnValue((user, ClientInfo(device_id, token_id)))
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
@@ -413,6 +427,7 @@ class Auth(object):
|
|||||||
"Unrecognised access token.",
|
"Unrecognised access token.",
|
||||||
errcode=Codes.UNKNOWN_TOKEN
|
errcode=Codes.UNKNOWN_TOKEN
|
||||||
)
|
)
|
||||||
|
request.authenticated_entity = service.sender
|
||||||
defer.returnValue(service)
|
defer.returnValue(service)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
@@ -424,8 +439,6 @@ class Auth(object):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def add_auth_events(self, builder, context):
|
def add_auth_events(self, builder, context):
|
||||||
yield run_on_reactor()
|
|
||||||
|
|
||||||
auth_ids = self.compute_auth_events(builder, context.current_state)
|
auth_ids = self.compute_auth_events(builder, context.current_state)
|
||||||
|
|
||||||
auth_events_entries = yield self.store.add_event_hashes(
|
auth_events_entries = yield self.store.add_event_hashes(
|
||||||
@@ -516,23 +529,22 @@ class Auth(object):
|
|||||||
|
|
||||||
# Check state_key
|
# Check state_key
|
||||||
if hasattr(event, "state_key"):
|
if hasattr(event, "state_key"):
|
||||||
if not event.state_key.startswith("_"):
|
if event.state_key.startswith("@"):
|
||||||
if event.state_key.startswith("@"):
|
if event.state_key != event.user_id:
|
||||||
if event.state_key != event.user_id:
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You are not allowed to set others state"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
sender_domain = UserID.from_string(
|
||||||
|
event.user_id
|
||||||
|
).domain
|
||||||
|
|
||||||
|
if sender_domain != event.state_key:
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
403,
|
403,
|
||||||
"You are not allowed to set others state"
|
"You are not allowed to set others state"
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
sender_domain = UserID.from_string(
|
|
||||||
event.user_id
|
|
||||||
).domain
|
|
||||||
|
|
||||||
if sender_domain != event.state_key:
|
|
||||||
raise AuthError(
|
|
||||||
403,
|
|
||||||
"You are not allowed to set others state"
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -571,25 +583,26 @@ class Auth(object):
|
|||||||
|
|
||||||
# Check other levels:
|
# Check other levels:
|
||||||
levels_to_check = [
|
levels_to_check = [
|
||||||
("users_default", []),
|
("users_default", None),
|
||||||
("events_default", []),
|
("events_default", None),
|
||||||
("ban", []),
|
("state_default", None),
|
||||||
("redact", []),
|
("ban", None),
|
||||||
("kick", []),
|
("redact", None),
|
||||||
("invite", []),
|
("kick", None),
|
||||||
|
("invite", None),
|
||||||
]
|
]
|
||||||
|
|
||||||
old_list = current_state.content.get("users")
|
old_list = current_state.content.get("users")
|
||||||
for user in set(old_list.keys() + user_list.keys()):
|
for user in set(old_list.keys() + user_list.keys()):
|
||||||
levels_to_check.append(
|
levels_to_check.append(
|
||||||
(user, ["users"])
|
(user, "users")
|
||||||
)
|
)
|
||||||
|
|
||||||
old_list = current_state.content.get("events")
|
old_list = current_state.content.get("events")
|
||||||
new_list = event.content.get("events")
|
new_list = event.content.get("events")
|
||||||
for ev_id in set(old_list.keys() + new_list.keys()):
|
for ev_id in set(old_list.keys() + new_list.keys()):
|
||||||
levels_to_check.append(
|
levels_to_check.append(
|
||||||
(ev_id, ["events"])
|
(ev_id, "events")
|
||||||
)
|
)
|
||||||
|
|
||||||
old_state = current_state.content
|
old_state = current_state.content
|
||||||
@@ -597,12 +610,10 @@ class Auth(object):
|
|||||||
|
|
||||||
for level_to_check, dir in levels_to_check:
|
for level_to_check, dir in levels_to_check:
|
||||||
old_loc = old_state
|
old_loc = old_state
|
||||||
for d in dir:
|
|
||||||
old_loc = old_loc.get(d, {})
|
|
||||||
|
|
||||||
new_loc = new_state
|
new_loc = new_state
|
||||||
for d in dir:
|
if dir:
|
||||||
new_loc = new_loc.get(d, {})
|
old_loc = old_loc.get(dir, {})
|
||||||
|
new_loc = new_loc.get(dir, {})
|
||||||
|
|
||||||
if level_to_check in old_loc:
|
if level_to_check in old_loc:
|
||||||
old_level = int(old_loc[level_to_check])
|
old_level = int(old_loc[level_to_check])
|
||||||
@@ -618,6 +629,14 @@ class Auth(object):
|
|||||||
if new_level == old_level:
|
if new_level == old_level:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if dir == "users" and level_to_check != event.user_id:
|
||||||
|
if old_level == user_level:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to remove ops level equal "
|
||||||
|
"to your own"
|
||||||
|
)
|
||||||
|
|
||||||
if old_level > user_level or new_level > user_level:
|
if old_level > user_level or new_level > user_level:
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
403,
|
403,
|
||||||
|
|||||||
@@ -75,6 +75,10 @@ class EventTypes(object):
|
|||||||
Redaction = "m.room.redaction"
|
Redaction = "m.room.redaction"
|
||||||
Feedback = "m.room.message.feedback"
|
Feedback = "m.room.message.feedback"
|
||||||
|
|
||||||
|
RoomHistoryVisibility = "m.room.history_visibility"
|
||||||
|
CanonicalAlias = "m.room.canonical_alias"
|
||||||
|
RoomAvatar = "m.room.avatar"
|
||||||
|
|
||||||
# These are used for validation
|
# These are used for validation
|
||||||
Message = "m.room.message"
|
Message = "m.room.message"
|
||||||
Topic = "m.room.topic"
|
Topic = "m.room.topic"
|
||||||
@@ -85,3 +89,8 @@ class RejectedReason(object):
|
|||||||
AUTH_ERROR = "auth_error"
|
AUTH_ERROR = "auth_error"
|
||||||
REPLACED = "replaced"
|
REPLACED = "replaced"
|
||||||
NOT_ANCESTOR = "not_ancestor"
|
NOT_ANCESTOR = "not_ancestor"
|
||||||
|
|
||||||
|
|
||||||
|
class RoomCreationPreset(object):
|
||||||
|
PRIVATE_CHAT = "private_chat"
|
||||||
|
PUBLIC_CHAT = "public_chat"
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class Codes(object):
|
|||||||
TOO_LARGE = "M_TOO_LARGE"
|
TOO_LARGE = "M_TOO_LARGE"
|
||||||
EXCLUSIVE = "M_EXCLUSIVE"
|
EXCLUSIVE = "M_EXCLUSIVE"
|
||||||
THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED"
|
THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED"
|
||||||
|
THREEPID_IN_USE = "THREEPID_IN_USE"
|
||||||
|
|
||||||
|
|
||||||
class CodeMessageException(RuntimeError):
|
class CodeMessageException(RuntimeError):
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
sys.dont_write_bytecode = True
|
sys.dont_write_bytecode = True
|
||||||
from synapse.python_dependencies import check_requirements
|
from synapse.python_dependencies import check_requirements, DEPENDENCY_LINKS
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
check_requirements()
|
check_requirements()
|
||||||
@@ -32,10 +32,9 @@ from synapse.server import HomeServer
|
|||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
from twisted.application import service
|
from twisted.application import service
|
||||||
from twisted.enterprise import adbapi
|
from twisted.enterprise import adbapi
|
||||||
from twisted.web.resource import Resource
|
from twisted.web.resource import Resource, EncodingResourceWrapper
|
||||||
from twisted.web.static import File
|
from twisted.web.static import File
|
||||||
from twisted.web.server import Site
|
from twisted.web.server import Site, GzipEncoderFactory, Request
|
||||||
from twisted.web.http import proxiedLogFormatter, combinedLogFormatter
|
|
||||||
from synapse.http.server import JsonResource, RootRedirect
|
from synapse.http.server import JsonResource, RootRedirect
|
||||||
from synapse.rest.media.v0.content_repository import ContentRepoResource
|
from synapse.rest.media.v0.content_repository import ContentRepoResource
|
||||||
from synapse.rest.media.v1.media_repository import MediaRepositoryResource
|
from synapse.rest.media.v1.media_repository import MediaRepositoryResource
|
||||||
@@ -54,21 +53,35 @@ from synapse.rest.client.v1 import ClientV1RestResource
|
|||||||
from synapse.rest.client.v2_alpha import ClientV2AlphaRestResource
|
from synapse.rest.client.v2_alpha import ClientV2AlphaRestResource
|
||||||
from synapse.metrics.resource import MetricsResource, METRICS_PREFIX
|
from synapse.metrics.resource import MetricsResource, METRICS_PREFIX
|
||||||
|
|
||||||
|
from synapse import events
|
||||||
|
|
||||||
from daemonize import Daemonize
|
from daemonize import Daemonize
|
||||||
import twisted.manhole.telnet
|
import twisted.manhole.telnet
|
||||||
|
|
||||||
import synapse
|
import synapse
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import resource
|
import resource
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("synapse.app.homeserver")
|
logger = logging.getLogger("synapse.app.homeserver")
|
||||||
|
|
||||||
|
|
||||||
|
class GzipFile(File):
|
||||||
|
def getChild(self, path, request):
|
||||||
|
child = File.getChild(self, path, request)
|
||||||
|
return EncodingResourceWrapper(child, [GzipEncoderFactory()])
|
||||||
|
|
||||||
|
|
||||||
|
def gz_wrap(r):
|
||||||
|
return EncodingResourceWrapper(r, [GzipEncoderFactory()])
|
||||||
|
|
||||||
|
|
||||||
class SynapseHomeServer(HomeServer):
|
class SynapseHomeServer(HomeServer):
|
||||||
|
|
||||||
def build_http_client(self):
|
def build_http_client(self):
|
||||||
@@ -84,17 +97,40 @@ class SynapseHomeServer(HomeServer):
|
|||||||
return JsonResource(self)
|
return JsonResource(self)
|
||||||
|
|
||||||
def build_resource_for_web_client(self):
|
def build_resource_for_web_client(self):
|
||||||
import syweb
|
webclient_path = self.get_config().web_client_location
|
||||||
syweb_path = os.path.dirname(syweb.__file__)
|
if not webclient_path:
|
||||||
webclient_path = os.path.join(syweb_path, "webclient")
|
try:
|
||||||
|
import syweb
|
||||||
|
except ImportError:
|
||||||
|
quit_with_error(
|
||||||
|
"Could not find a webclient.\n\n"
|
||||||
|
"Please either install the matrix-angular-sdk or configure\n"
|
||||||
|
"the location of the source to serve via the configuration\n"
|
||||||
|
"option `web_client_location`\n\n"
|
||||||
|
"To install the `matrix-angular-sdk` via pip, run:\n\n"
|
||||||
|
" pip install '%(dep)s'\n"
|
||||||
|
"\n"
|
||||||
|
"You can also disable hosting of the webclient via the\n"
|
||||||
|
"configuration option `web_client`\n"
|
||||||
|
% {"dep": DEPENDENCY_LINKS["matrix-angular-sdk"]}
|
||||||
|
)
|
||||||
|
syweb_path = os.path.dirname(syweb.__file__)
|
||||||
|
webclient_path = os.path.join(syweb_path, "webclient")
|
||||||
|
# GZip is disabled here due to
|
||||||
|
# https://twistedmatrix.com/trac/ticket/7678
|
||||||
|
# (It can stay enabled for the API resources: they call
|
||||||
|
# write() with the whole body and then finish() straight
|
||||||
|
# after and so do not trigger the bug.
|
||||||
|
# return GzipFile(webclient_path) # TODO configurable?
|
||||||
return File(webclient_path) # TODO configurable?
|
return File(webclient_path) # TODO configurable?
|
||||||
|
|
||||||
def build_resource_for_static_content(self):
|
def build_resource_for_static_content(self):
|
||||||
|
# This is old and should go away: not going to bother adding gzip
|
||||||
return File("static")
|
return File("static")
|
||||||
|
|
||||||
def build_resource_for_content_repo(self):
|
def build_resource_for_content_repo(self):
|
||||||
return ContentRepoResource(
|
return ContentRepoResource(
|
||||||
self, self.upload_dir, self.auth, self.content_addr
|
self, self.config.uploads_path, self.auth, self.content_addr
|
||||||
)
|
)
|
||||||
|
|
||||||
def build_resource_for_media_repository(self):
|
def build_resource_for_media_repository(self):
|
||||||
@@ -120,149 +156,105 @@ class SynapseHomeServer(HomeServer):
|
|||||||
**self.db_config.get("args", {})
|
**self.db_config.get("args", {})
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_resource_tree(self, redirect_root_to_web_client):
|
def _listener_http(self, config, listener_config):
|
||||||
"""Create the resource tree for this Home Server.
|
port = listener_config["port"]
|
||||||
|
bind_address = listener_config.get("bind_address", "")
|
||||||
|
tls = listener_config.get("tls", False)
|
||||||
|
site_tag = listener_config.get("tag", port)
|
||||||
|
|
||||||
This in unduly complicated because Twisted does not support putting
|
if tls and config.no_tls:
|
||||||
child resources more than 1 level deep at a time.
|
return
|
||||||
|
|
||||||
Args:
|
|
||||||
web_client (bool): True to enable the web client.
|
|
||||||
redirect_root_to_web_client (bool): True to redirect '/' to the
|
|
||||||
location of the web client. This does nothing if web_client is not
|
|
||||||
True.
|
|
||||||
"""
|
|
||||||
config = self.get_config()
|
|
||||||
web_client = config.web_client
|
|
||||||
|
|
||||||
# list containing (path_str, Resource) e.g:
|
|
||||||
# [ ("/aaa/bbb/cc", Resource1), ("/aaa/dummy", Resource2) ]
|
|
||||||
desired_tree = [
|
|
||||||
(CLIENT_PREFIX, self.get_resource_for_client()),
|
|
||||||
(CLIENT_V2_ALPHA_PREFIX, self.get_resource_for_client_v2_alpha()),
|
|
||||||
(FEDERATION_PREFIX, self.get_resource_for_federation()),
|
|
||||||
(CONTENT_REPO_PREFIX, self.get_resource_for_content_repo()),
|
|
||||||
(SERVER_KEY_PREFIX, self.get_resource_for_server_key()),
|
|
||||||
(SERVER_KEY_V2_PREFIX, self.get_resource_for_server_key_v2()),
|
|
||||||
(MEDIA_PREFIX, self.get_resource_for_media_repository()),
|
|
||||||
(STATIC_PREFIX, self.get_resource_for_static_content()),
|
|
||||||
]
|
|
||||||
|
|
||||||
if web_client:
|
|
||||||
logger.info("Adding the web client.")
|
|
||||||
desired_tree.append((WEB_CLIENT_PREFIX,
|
|
||||||
self.get_resource_for_web_client()))
|
|
||||||
|
|
||||||
if web_client and redirect_root_to_web_client:
|
|
||||||
self.root_resource = RootRedirect(WEB_CLIENT_PREFIX)
|
|
||||||
else:
|
|
||||||
self.root_resource = Resource()
|
|
||||||
|
|
||||||
metrics_resource = self.get_resource_for_metrics()
|
metrics_resource = self.get_resource_for_metrics()
|
||||||
if config.metrics_port is None and metrics_resource is not None:
|
|
||||||
desired_tree.append((METRICS_PREFIX, metrics_resource))
|
|
||||||
|
|
||||||
# ideally we'd just use getChild and putChild but getChild doesn't work
|
resources = {}
|
||||||
# unless you give it a Request object IN ADDITION to the name :/ So
|
for res in listener_config["resources"]:
|
||||||
# instead, we'll store a copy of this mapping so we can actually add
|
for name in res["names"]:
|
||||||
# extra resources to existing nodes. See self._resource_id for the key.
|
if name == "client":
|
||||||
resource_mappings = {}
|
if res["compress"]:
|
||||||
for full_path, res in desired_tree:
|
client_v1 = gz_wrap(self.get_resource_for_client())
|
||||||
logger.info("Attaching %s to path %s", res, full_path)
|
client_v2 = gz_wrap(self.get_resource_for_client_v2_alpha())
|
||||||
last_resource = self.root_resource
|
else:
|
||||||
for path_seg in full_path.split('/')[1:-1]:
|
client_v1 = self.get_resource_for_client()
|
||||||
if path_seg not in last_resource.listNames():
|
client_v2 = self.get_resource_for_client_v2_alpha()
|
||||||
# resource doesn't exist, so make a "dummy resource"
|
|
||||||
child_resource = Resource()
|
|
||||||
last_resource.putChild(path_seg, child_resource)
|
|
||||||
res_id = self._resource_id(last_resource, path_seg)
|
|
||||||
resource_mappings[res_id] = child_resource
|
|
||||||
last_resource = child_resource
|
|
||||||
else:
|
|
||||||
# we have an existing Resource, use that instead.
|
|
||||||
res_id = self._resource_id(last_resource, path_seg)
|
|
||||||
last_resource = resource_mappings[res_id]
|
|
||||||
|
|
||||||
# ===========================
|
resources.update({
|
||||||
# now attach the actual desired resource
|
CLIENT_PREFIX: client_v1,
|
||||||
last_path_seg = full_path.split('/')[-1]
|
CLIENT_V2_ALPHA_PREFIX: client_v2,
|
||||||
|
})
|
||||||
|
|
||||||
# if there is already a resource here, thieve its children and
|
if name == "federation":
|
||||||
# replace it
|
resources.update({
|
||||||
res_id = self._resource_id(last_resource, last_path_seg)
|
FEDERATION_PREFIX: self.get_resource_for_federation(),
|
||||||
if res_id in resource_mappings:
|
})
|
||||||
# there is a dummy resource at this path already, which needs
|
|
||||||
# to be replaced with the desired resource.
|
|
||||||
existing_dummy_resource = resource_mappings[res_id]
|
|
||||||
for child_name in existing_dummy_resource.listNames():
|
|
||||||
child_res_id = self._resource_id(existing_dummy_resource,
|
|
||||||
child_name)
|
|
||||||
child_resource = resource_mappings[child_res_id]
|
|
||||||
# steal the children
|
|
||||||
res.putChild(child_name, child_resource)
|
|
||||||
|
|
||||||
# finally, insert the desired resource in the right place
|
if name in ["static", "client"]:
|
||||||
last_resource.putChild(last_path_seg, res)
|
resources.update({
|
||||||
res_id = self._resource_id(last_resource, last_path_seg)
|
STATIC_PREFIX: self.get_resource_for_static_content(),
|
||||||
resource_mappings[res_id] = res
|
})
|
||||||
|
|
||||||
return self.root_resource
|
if name in ["media", "federation", "client"]:
|
||||||
|
resources.update({
|
||||||
|
MEDIA_PREFIX: self.get_resource_for_media_repository(),
|
||||||
|
CONTENT_REPO_PREFIX: self.get_resource_for_content_repo(),
|
||||||
|
})
|
||||||
|
|
||||||
def _resource_id(self, resource, path_seg):
|
if name in ["keys", "federation"]:
|
||||||
"""Construct an arbitrary resource ID so you can retrieve the mapping
|
resources.update({
|
||||||
later.
|
SERVER_KEY_PREFIX: self.get_resource_for_server_key(),
|
||||||
|
SERVER_KEY_V2_PREFIX: self.get_resource_for_server_key_v2(),
|
||||||
|
})
|
||||||
|
|
||||||
If you want to represent resource A putChild resource B with path C,
|
if name == "webclient":
|
||||||
the mapping should looks like _resource_id(A,C) = B.
|
resources[WEB_CLIENT_PREFIX] = self.get_resource_for_web_client()
|
||||||
|
|
||||||
Args:
|
if name == "metrics" and metrics_resource:
|
||||||
resource (Resource): The *parent* Resource
|
resources[METRICS_PREFIX] = metrics_resource
|
||||||
path_seg (str): The name of the child Resource to be attached.
|
|
||||||
Returns:
|
root_resource = create_resource_tree(resources)
|
||||||
str: A unique string which can be a key to the child Resource.
|
if tls:
|
||||||
"""
|
reactor.listenSSL(
|
||||||
return "%s-%s" % (resource, path_seg)
|
port,
|
||||||
|
SynapseSite(
|
||||||
|
"synapse.access.https.%s" % (site_tag,),
|
||||||
|
site_tag,
|
||||||
|
listener_config,
|
||||||
|
root_resource,
|
||||||
|
),
|
||||||
|
self.tls_context_factory,
|
||||||
|
interface=bind_address
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
reactor.listenTCP(
|
||||||
|
port,
|
||||||
|
SynapseSite(
|
||||||
|
"synapse.access.http.%s" % (site_tag,),
|
||||||
|
site_tag,
|
||||||
|
listener_config,
|
||||||
|
root_resource,
|
||||||
|
),
|
||||||
|
interface=bind_address
|
||||||
|
)
|
||||||
|
logger.info("Synapse now listening on port %d", port)
|
||||||
|
|
||||||
def start_listening(self):
|
def start_listening(self):
|
||||||
config = self.get_config()
|
config = self.get_config()
|
||||||
|
|
||||||
if not config.no_tls and config.bind_port is not None:
|
for listener in config.listeners:
|
||||||
reactor.listenSSL(
|
if listener["type"] == "http":
|
||||||
config.bind_port,
|
self._listener_http(config, listener)
|
||||||
SynapseSite(
|
elif listener["type"] == "manhole":
|
||||||
"synapse.access.https",
|
f = twisted.manhole.telnet.ShellFactory()
|
||||||
config,
|
f.username = "matrix"
|
||||||
self.root_resource,
|
f.password = "rabbithole"
|
||||||
),
|
f.namespace['hs'] = self
|
||||||
self.tls_context_factory,
|
reactor.listenTCP(
|
||||||
interface=config.bind_host
|
listener["port"],
|
||||||
)
|
f,
|
||||||
logger.info("Synapse now listening on port %d", config.bind_port)
|
interface=listener.get("bind_address", '127.0.0.1')
|
||||||
|
)
|
||||||
if config.unsecure_port is not None:
|
else:
|
||||||
reactor.listenTCP(
|
logger.warn("Unrecognized listener type: %s", listener["type"])
|
||||||
config.unsecure_port,
|
|
||||||
SynapseSite(
|
|
||||||
"synapse.access.http",
|
|
||||||
config,
|
|
||||||
self.root_resource,
|
|
||||||
),
|
|
||||||
interface=config.bind_host
|
|
||||||
)
|
|
||||||
logger.info("Synapse now listening on port %d", config.unsecure_port)
|
|
||||||
|
|
||||||
metrics_resource = self.get_resource_for_metrics()
|
|
||||||
if metrics_resource and config.metrics_port is not None:
|
|
||||||
reactor.listenTCP(
|
|
||||||
config.metrics_port,
|
|
||||||
SynapseSite(
|
|
||||||
"synapse.access.metrics",
|
|
||||||
config,
|
|
||||||
metrics_resource,
|
|
||||||
),
|
|
||||||
interface="127.0.0.1",
|
|
||||||
)
|
|
||||||
logger.info("Metrics now running on 127.0.0.1 port %d", config.metrics_port)
|
|
||||||
|
|
||||||
def run_startup_checks(self, db_conn, database_engine):
|
def run_startup_checks(self, db_conn, database_engine):
|
||||||
all_users_native = are_all_users_on_domain(
|
all_users_native = are_all_users_on_domain(
|
||||||
@@ -283,11 +275,10 @@ class SynapseHomeServer(HomeServer):
|
|||||||
|
|
||||||
def quit_with_error(error_string):
|
def quit_with_error(error_string):
|
||||||
message_lines = error_string.split("\n")
|
message_lines = error_string.split("\n")
|
||||||
line_length = max([len(l) for l in message_lines]) + 2
|
line_length = max([len(l) for l in message_lines if len(l) < 80]) + 2
|
||||||
sys.stderr.write("*" * line_length + '\n')
|
sys.stderr.write("*" * line_length + '\n')
|
||||||
for line in message_lines:
|
for line in message_lines:
|
||||||
if line.strip():
|
sys.stderr.write(" %s\n" % (line.rstrip(),))
|
||||||
sys.stderr.write(" %s\n" % (line.strip(),))
|
|
||||||
sys.stderr.write("*" * line_length + '\n')
|
sys.stderr.write("*" * line_length + '\n')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -395,10 +386,7 @@ def setup(config_options):
|
|||||||
logger.info("Server hostname: %s", config.server_name)
|
logger.info("Server hostname: %s", config.server_name)
|
||||||
logger.info("Server version: %s", version_string)
|
logger.info("Server version: %s", version_string)
|
||||||
|
|
||||||
if re.search(":[0-9]+$", config.server_name):
|
events.USE_FROZEN_DICTS = config.use_frozen_dicts
|
||||||
domain_with_port = config.server_name
|
|
||||||
else:
|
|
||||||
domain_with_port = "%s:%s" % (config.server_name, config.bind_port)
|
|
||||||
|
|
||||||
tls_context_factory = context_factory.ServerContextFactory(config)
|
tls_context_factory = context_factory.ServerContextFactory(config)
|
||||||
|
|
||||||
@@ -407,9 +395,6 @@ def setup(config_options):
|
|||||||
|
|
||||||
hs = SynapseHomeServer(
|
hs = SynapseHomeServer(
|
||||||
config.server_name,
|
config.server_name,
|
||||||
domain_with_port=domain_with_port,
|
|
||||||
upload_dir=os.path.abspath("uploads"),
|
|
||||||
db_name=config.database_path,
|
|
||||||
db_config=config.database_config,
|
db_config=config.database_config,
|
||||||
tls_context_factory=tls_context_factory,
|
tls_context_factory=tls_context_factory,
|
||||||
config=config,
|
config=config,
|
||||||
@@ -418,13 +403,7 @@ def setup(config_options):
|
|||||||
database_engine=database_engine,
|
database_engine=database_engine,
|
||||||
)
|
)
|
||||||
|
|
||||||
hs.create_resource_tree(
|
logger.info("Preparing database: %r...", config.database_config)
|
||||||
redirect_root_to_web_client=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
db_name = hs.get_db_name()
|
|
||||||
|
|
||||||
logger.info("Preparing database: %s...", db_name)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db_conn = database_engine.module.connect(
|
db_conn = database_engine.module.connect(
|
||||||
@@ -446,14 +425,7 @@ def setup(config_options):
|
|||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
logger.info("Database prepared in %s.", db_name)
|
logger.info("Database prepared in %r.", config.database_config)
|
||||||
|
|
||||||
if config.manhole:
|
|
||||||
f = twisted.manhole.telnet.ShellFactory()
|
|
||||||
f.username = "matrix"
|
|
||||||
f.password = "rabbithole"
|
|
||||||
f.namespace['hs'] = hs
|
|
||||||
reactor.listenTCP(config.manhole, f, interface='127.0.0.1')
|
|
||||||
|
|
||||||
hs.start_listening()
|
hs.start_listening()
|
||||||
|
|
||||||
@@ -480,35 +452,228 @@ class SynapseService(service.Service):
|
|||||||
return self._port.stopListening()
|
return self._port.stopListening()
|
||||||
|
|
||||||
|
|
||||||
|
class SynapseRequest(Request):
|
||||||
|
def __init__(self, site, *args, **kw):
|
||||||
|
Request.__init__(self, *args, **kw)
|
||||||
|
self.site = site
|
||||||
|
self.authenticated_entity = None
|
||||||
|
self.start_time = 0
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
# We overwrite this so that we don't log ``access_token``
|
||||||
|
return '<%s at 0x%x method=%s uri=%s clientproto=%s site=%s>' % (
|
||||||
|
self.__class__.__name__,
|
||||||
|
id(self),
|
||||||
|
self.method,
|
||||||
|
self.get_redacted_uri(),
|
||||||
|
self.clientproto,
|
||||||
|
self.site.site_tag,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_redacted_uri(self):
|
||||||
|
return re.sub(
|
||||||
|
r'(\?.*access_token=)[^&]*(.*)$',
|
||||||
|
r'\1<redacted>\2',
|
||||||
|
self.uri
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_user_agent(self):
|
||||||
|
return self.requestHeaders.getRawHeaders("User-Agent", [None])[-1]
|
||||||
|
|
||||||
|
def started_processing(self):
|
||||||
|
self.site.access_logger.info(
|
||||||
|
"%s - %s - Received request: %s %s",
|
||||||
|
self.getClientIP(),
|
||||||
|
self.site.site_tag,
|
||||||
|
self.method,
|
||||||
|
self.get_redacted_uri()
|
||||||
|
)
|
||||||
|
self.start_time = int(time.time() * 1000)
|
||||||
|
|
||||||
|
def finished_processing(self):
|
||||||
|
self.site.access_logger.info(
|
||||||
|
"%s - %s - {%s}"
|
||||||
|
" Processed request: %dms %sB %s \"%s %s %s\" \"%s\"",
|
||||||
|
self.getClientIP(),
|
||||||
|
self.site.site_tag,
|
||||||
|
self.authenticated_entity,
|
||||||
|
int(time.time() * 1000) - self.start_time,
|
||||||
|
self.sentLength,
|
||||||
|
self.code,
|
||||||
|
self.method,
|
||||||
|
self.get_redacted_uri(),
|
||||||
|
self.clientproto,
|
||||||
|
self.get_user_agent(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def processing(self):
|
||||||
|
self.started_processing()
|
||||||
|
yield
|
||||||
|
self.finished_processing()
|
||||||
|
|
||||||
|
|
||||||
|
class XForwardedForRequest(SynapseRequest):
|
||||||
|
def __init__(self, *args, **kw):
|
||||||
|
SynapseRequest.__init__(self, *args, **kw)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Add a layer on top of another request that only uses the value of an
|
||||||
|
X-Forwarded-For header as the result of C{getClientIP}.
|
||||||
|
"""
|
||||||
|
def getClientIP(self):
|
||||||
|
"""
|
||||||
|
@return: The client address (the first address) in the value of the
|
||||||
|
I{X-Forwarded-For header}. If the header is not present, return
|
||||||
|
C{b"-"}.
|
||||||
|
"""
|
||||||
|
return self.requestHeaders.getRawHeaders(
|
||||||
|
b"x-forwarded-for", [b"-"])[0].split(b",")[0].strip()
|
||||||
|
|
||||||
|
|
||||||
|
class SynapseRequestFactory(object):
|
||||||
|
def __init__(self, site, x_forwarded_for):
|
||||||
|
self.site = site
|
||||||
|
self.x_forwarded_for = x_forwarded_for
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
if self.x_forwarded_for:
|
||||||
|
return XForwardedForRequest(self.site, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
return SynapseRequest(self.site, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class SynapseSite(Site):
|
class SynapseSite(Site):
|
||||||
"""
|
"""
|
||||||
Subclass of a twisted http Site that does access logging with python's
|
Subclass of a twisted http Site that does access logging with python's
|
||||||
standard logging
|
standard logging
|
||||||
"""
|
"""
|
||||||
def __init__(self, logger_name, config, resource, *args, **kwargs):
|
def __init__(self, logger_name, site_tag, config, resource, *args, **kwargs):
|
||||||
Site.__init__(self, resource, *args, **kwargs)
|
Site.__init__(self, resource, *args, **kwargs)
|
||||||
if config.captcha_ip_origin_is_x_forwarded:
|
|
||||||
self._log_formatter = proxiedLogFormatter
|
self.site_tag = site_tag
|
||||||
else:
|
|
||||||
self._log_formatter = combinedLogFormatter
|
proxied = config.get("x_forwarded", False)
|
||||||
|
self.requestFactory = SynapseRequestFactory(self, proxied)
|
||||||
self.access_logger = logging.getLogger(logger_name)
|
self.access_logger = logging.getLogger(logger_name)
|
||||||
|
|
||||||
def log(self, request):
|
def log(self, request):
|
||||||
line = self._log_formatter(self._logDateTime, request)
|
pass
|
||||||
self.access_logger.info(line)
|
|
||||||
|
|
||||||
|
def create_resource_tree(desired_tree, redirect_root_to_web_client=True):
|
||||||
|
"""Create the resource tree for this Home Server.
|
||||||
|
|
||||||
|
This in unduly complicated because Twisted does not support putting
|
||||||
|
child resources more than 1 level deep at a time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
web_client (bool): True to enable the web client.
|
||||||
|
redirect_root_to_web_client (bool): True to redirect '/' to the
|
||||||
|
location of the web client. This does nothing if web_client is not
|
||||||
|
True.
|
||||||
|
"""
|
||||||
|
if redirect_root_to_web_client and WEB_CLIENT_PREFIX in desired_tree:
|
||||||
|
root_resource = RootRedirect(WEB_CLIENT_PREFIX)
|
||||||
|
else:
|
||||||
|
root_resource = Resource()
|
||||||
|
|
||||||
|
# ideally we'd just use getChild and putChild but getChild doesn't work
|
||||||
|
# unless you give it a Request object IN ADDITION to the name :/ So
|
||||||
|
# instead, we'll store a copy of this mapping so we can actually add
|
||||||
|
# extra resources to existing nodes. See self._resource_id for the key.
|
||||||
|
resource_mappings = {}
|
||||||
|
for full_path, res in desired_tree.items():
|
||||||
|
logger.info("Attaching %s to path %s", res, full_path)
|
||||||
|
last_resource = root_resource
|
||||||
|
for path_seg in full_path.split('/')[1:-1]:
|
||||||
|
if path_seg not in last_resource.listNames():
|
||||||
|
# resource doesn't exist, so make a "dummy resource"
|
||||||
|
child_resource = Resource()
|
||||||
|
last_resource.putChild(path_seg, child_resource)
|
||||||
|
res_id = _resource_id(last_resource, path_seg)
|
||||||
|
resource_mappings[res_id] = child_resource
|
||||||
|
last_resource = child_resource
|
||||||
|
else:
|
||||||
|
# we have an existing Resource, use that instead.
|
||||||
|
res_id = _resource_id(last_resource, path_seg)
|
||||||
|
last_resource = resource_mappings[res_id]
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# now attach the actual desired resource
|
||||||
|
last_path_seg = full_path.split('/')[-1]
|
||||||
|
|
||||||
|
# if there is already a resource here, thieve its children and
|
||||||
|
# replace it
|
||||||
|
res_id = _resource_id(last_resource, last_path_seg)
|
||||||
|
if res_id in resource_mappings:
|
||||||
|
# there is a dummy resource at this path already, which needs
|
||||||
|
# to be replaced with the desired resource.
|
||||||
|
existing_dummy_resource = resource_mappings[res_id]
|
||||||
|
for child_name in existing_dummy_resource.listNames():
|
||||||
|
child_res_id = _resource_id(
|
||||||
|
existing_dummy_resource, child_name
|
||||||
|
)
|
||||||
|
child_resource = resource_mappings[child_res_id]
|
||||||
|
# steal the children
|
||||||
|
res.putChild(child_name, child_resource)
|
||||||
|
|
||||||
|
# finally, insert the desired resource in the right place
|
||||||
|
last_resource.putChild(last_path_seg, res)
|
||||||
|
res_id = _resource_id(last_resource, last_path_seg)
|
||||||
|
resource_mappings[res_id] = res
|
||||||
|
|
||||||
|
return root_resource
|
||||||
|
|
||||||
|
|
||||||
|
def _resource_id(resource, path_seg):
|
||||||
|
"""Construct an arbitrary resource ID so you can retrieve the mapping
|
||||||
|
later.
|
||||||
|
|
||||||
|
If you want to represent resource A putChild resource B with path C,
|
||||||
|
the mapping should looks like _resource_id(A,C) = B.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource (Resource): The *parent* Resource
|
||||||
|
path_seg (str): The name of the child Resource to be attached.
|
||||||
|
Returns:
|
||||||
|
str: A unique string which can be a key to the child Resource.
|
||||||
|
"""
|
||||||
|
return "%s-%s" % (resource, path_seg)
|
||||||
|
|
||||||
|
|
||||||
def run(hs):
|
def run(hs):
|
||||||
|
PROFILE_SYNAPSE = False
|
||||||
|
if PROFILE_SYNAPSE:
|
||||||
|
def profile(func):
|
||||||
|
from cProfile import Profile
|
||||||
|
from threading import current_thread
|
||||||
|
|
||||||
|
def profiled(*args, **kargs):
|
||||||
|
profile = Profile()
|
||||||
|
profile.enable()
|
||||||
|
func(*args, **kargs)
|
||||||
|
profile.disable()
|
||||||
|
ident = current_thread().ident
|
||||||
|
profile.dump_stats("/tmp/%s.%s.%i.pstat" % (
|
||||||
|
hs.hostname, func.__name__, ident
|
||||||
|
))
|
||||||
|
|
||||||
|
return profiled
|
||||||
|
|
||||||
|
from twisted.python.threadpool import ThreadPool
|
||||||
|
ThreadPool._worker = profile(ThreadPool._worker)
|
||||||
|
reactor.run = profile(reactor.run)
|
||||||
|
|
||||||
def in_thread():
|
def in_thread():
|
||||||
with LoggingContext("run"):
|
with LoggingContext("run"):
|
||||||
change_resource_limit(hs.config.soft_file_limit)
|
change_resource_limit(hs.config.soft_file_limit)
|
||||||
|
|
||||||
reactor.run()
|
reactor.run()
|
||||||
|
|
||||||
if hs.config.daemonize:
|
if hs.config.daemonize:
|
||||||
|
|
||||||
print hs.config.pid_file
|
if hs.config.print_pidfile:
|
||||||
|
print hs.config.pid_file
|
||||||
|
|
||||||
daemon = Daemonize(
|
daemon = Daemonize(
|
||||||
app="synapse-homeserver",
|
app="synapse-homeserver",
|
||||||
|
|||||||
@@ -18,29 +18,33 @@ import sys
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import signal
|
import signal
|
||||||
|
import yaml
|
||||||
|
|
||||||
SYNAPSE = ["python", "-B", "-m", "synapse.app.homeserver"]
|
SYNAPSE = ["python", "-B", "-m", "synapse.app.homeserver"]
|
||||||
|
|
||||||
CONFIGFILE = "homeserver.yaml"
|
CONFIGFILE = "homeserver.yaml"
|
||||||
PIDFILE = "homeserver.pid"
|
|
||||||
|
|
||||||
GREEN = "\x1b[1;32m"
|
GREEN = "\x1b[1;32m"
|
||||||
NORMAL = "\x1b[m"
|
NORMAL = "\x1b[m"
|
||||||
|
|
||||||
|
if not os.path.exists(CONFIGFILE):
|
||||||
|
sys.stderr.write(
|
||||||
|
"No config file found\n"
|
||||||
|
"To generate a config file, run '%s -c %s --generate-config"
|
||||||
|
" --server-name=<server name>'\n" % (
|
||||||
|
" ".join(SYNAPSE), CONFIGFILE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
CONFIG = yaml.load(open(CONFIGFILE))
|
||||||
|
PIDFILE = CONFIG["pid_file"]
|
||||||
|
|
||||||
|
|
||||||
def start():
|
def start():
|
||||||
if not os.path.exists(CONFIGFILE):
|
|
||||||
sys.stderr.write(
|
|
||||||
"No config file found\n"
|
|
||||||
"To generate a config file, run '%s -c %s --generate-config"
|
|
||||||
" --server-name=<server name>'\n" % (
|
|
||||||
" ".join(SYNAPSE), CONFIGFILE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
print "Starting ...",
|
print "Starting ...",
|
||||||
args = SYNAPSE
|
args = SYNAPSE
|
||||||
args.extend(["--daemonize", "-c", CONFIGFILE, "--pid-file", PIDFILE])
|
args.extend(["--daemonize", "-c", CONFIGFILE])
|
||||||
subprocess.check_call(args)
|
subprocess.check_call(args)
|
||||||
print GREEN + "started" + NORMAL
|
print GREEN + "started" + NORMAL
|
||||||
|
|
||||||
|
|||||||
@@ -148,8 +148,8 @@ class ApplicationService(object):
|
|||||||
and self.is_interested_in_user(event.state_key)):
|
and self.is_interested_in_user(event.state_key)):
|
||||||
return True
|
return True
|
||||||
# check joined member events
|
# check joined member events
|
||||||
for member in member_list:
|
for user_id in member_list:
|
||||||
if self.is_interested_in_user(member.state_key):
|
if self.is_interested_in_user(user_id):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ class ApplicationService(object):
|
|||||||
restrict_to(str): The namespace to restrict regex tests to.
|
restrict_to(str): The namespace to restrict regex tests to.
|
||||||
aliases_for_event(list): A list of all the known room aliases for
|
aliases_for_event(list): A list of all the known room aliases for
|
||||||
this event.
|
this event.
|
||||||
member_list(list): A list of all joined room members in this room.
|
member_list(list): A list of all joined user_ids in this room.
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if this service would like to know about this event.
|
bool: True if this service would like to know about this event.
|
||||||
"""
|
"""
|
||||||
|
|||||||
30
synapse/config/__main__.py
Normal file
30
synapse/config/__main__.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 OpenMarket 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.
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
from homeserver import HomeServerConfig
|
||||||
|
|
||||||
|
action = sys.argv[1]
|
||||||
|
|
||||||
|
if action == "read":
|
||||||
|
key = sys.argv[2]
|
||||||
|
config = HomeServerConfig.load_config("", sys.argv[3:])
|
||||||
|
|
||||||
|
print getattr(config, key)
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
sys.stderr.write("Unknown command %r\n" % (action,))
|
||||||
|
sys.exit(1)
|
||||||
@@ -14,9 +14,10 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
|
import sys
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
|
||||||
class ConfigError(Exception):
|
class ConfigError(Exception):
|
||||||
@@ -24,18 +25,35 @@ class ConfigError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class Config(object):
|
class Config(object):
|
||||||
def __init__(self, args):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_size(string):
|
def parse_size(value):
|
||||||
|
if isinstance(value, int) or isinstance(value, long):
|
||||||
|
return value
|
||||||
sizes = {"K": 1024, "M": 1024 * 1024}
|
sizes = {"K": 1024, "M": 1024 * 1024}
|
||||||
size = 1
|
size = 1
|
||||||
suffix = string[-1]
|
suffix = value[-1]
|
||||||
if suffix in sizes:
|
if suffix in sizes:
|
||||||
string = string[:-1]
|
value = value[:-1]
|
||||||
size = sizes[suffix]
|
size = sizes[suffix]
|
||||||
return int(string) * size
|
return int(value) * size
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_duration(value):
|
||||||
|
if isinstance(value, int) or isinstance(value, long):
|
||||||
|
return value
|
||||||
|
second = 1000
|
||||||
|
hour = 60 * 60 * second
|
||||||
|
day = 24 * hour
|
||||||
|
week = 7 * day
|
||||||
|
year = 365 * day
|
||||||
|
sizes = {"s": second, "h": hour, "d": day, "w": week, "y": year}
|
||||||
|
size = 1
|
||||||
|
suffix = value[-1]
|
||||||
|
if suffix in sizes:
|
||||||
|
value = value[:-1]
|
||||||
|
size = sizes[suffix]
|
||||||
|
return int(value) * size
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def abspath(file_path):
|
def abspath(file_path):
|
||||||
@@ -77,17 +95,6 @@ class Config(object):
|
|||||||
with open(file_path) as file_stream:
|
with open(file_path) as file_stream:
|
||||||
return file_stream.read()
|
return file_stream.read()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def read_yaml_file(cls, file_path, config_name):
|
|
||||||
cls.check_file(file_path, config_name)
|
|
||||||
with open(file_path) as file_stream:
|
|
||||||
try:
|
|
||||||
return yaml.load(file_stream)
|
|
||||||
except:
|
|
||||||
raise ConfigError(
|
|
||||||
"Error parsing yaml in file %r" % (file_path,)
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def default_path(name):
|
def default_path(name):
|
||||||
return os.path.abspath(os.path.join(os.path.curdir, name))
|
return os.path.abspath(os.path.join(os.path.curdir, name))
|
||||||
@@ -97,84 +104,171 @@ class Config(object):
|
|||||||
with open(file_path) as file_stream:
|
with open(file_path) as file_stream:
|
||||||
return yaml.load(file_stream)
|
return yaml.load(file_stream)
|
||||||
|
|
||||||
@classmethod
|
def invoke_all(self, name, *args, **kargs):
|
||||||
def add_arguments(cls, parser):
|
results = []
|
||||||
pass
|
for cls in type(self).mro():
|
||||||
|
if name in cls.__dict__:
|
||||||
|
results.append(getattr(cls, name)(self, *args, **kargs))
|
||||||
|
return results
|
||||||
|
|
||||||
@classmethod
|
def generate_config(self, config_dir_path, server_name):
|
||||||
def generate_config(cls, args, config_dir_path):
|
default_config = "# vim:ft=yaml\n"
|
||||||
pass
|
|
||||||
|
default_config += "\n\n".join(dedent(conf) for conf in self.invoke_all(
|
||||||
|
"default_config", config_dir_path, server_name
|
||||||
|
))
|
||||||
|
|
||||||
|
config = yaml.load(default_config)
|
||||||
|
|
||||||
|
return default_config, config
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load_config(cls, description, argv, generate_section=None):
|
def load_config(cls, description, argv, generate_section=None):
|
||||||
|
obj = cls()
|
||||||
|
|
||||||
config_parser = argparse.ArgumentParser(add_help=False)
|
config_parser = argparse.ArgumentParser(add_help=False)
|
||||||
config_parser.add_argument(
|
config_parser.add_argument(
|
||||||
"-c", "--config-path",
|
"-c", "--config-path",
|
||||||
|
action="append",
|
||||||
metavar="CONFIG_FILE",
|
metavar="CONFIG_FILE",
|
||||||
help="Specify config file"
|
help="Specify config file. Can be given multiple times and"
|
||||||
|
" may specify directories containing *.yaml files."
|
||||||
)
|
)
|
||||||
config_parser.add_argument(
|
config_parser.add_argument(
|
||||||
"--generate-config",
|
"--generate-config",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Generate config file"
|
help="Generate a config file for the server name"
|
||||||
|
)
|
||||||
|
config_parser.add_argument(
|
||||||
|
"--generate-keys",
|
||||||
|
action="store_true",
|
||||||
|
help="Generate any missing key files then exit"
|
||||||
|
)
|
||||||
|
config_parser.add_argument(
|
||||||
|
"--keys-directory",
|
||||||
|
metavar="DIRECTORY",
|
||||||
|
help="Used with 'generate-*' options to specify where files such as"
|
||||||
|
" certs and signing keys should be stored in, unless explicitly"
|
||||||
|
" specified in the config."
|
||||||
|
)
|
||||||
|
config_parser.add_argument(
|
||||||
|
"-H", "--server-name",
|
||||||
|
help="The server name to generate a config file for"
|
||||||
)
|
)
|
||||||
config_args, remaining_args = config_parser.parse_known_args(argv)
|
config_args, remaining_args = config_parser.parse_known_args(argv)
|
||||||
|
|
||||||
|
generate_keys = config_args.generate_keys
|
||||||
|
|
||||||
|
config_files = []
|
||||||
|
if config_args.config_path:
|
||||||
|
for config_path in config_args.config_path:
|
||||||
|
if os.path.isdir(config_path):
|
||||||
|
# We accept specifying directories as config paths, we search
|
||||||
|
# inside that directory for all files matching *.yaml, and then
|
||||||
|
# we apply them in *sorted* order.
|
||||||
|
files = []
|
||||||
|
for entry in os.listdir(config_path):
|
||||||
|
entry_path = os.path.join(config_path, entry)
|
||||||
|
if not os.path.isfile(entry_path):
|
||||||
|
print (
|
||||||
|
"Found subdirectory in config directory: %r. IGNORING."
|
||||||
|
) % (entry_path, )
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not entry.endswith(".yaml"):
|
||||||
|
print (
|
||||||
|
"Found file in config directory that does not"
|
||||||
|
" end in '.yaml': %r. IGNORING."
|
||||||
|
) % (entry_path, )
|
||||||
|
continue
|
||||||
|
|
||||||
|
config_files.extend(sorted(files))
|
||||||
|
else:
|
||||||
|
config_files.append(config_path)
|
||||||
|
|
||||||
if config_args.generate_config:
|
if config_args.generate_config:
|
||||||
if not config_args.config_path:
|
if not config_files:
|
||||||
config_parser.error(
|
config_parser.error(
|
||||||
"Must specify where to generate the config file"
|
"Must supply a config file.\nA config file can be automatically"
|
||||||
|
" generated using \"--generate-config -H SERVER_NAME"
|
||||||
|
" -c CONFIG-FILE\""
|
||||||
)
|
)
|
||||||
config_dir_path = os.path.dirname(config_args.config_path)
|
(config_path,) = config_files
|
||||||
if os.path.exists(config_args.config_path):
|
if not os.path.exists(config_path):
|
||||||
defaults = cls.read_config_file(config_args.config_path)
|
if config_args.keys_directory:
|
||||||
|
config_dir_path = config_args.keys_directory
|
||||||
|
else:
|
||||||
|
config_dir_path = os.path.dirname(config_path)
|
||||||
|
config_dir_path = os.path.abspath(config_dir_path)
|
||||||
|
|
||||||
|
server_name = config_args.server_name
|
||||||
|
if not server_name:
|
||||||
|
print "Must specify a server_name to a generate config for."
|
||||||
|
sys.exit(1)
|
||||||
|
if not os.path.exists(config_dir_path):
|
||||||
|
os.makedirs(config_dir_path)
|
||||||
|
with open(config_path, "wb") as config_file:
|
||||||
|
config_bytes, config = obj.generate_config(
|
||||||
|
config_dir_path, server_name
|
||||||
|
)
|
||||||
|
obj.invoke_all("generate_files", config)
|
||||||
|
config_file.write(config_bytes)
|
||||||
|
print (
|
||||||
|
"A config file has been generated in %r for server name"
|
||||||
|
" %r with corresponding SSL keys and self-signed"
|
||||||
|
" certificates. Please review this file and customise it"
|
||||||
|
" to your needs."
|
||||||
|
) % (config_path, server_name)
|
||||||
|
print (
|
||||||
|
"If this server name is incorrect, you will need to"
|
||||||
|
" regenerate the SSL certificates"
|
||||||
|
)
|
||||||
|
sys.exit(0)
|
||||||
else:
|
else:
|
||||||
defaults = {}
|
print (
|
||||||
else:
|
"Config file %r already exists. Generating any missing key"
|
||||||
if config_args.config_path:
|
" files."
|
||||||
defaults = cls.read_config_file(config_args.config_path)
|
) % (config_path,)
|
||||||
else:
|
generate_keys = True
|
||||||
defaults = {}
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
parents=[config_parser],
|
parents=[config_parser],
|
||||||
description=description,
|
description=description,
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
)
|
)
|
||||||
cls.add_arguments(parser)
|
|
||||||
parser.set_defaults(**defaults)
|
|
||||||
|
|
||||||
|
obj.invoke_all("add_arguments", parser)
|
||||||
args = parser.parse_args(remaining_args)
|
args = parser.parse_args(remaining_args)
|
||||||
|
|
||||||
if config_args.generate_config:
|
if not config_files:
|
||||||
config_dir_path = os.path.dirname(config_args.config_path)
|
config_parser.error(
|
||||||
config_dir_path = os.path.abspath(config_dir_path)
|
"Must supply a config file.\nA config file can be automatically"
|
||||||
if not os.path.exists(config_dir_path):
|
" generated using \"--generate-config -H SERVER_NAME"
|
||||||
os.makedirs(config_dir_path)
|
" -c CONFIG-FILE\""
|
||||||
cls.generate_config(args, config_dir_path)
|
|
||||||
config = {}
|
|
||||||
for key, value in vars(args).items():
|
|
||||||
if (key not in set(["config_path", "generate_config"])
|
|
||||||
and value is not None):
|
|
||||||
config[key] = value
|
|
||||||
with open(config_args.config_path, "w") as config_file:
|
|
||||||
# TODO(mark/paul) We might want to output emacs-style mode
|
|
||||||
# markers as well as vim-style mode markers into the file,
|
|
||||||
# to further hint to people this is a YAML file.
|
|
||||||
config_file.write("# vim:ft=yaml\n")
|
|
||||||
yaml.dump(config, config_file, default_flow_style=False)
|
|
||||||
print (
|
|
||||||
"A config file has been generated in %s for server name"
|
|
||||||
" '%s' with corresponding SSL keys and self-signed"
|
|
||||||
" certificates. Please review this file and customise it to"
|
|
||||||
" your needs."
|
|
||||||
) % (
|
|
||||||
config_args.config_path, config['server_name']
|
|
||||||
)
|
|
||||||
print (
|
|
||||||
"If this server name is incorrect, you will need to regenerate"
|
|
||||||
" the SSL certificates"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if config_args.keys_directory:
|
||||||
|
config_dir_path = config_args.keys_directory
|
||||||
|
else:
|
||||||
|
config_dir_path = os.path.dirname(config_args.config_path[-1])
|
||||||
|
config_dir_path = os.path.abspath(config_dir_path)
|
||||||
|
|
||||||
|
specified_config = {}
|
||||||
|
for config_file in config_files:
|
||||||
|
yaml_config = cls.read_config_file(config_file)
|
||||||
|
specified_config.update(yaml_config)
|
||||||
|
|
||||||
|
server_name = specified_config["server_name"]
|
||||||
|
_, config = obj.generate_config(config_dir_path, server_name)
|
||||||
|
config.pop("log_config")
|
||||||
|
config.update(specified_config)
|
||||||
|
|
||||||
|
if generate_keys:
|
||||||
|
obj.invoke_all("generate_files", config)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
return cls(args)
|
obj.invoke_all("read_config", config)
|
||||||
|
|
||||||
|
obj.invoke_all("read_arguments", args)
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|||||||
@@ -17,15 +17,11 @@ from ._base import Config
|
|||||||
|
|
||||||
class AppServiceConfig(Config):
|
class AppServiceConfig(Config):
|
||||||
|
|
||||||
def __init__(self, args):
|
def read_config(self, config):
|
||||||
super(AppServiceConfig, self).__init__(args)
|
self.app_service_config_files = config.get("app_service_config_files", [])
|
||||||
self.app_service_config_files = args.app_service_config_files
|
|
||||||
|
|
||||||
@classmethod
|
def default_config(cls, config_dir_path, server_name):
|
||||||
def add_arguments(cls, parser):
|
return """\
|
||||||
super(AppServiceConfig, cls).add_arguments(parser)
|
# A list of application service config file to use
|
||||||
group = parser.add_argument_group("appservice")
|
app_service_config_files: []
|
||||||
group.add_argument(
|
"""
|
||||||
"--app-service-config-files", type=str, nargs='+',
|
|
||||||
help="A list of application service config files to use."
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -17,42 +17,31 @@ from ._base import Config
|
|||||||
|
|
||||||
class CaptchaConfig(Config):
|
class CaptchaConfig(Config):
|
||||||
|
|
||||||
def __init__(self, args):
|
def read_config(self, config):
|
||||||
super(CaptchaConfig, self).__init__(args)
|
self.recaptcha_private_key = config["recaptcha_private_key"]
|
||||||
self.recaptcha_private_key = args.recaptcha_private_key
|
self.recaptcha_public_key = config["recaptcha_public_key"]
|
||||||
self.recaptcha_public_key = args.recaptcha_public_key
|
self.enable_registration_captcha = config["enable_registration_captcha"]
|
||||||
self.enable_registration_captcha = args.enable_registration_captcha
|
self.captcha_bypass_secret = config.get("captcha_bypass_secret")
|
||||||
|
self.recaptcha_siteverify_api = config["recaptcha_siteverify_api"]
|
||||||
|
|
||||||
# XXX: This is used for more than just captcha
|
def default_config(self, config_dir_path, server_name):
|
||||||
self.captcha_ip_origin_is_x_forwarded = (
|
return """\
|
||||||
args.captcha_ip_origin_is_x_forwarded
|
## Captcha ##
|
||||||
)
|
|
||||||
self.captcha_bypass_secret = args.captcha_bypass_secret
|
|
||||||
|
|
||||||
@classmethod
|
# This Home Server's ReCAPTCHA public key.
|
||||||
def add_arguments(cls, parser):
|
recaptcha_private_key: "YOUR_PRIVATE_KEY"
|
||||||
super(CaptchaConfig, cls).add_arguments(parser)
|
|
||||||
group = parser.add_argument_group("recaptcha")
|
# This Home Server's ReCAPTCHA private key.
|
||||||
group.add_argument(
|
recaptcha_public_key: "YOUR_PUBLIC_KEY"
|
||||||
"--recaptcha-public-key", type=str, default="YOUR_PUBLIC_KEY",
|
|
||||||
help="This Home Server's ReCAPTCHA public key."
|
# Enables ReCaptcha checks when registering, preventing signup
|
||||||
)
|
# unless a captcha is answered. Requires a valid ReCaptcha
|
||||||
group.add_argument(
|
# public/private key.
|
||||||
"--recaptcha-private-key", type=str, default="YOUR_PRIVATE_KEY",
|
enable_registration_captcha: False
|
||||||
help="This Home Server's ReCAPTCHA private key."
|
|
||||||
)
|
# A secret key used to bypass the captcha test entirely.
|
||||||
group.add_argument(
|
#captcha_bypass_secret: "YOUR_SECRET_HERE"
|
||||||
"--enable-registration-captcha", type=bool, default=False,
|
|
||||||
help="Enables ReCaptcha checks when registering, preventing signup"
|
# The API endpoint to use for verifying m.login.recaptcha responses.
|
||||||
+ " unless a captcha is answered. Requires a valid ReCaptcha "
|
recaptcha_siteverify_api: "https://www.google.com/recaptcha/api/siteverify"
|
||||||
+ "public/private key."
|
"""
|
||||||
)
|
|
||||||
group.add_argument(
|
|
||||||
"--captcha_ip_origin_is_x_forwarded", type=bool, default=False,
|
|
||||||
help="When checking captchas, use the X-Forwarded-For (XFF) header"
|
|
||||||
+ " as the client IP and not the actual client IP."
|
|
||||||
)
|
|
||||||
group.add_argument(
|
|
||||||
"--captcha_bypass_secret", type=str,
|
|
||||||
help="A secret key used to bypass the captcha test entirely."
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -14,28 +14,21 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from ._base import Config
|
from ._base import Config
|
||||||
import os
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseConfig(Config):
|
class DatabaseConfig(Config):
|
||||||
def __init__(self, args):
|
|
||||||
super(DatabaseConfig, self).__init__(args)
|
|
||||||
if args.database_path == ":memory:":
|
|
||||||
self.database_path = ":memory:"
|
|
||||||
else:
|
|
||||||
self.database_path = self.abspath(args.database_path)
|
|
||||||
self.event_cache_size = self.parse_size(args.event_cache_size)
|
|
||||||
|
|
||||||
if args.database_config:
|
def read_config(self, config):
|
||||||
with open(args.database_config) as f:
|
self.event_cache_size = self.parse_size(
|
||||||
self.database_config = yaml.safe_load(f)
|
config.get("event_cache_size", "10K")
|
||||||
else:
|
)
|
||||||
|
|
||||||
|
self.database_config = config.get("database")
|
||||||
|
|
||||||
|
if self.database_config is None:
|
||||||
self.database_config = {
|
self.database_config = {
|
||||||
"name": "sqlite3",
|
"name": "sqlite3",
|
||||||
"args": {
|
"args": {},
|
||||||
"database": self.database_path,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
name = self.database_config.get("name", None)
|
name = self.database_config.get("name", None)
|
||||||
@@ -50,24 +43,37 @@ class DatabaseConfig(Config):
|
|||||||
else:
|
else:
|
||||||
raise RuntimeError("Unsupported database type '%s'" % (name,))
|
raise RuntimeError("Unsupported database type '%s'" % (name,))
|
||||||
|
|
||||||
@classmethod
|
self.set_databasepath(config.get("database_path"))
|
||||||
def add_arguments(cls, parser):
|
|
||||||
super(DatabaseConfig, cls).add_arguments(parser)
|
def default_config(self, config, config_dir_path):
|
||||||
|
database_path = self.abspath("homeserver.db")
|
||||||
|
return """\
|
||||||
|
# Database configuration
|
||||||
|
database:
|
||||||
|
# The database engine name
|
||||||
|
name: "sqlite3"
|
||||||
|
# Arguments to pass to the engine
|
||||||
|
args:
|
||||||
|
# Path to the database
|
||||||
|
database: "%(database_path)s"
|
||||||
|
|
||||||
|
# Number of events to cache in memory.
|
||||||
|
event_cache_size: "10K"
|
||||||
|
""" % locals()
|
||||||
|
|
||||||
|
def read_arguments(self, args):
|
||||||
|
self.set_databasepath(args.database_path)
|
||||||
|
|
||||||
|
def set_databasepath(self, database_path):
|
||||||
|
if database_path != ":memory:":
|
||||||
|
database_path = self.abspath(database_path)
|
||||||
|
if self.database_config.get("name", None) == "sqlite3":
|
||||||
|
if database_path is not None:
|
||||||
|
self.database_config["args"]["database"] = database_path
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
db_group = parser.add_argument_group("database")
|
db_group = parser.add_argument_group("database")
|
||||||
db_group.add_argument(
|
db_group.add_argument(
|
||||||
"-d", "--database-path", default="homeserver.db",
|
"-d", "--database-path", metavar="SQLITE_DATABASE_PATH",
|
||||||
metavar="SQLITE_DATABASE_PATH", help="The database name."
|
help="The path to a sqlite database to use."
|
||||||
)
|
)
|
||||||
db_group.add_argument(
|
|
||||||
"--event-cache-size", default="100K",
|
|
||||||
help="Number of events to cache in memory."
|
|
||||||
)
|
|
||||||
db_group.add_argument(
|
|
||||||
"--database-config", default=None,
|
|
||||||
help="Location of the database configuration file."
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def generate_config(cls, args, config_dir_path):
|
|
||||||
super(DatabaseConfig, cls).generate_config(args, config_dir_path)
|
|
||||||
args.database_path = os.path.abspath(args.database_path)
|
|
||||||
|
|||||||
@@ -25,15 +25,18 @@ from .registration import RegistrationConfig
|
|||||||
from .metrics import MetricsConfig
|
from .metrics import MetricsConfig
|
||||||
from .appservice import AppServiceConfig
|
from .appservice import AppServiceConfig
|
||||||
from .key import KeyConfig
|
from .key import KeyConfig
|
||||||
|
from .saml2 import SAML2Config
|
||||||
|
|
||||||
|
|
||||||
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
||||||
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
|
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
|
||||||
VoipConfig, RegistrationConfig,
|
VoipConfig, RegistrationConfig, MetricsConfig,
|
||||||
MetricsConfig, AppServiceConfig, KeyConfig,):
|
AppServiceConfig, KeyConfig, SAML2Config, ):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys
|
||||||
HomeServerConfig.load_config("Generate config", sys.argv[1:], "HomeServer")
|
sys.stdout.write(
|
||||||
|
HomeServerConfig().generate_config(sys.argv[1], sys.argv[2])[0]
|
||||||
|
)
|
||||||
|
|||||||
@@ -20,48 +20,58 @@ from syutil.crypto.signing_key import (
|
|||||||
is_signing_algorithm_supported, decode_verify_key_bytes
|
is_signing_algorithm_supported, decode_verify_key_bytes
|
||||||
)
|
)
|
||||||
from syutil.base64util import decode_base64
|
from syutil.base64util import decode_base64
|
||||||
|
from synapse.util.stringutils import random_string
|
||||||
|
|
||||||
|
|
||||||
class KeyConfig(Config):
|
class KeyConfig(Config):
|
||||||
|
|
||||||
def __init__(self, args):
|
def read_config(self, config):
|
||||||
super(KeyConfig, self).__init__(args)
|
self.signing_key = self.read_signing_key(config["signing_key_path"])
|
||||||
self.signing_key = self.read_signing_key(args.signing_key_path)
|
|
||||||
self.old_signing_keys = self.read_old_signing_keys(
|
self.old_signing_keys = self.read_old_signing_keys(
|
||||||
args.old_signing_key_path
|
config["old_signing_keys"]
|
||||||
|
)
|
||||||
|
self.key_refresh_interval = self.parse_duration(
|
||||||
|
config["key_refresh_interval"]
|
||||||
)
|
)
|
||||||
self.key_refresh_interval = args.key_refresh_interval
|
|
||||||
self.perspectives = self.read_perspectives(
|
self.perspectives = self.read_perspectives(
|
||||||
args.perspectives_config_path
|
config["perspectives"]
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
def default_config(self, config_dir_path, server_name):
|
||||||
def add_arguments(cls, parser):
|
base_key_name = os.path.join(config_dir_path, server_name)
|
||||||
super(KeyConfig, cls).add_arguments(parser)
|
return """\
|
||||||
key_group = parser.add_argument_group("keys")
|
## Signing Keys ##
|
||||||
key_group.add_argument("--signing-key-path",
|
|
||||||
help="The signing key to sign messages with")
|
|
||||||
key_group.add_argument("--old-signing-key-path",
|
|
||||||
help="The keys that the server used to sign"
|
|
||||||
" sign messages with but won't use"
|
|
||||||
" to sign new messages. E.g. it has"
|
|
||||||
" lost its private key")
|
|
||||||
key_group.add_argument("--key-refresh-interval",
|
|
||||||
default=24 * 60 * 60 * 1000, # 1 Day
|
|
||||||
help="How long a key response is valid for."
|
|
||||||
" Used to set the exipiry in /key/v2/."
|
|
||||||
" Controls how frequently servers will"
|
|
||||||
" query what keys are still valid")
|
|
||||||
key_group.add_argument("--perspectives-config-path",
|
|
||||||
help="The trusted servers to download signing"
|
|
||||||
" keys from")
|
|
||||||
|
|
||||||
def read_perspectives(self, perspectives_config_path):
|
# Path to the signing key to sign messages with
|
||||||
config = self.read_yaml_file(
|
signing_key_path: "%(base_key_name)s.signing.key"
|
||||||
perspectives_config_path, "perspectives_config_path"
|
|
||||||
)
|
# The keys that the server used to sign messages with but won't use
|
||||||
|
# to sign new messages. E.g. it has lost its private key
|
||||||
|
old_signing_keys: {}
|
||||||
|
# "ed25519:auto":
|
||||||
|
# # Base64 encoded public key
|
||||||
|
# key: "The public part of your old signing key."
|
||||||
|
# # Millisecond POSIX timestamp when the key expired.
|
||||||
|
# expired_ts: 123456789123
|
||||||
|
|
||||||
|
# How long key response published by this server is valid for.
|
||||||
|
# Used to set the valid_until_ts in /key/v2 APIs.
|
||||||
|
# Determines how quickly servers will query to check which keys
|
||||||
|
# are still valid.
|
||||||
|
key_refresh_interval: "1d" # 1 Day.
|
||||||
|
|
||||||
|
# The trusted servers to download signing keys from.
|
||||||
|
perspectives:
|
||||||
|
servers:
|
||||||
|
"matrix.org":
|
||||||
|
verify_keys:
|
||||||
|
"ed25519:auto":
|
||||||
|
key: "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw"
|
||||||
|
""" % locals()
|
||||||
|
|
||||||
|
def read_perspectives(self, perspectives_config):
|
||||||
servers = {}
|
servers = {}
|
||||||
for server_name, server_config in config["servers"].items():
|
for server_name, server_config in perspectives_config["servers"].items():
|
||||||
for key_id, key_data in server_config["verify_keys"].items():
|
for key_id, key_data in server_config["verify_keys"].items():
|
||||||
if is_signing_algorithm_supported(key_id):
|
if is_signing_algorithm_supported(key_id):
|
||||||
key_base64 = key_data["key"]
|
key_base64 = key_data["key"]
|
||||||
@@ -82,66 +92,42 @@ class KeyConfig(Config):
|
|||||||
" Try running again with --generate-config"
|
" Try running again with --generate-config"
|
||||||
)
|
)
|
||||||
|
|
||||||
def read_old_signing_keys(self, old_signing_key_path):
|
def read_old_signing_keys(self, old_signing_keys):
|
||||||
old_signing_keys = self.read_file(
|
keys = {}
|
||||||
old_signing_key_path, "old_signing_key"
|
for key_id, key_data in old_signing_keys.items():
|
||||||
)
|
if is_signing_algorithm_supported(key_id):
|
||||||
try:
|
key_base64 = key_data["key"]
|
||||||
return syutil.crypto.signing_key.read_old_signing_keys(
|
key_bytes = decode_base64(key_base64)
|
||||||
old_signing_keys.splitlines(True)
|
verify_key = decode_verify_key_bytes(key_id, key_bytes)
|
||||||
)
|
verify_key.expired_ts = key_data["expired_ts"]
|
||||||
except Exception:
|
keys[key_id] = verify_key
|
||||||
raise ConfigError(
|
else:
|
||||||
"Error reading old signing keys."
|
raise ConfigError(
|
||||||
)
|
"Unsupported signing algorithm for old key: %r" % (key_id,)
|
||||||
|
)
|
||||||
|
return keys
|
||||||
|
|
||||||
@classmethod
|
def generate_files(self, config):
|
||||||
def generate_config(cls, args, config_dir_path):
|
signing_key_path = config["signing_key_path"]
|
||||||
super(KeyConfig, cls).generate_config(args, config_dir_path)
|
if not os.path.exists(signing_key_path):
|
||||||
base_key_name = os.path.join(config_dir_path, args.server_name)
|
with open(signing_key_path, "w") as signing_key_file:
|
||||||
|
key_id = "a_" + random_string(4)
|
||||||
args.pid_file = os.path.abspath(args.pid_file)
|
|
||||||
|
|
||||||
if not args.signing_key_path:
|
|
||||||
args.signing_key_path = base_key_name + ".signing.key"
|
|
||||||
|
|
||||||
if not os.path.exists(args.signing_key_path):
|
|
||||||
with open(args.signing_key_path, "w") as signing_key_file:
|
|
||||||
syutil.crypto.signing_key.write_signing_keys(
|
syutil.crypto.signing_key.write_signing_keys(
|
||||||
signing_key_file,
|
signing_key_file,
|
||||||
(syutil.crypto.signing_key.generate_signing_key("auto"),),
|
(syutil.crypto.signing_key.generate_signing_key(key_id),),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
signing_keys = cls.read_file(args.signing_key_path, "signing_key")
|
signing_keys = self.read_file(signing_key_path, "signing_key")
|
||||||
if len(signing_keys.split("\n")[0].split()) == 1:
|
if len(signing_keys.split("\n")[0].split()) == 1:
|
||||||
# handle keys in the old format.
|
# handle keys in the old format.
|
||||||
|
key_id = "a_" + random_string(4)
|
||||||
key = syutil.crypto.signing_key.decode_signing_key_base64(
|
key = syutil.crypto.signing_key.decode_signing_key_base64(
|
||||||
syutil.crypto.signing_key.NACL_ED25519,
|
syutil.crypto.signing_key.NACL_ED25519,
|
||||||
"auto",
|
key_id,
|
||||||
signing_keys.split("\n")[0]
|
signing_keys.split("\n")[0]
|
||||||
)
|
)
|
||||||
with open(args.signing_key_path, "w") as signing_key_file:
|
with open(signing_key_path, "w") as signing_key_file:
|
||||||
syutil.crypto.signing_key.write_signing_keys(
|
syutil.crypto.signing_key.write_signing_keys(
|
||||||
signing_key_file,
|
signing_key_file,
|
||||||
(key,),
|
(key,),
|
||||||
)
|
)
|
||||||
|
|
||||||
if not args.old_signing_key_path:
|
|
||||||
args.old_signing_key_path = base_key_name + ".old.signing.keys"
|
|
||||||
|
|
||||||
if not os.path.exists(args.old_signing_key_path):
|
|
||||||
with open(args.old_signing_key_path, "w"):
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not args.perspectives_config_path:
|
|
||||||
args.perspectives_config_path = base_key_name + ".perspectives"
|
|
||||||
|
|
||||||
if not os.path.exists(args.perspectives_config_path):
|
|
||||||
with open(args.perspectives_config_path, "w") as perspectives_file:
|
|
||||||
perspectives_file.write(
|
|
||||||
'servers:\n'
|
|
||||||
' matrix.org:\n'
|
|
||||||
' verify_keys:\n'
|
|
||||||
' "ed25519:auto":\n'
|
|
||||||
' key: "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw"\n'
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -19,25 +19,88 @@ from twisted.python.log import PythonLoggingObserver
|
|||||||
import logging
|
import logging
|
||||||
import logging.config
|
import logging.config
|
||||||
import yaml
|
import yaml
|
||||||
|
from string import Template
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_LOG_CONFIG = Template("""
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
formatters:
|
||||||
|
precise:
|
||||||
|
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s\
|
||||||
|
- %(message)s'
|
||||||
|
|
||||||
|
filters:
|
||||||
|
context:
|
||||||
|
(): synapse.util.logcontext.LoggingContextFilter
|
||||||
|
request: ""
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
file:
|
||||||
|
class: logging.handlers.RotatingFileHandler
|
||||||
|
formatter: precise
|
||||||
|
filename: ${log_file}
|
||||||
|
maxBytes: 104857600
|
||||||
|
backupCount: 10
|
||||||
|
filters: [context]
|
||||||
|
level: INFO
|
||||||
|
console:
|
||||||
|
class: logging.StreamHandler
|
||||||
|
formatter: precise
|
||||||
|
|
||||||
|
loggers:
|
||||||
|
synapse:
|
||||||
|
level: INFO
|
||||||
|
|
||||||
|
synapse.storage.SQL:
|
||||||
|
level: INFO
|
||||||
|
|
||||||
|
root:
|
||||||
|
level: INFO
|
||||||
|
handlers: [file, console]
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
class LoggingConfig(Config):
|
class LoggingConfig(Config):
|
||||||
def __init__(self, args):
|
|
||||||
super(LoggingConfig, self).__init__(args)
|
|
||||||
self.verbosity = int(args.verbose) if args.verbose else None
|
|
||||||
self.log_config = self.abspath(args.log_config)
|
|
||||||
self.log_file = self.abspath(args.log_file)
|
|
||||||
|
|
||||||
@classmethod
|
def read_config(self, config):
|
||||||
|
self.verbosity = config.get("verbose", 0)
|
||||||
|
self.log_config = self.abspath(config.get("log_config"))
|
||||||
|
self.log_file = self.abspath(config.get("log_file"))
|
||||||
|
|
||||||
|
def default_config(self, config_dir_path, server_name):
|
||||||
|
log_file = self.abspath("homeserver.log")
|
||||||
|
log_config = self.abspath(
|
||||||
|
os.path.join(config_dir_path, server_name + ".log.config")
|
||||||
|
)
|
||||||
|
return """
|
||||||
|
# Logging verbosity level.
|
||||||
|
verbose: 0
|
||||||
|
|
||||||
|
# File to write logging to
|
||||||
|
log_file: "%(log_file)s"
|
||||||
|
|
||||||
|
# A yaml python logging config file
|
||||||
|
log_config: "%(log_config)s"
|
||||||
|
""" % locals()
|
||||||
|
|
||||||
|
def read_arguments(self, args):
|
||||||
|
if args.verbose is not None:
|
||||||
|
self.verbosity = args.verbose
|
||||||
|
if args.log_config is not None:
|
||||||
|
self.log_config = args.log_config
|
||||||
|
if args.log_file is not None:
|
||||||
|
self.log_file = args.log_file
|
||||||
|
|
||||||
def add_arguments(cls, parser):
|
def add_arguments(cls, parser):
|
||||||
super(LoggingConfig, cls).add_arguments(parser)
|
|
||||||
logging_group = parser.add_argument_group("logging")
|
logging_group = parser.add_argument_group("logging")
|
||||||
logging_group.add_argument(
|
logging_group.add_argument(
|
||||||
'-v', '--verbose', dest="verbose", action='count',
|
'-v', '--verbose', dest="verbose", action='count',
|
||||||
help="The verbosity level."
|
help="The verbosity level."
|
||||||
)
|
)
|
||||||
logging_group.add_argument(
|
logging_group.add_argument(
|
||||||
'-f', '--log-file', dest="log_file", default="homeserver.log",
|
'-f', '--log-file', dest="log_file",
|
||||||
help="File to log to."
|
help="File to log to."
|
||||||
)
|
)
|
||||||
logging_group.add_argument(
|
logging_group.add_argument(
|
||||||
@@ -45,6 +108,14 @@ class LoggingConfig(Config):
|
|||||||
help="Python logging config file"
|
help="Python logging config file"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def generate_files(self, config):
|
||||||
|
log_config = config.get("log_config")
|
||||||
|
if log_config and not os.path.exists(log_config):
|
||||||
|
with open(log_config, "wb") as log_config_file:
|
||||||
|
log_config_file.write(
|
||||||
|
DEFAULT_LOG_CONFIG.substitute(log_file=config["log_file"])
|
||||||
|
)
|
||||||
|
|
||||||
def setup_logging(self):
|
def setup_logging(self):
|
||||||
log_format = (
|
log_format = (
|
||||||
"%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s"
|
"%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s"
|
||||||
|
|||||||
@@ -17,20 +17,15 @@ from ._base import Config
|
|||||||
|
|
||||||
|
|
||||||
class MetricsConfig(Config):
|
class MetricsConfig(Config):
|
||||||
def __init__(self, args):
|
def read_config(self, config):
|
||||||
super(MetricsConfig, self).__init__(args)
|
self.enable_metrics = config["enable_metrics"]
|
||||||
self.enable_metrics = args.enable_metrics
|
self.metrics_port = config.get("metrics_port")
|
||||||
self.metrics_port = args.metrics_port
|
self.metrics_bind_host = config.get("metrics_bind_host", "127.0.0.1")
|
||||||
|
|
||||||
@classmethod
|
def default_config(self, config_dir_path, server_name):
|
||||||
def add_arguments(cls, parser):
|
return """\
|
||||||
super(MetricsConfig, cls).add_arguments(parser)
|
## Metrics ###
|
||||||
metrics_group = parser.add_argument_group("metrics")
|
|
||||||
metrics_group.add_argument(
|
# Enable collection and rendering of performance metrics
|
||||||
'--enable-metrics', dest="enable_metrics", action="store_true",
|
enable_metrics: False
|
||||||
help="Enable collection and rendering of performance metrics"
|
"""
|
||||||
)
|
|
||||||
metrics_group.add_argument(
|
|
||||||
'--metrics-port', metavar="PORT", type=int,
|
|
||||||
help="Separate port to accept metrics requests on (on localhost)"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -17,56 +17,42 @@ from ._base import Config
|
|||||||
|
|
||||||
class RatelimitConfig(Config):
|
class RatelimitConfig(Config):
|
||||||
|
|
||||||
def __init__(self, args):
|
def read_config(self, config):
|
||||||
super(RatelimitConfig, self).__init__(args)
|
self.rc_messages_per_second = config["rc_messages_per_second"]
|
||||||
self.rc_messages_per_second = args.rc_messages_per_second
|
self.rc_message_burst_count = config["rc_message_burst_count"]
|
||||||
self.rc_message_burst_count = args.rc_message_burst_count
|
|
||||||
|
|
||||||
self.federation_rc_window_size = args.federation_rc_window_size
|
self.federation_rc_window_size = config["federation_rc_window_size"]
|
||||||
self.federation_rc_sleep_limit = args.federation_rc_sleep_limit
|
self.federation_rc_sleep_limit = config["federation_rc_sleep_limit"]
|
||||||
self.federation_rc_sleep_delay = args.federation_rc_sleep_delay
|
self.federation_rc_sleep_delay = config["federation_rc_sleep_delay"]
|
||||||
self.federation_rc_reject_limit = args.federation_rc_reject_limit
|
self.federation_rc_reject_limit = config["federation_rc_reject_limit"]
|
||||||
self.federation_rc_concurrent = args.federation_rc_concurrent
|
self.federation_rc_concurrent = config["federation_rc_concurrent"]
|
||||||
|
|
||||||
@classmethod
|
def default_config(self, config_dir_path, server_name):
|
||||||
def add_arguments(cls, parser):
|
return """\
|
||||||
super(RatelimitConfig, cls).add_arguments(parser)
|
## Ratelimiting ##
|
||||||
rc_group = parser.add_argument_group("ratelimiting")
|
|
||||||
rc_group.add_argument(
|
|
||||||
"--rc-messages-per-second", type=float, default=0.2,
|
|
||||||
help="number of messages a client can send per second"
|
|
||||||
)
|
|
||||||
rc_group.add_argument(
|
|
||||||
"--rc-message-burst-count", type=float, default=10,
|
|
||||||
help="number of message a client can send before being throttled"
|
|
||||||
)
|
|
||||||
|
|
||||||
rc_group.add_argument(
|
# Number of messages a client can send per second
|
||||||
"--federation-rc-window-size", type=int, default=10000,
|
rc_messages_per_second: 0.2
|
||||||
help="The federation window size in milliseconds",
|
|
||||||
)
|
|
||||||
|
|
||||||
rc_group.add_argument(
|
# Number of message a client can send before being throttled
|
||||||
"--federation-rc-sleep-limit", type=int, default=10,
|
rc_message_burst_count: 10.0
|
||||||
help="The number of federation requests from a single server"
|
|
||||||
" in a window before the server will delay processing the"
|
|
||||||
" request.",
|
|
||||||
)
|
|
||||||
|
|
||||||
rc_group.add_argument(
|
# The federation window size in milliseconds
|
||||||
"--federation-rc-sleep-delay", type=int, default=500,
|
federation_rc_window_size: 1000
|
||||||
help="The duration in milliseconds to delay processing events from"
|
|
||||||
" remote servers by if they go over the sleep limit.",
|
|
||||||
)
|
|
||||||
|
|
||||||
rc_group.add_argument(
|
# The number of federation requests from a single server in a window
|
||||||
"--federation-rc-reject-limit", type=int, default=50,
|
# before the server will delay processing the request.
|
||||||
help="The maximum number of concurrent federation requests allowed"
|
federation_rc_sleep_limit: 10
|
||||||
" from a single server",
|
|
||||||
)
|
|
||||||
|
|
||||||
rc_group.add_argument(
|
# The duration in milliseconds to delay processing events from
|
||||||
"--federation-rc-concurrent", type=int, default=3,
|
# remote servers by if they go over the sleep limit.
|
||||||
help="The number of federation requests to concurrently process"
|
federation_rc_sleep_delay: 500
|
||||||
" from a single server",
|
|
||||||
)
|
# The maximum number of concurrent federation requests allowed
|
||||||
|
# from a single server
|
||||||
|
federation_rc_reject_limit: 50
|
||||||
|
|
||||||
|
# The number of federation requests to concurrently process from a
|
||||||
|
# single server
|
||||||
|
federation_rc_concurrent: 3
|
||||||
|
"""
|
||||||
|
|||||||
@@ -17,45 +17,44 @@ from ._base import Config
|
|||||||
|
|
||||||
from synapse.util.stringutils import random_string_with_symbols
|
from synapse.util.stringutils import random_string_with_symbols
|
||||||
|
|
||||||
import distutils.util
|
from distutils.util import strtobool
|
||||||
|
|
||||||
|
|
||||||
class RegistrationConfig(Config):
|
class RegistrationConfig(Config):
|
||||||
|
|
||||||
def __init__(self, args):
|
def read_config(self, config):
|
||||||
super(RegistrationConfig, self).__init__(args)
|
|
||||||
|
|
||||||
# `args.enable_registration` may either be a bool or a string depending
|
|
||||||
# on if the option was given a value (e.g. --enable-registration=true
|
|
||||||
# would set `args.enable_registration` to "true" not True.)
|
|
||||||
self.disable_registration = not bool(
|
self.disable_registration = not bool(
|
||||||
distutils.util.strtobool(str(args.enable_registration))
|
strtobool(str(config["enable_registration"]))
|
||||||
)
|
)
|
||||||
self.registration_shared_secret = args.registration_shared_secret
|
if "disable_registration" in config:
|
||||||
|
self.disable_registration = bool(
|
||||||
|
strtobool(str(config["disable_registration"]))
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
self.registration_shared_secret = config.get("registration_shared_secret")
|
||||||
def add_arguments(cls, parser):
|
|
||||||
super(RegistrationConfig, cls).add_arguments(parser)
|
def default_config(self, config_dir, server_name):
|
||||||
|
registration_shared_secret = random_string_with_symbols(50)
|
||||||
|
return """\
|
||||||
|
## Registration ##
|
||||||
|
|
||||||
|
# Enable registration for new users.
|
||||||
|
enable_registration: False
|
||||||
|
|
||||||
|
# If set, allows registration by anyone who also has the shared
|
||||||
|
# secret, even if registration is otherwise disabled.
|
||||||
|
registration_shared_secret: "%(registration_shared_secret)s"
|
||||||
|
""" % locals()
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
reg_group = parser.add_argument_group("registration")
|
reg_group = parser.add_argument_group("registration")
|
||||||
|
|
||||||
reg_group.add_argument(
|
reg_group.add_argument(
|
||||||
"--enable-registration",
|
"--enable-registration", action="store_true", default=None,
|
||||||
const=True,
|
help="Enable registration for new users."
|
||||||
default=False,
|
|
||||||
nargs='?',
|
|
||||||
help="Enable registration for new users.",
|
|
||||||
)
|
|
||||||
reg_group.add_argument(
|
|
||||||
"--registration-shared-secret", type=str,
|
|
||||||
help="If set, allows registration by anyone who also has the shared"
|
|
||||||
" secret, even if registration is otherwise disabled.",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
def read_arguments(self, args):
|
||||||
def generate_config(cls, args, config_dir_path):
|
if args.enable_registration is not None:
|
||||||
super(RegistrationConfig, cls).generate_config(args, config_dir_path)
|
self.disable_registration = not bool(
|
||||||
if args.enable_registration is None:
|
strtobool(str(args.enable_registration))
|
||||||
args.enable_registration = False
|
)
|
||||||
|
|
||||||
if args.registration_shared_secret is None:
|
|
||||||
args.registration_shared_secret = random_string_with_symbols(50)
|
|
||||||
|
|||||||
@@ -14,35 +14,87 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from ._base import Config
|
from ._base import Config
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
ThumbnailRequirement = namedtuple(
|
||||||
|
"ThumbnailRequirement", ["width", "height", "method", "media_type"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_thumbnail_requirements(thumbnail_sizes):
|
||||||
|
""" Takes a list of dictionaries with "width", "height", and "method" keys
|
||||||
|
and creates a map from image media types to the thumbnail size, thumnailing
|
||||||
|
method, and thumbnail media type to precalculate
|
||||||
|
|
||||||
|
Args:
|
||||||
|
thumbnail_sizes(list): List of dicts with "width", "height", and
|
||||||
|
"method" keys
|
||||||
|
Returns:
|
||||||
|
Dictionary mapping from media type string to list of
|
||||||
|
ThumbnailRequirement tuples.
|
||||||
|
"""
|
||||||
|
requirements = {}
|
||||||
|
for size in thumbnail_sizes:
|
||||||
|
width = size["width"]
|
||||||
|
height = size["height"]
|
||||||
|
method = size["method"]
|
||||||
|
jpeg_thumbnail = ThumbnailRequirement(width, height, method, "image/jpeg")
|
||||||
|
png_thumbnail = ThumbnailRequirement(width, height, method, "image/png")
|
||||||
|
requirements.setdefault("image/jpeg", []).append(jpeg_thumbnail)
|
||||||
|
requirements.setdefault("image/gif", []).append(png_thumbnail)
|
||||||
|
requirements.setdefault("image/png", []).append(png_thumbnail)
|
||||||
|
return {
|
||||||
|
media_type: tuple(thumbnails)
|
||||||
|
for media_type, thumbnails in requirements.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ContentRepositoryConfig(Config):
|
class ContentRepositoryConfig(Config):
|
||||||
def __init__(self, args):
|
def read_config(self, config):
|
||||||
super(ContentRepositoryConfig, self).__init__(args)
|
self.max_upload_size = self.parse_size(config["max_upload_size"])
|
||||||
self.max_upload_size = self.parse_size(args.max_upload_size)
|
self.max_image_pixels = self.parse_size(config["max_image_pixels"])
|
||||||
self.max_image_pixels = self.parse_size(args.max_image_pixels)
|
self.media_store_path = self.ensure_directory(config["media_store_path"])
|
||||||
self.media_store_path = self.ensure_directory(args.media_store_path)
|
self.uploads_path = self.ensure_directory(config["uploads_path"])
|
||||||
|
self.dynamic_thumbnails = config["dynamic_thumbnails"]
|
||||||
|
self.thumbnail_requirements = parse_thumbnail_requirements(
|
||||||
|
config["thumbnail_sizes"]
|
||||||
|
)
|
||||||
|
|
||||||
def parse_size(self, string):
|
def default_config(self, config_dir_path, server_name):
|
||||||
sizes = {"K": 1024, "M": 1024 * 1024}
|
media_store = self.default_path("media_store")
|
||||||
size = 1
|
uploads_path = self.default_path("uploads")
|
||||||
suffix = string[-1]
|
return """
|
||||||
if suffix in sizes:
|
# Directory where uploaded images and attachments are stored.
|
||||||
string = string[:-1]
|
media_store_path: "%(media_store)s"
|
||||||
size = sizes[suffix]
|
|
||||||
return int(string) * size
|
|
||||||
|
|
||||||
@classmethod
|
# Directory where in-progress uploads are stored.
|
||||||
def add_arguments(cls, parser):
|
uploads_path: "%(uploads_path)s"
|
||||||
super(ContentRepositoryConfig, cls).add_arguments(parser)
|
|
||||||
db_group = parser.add_argument_group("content_repository")
|
# The largest allowed upload size in bytes
|
||||||
db_group.add_argument(
|
max_upload_size: "10M"
|
||||||
"--max-upload-size", default="10M"
|
|
||||||
)
|
# Maximum number of pixels that will be thumbnailed
|
||||||
db_group.add_argument(
|
max_image_pixels: "32M"
|
||||||
"--media-store-path", default=cls.default_path("media_store")
|
|
||||||
)
|
# Whether to generate new thumbnails on the fly to precisely match
|
||||||
db_group.add_argument(
|
# the resolution requested by the client. If true then whenever
|
||||||
"--max-image-pixels", default="32M",
|
# a new resolution is requested by the client the server will
|
||||||
help="Maximum number of pixels that will be thumbnailed"
|
# generate a new thumbnail. If false the server will pick a thumbnail
|
||||||
)
|
# from a precalcualted list.
|
||||||
|
dynamic_thumbnails: false
|
||||||
|
|
||||||
|
# List of thumbnail to precalculate when an image is uploaded.
|
||||||
|
thumbnail_sizes:
|
||||||
|
- width: 32
|
||||||
|
height: 32
|
||||||
|
method: crop
|
||||||
|
- width: 96
|
||||||
|
height: 96
|
||||||
|
method: crop
|
||||||
|
- width: 320
|
||||||
|
height: 240
|
||||||
|
method: scale
|
||||||
|
- width: 640
|
||||||
|
height: 480
|
||||||
|
method: scale
|
||||||
|
""" % locals()
|
||||||
|
|||||||
54
synapse/config/saml2.py
Normal file
54
synapse/config/saml2.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 Ericsson
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from ._base import Config
|
||||||
|
|
||||||
|
|
||||||
|
class SAML2Config(Config):
|
||||||
|
"""SAML2 Configuration
|
||||||
|
Synapse uses pysaml2 libraries for providing SAML2 support
|
||||||
|
|
||||||
|
config_path: Path to the sp_conf.py configuration file
|
||||||
|
idp_redirect_url: Identity provider URL which will redirect
|
||||||
|
the user back to /login/saml2 with proper info.
|
||||||
|
|
||||||
|
sp_conf.py file is something like:
|
||||||
|
https://github.com/rohe/pysaml2/blob/master/example/sp-repoze/sp_conf.py.example
|
||||||
|
|
||||||
|
More information: https://pythonhosted.org/pysaml2/howto/config.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
def read_config(self, config):
|
||||||
|
saml2_config = config.get("saml2_config", None)
|
||||||
|
if saml2_config:
|
||||||
|
self.saml2_enabled = True
|
||||||
|
self.saml2_config_path = saml2_config["config_path"]
|
||||||
|
self.saml2_idp_redirect_url = saml2_config["idp_redirect_url"]
|
||||||
|
else:
|
||||||
|
self.saml2_enabled = False
|
||||||
|
self.saml2_config_path = None
|
||||||
|
self.saml2_idp_redirect_url = None
|
||||||
|
|
||||||
|
def default_config(self, config_dir_path, server_name):
|
||||||
|
return """
|
||||||
|
# Enable SAML2 for registration and login. Uses pysaml2
|
||||||
|
# config_path: Path to the sp_conf.py configuration file
|
||||||
|
# idp_redirect_url: Identity provider URL which will redirect
|
||||||
|
# the user back to /login/saml2 with proper info.
|
||||||
|
# See pysaml2 docs for format of config.
|
||||||
|
#saml2_config:
|
||||||
|
# config_path: "%s/sp_conf.py"
|
||||||
|
# idp_redirect_url: "http://%s/idp"
|
||||||
|
""" % (config_dir_path, server_name)
|
||||||
@@ -17,64 +17,212 @@ from ._base import Config
|
|||||||
|
|
||||||
|
|
||||||
class ServerConfig(Config):
|
class ServerConfig(Config):
|
||||||
def __init__(self, args):
|
|
||||||
super(ServerConfig, self).__init__(args)
|
|
||||||
self.server_name = args.server_name
|
|
||||||
self.bind_port = args.bind_port
|
|
||||||
self.bind_host = args.bind_host
|
|
||||||
self.unsecure_port = args.unsecure_port
|
|
||||||
self.daemonize = args.daemonize
|
|
||||||
self.pid_file = self.abspath(args.pid_file)
|
|
||||||
self.web_client = args.web_client
|
|
||||||
self.manhole = args.manhole
|
|
||||||
self.soft_file_limit = args.soft_file_limit
|
|
||||||
|
|
||||||
if not args.content_addr:
|
def read_config(self, config):
|
||||||
host = args.server_name
|
self.server_name = config["server_name"]
|
||||||
|
self.pid_file = self.abspath(config.get("pid_file"))
|
||||||
|
self.web_client = config["web_client"]
|
||||||
|
self.web_client_location = config.get("web_client_location", None)
|
||||||
|
self.soft_file_limit = config["soft_file_limit"]
|
||||||
|
self.daemonize = config.get("daemonize")
|
||||||
|
self.print_pidfile = config.get("print_pidfile")
|
||||||
|
self.use_frozen_dicts = config.get("use_frozen_dicts", True)
|
||||||
|
|
||||||
|
self.listeners = config.get("listeners", [])
|
||||||
|
|
||||||
|
bind_port = config.get("bind_port")
|
||||||
|
if bind_port:
|
||||||
|
self.listeners = []
|
||||||
|
bind_host = config.get("bind_host", "")
|
||||||
|
gzip_responses = config.get("gzip_responses", True)
|
||||||
|
|
||||||
|
names = ["client", "webclient"] if self.web_client else ["client"]
|
||||||
|
|
||||||
|
self.listeners.append({
|
||||||
|
"port": bind_port,
|
||||||
|
"bind_address": bind_host,
|
||||||
|
"tls": True,
|
||||||
|
"type": "http",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"names": names,
|
||||||
|
"compress": gzip_responses,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": ["federation"],
|
||||||
|
"compress": False,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
unsecure_port = config.get("unsecure_port", bind_port - 400)
|
||||||
|
if unsecure_port:
|
||||||
|
self.listeners.append({
|
||||||
|
"port": unsecure_port,
|
||||||
|
"bind_address": bind_host,
|
||||||
|
"tls": False,
|
||||||
|
"type": "http",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"names": names,
|
||||||
|
"compress": gzip_responses,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": ["federation"],
|
||||||
|
"compress": False,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
manhole = config.get("manhole")
|
||||||
|
if manhole:
|
||||||
|
self.listeners.append({
|
||||||
|
"port": manhole,
|
||||||
|
"bind_address": "127.0.0.1",
|
||||||
|
"type": "manhole",
|
||||||
|
})
|
||||||
|
|
||||||
|
metrics_port = config.get("metrics_port")
|
||||||
|
if metrics_port:
|
||||||
|
self.listeners.append({
|
||||||
|
"port": metrics_port,
|
||||||
|
"bind_address": config.get("metrics_bind_host", "127.0.0.1"),
|
||||||
|
"tls": False,
|
||||||
|
"type": "http",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"names": ["metrics"],
|
||||||
|
"compress": False,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
# Attempt to guess the content_addr for the v0 content repostitory
|
||||||
|
content_addr = config.get("content_addr")
|
||||||
|
if not content_addr:
|
||||||
|
for listener in self.listeners:
|
||||||
|
if listener["type"] == "http" and not listener.get("tls", False):
|
||||||
|
unsecure_port = listener["port"]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Could not determine 'content_addr'")
|
||||||
|
|
||||||
|
host = self.server_name
|
||||||
if ':' not in host:
|
if ':' not in host:
|
||||||
host = "%s:%d" % (host, args.unsecure_port)
|
host = "%s:%d" % (host, unsecure_port)
|
||||||
else:
|
else:
|
||||||
host = host.split(':')[0]
|
host = host.split(':')[0]
|
||||||
host = "%s:%d" % (host, args.unsecure_port)
|
host = "%s:%d" % (host, unsecure_port)
|
||||||
args.content_addr = "http://%s" % (host,)
|
content_addr = "http://%s" % (host,)
|
||||||
|
|
||||||
self.content_addr = args.content_addr
|
self.content_addr = content_addr
|
||||||
|
|
||||||
@classmethod
|
def default_config(self, config_dir_path, server_name):
|
||||||
def add_arguments(cls, parser):
|
if ":" in server_name:
|
||||||
super(ServerConfig, cls).add_arguments(parser)
|
bind_port = int(server_name.split(":")[1])
|
||||||
|
unsecure_port = bind_port - 400
|
||||||
|
else:
|
||||||
|
bind_port = 8448
|
||||||
|
unsecure_port = 8008
|
||||||
|
|
||||||
|
pid_file = self.abspath("homeserver.pid")
|
||||||
|
return """\
|
||||||
|
## Server ##
|
||||||
|
|
||||||
|
# The domain name of the server, with optional explicit port.
|
||||||
|
# This is used by remote servers to connect to this server,
|
||||||
|
# e.g. matrix.org, localhost:8080, etc.
|
||||||
|
server_name: "%(server_name)s"
|
||||||
|
|
||||||
|
# When running as a daemon, the file to store the pid in
|
||||||
|
pid_file: %(pid_file)s
|
||||||
|
|
||||||
|
# Whether to serve a web client from the HTTP/HTTPS root resource.
|
||||||
|
web_client: True
|
||||||
|
|
||||||
|
# Set the soft limit on the number of file descriptors synapse can use
|
||||||
|
# Zero is used to indicate synapse should set the soft limit to the
|
||||||
|
# hard limit.
|
||||||
|
soft_file_limit: 0
|
||||||
|
|
||||||
|
# List of ports that Synapse should listen on, their purpose and their
|
||||||
|
# configuration.
|
||||||
|
listeners:
|
||||||
|
# Main HTTPS listener
|
||||||
|
# For when matrix traffic is sent directly to synapse.
|
||||||
|
-
|
||||||
|
# The port to listen for HTTPS requests on.
|
||||||
|
port: %(bind_port)s
|
||||||
|
|
||||||
|
# Local interface to listen on.
|
||||||
|
# The empty string will cause synapse to listen on all interfaces.
|
||||||
|
bind_address: ''
|
||||||
|
|
||||||
|
# This is a 'http' listener, allows us to specify 'resources'.
|
||||||
|
type: http
|
||||||
|
|
||||||
|
tls: true
|
||||||
|
|
||||||
|
# Use the X-Forwarded-For (XFF) header as the client IP and not the
|
||||||
|
# actual client IP.
|
||||||
|
x_forwarded: false
|
||||||
|
|
||||||
|
# List of HTTP resources to serve on this listener.
|
||||||
|
resources:
|
||||||
|
-
|
||||||
|
# List of resources to host on this listener.
|
||||||
|
names:
|
||||||
|
- client # The client-server APIs, both v1 and v2
|
||||||
|
- webclient # The bundled webclient.
|
||||||
|
|
||||||
|
# Should synapse compress HTTP responses to clients that support it?
|
||||||
|
# This should be disabled if running synapse behind a load balancer
|
||||||
|
# that can do automatic compression.
|
||||||
|
compress: true
|
||||||
|
|
||||||
|
- names: [federation] # Federation APIs
|
||||||
|
compress: false
|
||||||
|
|
||||||
|
# Unsecure HTTP listener,
|
||||||
|
# For when matrix traffic passes through loadbalancer that unwraps TLS.
|
||||||
|
- port: %(unsecure_port)s
|
||||||
|
tls: false
|
||||||
|
bind_address: ''
|
||||||
|
type: http
|
||||||
|
|
||||||
|
x_forwarded: false
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- names: [client, webclient]
|
||||||
|
compress: true
|
||||||
|
- names: [federation]
|
||||||
|
compress: false
|
||||||
|
|
||||||
|
# Turn on the twisted telnet manhole service on localhost on the given
|
||||||
|
# port.
|
||||||
|
# - port: 9000
|
||||||
|
# bind_address: 127.0.0.1
|
||||||
|
# type: manhole
|
||||||
|
""" % locals()
|
||||||
|
|
||||||
|
def read_arguments(self, args):
|
||||||
|
if args.manhole is not None:
|
||||||
|
self.manhole = args.manhole
|
||||||
|
if args.daemonize is not None:
|
||||||
|
self.daemonize = args.daemonize
|
||||||
|
if args.print_pidfile is not None:
|
||||||
|
self.print_pidfile = args.print_pidfile
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
server_group = parser.add_argument_group("server")
|
server_group = parser.add_argument_group("server")
|
||||||
server_group.add_argument(
|
|
||||||
"-H", "--server-name", default="localhost",
|
|
||||||
help="The domain name of the server, with optional explicit port. "
|
|
||||||
"This is used by remote servers to connect to this server, "
|
|
||||||
"e.g. matrix.org, localhost:8080, etc."
|
|
||||||
)
|
|
||||||
server_group.add_argument("-p", "--bind-port", metavar="PORT",
|
|
||||||
type=int, help="https port to listen on",
|
|
||||||
default=8448)
|
|
||||||
server_group.add_argument("--unsecure-port", metavar="PORT",
|
|
||||||
type=int, help="http port to listen on",
|
|
||||||
default=8008)
|
|
||||||
server_group.add_argument("--bind-host", default="",
|
|
||||||
help="Local interface to listen on")
|
|
||||||
server_group.add_argument("-D", "--daemonize", action='store_true',
|
server_group.add_argument("-D", "--daemonize", action='store_true',
|
||||||
|
default=None,
|
||||||
help="Daemonize the home server")
|
help="Daemonize the home server")
|
||||||
server_group.add_argument('--pid-file', default="homeserver.pid",
|
server_group.add_argument("--print-pidfile", action='store_true',
|
||||||
help="When running as a daemon, the file to"
|
default=None,
|
||||||
" store the pid in")
|
help="Print the path to the pidfile just"
|
||||||
server_group.add_argument('--web_client', default=True, type=bool,
|
" before daemonizing")
|
||||||
help="Whether or not to serve a web client")
|
|
||||||
server_group.add_argument("--manhole", metavar="PORT", dest="manhole",
|
server_group.add_argument("--manhole", metavar="PORT", dest="manhole",
|
||||||
type=int,
|
type=int,
|
||||||
help="Turn on the twisted telnet manhole"
|
help="Turn on the twisted telnet manhole"
|
||||||
" service on the given port.")
|
" service on the given port.")
|
||||||
server_group.add_argument("--content-addr", default=None,
|
|
||||||
help="The host and scheme to use for the "
|
|
||||||
"content repository")
|
|
||||||
server_group.add_argument("--soft-file-limit", type=int, default=0,
|
|
||||||
help="Set the soft limit on the number of "
|
|
||||||
"file descriptors synapse can use. "
|
|
||||||
"Zero is used to indicate synapse "
|
|
||||||
"should set the soft limit to the hard"
|
|
||||||
"limit.")
|
|
||||||
|
|||||||
@@ -23,37 +23,49 @@ GENERATE_DH_PARAMS = False
|
|||||||
|
|
||||||
|
|
||||||
class TlsConfig(Config):
|
class TlsConfig(Config):
|
||||||
def __init__(self, args):
|
def read_config(self, config):
|
||||||
super(TlsConfig, self).__init__(args)
|
|
||||||
self.tls_certificate = self.read_tls_certificate(
|
self.tls_certificate = self.read_tls_certificate(
|
||||||
args.tls_certificate_path
|
config.get("tls_certificate_path")
|
||||||
)
|
)
|
||||||
|
self.tls_certificate_file = config.get("tls_certificate_path")
|
||||||
|
|
||||||
self.no_tls = args.no_tls
|
self.no_tls = config.get("no_tls", False)
|
||||||
|
|
||||||
if self.no_tls:
|
if self.no_tls:
|
||||||
self.tls_private_key = None
|
self.tls_private_key = None
|
||||||
else:
|
else:
|
||||||
self.tls_private_key = self.read_tls_private_key(
|
self.tls_private_key = self.read_tls_private_key(
|
||||||
args.tls_private_key_path
|
config.get("tls_private_key_path")
|
||||||
)
|
)
|
||||||
|
|
||||||
self.tls_dh_params_path = self.check_file(
|
self.tls_dh_params_path = self.check_file(
|
||||||
args.tls_dh_params_path, "tls_dh_params"
|
config.get("tls_dh_params_path"), "tls_dh_params"
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
def default_config(self, config_dir_path, server_name):
|
||||||
def add_arguments(cls, parser):
|
base_key_name = os.path.join(config_dir_path, server_name)
|
||||||
super(TlsConfig, cls).add_arguments(parser)
|
|
||||||
tls_group = parser.add_argument_group("tls")
|
tls_certificate_path = base_key_name + ".tls.crt"
|
||||||
tls_group.add_argument("--tls-certificate-path",
|
tls_private_key_path = base_key_name + ".tls.key"
|
||||||
help="PEM encoded X509 certificate for TLS")
|
tls_dh_params_path = base_key_name + ".tls.dh"
|
||||||
tls_group.add_argument("--tls-private-key-path",
|
|
||||||
help="PEM encoded private key for TLS")
|
return """\
|
||||||
tls_group.add_argument("--tls-dh-params-path",
|
# PEM encoded X509 certificate for TLS.
|
||||||
help="PEM dh parameters for ephemeral keys")
|
# You can replace the self-signed certificate that synapse
|
||||||
tls_group.add_argument("--no-tls", action='store_true',
|
# autogenerates on launch with your own SSL certificate + key pair
|
||||||
help="Don't bind to the https port.")
|
# if you like. Any required intermediary certificates can be
|
||||||
|
# appended after the primary certificate in hierarchical order.
|
||||||
|
tls_certificate_path: "%(tls_certificate_path)s"
|
||||||
|
|
||||||
|
# PEM encoded private key for TLS
|
||||||
|
tls_private_key_path: "%(tls_private_key_path)s"
|
||||||
|
|
||||||
|
# PEM dh parameters for ephemeral keys
|
||||||
|
tls_dh_params_path: "%(tls_dh_params_path)s"
|
||||||
|
|
||||||
|
# Don't bind to the https port
|
||||||
|
no_tls: False
|
||||||
|
""" % locals()
|
||||||
|
|
||||||
def read_tls_certificate(self, cert_path):
|
def read_tls_certificate(self, cert_path):
|
||||||
cert_pem = self.read_file(cert_path, "tls_certificate")
|
cert_pem = self.read_file(cert_path, "tls_certificate")
|
||||||
@@ -63,22 +75,13 @@ class TlsConfig(Config):
|
|||||||
private_key_pem = self.read_file(private_key_path, "tls_private_key")
|
private_key_pem = self.read_file(private_key_path, "tls_private_key")
|
||||||
return crypto.load_privatekey(crypto.FILETYPE_PEM, private_key_pem)
|
return crypto.load_privatekey(crypto.FILETYPE_PEM, private_key_pem)
|
||||||
|
|
||||||
@classmethod
|
def generate_files(self, config):
|
||||||
def generate_config(cls, args, config_dir_path):
|
tls_certificate_path = config["tls_certificate_path"]
|
||||||
super(TlsConfig, cls).generate_config(args, config_dir_path)
|
tls_private_key_path = config["tls_private_key_path"]
|
||||||
base_key_name = os.path.join(config_dir_path, args.server_name)
|
tls_dh_params_path = config["tls_dh_params_path"]
|
||||||
|
|
||||||
if args.tls_certificate_path is None:
|
if not os.path.exists(tls_private_key_path):
|
||||||
args.tls_certificate_path = base_key_name + ".tls.crt"
|
with open(tls_private_key_path, "w") as private_key_file:
|
||||||
|
|
||||||
if args.tls_private_key_path is None:
|
|
||||||
args.tls_private_key_path = base_key_name + ".tls.key"
|
|
||||||
|
|
||||||
if args.tls_dh_params_path is None:
|
|
||||||
args.tls_dh_params_path = base_key_name + ".tls.dh"
|
|
||||||
|
|
||||||
if not os.path.exists(args.tls_private_key_path):
|
|
||||||
with open(args.tls_private_key_path, "w") as private_key_file:
|
|
||||||
tls_private_key = crypto.PKey()
|
tls_private_key = crypto.PKey()
|
||||||
tls_private_key.generate_key(crypto.TYPE_RSA, 2048)
|
tls_private_key.generate_key(crypto.TYPE_RSA, 2048)
|
||||||
private_key_pem = crypto.dump_privatekey(
|
private_key_pem = crypto.dump_privatekey(
|
||||||
@@ -86,17 +89,17 @@ class TlsConfig(Config):
|
|||||||
)
|
)
|
||||||
private_key_file.write(private_key_pem)
|
private_key_file.write(private_key_pem)
|
||||||
else:
|
else:
|
||||||
with open(args.tls_private_key_path) as private_key_file:
|
with open(tls_private_key_path) as private_key_file:
|
||||||
private_key_pem = private_key_file.read()
|
private_key_pem = private_key_file.read()
|
||||||
tls_private_key = crypto.load_privatekey(
|
tls_private_key = crypto.load_privatekey(
|
||||||
crypto.FILETYPE_PEM, private_key_pem
|
crypto.FILETYPE_PEM, private_key_pem
|
||||||
)
|
)
|
||||||
|
|
||||||
if not os.path.exists(args.tls_certificate_path):
|
if not os.path.exists(tls_certificate_path):
|
||||||
with open(args.tls_certificate_path, "w") as certifcate_file:
|
with open(tls_certificate_path, "w") as certificate_file:
|
||||||
cert = crypto.X509()
|
cert = crypto.X509()
|
||||||
subject = cert.get_subject()
|
subject = cert.get_subject()
|
||||||
subject.CN = args.server_name
|
subject.CN = config["server_name"]
|
||||||
|
|
||||||
cert.set_serial_number(1000)
|
cert.set_serial_number(1000)
|
||||||
cert.gmtime_adj_notBefore(0)
|
cert.gmtime_adj_notBefore(0)
|
||||||
@@ -108,18 +111,18 @@ class TlsConfig(Config):
|
|||||||
|
|
||||||
cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
||||||
|
|
||||||
certifcate_file.write(cert_pem)
|
certificate_file.write(cert_pem)
|
||||||
|
|
||||||
if not os.path.exists(args.tls_dh_params_path):
|
if not os.path.exists(tls_dh_params_path):
|
||||||
if GENERATE_DH_PARAMS:
|
if GENERATE_DH_PARAMS:
|
||||||
subprocess.check_call([
|
subprocess.check_call([
|
||||||
"openssl", "dhparam",
|
"openssl", "dhparam",
|
||||||
"-outform", "PEM",
|
"-outform", "PEM",
|
||||||
"-out", args.tls_dh_params_path,
|
"-out", tls_dh_params_path,
|
||||||
"2048"
|
"2048"
|
||||||
])
|
])
|
||||||
else:
|
else:
|
||||||
with open(args.tls_dh_params_path, "w") as dh_params_file:
|
with open(tls_dh_params_path, "w") as dh_params_file:
|
||||||
dh_params_file.write(
|
dh_params_file.write(
|
||||||
"2048-bit DH parameters taken from rfc3526\n"
|
"2048-bit DH parameters taken from rfc3526\n"
|
||||||
"-----BEGIN DH PARAMETERS-----\n"
|
"-----BEGIN DH PARAMETERS-----\n"
|
||||||
|
|||||||
@@ -17,28 +17,21 @@ from ._base import Config
|
|||||||
|
|
||||||
class VoipConfig(Config):
|
class VoipConfig(Config):
|
||||||
|
|
||||||
def __init__(self, args):
|
def read_config(self, config):
|
||||||
super(VoipConfig, self).__init__(args)
|
self.turn_uris = config.get("turn_uris", [])
|
||||||
self.turn_uris = args.turn_uris
|
self.turn_shared_secret = config["turn_shared_secret"]
|
||||||
self.turn_shared_secret = args.turn_shared_secret
|
self.turn_user_lifetime = self.parse_duration(config["turn_user_lifetime"])
|
||||||
self.turn_user_lifetime = args.turn_user_lifetime
|
|
||||||
|
|
||||||
@classmethod
|
def default_config(self, config_dir_path, server_name):
|
||||||
def add_arguments(cls, parser):
|
return """\
|
||||||
super(VoipConfig, cls).add_arguments(parser)
|
## Turn ##
|
||||||
group = parser.add_argument_group("voip")
|
|
||||||
group.add_argument(
|
# The public URIs of the TURN server to give to clients
|
||||||
"--turn-uris", type=str, default=None, action='append',
|
turn_uris: []
|
||||||
help="The public URIs of the TURN server to give to clients"
|
|
||||||
)
|
# The shared secret used to compute passwords for the TURN server
|
||||||
group.add_argument(
|
turn_shared_secret: "YOUR_SHARED_SECRET"
|
||||||
"--turn-shared-secret", type=str, default=None,
|
|
||||||
help=(
|
# How long generated TURN credentials last
|
||||||
"The shared secret used to compute passwords for the TURN"
|
turn_user_lifetime: "1h"
|
||||||
" server"
|
"""
|
||||||
)
|
|
||||||
)
|
|
||||||
group.add_argument(
|
|
||||||
"--turn-user-lifetime", type=int, default=(1000 * 60 * 60),
|
|
||||||
help="How long generated TURN credentials last, in ms"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ class ServerContextFactory(ssl.ContextFactory):
|
|||||||
_ecCurve = _OpenSSLECCurve(_defaultCurveName)
|
_ecCurve = _OpenSSLECCurve(_defaultCurveName)
|
||||||
_ecCurve.addECKeyToContext(context)
|
_ecCurve.addECKeyToContext(context)
|
||||||
except:
|
except:
|
||||||
logger.exception("Failed to enable eliptic curve for TLS")
|
logger.exception("Failed to enable elliptic curve for TLS")
|
||||||
context.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
|
context.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
|
||||||
context.use_certificate(config.tls_certificate)
|
context.use_certificate_chain_file(config.tls_certificate_file)
|
||||||
|
|
||||||
if not config.no_tls:
|
if not config.no_tls:
|
||||||
context.use_privatekey(config.tls_private_key)
|
context.use_privatekey(config.tls_private_key)
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ from twisted.web.http import HTTPClient
|
|||||||
from twisted.internet.protocol import Factory
|
from twisted.internet.protocol import Factory
|
||||||
from twisted.internet import defer, reactor
|
from twisted.internet import defer, reactor
|
||||||
from synapse.http.endpoint import matrix_federation_endpoint
|
from synapse.http.endpoint import matrix_federation_endpoint
|
||||||
from synapse.util.logcontext import PreserveLoggingContext
|
from synapse.util.logcontext import (
|
||||||
|
preserve_context_over_fn, preserve_context_over_deferred
|
||||||
|
)
|
||||||
import simplejson as json
|
import simplejson as json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -40,11 +42,14 @@ def fetch_server_key(server_name, ssl_context_factory, path=KEY_API_V1):
|
|||||||
|
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
try:
|
try:
|
||||||
with PreserveLoggingContext():
|
protocol = yield preserve_context_over_fn(
|
||||||
protocol = yield endpoint.connect(factory)
|
endpoint.connect, factory
|
||||||
server_response, server_certificate = yield protocol.remote_key
|
)
|
||||||
defer.returnValue((server_response, server_certificate))
|
server_response, server_certificate = yield preserve_context_over_deferred(
|
||||||
return
|
protocol.remote_key
|
||||||
|
)
|
||||||
|
defer.returnValue((server_response, server_certificate))
|
||||||
|
return
|
||||||
except SynapseKeyClientError as e:
|
except SynapseKeyClientError as e:
|
||||||
logger.exception("Error getting key for %r" % (server_name,))
|
logger.exception("Error getting key for %r" % (server_name,))
|
||||||
if e.status.startswith("4"):
|
if e.status.startswith("4"):
|
||||||
|
|||||||
@@ -25,11 +25,13 @@ from syutil.base64util import decode_base64, encode_base64
|
|||||||
from synapse.api.errors import SynapseError, Codes
|
from synapse.api.errors import SynapseError, Codes
|
||||||
|
|
||||||
from synapse.util.retryutils import get_retry_limiter
|
from synapse.util.retryutils import get_retry_limiter
|
||||||
|
from synapse.util import unwrapFirstError
|
||||||
|
|
||||||
from synapse.util.async import create_observer
|
from synapse.util.async import ObservableDeferred
|
||||||
|
|
||||||
from OpenSSL import crypto
|
from OpenSSL import crypto
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
import urllib
|
import urllib
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
@@ -38,6 +40,9 @@ import logging
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
KeyGroup = namedtuple("KeyGroup", ("server_name", "group_id", "key_ids"))
|
||||||
|
|
||||||
|
|
||||||
class Keyring(object):
|
class Keyring(object):
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
@@ -49,131 +54,325 @@ class Keyring(object):
|
|||||||
|
|
||||||
self.key_downloads = {}
|
self.key_downloads = {}
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def verify_json_for_server(self, server_name, json_object):
|
def verify_json_for_server(self, server_name, json_object):
|
||||||
logger.debug("Verifying for %s", server_name)
|
return self.verify_json_objects_for_server(
|
||||||
key_ids = signature_ids(json_object, server_name)
|
[(server_name, json_object)]
|
||||||
if not key_ids:
|
)[0]
|
||||||
raise SynapseError(
|
|
||||||
400,
|
|
||||||
"Not signed with a supported algorithm",
|
|
||||||
Codes.UNAUTHORIZED,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
verify_key = yield self.get_server_verify_key(server_name, key_ids)
|
|
||||||
except IOError as e:
|
|
||||||
logger.warn(
|
|
||||||
"Got IOError when downloading keys for %s: %s %s",
|
|
||||||
server_name, type(e).__name__, str(e.message),
|
|
||||||
)
|
|
||||||
raise SynapseError(
|
|
||||||
502,
|
|
||||||
"Error downloading keys for %s" % (server_name,),
|
|
||||||
Codes.UNAUTHORIZED,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warn(
|
|
||||||
"Got Exception when downloading keys for %s: %s %s",
|
|
||||||
server_name, type(e).__name__, str(e.message),
|
|
||||||
)
|
|
||||||
raise SynapseError(
|
|
||||||
401,
|
|
||||||
"No key for %s with id %s" % (server_name, key_ids),
|
|
||||||
Codes.UNAUTHORIZED,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
def verify_json_objects_for_server(self, server_and_json):
|
||||||
verify_signed_json(json_object, server_name, verify_key)
|
"""Bulk verfies signatures of json objects, bulk fetching keys as
|
||||||
except:
|
necessary.
|
||||||
raise SynapseError(
|
|
||||||
401,
|
|
||||||
"Invalid signature for server %s with key %s:%s" % (
|
|
||||||
server_name, verify_key.alg, verify_key.version
|
|
||||||
),
|
|
||||||
Codes.UNAUTHORIZED,
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def get_server_verify_key(self, server_name, key_ids):
|
|
||||||
"""Finds a verification key for the server with one of the key ids.
|
|
||||||
Trys to fetch the key from a trusted perspective server first.
|
|
||||||
Args:
|
Args:
|
||||||
server_name(str): The name of the server to fetch a key for.
|
server_and_json (list): List of pairs of (server_name, json_object)
|
||||||
keys_ids (list of str): The key_ids to check for.
|
|
||||||
|
Returns:
|
||||||
|
list of deferreds indicating success or failure to verify each
|
||||||
|
json object's signature for the given server_name.
|
||||||
"""
|
"""
|
||||||
cached = yield self.store.get_server_verify_keys(server_name, key_ids)
|
group_id_to_json = {}
|
||||||
|
group_id_to_group = {}
|
||||||
|
group_ids = []
|
||||||
|
|
||||||
if cached:
|
next_group_id = 0
|
||||||
defer.returnValue(cached[0])
|
deferreds = {}
|
||||||
return
|
|
||||||
|
|
||||||
download = self.key_downloads.get(server_name)
|
for server_name, json_object in server_and_json:
|
||||||
|
logger.debug("Verifying for %s", server_name)
|
||||||
|
group_id = next_group_id
|
||||||
|
next_group_id += 1
|
||||||
|
group_ids.append(group_id)
|
||||||
|
|
||||||
if download is None:
|
key_ids = signature_ids(json_object, server_name)
|
||||||
download = self._get_server_verify_key_impl(server_name, key_ids)
|
if not key_ids:
|
||||||
self.key_downloads[server_name] = download
|
deferreds[group_id] = defer.fail(SynapseError(
|
||||||
|
400,
|
||||||
|
"Not signed with a supported algorithm",
|
||||||
|
Codes.UNAUTHORIZED,
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
deferreds[group_id] = defer.Deferred()
|
||||||
|
|
||||||
@download.addBoth
|
group = KeyGroup(server_name, group_id, key_ids)
|
||||||
def callback(ret):
|
|
||||||
del self.key_downloads[server_name]
|
|
||||||
return ret
|
|
||||||
|
|
||||||
r = yield create_observer(download)
|
group_id_to_group[group_id] = group
|
||||||
defer.returnValue(r)
|
group_id_to_json[group_id] = json_object
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _get_server_verify_key_impl(self, server_name, key_ids):
|
def handle_key_deferred(group, deferred):
|
||||||
keys = None
|
server_name = group.server_name
|
||||||
|
try:
|
||||||
|
_, _, key_id, verify_key = yield deferred
|
||||||
|
except IOError as e:
|
||||||
|
logger.warn(
|
||||||
|
"Got IOError when downloading keys for %s: %s %s",
|
||||||
|
server_name, type(e).__name__, str(e.message),
|
||||||
|
)
|
||||||
|
raise SynapseError(
|
||||||
|
502,
|
||||||
|
"Error downloading keys for %s" % (server_name,),
|
||||||
|
Codes.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(
|
||||||
|
"Got Exception when downloading keys for %s: %s %s",
|
||||||
|
server_name, type(e).__name__, str(e.message),
|
||||||
|
)
|
||||||
|
raise SynapseError(
|
||||||
|
401,
|
||||||
|
"No key for %s with id %s" % (server_name, key_ids),
|
||||||
|
Codes.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
|
||||||
perspective_results = []
|
json_object = group_id_to_json[group.group_id]
|
||||||
for perspective_name, perspective_keys in self.perspective_servers.items():
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def get_key():
|
|
||||||
try:
|
|
||||||
result = yield self.get_server_verify_key_v2_indirect(
|
|
||||||
server_name, key_ids, perspective_name, perspective_keys
|
|
||||||
)
|
|
||||||
defer.returnValue(result)
|
|
||||||
except:
|
|
||||||
logging.info(
|
|
||||||
"Unable to getting key %r for %r from %r",
|
|
||||||
key_ids, server_name, perspective_name,
|
|
||||||
)
|
|
||||||
perspective_results.append(get_key())
|
|
||||||
|
|
||||||
perspective_results = yield defer.gatherResults(perspective_results)
|
try:
|
||||||
|
verify_signed_json(json_object, server_name, verify_key)
|
||||||
|
except:
|
||||||
|
raise SynapseError(
|
||||||
|
401,
|
||||||
|
"Invalid signature for server %s with key %s:%s" % (
|
||||||
|
server_name, verify_key.alg, verify_key.version
|
||||||
|
),
|
||||||
|
Codes.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
|
||||||
for results in perspective_results:
|
server_to_deferred = {
|
||||||
if results is not None:
|
server_name: defer.Deferred()
|
||||||
keys = results
|
for server_name, _ in server_and_json
|
||||||
|
}
|
||||||
|
|
||||||
limiter = yield get_retry_limiter(
|
# We want to wait for any previous lookups to complete before
|
||||||
server_name,
|
# proceeding.
|
||||||
self.clock,
|
wait_on_deferred = self.wait_for_previous_lookups(
|
||||||
self.store,
|
[server_name for server_name, _ in server_and_json],
|
||||||
|
server_to_deferred,
|
||||||
)
|
)
|
||||||
|
|
||||||
with limiter:
|
# Actually start fetching keys.
|
||||||
if keys is None:
|
wait_on_deferred.addBoth(
|
||||||
|
lambda _: self.get_server_verify_keys(group_id_to_group, deferreds)
|
||||||
|
)
|
||||||
|
|
||||||
|
# When we've finished fetching all the keys for a given server_name,
|
||||||
|
# resolve the deferred passed to `wait_for_previous_lookups` so that
|
||||||
|
# any lookups waiting will proceed.
|
||||||
|
server_to_gids = {}
|
||||||
|
|
||||||
|
def remove_deferreds(res, server_name, group_id):
|
||||||
|
server_to_gids[server_name].discard(group_id)
|
||||||
|
if not server_to_gids[server_name]:
|
||||||
|
server_to_deferred.pop(server_name).callback(None)
|
||||||
|
return res
|
||||||
|
|
||||||
|
for g_id, deferred in deferreds.items():
|
||||||
|
server_name = group_id_to_group[g_id].server_name
|
||||||
|
server_to_gids.setdefault(server_name, set()).add(g_id)
|
||||||
|
deferred.addBoth(remove_deferreds, server_name, g_id)
|
||||||
|
|
||||||
|
# Pass those keys to handle_key_deferred so that the json object
|
||||||
|
# signatures can be verified
|
||||||
|
return [
|
||||||
|
handle_key_deferred(
|
||||||
|
group_id_to_group[g_id],
|
||||||
|
deferreds[g_id],
|
||||||
|
)
|
||||||
|
for g_id in group_ids
|
||||||
|
]
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def wait_for_previous_lookups(self, server_names, server_to_deferred):
|
||||||
|
"""Waits for any previous key lookups for the given servers to finish.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
server_names (list): list of server_names we want to lookup
|
||||||
|
server_to_deferred (dict): server_name to deferred which gets
|
||||||
|
resolved once we've finished looking up keys for that server
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
wait_on = [
|
||||||
|
self.key_downloads[server_name]
|
||||||
|
for server_name in server_names
|
||||||
|
if server_name in self.key_downloads
|
||||||
|
]
|
||||||
|
if wait_on:
|
||||||
|
yield defer.DeferredList(wait_on)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
for server_name, deferred in server_to_deferred:
|
||||||
|
self.key_downloads[server_name] = ObservableDeferred(deferred)
|
||||||
|
|
||||||
|
def get_server_verify_keys(self, group_id_to_group, group_id_to_deferred):
|
||||||
|
"""Takes a dict of KeyGroups and tries to find at least one key for
|
||||||
|
each group.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# These are functions that produce keys given a list of key ids
|
||||||
|
key_fetch_fns = (
|
||||||
|
self.get_keys_from_store, # First try the local store
|
||||||
|
self.get_keys_from_perspectives, # Then try via perspectives
|
||||||
|
self.get_keys_from_server, # Then try directly
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def do_iterations():
|
||||||
|
merged_results = {}
|
||||||
|
|
||||||
|
missing_keys = {
|
||||||
|
group.server_name: key_id
|
||||||
|
for group in group_id_to_group.values()
|
||||||
|
for key_id in group.key_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
for fn in key_fetch_fns:
|
||||||
|
results = yield fn(missing_keys.items())
|
||||||
|
merged_results.update(results)
|
||||||
|
|
||||||
|
# We now need to figure out which groups we have keys for
|
||||||
|
# and which we don't
|
||||||
|
missing_groups = {}
|
||||||
|
for group in group_id_to_group.values():
|
||||||
|
for key_id in group.key_ids:
|
||||||
|
if key_id in merged_results[group.server_name]:
|
||||||
|
group_id_to_deferred[group.group_id].callback((
|
||||||
|
group.group_id,
|
||||||
|
group.server_name,
|
||||||
|
key_id,
|
||||||
|
merged_results[group.server_name][key_id],
|
||||||
|
))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
missing_groups.setdefault(
|
||||||
|
group.server_name, []
|
||||||
|
).append(group)
|
||||||
|
|
||||||
|
if not missing_groups:
|
||||||
|
break
|
||||||
|
|
||||||
|
missing_keys = {
|
||||||
|
server_name: set(
|
||||||
|
key_id for group in groups for key_id in group.key_ids
|
||||||
|
)
|
||||||
|
for server_name, groups in missing_groups.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
for group in missing_groups.values():
|
||||||
|
group_id_to_deferred[group.group_id].errback(SynapseError(
|
||||||
|
401,
|
||||||
|
"No key for %s with id %s" % (
|
||||||
|
group.server_name, group.key_ids,
|
||||||
|
),
|
||||||
|
Codes.UNAUTHORIZED,
|
||||||
|
))
|
||||||
|
|
||||||
|
def on_err(err):
|
||||||
|
for deferred in group_id_to_deferred.values():
|
||||||
|
if not deferred.called:
|
||||||
|
deferred.errback(err)
|
||||||
|
|
||||||
|
do_iterations().addErrback(on_err)
|
||||||
|
|
||||||
|
return group_id_to_deferred
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_keys_from_store(self, server_name_and_key_ids):
|
||||||
|
res = yield defer.gatherResults(
|
||||||
|
[
|
||||||
|
self.store.get_server_verify_keys(server_name, key_ids)
|
||||||
|
for server_name, key_ids in server_name_and_key_ids
|
||||||
|
],
|
||||||
|
consumeErrors=True,
|
||||||
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
|
defer.returnValue(dict(zip(
|
||||||
|
[server_name for server_name, _ in server_name_and_key_ids],
|
||||||
|
res
|
||||||
|
)))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_keys_from_perspectives(self, server_name_and_key_ids):
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_key(perspective_name, perspective_keys):
|
||||||
|
try:
|
||||||
|
result = yield self.get_server_verify_key_v2_indirect(
|
||||||
|
server_name_and_key_ids, perspective_name, perspective_keys
|
||||||
|
)
|
||||||
|
defer.returnValue(result)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(
|
||||||
|
"Unable to get key from %r: %s %s",
|
||||||
|
perspective_name,
|
||||||
|
type(e).__name__, str(e.message),
|
||||||
|
)
|
||||||
|
defer.returnValue({})
|
||||||
|
|
||||||
|
results = yield defer.gatherResults(
|
||||||
|
[
|
||||||
|
get_key(p_name, p_keys)
|
||||||
|
for p_name, p_keys in self.perspective_servers.items()
|
||||||
|
],
|
||||||
|
consumeErrors=True,
|
||||||
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
|
union_of_keys = {}
|
||||||
|
for result in results:
|
||||||
|
for server_name, keys in result.items():
|
||||||
|
union_of_keys.setdefault(server_name, {}).update(keys)
|
||||||
|
|
||||||
|
defer.returnValue(union_of_keys)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_keys_from_server(self, server_name_and_key_ids):
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_key(server_name, key_ids):
|
||||||
|
limiter = yield get_retry_limiter(
|
||||||
|
server_name,
|
||||||
|
self.clock,
|
||||||
|
self.store,
|
||||||
|
)
|
||||||
|
with limiter:
|
||||||
|
keys = None
|
||||||
try:
|
try:
|
||||||
keys = yield self.get_server_verify_key_v2_direct(
|
keys = yield self.get_server_verify_key_v2_direct(
|
||||||
server_name, key_ids
|
server_name, key_ids
|
||||||
)
|
)
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
logger.info(
|
||||||
|
"Unable to getting key %r for %r directly: %s %s",
|
||||||
|
key_ids, server_name,
|
||||||
|
type(e).__name__, str(e.message),
|
||||||
|
)
|
||||||
|
|
||||||
keys = yield self.get_server_verify_key_v1_direct(
|
if not keys:
|
||||||
server_name, key_ids
|
keys = yield self.get_server_verify_key_v1_direct(
|
||||||
)
|
server_name, key_ids
|
||||||
|
)
|
||||||
|
|
||||||
for key_id in key_ids:
|
keys = {server_name: keys}
|
||||||
if key_id in keys:
|
|
||||||
defer.returnValue(keys[key_id])
|
defer.returnValue(keys)
|
||||||
return
|
|
||||||
raise ValueError("No verification key found for given key ids")
|
results = yield defer.gatherResults(
|
||||||
|
[
|
||||||
|
get_key(server_name, key_ids)
|
||||||
|
for server_name, key_ids in server_name_and_key_ids
|
||||||
|
],
|
||||||
|
consumeErrors=True,
|
||||||
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
|
merged = {}
|
||||||
|
for result in results:
|
||||||
|
merged.update(result)
|
||||||
|
|
||||||
|
defer.returnValue({
|
||||||
|
server_name: keys
|
||||||
|
for server_name, keys in merged.items()
|
||||||
|
if keys
|
||||||
|
})
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_server_verify_key_v2_indirect(self, server_name, key_ids,
|
def get_server_verify_key_v2_indirect(self, server_names_and_key_ids,
|
||||||
perspective_name,
|
perspective_name,
|
||||||
perspective_keys):
|
perspective_keys):
|
||||||
limiter = yield get_retry_limiter(
|
limiter = yield get_retry_limiter(
|
||||||
@@ -184,7 +383,7 @@ class Keyring(object):
|
|||||||
# TODO(mark): Set the minimum_valid_until_ts to that needed by
|
# TODO(mark): Set the minimum_valid_until_ts to that needed by
|
||||||
# the events being validated or the current time if validating
|
# the events being validated or the current time if validating
|
||||||
# an incoming request.
|
# an incoming request.
|
||||||
responses = yield self.client.post_json(
|
query_response = yield self.client.post_json(
|
||||||
destination=perspective_name,
|
destination=perspective_name,
|
||||||
path=b"/_matrix/key/v2/query",
|
path=b"/_matrix/key/v2/query",
|
||||||
data={
|
data={
|
||||||
@@ -194,12 +393,15 @@ class Keyring(object):
|
|||||||
u"minimum_valid_until_ts": 0
|
u"minimum_valid_until_ts": 0
|
||||||
} for key_id in key_ids
|
} for key_id in key_ids
|
||||||
}
|
}
|
||||||
|
for server_name, key_ids in server_names_and_key_ids
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
keys = {}
|
keys = {}
|
||||||
|
|
||||||
|
responses = query_response["server_keys"]
|
||||||
|
|
||||||
for response in responses:
|
for response in responses:
|
||||||
if (u"signatures" not in response
|
if (u"signatures" not in response
|
||||||
or perspective_name not in response[u"signatures"]):
|
or perspective_name not in response[u"signatures"]):
|
||||||
@@ -231,23 +433,29 @@ class Keyring(object):
|
|||||||
" server %r" % (perspective_name,)
|
" server %r" % (perspective_name,)
|
||||||
)
|
)
|
||||||
|
|
||||||
response_keys = yield self.process_v2_response(
|
processed_response = yield self.process_v2_response(
|
||||||
server_name, perspective_name, response
|
perspective_name, response
|
||||||
)
|
)
|
||||||
|
|
||||||
keys.update(response_keys)
|
for server_name, response_keys in processed_response.items():
|
||||||
|
keys.setdefault(server_name, {}).update(response_keys)
|
||||||
|
|
||||||
yield self.store_keys(
|
yield defer.gatherResults(
|
||||||
server_name=server_name,
|
[
|
||||||
from_server=perspective_name,
|
self.store_keys(
|
||||||
verify_keys=keys,
|
server_name=server_name,
|
||||||
)
|
from_server=perspective_name,
|
||||||
|
verify_keys=response_keys,
|
||||||
|
)
|
||||||
|
for server_name, response_keys in keys.items()
|
||||||
|
],
|
||||||
|
consumeErrors=True
|
||||||
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
defer.returnValue(keys)
|
defer.returnValue(keys)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_server_verify_key_v2_direct(self, server_name, key_ids):
|
def get_server_verify_key_v2_direct(self, server_name, key_ids):
|
||||||
|
|
||||||
keys = {}
|
keys = {}
|
||||||
|
|
||||||
for requested_key_id in key_ids:
|
for requested_key_id in key_ids:
|
||||||
@@ -283,25 +491,30 @@ class Keyring(object):
|
|||||||
raise ValueError("TLS certificate not allowed by fingerprints")
|
raise ValueError("TLS certificate not allowed by fingerprints")
|
||||||
|
|
||||||
response_keys = yield self.process_v2_response(
|
response_keys = yield self.process_v2_response(
|
||||||
server_name=server_name,
|
|
||||||
from_server=server_name,
|
from_server=server_name,
|
||||||
requested_id=requested_key_id,
|
requested_ids=[requested_key_id],
|
||||||
response_json=response,
|
response_json=response,
|
||||||
)
|
)
|
||||||
|
|
||||||
keys.update(response_keys)
|
keys.update(response_keys)
|
||||||
|
|
||||||
yield self.store_keys(
|
yield defer.gatherResults(
|
||||||
server_name=server_name,
|
[
|
||||||
from_server=server_name,
|
self.store_keys(
|
||||||
verify_keys=keys,
|
server_name=key_server_name,
|
||||||
)
|
from_server=server_name,
|
||||||
|
verify_keys=verify_keys,
|
||||||
|
)
|
||||||
|
for key_server_name, verify_keys in keys.items()
|
||||||
|
],
|
||||||
|
consumeErrors=True
|
||||||
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
defer.returnValue(keys)
|
defer.returnValue(keys)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def process_v2_response(self, server_name, from_server, response_json,
|
def process_v2_response(self, from_server, response_json,
|
||||||
requested_id=None):
|
requested_ids=[]):
|
||||||
time_now_ms = self.clock.time_msec()
|
time_now_ms = self.clock.time_msec()
|
||||||
response_keys = {}
|
response_keys = {}
|
||||||
verify_keys = {}
|
verify_keys = {}
|
||||||
@@ -323,7 +536,9 @@ class Keyring(object):
|
|||||||
verify_key.time_added = time_now_ms
|
verify_key.time_added = time_now_ms
|
||||||
old_verify_keys[key_id] = verify_key
|
old_verify_keys[key_id] = verify_key
|
||||||
|
|
||||||
for key_id in response_json["signatures"][server_name]:
|
results = {}
|
||||||
|
server_name = response_json["server_name"]
|
||||||
|
for key_id in response_json["signatures"].get(server_name, {}):
|
||||||
if key_id not in response_json["verify_keys"]:
|
if key_id not in response_json["verify_keys"]:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Key response must include verification keys for all"
|
"Key response must include verification keys for all"
|
||||||
@@ -345,28 +560,31 @@ class Keyring(object):
|
|||||||
signed_key_json_bytes = encode_canonical_json(signed_key_json)
|
signed_key_json_bytes = encode_canonical_json(signed_key_json)
|
||||||
ts_valid_until_ms = signed_key_json[u"valid_until_ts"]
|
ts_valid_until_ms = signed_key_json[u"valid_until_ts"]
|
||||||
|
|
||||||
updated_key_ids = set()
|
updated_key_ids = set(requested_ids)
|
||||||
if requested_id is not None:
|
|
||||||
updated_key_ids.add(requested_id)
|
|
||||||
updated_key_ids.update(verify_keys)
|
updated_key_ids.update(verify_keys)
|
||||||
updated_key_ids.update(old_verify_keys)
|
updated_key_ids.update(old_verify_keys)
|
||||||
|
|
||||||
response_keys.update(verify_keys)
|
response_keys.update(verify_keys)
|
||||||
response_keys.update(old_verify_keys)
|
response_keys.update(old_verify_keys)
|
||||||
|
|
||||||
for key_id in updated_key_ids:
|
yield defer.gatherResults(
|
||||||
yield self.store.store_server_keys_json(
|
[
|
||||||
server_name=server_name,
|
self.store.store_server_keys_json(
|
||||||
key_id=key_id,
|
server_name=server_name,
|
||||||
from_server=server_name,
|
key_id=key_id,
|
||||||
ts_now_ms=time_now_ms,
|
from_server=server_name,
|
||||||
ts_expires_ms=ts_valid_until_ms,
|
ts_now_ms=time_now_ms,
|
||||||
key_json_bytes=signed_key_json_bytes,
|
ts_expires_ms=ts_valid_until_ms,
|
||||||
)
|
key_json_bytes=signed_key_json_bytes,
|
||||||
|
)
|
||||||
|
for key_id in updated_key_ids
|
||||||
|
],
|
||||||
|
consumeErrors=True,
|
||||||
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
defer.returnValue(response_keys)
|
results[server_name] = response_keys
|
||||||
|
|
||||||
raise ValueError("No verification key found for given key ids")
|
defer.returnValue(results)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_server_verify_key_v1_direct(self, server_name, key_ids):
|
def get_server_verify_key_v1_direct(self, server_name, key_ids):
|
||||||
@@ -450,8 +668,13 @@ class Keyring(object):
|
|||||||
Returns:
|
Returns:
|
||||||
A deferred that completes when the keys are stored.
|
A deferred that completes when the keys are stored.
|
||||||
"""
|
"""
|
||||||
for key_id, key in verify_keys.items():
|
# TODO(markjh): Store whether the keys have expired.
|
||||||
# TODO(markjh): Store whether the keys have expired.
|
yield defer.gatherResults(
|
||||||
yield self.store.store_server_verify_key(
|
[
|
||||||
server_name, server_name, key.time_added, key
|
self.store.store_server_verify_key(
|
||||||
)
|
server_name, server_name, key.time_added, key
|
||||||
|
)
|
||||||
|
for key_id, key in verify_keys.items()
|
||||||
|
],
|
||||||
|
consumeErrors=True,
|
||||||
|
).addErrback(unwrapFirstError)
|
||||||
|
|||||||
@@ -16,6 +16,12 @@
|
|||||||
from synapse.util.frozenutils import freeze
|
from synapse.util.frozenutils import freeze
|
||||||
|
|
||||||
|
|
||||||
|
# Whether we should use frozen_dict in FrozenEvent. Using frozen_dicts prevents
|
||||||
|
# bugs where we accidentally share e.g. signature dicts. However, converting
|
||||||
|
# a dict to frozen_dicts is expensive.
|
||||||
|
USE_FROZEN_DICTS = True
|
||||||
|
|
||||||
|
|
||||||
class _EventInternalMetadata(object):
|
class _EventInternalMetadata(object):
|
||||||
def __init__(self, internal_metadata_dict):
|
def __init__(self, internal_metadata_dict):
|
||||||
self.__dict__ = dict(internal_metadata_dict)
|
self.__dict__ = dict(internal_metadata_dict)
|
||||||
@@ -84,7 +90,7 @@ class EventBase(object):
|
|||||||
d = dict(self._event_dict)
|
d = dict(self._event_dict)
|
||||||
d.update({
|
d.update({
|
||||||
"signatures": self.signatures,
|
"signatures": self.signatures,
|
||||||
"unsigned": self.unsigned,
|
"unsigned": dict(self.unsigned),
|
||||||
})
|
})
|
||||||
|
|
||||||
return d
|
return d
|
||||||
@@ -103,6 +109,9 @@ class EventBase(object):
|
|||||||
pdu_json.setdefault("unsigned", {})["age"] = int(age)
|
pdu_json.setdefault("unsigned", {})["age"] = int(age)
|
||||||
del pdu_json["unsigned"]["age_ts"]
|
del pdu_json["unsigned"]["age_ts"]
|
||||||
|
|
||||||
|
# This may be a frozen event
|
||||||
|
pdu_json["unsigned"].pop("redacted_because", None)
|
||||||
|
|
||||||
return pdu_json
|
return pdu_json
|
||||||
|
|
||||||
def __set__(self, instance, value):
|
def __set__(self, instance, value):
|
||||||
@@ -122,7 +131,10 @@ class FrozenEvent(EventBase):
|
|||||||
|
|
||||||
unsigned = dict(event_dict.pop("unsigned", {}))
|
unsigned = dict(event_dict.pop("unsigned", {}))
|
||||||
|
|
||||||
frozen_dict = freeze(event_dict)
|
if USE_FROZEN_DICTS:
|
||||||
|
frozen_dict = freeze(event_dict)
|
||||||
|
else:
|
||||||
|
frozen_dict = event_dict
|
||||||
|
|
||||||
super(FrozenEvent, self).__init__(
|
super(FrozenEvent, self).__init__(
|
||||||
frozen_dict,
|
frozen_dict,
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ def prune_event(event):
|
|||||||
)
|
)
|
||||||
elif event_type == EventTypes.Aliases:
|
elif event_type == EventTypes.Aliases:
|
||||||
add_fields("aliases")
|
add_fields("aliases")
|
||||||
|
elif event_type == EventTypes.RoomHistoryVisibility:
|
||||||
|
add_fields("history_visibility")
|
||||||
|
|
||||||
allowed_fields = {
|
allowed_fields = {
|
||||||
k: v
|
k: v
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ from twisted.internet import defer
|
|||||||
|
|
||||||
from synapse.events.utils import prune_event
|
from synapse.events.utils import prune_event
|
||||||
|
|
||||||
from syutil.jsonutil import encode_canonical_json
|
|
||||||
|
|
||||||
from synapse.crypto.event_signing import check_event_content_hash
|
from synapse.crypto.event_signing import check_event_content_hash
|
||||||
|
|
||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
|
|
||||||
|
from synapse.util import unwrapFirstError
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
@@ -32,7 +32,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class FederationBase(object):
|
class FederationBase(object):
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _check_sigs_and_hash_and_fetch(self, origin, pdus, outlier=False):
|
def _check_sigs_and_hash_and_fetch(self, origin, pdus, outlier=False,
|
||||||
|
include_none=False):
|
||||||
"""Takes a list of PDUs and checks the signatures and hashs of each
|
"""Takes a list of PDUs and checks the signatures and hashs of each
|
||||||
one. If a PDU fails its signature check then we check if we have it in
|
one. If a PDU fails its signature check then we check if we have it in
|
||||||
the database and if not then request if from the originating server of
|
the database and if not then request if from the originating server of
|
||||||
@@ -50,84 +51,108 @@ class FederationBase(object):
|
|||||||
Returns:
|
Returns:
|
||||||
Deferred : A list of PDUs that have valid signatures and hashes.
|
Deferred : A list of PDUs that have valid signatures and hashes.
|
||||||
"""
|
"""
|
||||||
|
deferreds = self._check_sigs_and_hashes(pdus)
|
||||||
|
|
||||||
signed_pdus = []
|
def callback(pdu):
|
||||||
|
return pdu
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
def errback(failure, pdu):
|
||||||
def do(pdu):
|
failure.trap(SynapseError)
|
||||||
try:
|
return None
|
||||||
new_pdu = yield self._check_sigs_and_hash(pdu)
|
|
||||||
signed_pdus.append(new_pdu)
|
|
||||||
except SynapseError:
|
|
||||||
# FIXME: We should handle signature failures more gracefully.
|
|
||||||
|
|
||||||
|
def try_local_db(res, pdu):
|
||||||
|
if not res:
|
||||||
# Check local db.
|
# Check local db.
|
||||||
new_pdu = yield self.store.get_event(
|
return self.store.get_event(
|
||||||
pdu.event_id,
|
pdu.event_id,
|
||||||
allow_rejected=True,
|
allow_rejected=True,
|
||||||
allow_none=True,
|
allow_none=True,
|
||||||
)
|
)
|
||||||
if new_pdu:
|
return res
|
||||||
signed_pdus.append(new_pdu)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check pdu.origin
|
def try_remote(res, pdu):
|
||||||
if pdu.origin != origin:
|
if not res and pdu.origin != origin:
|
||||||
try:
|
return self.get_pdu(
|
||||||
new_pdu = yield self.get_pdu(
|
destinations=[pdu.origin],
|
||||||
destinations=[pdu.origin],
|
event_id=pdu.event_id,
|
||||||
event_id=pdu.event_id,
|
outlier=outlier,
|
||||||
outlier=outlier,
|
timeout=10000,
|
||||||
)
|
).addErrback(lambda e: None)
|
||||||
|
return res
|
||||||
if new_pdu:
|
|
||||||
signed_pdus.append(new_pdu)
|
|
||||||
return
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
def warn(res, pdu):
|
||||||
|
if not res:
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"Failed to find copy of %s with valid signature",
|
"Failed to find copy of %s with valid signature",
|
||||||
pdu.event_id,
|
pdu.event_id,
|
||||||
)
|
)
|
||||||
|
return res
|
||||||
|
|
||||||
yield defer.gatherResults(
|
for pdu, deferred in zip(pdus, deferreds):
|
||||||
[do(pdu) for pdu in pdus],
|
deferred.addCallbacks(
|
||||||
|
callback, errback, errbackArgs=[pdu]
|
||||||
|
).addCallback(
|
||||||
|
try_local_db, pdu
|
||||||
|
).addCallback(
|
||||||
|
try_remote, pdu
|
||||||
|
).addCallback(
|
||||||
|
warn, pdu
|
||||||
|
)
|
||||||
|
|
||||||
|
valid_pdus = yield defer.gatherResults(
|
||||||
|
deferreds,
|
||||||
consumeErrors=True
|
consumeErrors=True
|
||||||
)
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
defer.returnValue(signed_pdus)
|
if include_none:
|
||||||
|
defer.returnValue(valid_pdus)
|
||||||
|
else:
|
||||||
|
defer.returnValue([p for p in valid_pdus if p])
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _check_sigs_and_hash(self, pdu):
|
def _check_sigs_and_hash(self, pdu):
|
||||||
"""Throws a SynapseError if the PDU does not have the correct
|
return self._check_sigs_and_hashes([pdu])[0]
|
||||||
|
|
||||||
|
def _check_sigs_and_hashes(self, pdus):
|
||||||
|
"""Throws a SynapseError if a PDU does not have the correct
|
||||||
signatures.
|
signatures.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
FrozenEvent: Either the given event or it redacted if it failed the
|
FrozenEvent: Either the given event or it redacted if it failed the
|
||||||
content hash check.
|
content hash check.
|
||||||
"""
|
"""
|
||||||
# Check signatures are correct.
|
|
||||||
redacted_event = prune_event(pdu)
|
|
||||||
redacted_pdu_json = redacted_event.get_pdu_json()
|
|
||||||
|
|
||||||
try:
|
redacted_pdus = [
|
||||||
yield self.keyring.verify_json_for_server(
|
prune_event(pdu)
|
||||||
pdu.origin, redacted_pdu_json
|
for pdu in pdus
|
||||||
)
|
]
|
||||||
except SynapseError:
|
|
||||||
|
deferreds = self.keyring.verify_json_objects_for_server([
|
||||||
|
(p.origin, p.get_pdu_json())
|
||||||
|
for p in redacted_pdus
|
||||||
|
])
|
||||||
|
|
||||||
|
def callback(_, pdu, redacted):
|
||||||
|
if not check_event_content_hash(pdu):
|
||||||
|
logger.warn(
|
||||||
|
"Event content has been tampered, redacting %s: %s",
|
||||||
|
pdu.event_id, pdu.get_pdu_json()
|
||||||
|
)
|
||||||
|
return redacted
|
||||||
|
return pdu
|
||||||
|
|
||||||
|
def errback(failure, pdu):
|
||||||
|
failure.trap(SynapseError)
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"Signature check failed for %s redacted to %s",
|
"Signature check failed for %s",
|
||||||
encode_canonical_json(pdu.get_pdu_json()),
|
pdu.event_id,
|
||||||
encode_canonical_json(redacted_pdu_json),
|
|
||||||
)
|
)
|
||||||
raise
|
return failure
|
||||||
|
|
||||||
if not check_event_content_hash(pdu):
|
for deferred, pdu, redacted in zip(deferreds, pdus, redacted_pdus):
|
||||||
logger.warn(
|
deferred.addCallbacks(
|
||||||
"Event content has been tampered, redacting %s, %s",
|
callback, errback,
|
||||||
pdu.event_id, encode_canonical_json(pdu.get_dict())
|
callbackArgs=[pdu, redacted],
|
||||||
|
errbackArgs=[pdu],
|
||||||
)
|
)
|
||||||
defer.returnValue(redacted_event)
|
|
||||||
|
|
||||||
defer.returnValue(pdu)
|
return deferreds
|
||||||
|
|||||||
@@ -22,13 +22,15 @@ from .units import Edu
|
|||||||
from synapse.api.errors import (
|
from synapse.api.errors import (
|
||||||
CodeMessageException, HttpResponseException, SynapseError,
|
CodeMessageException, HttpResponseException, SynapseError,
|
||||||
)
|
)
|
||||||
from synapse.util.expiringcache import ExpiringCache
|
from synapse.util import unwrapFirstError
|
||||||
|
from synapse.util.caches.expiringcache import ExpiringCache
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
from synapse.events import FrozenEvent
|
from synapse.events import FrozenEvent
|
||||||
import synapse.metrics
|
import synapse.metrics
|
||||||
|
|
||||||
from synapse.util.retryutils import get_retry_limiter, NotRetryingDestination
|
from synapse.util.retryutils import get_retry_limiter, NotRetryingDestination
|
||||||
|
|
||||||
|
import copy
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
@@ -132,6 +134,36 @@ class FederationClient(FederationBase):
|
|||||||
destination, query_type, args, retry_on_dns_fail=retry_on_dns_fail
|
destination, query_type, args, retry_on_dns_fail=retry_on_dns_fail
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@log_function
|
||||||
|
def query_client_keys(self, destination, content):
|
||||||
|
"""Query device keys for a device hosted on a remote server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
destination (str): Domain name of the remote homeserver
|
||||||
|
content (dict): The query content.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
a Deferred which will eventually yield a JSON object from the
|
||||||
|
response
|
||||||
|
"""
|
||||||
|
sent_queries_counter.inc("client_device_keys")
|
||||||
|
return self.transport_layer.query_client_keys(destination, content)
|
||||||
|
|
||||||
|
@log_function
|
||||||
|
def claim_client_keys(self, destination, content):
|
||||||
|
"""Claims one-time keys for a device hosted on a remote server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
destination (str): Domain name of the remote homeserver
|
||||||
|
content (dict): The query content.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
a Deferred which will eventually yield a JSON object from the
|
||||||
|
response
|
||||||
|
"""
|
||||||
|
sent_queries_counter.inc("client_one_time_keys")
|
||||||
|
return self.transport_layer.claim_client_keys(destination, content)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def backfill(self, dest, context, limit, extremities):
|
def backfill(self, dest, context, limit, extremities):
|
||||||
@@ -164,16 +196,17 @@ class FederationClient(FederationBase):
|
|||||||
for p in transaction_data["pdus"]
|
for p in transaction_data["pdus"]
|
||||||
]
|
]
|
||||||
|
|
||||||
for i, pdu in enumerate(pdus):
|
# FIXME: We should handle signature failures more gracefully.
|
||||||
pdus[i] = yield self._check_sigs_and_hash(pdu)
|
pdus[:] = yield defer.gatherResults(
|
||||||
|
self._check_sigs_and_hashes(pdus),
|
||||||
# FIXME: We should handle signature failures more gracefully.
|
consumeErrors=True,
|
||||||
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
defer.returnValue(pdus)
|
defer.returnValue(pdus)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def get_pdu(self, destinations, event_id, outlier=False):
|
def get_pdu(self, destinations, event_id, outlier=False, timeout=None):
|
||||||
"""Requests the PDU with given origin and ID from the remote home
|
"""Requests the PDU with given origin and ID from the remote home
|
||||||
servers.
|
servers.
|
||||||
|
|
||||||
@@ -189,6 +222,8 @@ class FederationClient(FederationBase):
|
|||||||
outlier (bool): Indicates whether the PDU is an `outlier`, i.e. if
|
outlier (bool): Indicates whether the PDU is an `outlier`, i.e. if
|
||||||
it's from an arbitary point in the context as opposed to part
|
it's from an arbitary point in the context as opposed to part
|
||||||
of the current block of PDUs. Defaults to `False`
|
of the current block of PDUs. Defaults to `False`
|
||||||
|
timeout (int): How long to try (in ms) each destination for before
|
||||||
|
moving to the next destination. None indicates no timeout.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Deferred: Results in the requested PDU.
|
Deferred: Results in the requested PDU.
|
||||||
@@ -212,7 +247,7 @@ class FederationClient(FederationBase):
|
|||||||
|
|
||||||
with limiter:
|
with limiter:
|
||||||
transaction_data = yield self.transport_layer.get_event(
|
transaction_data = yield self.transport_layer.get_event(
|
||||||
destination, event_id
|
destination, event_id, timeout=timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug("transaction_data %r", transaction_data)
|
logger.debug("transaction_data %r", transaction_data)
|
||||||
@@ -222,11 +257,11 @@ class FederationClient(FederationBase):
|
|||||||
for p in transaction_data["pdus"]
|
for p in transaction_data["pdus"]
|
||||||
]
|
]
|
||||||
|
|
||||||
if pdu_list:
|
if pdu_list and pdu_list[0]:
|
||||||
pdu = pdu_list[0]
|
pdu = pdu_list[0]
|
||||||
|
|
||||||
# Check signatures are correct.
|
# Check signatures are correct.
|
||||||
pdu = yield self._check_sigs_and_hash(pdu)
|
pdu = yield self._check_sigs_and_hashes([pdu])[0]
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -255,7 +290,7 @@ class FederationClient(FederationBase):
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self._get_pdu_cache is not None:
|
if self._get_pdu_cache is not None and pdu:
|
||||||
self._get_pdu_cache[event_id] = pdu
|
self._get_pdu_cache[event_id] = pdu
|
||||||
|
|
||||||
defer.returnValue(pdu)
|
defer.returnValue(pdu)
|
||||||
@@ -323,6 +358,9 @@ class FederationClient(FederationBase):
|
|||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def make_join(self, destinations, room_id, user_id):
|
def make_join(self, destinations, room_id, user_id):
|
||||||
for destination in destinations:
|
for destination in destinations:
|
||||||
|
if destination == self.server_name:
|
||||||
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ret = yield self.transport_layer.make_join(
|
ret = yield self.transport_layer.make_join(
|
||||||
destination, room_id, user_id
|
destination, room_id, user_id
|
||||||
@@ -349,6 +387,9 @@ class FederationClient(FederationBase):
|
|||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def send_join(self, destinations, pdu):
|
def send_join(self, destinations, pdu):
|
||||||
for destination in destinations:
|
for destination in destinations:
|
||||||
|
if destination == self.server_name:
|
||||||
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
time_now = self._clock.time_msec()
|
time_now = self._clock.time_msec()
|
||||||
_, content = yield self.transport_layer.send_join(
|
_, content = yield self.transport_layer.send_join(
|
||||||
@@ -370,13 +411,39 @@ class FederationClient(FederationBase):
|
|||||||
for p in content.get("auth_chain", [])
|
for p in content.get("auth_chain", [])
|
||||||
]
|
]
|
||||||
|
|
||||||
signed_state = yield self._check_sigs_and_hash_and_fetch(
|
pdus = {
|
||||||
destination, state, outlier=True
|
p.event_id: p
|
||||||
|
for p in itertools.chain(state, auth_chain)
|
||||||
|
}
|
||||||
|
|
||||||
|
valid_pdus = yield self._check_sigs_and_hash_and_fetch(
|
||||||
|
destination, pdus.values(),
|
||||||
|
outlier=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
signed_auth = yield self._check_sigs_and_hash_and_fetch(
|
valid_pdus_map = {
|
||||||
destination, auth_chain, outlier=True
|
p.event_id: p
|
||||||
)
|
for p in valid_pdus
|
||||||
|
}
|
||||||
|
|
||||||
|
# NB: We *need* to copy to ensure that we don't have multiple
|
||||||
|
# references being passed on, as that causes... issues.
|
||||||
|
signed_state = [
|
||||||
|
copy.copy(valid_pdus_map[p.event_id])
|
||||||
|
for p in state
|
||||||
|
if p.event_id in valid_pdus_map
|
||||||
|
]
|
||||||
|
|
||||||
|
signed_auth = [
|
||||||
|
valid_pdus_map[p.event_id]
|
||||||
|
for p in auth_chain
|
||||||
|
if p.event_id in valid_pdus_map
|
||||||
|
]
|
||||||
|
|
||||||
|
# NB: We *need* to copy to ensure that we don't have multiple
|
||||||
|
# references being passed on, as that causes... issues.
|
||||||
|
for s in signed_state:
|
||||||
|
s.internal_metadata = copy.deepcopy(s.internal_metadata)
|
||||||
|
|
||||||
auth_chain.sort(key=lambda e: e.depth)
|
auth_chain.sort(key=lambda e: e.depth)
|
||||||
|
|
||||||
@@ -388,7 +455,7 @@ class FederationClient(FederationBase):
|
|||||||
except CodeMessageException:
|
except CodeMessageException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(
|
logger.exception(
|
||||||
"Failed to send_join via %s: %s",
|
"Failed to send_join via %s: %s",
|
||||||
destination, e.message
|
destination, e.message
|
||||||
)
|
)
|
||||||
@@ -491,7 +558,7 @@ class FederationClient(FederationBase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
signed_events = yield self._check_sigs_and_hash_and_fetch(
|
signed_events = yield self._check_sigs_and_hash_and_fetch(
|
||||||
destination, events, outlier=True
|
destination, events, outlier=False
|
||||||
)
|
)
|
||||||
|
|
||||||
have_gotten_all_from_destination = True
|
have_gotten_all_from_destination = True
|
||||||
@@ -518,7 +585,7 @@ class FederationClient(FederationBase):
|
|||||||
# Are we missing any?
|
# Are we missing any?
|
||||||
|
|
||||||
seen_events = set(earliest_events_ids)
|
seen_events = set(earliest_events_ids)
|
||||||
seen_events.update(e.event_id for e in signed_events)
|
seen_events.update(e.event_id for e in signed_events if e)
|
||||||
|
|
||||||
missing_events = {}
|
missing_events = {}
|
||||||
for e in itertools.chain(latest_events, signed_events):
|
for e in itertools.chain(latest_events, signed_events):
|
||||||
@@ -561,7 +628,7 @@ class FederationClient(FederationBase):
|
|||||||
|
|
||||||
res = yield defer.DeferredList(deferreds, consumeErrors=True)
|
res = yield defer.DeferredList(deferreds, consumeErrors=True)
|
||||||
for (result, val), (e_id, _) in zip(res, ordered_missing):
|
for (result, val), (e_id, _) in zip(res, ordered_missing):
|
||||||
if result:
|
if result and val:
|
||||||
signed_events.append(val)
|
signed_events.append(val)
|
||||||
else:
|
else:
|
||||||
failed_to_fetch.add(e_id)
|
failed_to_fetch.add(e_id)
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from .federation_base import FederationBase
|
|||||||
from .units import Transaction, Edu
|
from .units import Transaction, Edu
|
||||||
|
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
from synapse.util.logcontext import PreserveLoggingContext
|
|
||||||
from synapse.events import FrozenEvent
|
from synapse.events import FrozenEvent
|
||||||
import synapse.metrics
|
import synapse.metrics
|
||||||
|
|
||||||
@@ -28,6 +27,7 @@ from synapse.api.errors import FederationError, SynapseError
|
|||||||
|
|
||||||
from synapse.crypto.event_signing import compute_event_signature
|
from synapse.crypto.event_signing import compute_event_signature
|
||||||
|
|
||||||
|
import simplejson as json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
@@ -123,29 +123,28 @@ class FederationServer(FederationBase):
|
|||||||
|
|
||||||
logger.debug("[%s] Transaction is new", transaction.transaction_id)
|
logger.debug("[%s] Transaction is new", transaction.transaction_id)
|
||||||
|
|
||||||
with PreserveLoggingContext():
|
results = []
|
||||||
results = []
|
|
||||||
|
|
||||||
for pdu in pdu_list:
|
for pdu in pdu_list:
|
||||||
d = self._handle_new_pdu(transaction.origin, pdu)
|
d = self._handle_new_pdu(transaction.origin, pdu)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield d
|
yield d
|
||||||
results.append({})
|
results.append({})
|
||||||
except FederationError as e:
|
except FederationError as e:
|
||||||
self.send_failure(e, transaction.origin)
|
self.send_failure(e, transaction.origin)
|
||||||
results.append({"error": str(e)})
|
results.append({"error": str(e)})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results.append({"error": str(e)})
|
results.append({"error": str(e)})
|
||||||
logger.exception("Failed to handle PDU")
|
logger.exception("Failed to handle PDU")
|
||||||
|
|
||||||
if hasattr(transaction, "edus"):
|
if hasattr(transaction, "edus"):
|
||||||
for edu in [Edu(**x) for x in transaction.edus]:
|
for edu in [Edu(**x) for x in transaction.edus]:
|
||||||
self.received_edu(
|
self.received_edu(
|
||||||
transaction.origin,
|
transaction.origin,
|
||||||
edu.edu_type,
|
edu.edu_type,
|
||||||
edu.content
|
edu.content
|
||||||
)
|
)
|
||||||
|
|
||||||
for failure in getattr(transaction, "pdu_failures", []):
|
for failure in getattr(transaction, "pdu_failures", []):
|
||||||
logger.info("Got failure %r", failure)
|
logger.info("Got failure %r", failure)
|
||||||
@@ -314,6 +313,48 @@ class FederationServer(FederationBase):
|
|||||||
(200, send_content)
|
(200, send_content)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
@log_function
|
||||||
|
def on_query_client_keys(self, origin, content):
|
||||||
|
query = []
|
||||||
|
for user_id, device_ids in content.get("device_keys", {}).items():
|
||||||
|
if not device_ids:
|
||||||
|
query.append((user_id, None))
|
||||||
|
else:
|
||||||
|
for device_id in device_ids:
|
||||||
|
query.append((user_id, device_id))
|
||||||
|
|
||||||
|
results = yield self.store.get_e2e_device_keys(query)
|
||||||
|
|
||||||
|
json_result = {}
|
||||||
|
for user_id, device_keys in results.items():
|
||||||
|
for device_id, json_bytes in device_keys.items():
|
||||||
|
json_result.setdefault(user_id, {})[device_id] = json.loads(
|
||||||
|
json_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue({"device_keys": json_result})
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
@log_function
|
||||||
|
def on_claim_client_keys(self, origin, content):
|
||||||
|
query = []
|
||||||
|
for user_id, device_keys in content.get("one_time_keys", {}).items():
|
||||||
|
for device_id, algorithm in device_keys.items():
|
||||||
|
query.append((user_id, device_id, algorithm))
|
||||||
|
|
||||||
|
results = yield self.store.claim_e2e_one_time_keys(query)
|
||||||
|
|
||||||
|
json_result = {}
|
||||||
|
for user_id, device_keys in results.items():
|
||||||
|
for device_id, keys in device_keys.items():
|
||||||
|
for key_id, json_bytes in keys.items():
|
||||||
|
json_result.setdefault(user_id, {})[device_id] = {
|
||||||
|
key_id: json.loads(json_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer.returnValue({"one_time_keys": json_result})
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def on_get_missing_events(self, origin, room_id, earliest_events,
|
def on_get_missing_events(self, origin, room_id, earliest_events,
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ from twisted.internet import defer
|
|||||||
|
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
|
|
||||||
from syutil.jsonutil import encode_canonical_json
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
@@ -71,7 +69,7 @@ class TransactionActions(object):
|
|||||||
transaction.transaction_id,
|
transaction.transaction_id,
|
||||||
transaction.origin,
|
transaction.origin,
|
||||||
code,
|
code,
|
||||||
encode_canonical_json(response)
|
response,
|
||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@@ -101,5 +99,5 @@ class TransactionActions(object):
|
|||||||
transaction.transaction_id,
|
transaction.transaction_id,
|
||||||
transaction.destination,
|
transaction.destination,
|
||||||
response_code,
|
response_code,
|
||||||
encode_canonical_json(response_dict)
|
response_dict,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ class TransactionQueue(object):
|
|||||||
return not destination.startswith("localhost")
|
return not destination.startswith("localhost")
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
|
||||||
def enqueue_pdu(self, pdu, destinations, order):
|
def enqueue_pdu(self, pdu, destinations, order):
|
||||||
# We loop through all destinations to see whether we already have
|
# We loop through all destinations to see whether we already have
|
||||||
# a transaction in progress. If we do, stick it in the pending_pdus
|
# a transaction in progress. If we do, stick it in the pending_pdus
|
||||||
@@ -208,13 +207,13 @@ class TransactionQueue(object):
|
|||||||
# request at which point pending_pdus_by_dest just keeps growing.
|
# request at which point pending_pdus_by_dest just keeps growing.
|
||||||
# we need application-layer timeouts of some flavour of these
|
# we need application-layer timeouts of some flavour of these
|
||||||
# requests
|
# requests
|
||||||
logger.info(
|
logger.debug(
|
||||||
"TX [%s] Transaction already in progress",
|
"TX [%s] Transaction already in progress",
|
||||||
destination
|
destination
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info("TX [%s] _attempt_new_transaction", destination)
|
logger.debug("TX [%s] _attempt_new_transaction", destination)
|
||||||
|
|
||||||
# list of (pending_pdu, deferred, order)
|
# list of (pending_pdu, deferred, order)
|
||||||
pending_pdus = self.pending_pdus_by_dest.pop(destination, [])
|
pending_pdus = self.pending_pdus_by_dest.pop(destination, [])
|
||||||
@@ -222,11 +221,11 @@ class TransactionQueue(object):
|
|||||||
pending_failures = self.pending_failures_by_dest.pop(destination, [])
|
pending_failures = self.pending_failures_by_dest.pop(destination, [])
|
||||||
|
|
||||||
if pending_pdus:
|
if pending_pdus:
|
||||||
logger.info("TX [%s] len(pending_pdus_by_dest[dest]) = %d",
|
logger.debug("TX [%s] len(pending_pdus_by_dest[dest]) = %d",
|
||||||
destination, len(pending_pdus))
|
destination, len(pending_pdus))
|
||||||
|
|
||||||
if not pending_pdus and not pending_edus and not pending_failures:
|
if not pending_pdus and not pending_edus and not pending_failures:
|
||||||
logger.info("TX [%s] Nothing to send", destination)
|
logger.debug("TX [%s] Nothing to send", destination)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Sort based on the order field
|
# Sort based on the order field
|
||||||
@@ -243,6 +242,8 @@ class TransactionQueue(object):
|
|||||||
try:
|
try:
|
||||||
self.pending_transactions[destination] = 1
|
self.pending_transactions[destination] = 1
|
||||||
|
|
||||||
|
txn_id = str(self._next_txn_id)
|
||||||
|
|
||||||
limiter = yield get_retry_limiter(
|
limiter = yield get_retry_limiter(
|
||||||
destination,
|
destination,
|
||||||
self._clock,
|
self._clock,
|
||||||
@@ -250,9 +251,9 @@ class TransactionQueue(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"TX [%s] Attempting new transaction"
|
"TX [%s] {%s} Attempting new transaction"
|
||||||
" (pdus: %d, edus: %d, failures: %d)",
|
" (pdus: %d, edus: %d, failures: %d)",
|
||||||
destination,
|
destination, txn_id,
|
||||||
len(pending_pdus),
|
len(pending_pdus),
|
||||||
len(pending_edus),
|
len(pending_edus),
|
||||||
len(pending_failures)
|
len(pending_failures)
|
||||||
@@ -262,7 +263,7 @@ class TransactionQueue(object):
|
|||||||
|
|
||||||
transaction = Transaction.create_new(
|
transaction = Transaction.create_new(
|
||||||
origin_server_ts=int(self._clock.time_msec()),
|
origin_server_ts=int(self._clock.time_msec()),
|
||||||
transaction_id=str(self._next_txn_id),
|
transaction_id=txn_id,
|
||||||
origin=self.server_name,
|
origin=self.server_name,
|
||||||
destination=destination,
|
destination=destination,
|
||||||
pdus=pdus,
|
pdus=pdus,
|
||||||
@@ -276,9 +277,13 @@ class TransactionQueue(object):
|
|||||||
|
|
||||||
logger.debug("TX [%s] Persisted transaction", destination)
|
logger.debug("TX [%s] Persisted transaction", destination)
|
||||||
logger.info(
|
logger.info(
|
||||||
"TX [%s] Sending transaction [%s]",
|
"TX [%s] {%s} Sending transaction [%s],"
|
||||||
destination,
|
" (PDUs: %d, EDUs: %d, failures: %d)",
|
||||||
|
destination, txn_id,
|
||||||
transaction.transaction_id,
|
transaction.transaction_id,
|
||||||
|
len(pending_pdus),
|
||||||
|
len(pending_edus),
|
||||||
|
len(pending_failures),
|
||||||
)
|
)
|
||||||
|
|
||||||
with limiter:
|
with limiter:
|
||||||
@@ -314,7 +319,10 @@ class TransactionQueue(object):
|
|||||||
code = e.code
|
code = e.code
|
||||||
response = e.response
|
response = e.response
|
||||||
|
|
||||||
logger.info("TX [%s] got %d response", destination, code)
|
logger.info(
|
||||||
|
"TX [%s] {%s} got %d response",
|
||||||
|
destination, txn_id, code
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug("TX [%s] Sent transaction", destination)
|
logger.debug("TX [%s] Sent transaction", destination)
|
||||||
logger.debug("TX [%s] Marking as delivered...", destination)
|
logger.debug("TX [%s] Marking as delivered...", destination)
|
||||||
|
|||||||
@@ -50,13 +50,15 @@ class TransportLayerClient(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def get_event(self, destination, event_id):
|
def get_event(self, destination, event_id, timeout=None):
|
||||||
""" Requests the pdu with give id and origin from the given server.
|
""" Requests the pdu with give id and origin from the given server.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
destination (str): The host name of the remote home server we want
|
destination (str): The host name of the remote home server we want
|
||||||
to get the state from.
|
to get the state from.
|
||||||
event_id (str): The id of the event being requested.
|
event_id (str): The id of the event being requested.
|
||||||
|
timeout (int): How long to try (in ms) the destination for before
|
||||||
|
giving up. None indicates no timeout.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Deferred: Results in a dict received from the remote homeserver.
|
Deferred: Results in a dict received from the remote homeserver.
|
||||||
@@ -65,7 +67,7 @@ class TransportLayerClient(object):
|
|||||||
destination, event_id)
|
destination, event_id)
|
||||||
|
|
||||||
path = PREFIX + "/event/%s/" % (event_id, )
|
path = PREFIX + "/event/%s/" % (event_id, )
|
||||||
return self.client.get_json(destination, path=path)
|
return self.client.get_json(destination, path=path, timeout=timeout)
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def backfill(self, destination, room_id, event_tuples, limit):
|
def backfill(self, destination, room_id, event_tuples, limit):
|
||||||
@@ -220,6 +222,76 @@ class TransportLayerClient(object):
|
|||||||
|
|
||||||
defer.returnValue(content)
|
defer.returnValue(content)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
@log_function
|
||||||
|
def query_client_keys(self, destination, query_content):
|
||||||
|
"""Query the device keys for a list of user ids hosted on a remote
|
||||||
|
server.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
{
|
||||||
|
"device_keys": {
|
||||||
|
"<user_id>": ["<device_id>"]
|
||||||
|
} }
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"device_keys": {
|
||||||
|
"<user_id>": {
|
||||||
|
"<device_id>": {...}
|
||||||
|
} } }
|
||||||
|
|
||||||
|
Args:
|
||||||
|
destination(str): The server to query.
|
||||||
|
query_content(dict): The user ids to query.
|
||||||
|
Returns:
|
||||||
|
A dict containg the device keys.
|
||||||
|
"""
|
||||||
|
path = PREFIX + "/user/keys/query"
|
||||||
|
|
||||||
|
content = yield self.client.post_json(
|
||||||
|
destination=destination,
|
||||||
|
path=path,
|
||||||
|
data=query_content,
|
||||||
|
)
|
||||||
|
defer.returnValue(content)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
@log_function
|
||||||
|
def claim_client_keys(self, destination, query_content):
|
||||||
|
"""Claim one-time keys for a list of devices hosted on a remote server.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
{
|
||||||
|
"one_time_keys": {
|
||||||
|
"<user_id>": {
|
||||||
|
"<device_id>": "<algorithm>"
|
||||||
|
} } }
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"device_keys": {
|
||||||
|
"<user_id>": {
|
||||||
|
"<device_id>": {
|
||||||
|
"<algorithm>:<key_id>": "<key_base64>"
|
||||||
|
} } } }
|
||||||
|
|
||||||
|
Args:
|
||||||
|
destination(str): The server to query.
|
||||||
|
query_content(dict): The user ids to query.
|
||||||
|
Returns:
|
||||||
|
A dict containg the one-time keys.
|
||||||
|
"""
|
||||||
|
|
||||||
|
path = PREFIX + "/user/keys/claim"
|
||||||
|
|
||||||
|
content = yield self.client.post_json(
|
||||||
|
destination=destination,
|
||||||
|
path=path,
|
||||||
|
data=query_content,
|
||||||
|
)
|
||||||
|
defer.returnValue(content)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def get_missing_events(self, destination, room_id, earliest_events,
|
def get_missing_events(self, destination, room_id, earliest_events,
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ class TransportLayerServer(object):
|
|||||||
|
|
||||||
yield self.keyring.verify_json_for_server(origin, json_request)
|
yield self.keyring.verify_json_for_server(origin, json_request)
|
||||||
|
|
||||||
|
logger.info("Request from %s", origin)
|
||||||
|
request.authenticated_entity = origin
|
||||||
|
|
||||||
defer.returnValue((origin, content))
|
defer.returnValue((origin, content))
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
@@ -196,6 +199,14 @@ class FederationSendServlet(BaseFederationServlet):
|
|||||||
transaction_id, str(transaction_data)
|
transaction_id, str(transaction_data)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Received txn %s from %s. (PDUs: %d, EDUs: %d, failures: %d)",
|
||||||
|
transaction_id, origin,
|
||||||
|
len(transaction_data.get("pdus", [])),
|
||||||
|
len(transaction_data.get("edus", [])),
|
||||||
|
len(transaction_data.get("failures", [])),
|
||||||
|
)
|
||||||
|
|
||||||
# We should ideally be getting this from the security layer.
|
# We should ideally be getting this from the security layer.
|
||||||
# origin = body["origin"]
|
# origin = body["origin"]
|
||||||
|
|
||||||
@@ -314,6 +325,24 @@ class FederationInviteServlet(BaseFederationServlet):
|
|||||||
defer.returnValue((200, content))
|
defer.returnValue((200, content))
|
||||||
|
|
||||||
|
|
||||||
|
class FederationClientKeysQueryServlet(BaseFederationServlet):
|
||||||
|
PATH = "/user/keys/query"
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_POST(self, origin, content, query):
|
||||||
|
response = yield self.handler.on_query_client_keys(origin, content)
|
||||||
|
defer.returnValue((200, response))
|
||||||
|
|
||||||
|
|
||||||
|
class FederationClientKeysClaimServlet(BaseFederationServlet):
|
||||||
|
PATH = "/user/keys/claim"
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_POST(self, origin, content, query):
|
||||||
|
response = yield self.handler.on_claim_client_keys(origin, content)
|
||||||
|
defer.returnValue((200, response))
|
||||||
|
|
||||||
|
|
||||||
class FederationQueryAuthServlet(BaseFederationServlet):
|
class FederationQueryAuthServlet(BaseFederationServlet):
|
||||||
PATH = "/query_auth/([^/]*)/([^/]*)"
|
PATH = "/query_auth/([^/]*)/([^/]*)"
|
||||||
|
|
||||||
@@ -362,4 +391,6 @@ SERVLET_CLASSES = (
|
|||||||
FederationQueryAuthServlet,
|
FederationQueryAuthServlet,
|
||||||
FederationGetMissingEventsServlet,
|
FederationGetMissingEventsServlet,
|
||||||
FederationEventAuthServlet,
|
FederationEventAuthServlet,
|
||||||
|
FederationClientKeysQueryServlet,
|
||||||
|
FederationClientKeysClaimServlet,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ from .room import (
|
|||||||
from .message import MessageHandler
|
from .message import MessageHandler
|
||||||
from .events import EventStreamHandler, EventHandler
|
from .events import EventStreamHandler, EventHandler
|
||||||
from .federation import FederationHandler
|
from .federation import FederationHandler
|
||||||
from .login import LoginHandler
|
|
||||||
from .profile import ProfileHandler
|
from .profile import ProfileHandler
|
||||||
from .presence import PresenceHandler
|
from .presence import PresenceHandler
|
||||||
from .directory import DirectoryHandler
|
from .directory import DirectoryHandler
|
||||||
@@ -32,6 +31,7 @@ from .appservice import ApplicationServicesHandler
|
|||||||
from .sync import SyncHandler
|
from .sync import SyncHandler
|
||||||
from .auth import AuthHandler
|
from .auth import AuthHandler
|
||||||
from .identity import IdentityHandler
|
from .identity import IdentityHandler
|
||||||
|
from .receipts import ReceiptsHandler
|
||||||
|
|
||||||
|
|
||||||
class Handlers(object):
|
class Handlers(object):
|
||||||
@@ -53,10 +53,10 @@ class Handlers(object):
|
|||||||
self.profile_handler = ProfileHandler(hs)
|
self.profile_handler = ProfileHandler(hs)
|
||||||
self.presence_handler = PresenceHandler(hs)
|
self.presence_handler = PresenceHandler(hs)
|
||||||
self.room_list_handler = RoomListHandler(hs)
|
self.room_list_handler = RoomListHandler(hs)
|
||||||
self.login_handler = LoginHandler(hs)
|
|
||||||
self.directory_handler = DirectoryHandler(hs)
|
self.directory_handler = DirectoryHandler(hs)
|
||||||
self.typing_notification_handler = TypingNotificationHandler(hs)
|
self.typing_notification_handler = TypingNotificationHandler(hs)
|
||||||
self.admin_handler = AdminHandler(hs)
|
self.admin_handler = AdminHandler(hs)
|
||||||
|
self.receipts_handler = ReceiptsHandler(hs)
|
||||||
asapi = ApplicationServiceApi(hs)
|
asapi = ApplicationServiceApi(hs)
|
||||||
self.appservice_handler = ApplicationServicesHandler(
|
self.appservice_handler = ApplicationServicesHandler(
|
||||||
hs, asapi, AppServiceScheduler(
|
hs, asapi, AppServiceScheduler(
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ from twisted.internet import defer
|
|||||||
from synapse.api.errors import LimitExceededError, SynapseError
|
from synapse.api.errors import LimitExceededError, SynapseError
|
||||||
from synapse.crypto.event_signing import add_hashes_and_signatures
|
from synapse.crypto.event_signing import add_hashes_and_signatures
|
||||||
from synapse.api.constants import Membership, EventTypes
|
from synapse.api.constants import Membership, EventTypes
|
||||||
from synapse.types import UserID
|
from synapse.types import UserID, RoomAlias
|
||||||
|
|
||||||
|
from synapse.util.logcontext import PreserveLoggingContext
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -76,7 +78,9 @@ class BaseHandler(object):
|
|||||||
context = yield state_handler.compute_event_context(builder)
|
context = yield state_handler.compute_event_context(builder)
|
||||||
|
|
||||||
if builder.is_state():
|
if builder.is_state():
|
||||||
builder.prev_state = context.prev_state_events
|
builder.prev_state = yield self.store.add_event_hashes(
|
||||||
|
context.prev_state_events
|
||||||
|
)
|
||||||
|
|
||||||
yield self.auth.add_auth_events(builder, context)
|
yield self.auth.add_auth_events(builder, context)
|
||||||
|
|
||||||
@@ -103,7 +107,25 @@ class BaseHandler(object):
|
|||||||
if not suppress_auth:
|
if not suppress_auth:
|
||||||
self.auth.check(event, auth_events=context.current_state)
|
self.auth.check(event, auth_events=context.current_state)
|
||||||
|
|
||||||
yield self.store.persist_event(event, context=context)
|
if event.type == EventTypes.CanonicalAlias:
|
||||||
|
# Check the alias is acually valid (at this time at least)
|
||||||
|
room_alias_str = event.content.get("alias", None)
|
||||||
|
if room_alias_str:
|
||||||
|
room_alias = RoomAlias.from_string(room_alias_str)
|
||||||
|
directory_handler = self.hs.get_handlers().directory_handler
|
||||||
|
mapping = yield directory_handler.get_association(room_alias)
|
||||||
|
|
||||||
|
if mapping["room_id"] != event.room_id:
|
||||||
|
raise SynapseError(
|
||||||
|
400,
|
||||||
|
"Room alias %s does not point to the room" % (
|
||||||
|
room_alias_str,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(event_stream_id, max_stream_id) = yield self.store.persist_event(
|
||||||
|
event, context=context
|
||||||
|
)
|
||||||
|
|
||||||
federation_handler = self.hs.get_handlers().federation_handler
|
federation_handler = self.hs.get_handlers().federation_handler
|
||||||
|
|
||||||
@@ -137,10 +159,12 @@ class BaseHandler(object):
|
|||||||
"Failed to get destination from event %s", s.event_id
|
"Failed to get destination from event %s", s.event_id
|
||||||
)
|
)
|
||||||
|
|
||||||
# Don't block waiting on waking up all the listeners.
|
with PreserveLoggingContext():
|
||||||
notify_d = self.notifier.on_new_room_event(
|
# Don't block waiting on waking up all the listeners.
|
||||||
event, extra_users=extra_users
|
notify_d = self.notifier.on_new_room_event(
|
||||||
)
|
event, event_stream_id, max_stream_id,
|
||||||
|
extra_users=extra_users
|
||||||
|
)
|
||||||
|
|
||||||
def log_failure(f):
|
def log_failure(f):
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -150,8 +174,6 @@ class BaseHandler(object):
|
|||||||
|
|
||||||
notify_d.addErrback(log_failure)
|
notify_d.addErrback(log_failure)
|
||||||
|
|
||||||
fed_d = federation_handler.handle_new_event(
|
federation_handler.handle_new_event(
|
||||||
event, destinations=destinations,
|
event, destinations=destinations,
|
||||||
)
|
)
|
||||||
|
|
||||||
fed_d.addErrback(log_failure)
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.constants import EventTypes, Membership
|
from synapse.api.constants import EventTypes
|
||||||
from synapse.appservice import ApplicationService
|
from synapse.appservice import ApplicationService
|
||||||
from synapse.types import UserID
|
from synapse.types import UserID
|
||||||
|
|
||||||
@@ -147,10 +147,7 @@ class ApplicationServicesHandler(object):
|
|||||||
)
|
)
|
||||||
# We need to know the members associated with this event.room_id,
|
# We need to know the members associated with this event.room_id,
|
||||||
# if any.
|
# if any.
|
||||||
member_list = yield self.store.get_room_members(
|
member_list = yield self.store.get_users_in_room(event.room_id)
|
||||||
room_id=event.room_id,
|
|
||||||
membership=Membership.JOIN
|
|
||||||
)
|
|
||||||
|
|
||||||
services = yield self.store.get_app_services()
|
services = yield self.store.get_app_services()
|
||||||
interested_list = [
|
interested_list = [
|
||||||
@@ -180,7 +177,7 @@ class ApplicationServicesHandler(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
user_info = yield self.store.get_user_by_id(user_id)
|
user_info = yield self.store.get_user_by_id(user_id)
|
||||||
if len(user_info) > 0:
|
if user_info:
|
||||||
defer.returnValue(False)
|
defer.returnValue(False)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -47,17 +47,24 @@ class AuthHandler(BaseHandler):
|
|||||||
self.sessions = {}
|
self.sessions = {}
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def check_auth(self, flows, clientdict, clientip=None):
|
def check_auth(self, flows, clientdict, clientip):
|
||||||
"""
|
"""
|
||||||
Takes a dictionary sent by the client in the login / registration
|
Takes a dictionary sent by the client in the login / registration
|
||||||
protocol and handles the login flow.
|
protocol and handles the login flow.
|
||||||
|
|
||||||
|
As a side effect, this function fills in the 'creds' key on the user's
|
||||||
|
session with a map, which maps each auth-type (str) to the relevant
|
||||||
|
identity authenticated by that auth-type (mostly str, but for captcha, bool).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
flows: list of list of stages
|
flows (list): A list of login flows. Each flow is an ordered list of
|
||||||
authdict: The dictionary from the client root level, not the
|
strings representing auth-types. At least one full
|
||||||
'auth' key: this method prompts for auth if none is sent.
|
flow must be completed in order for auth to be successful.
|
||||||
|
clientdict: The dictionary from the client root level, not the
|
||||||
|
'auth' key: this method prompts for auth if none is sent.
|
||||||
|
clientip (str): The IP address of the client.
|
||||||
Returns:
|
Returns:
|
||||||
A tuple of authed, dict, dict where authed is true if the client
|
A tuple of (authed, dict, dict) where authed is true if the client
|
||||||
has successfully completed an auth flow. If it is true, the first
|
has successfully completed an auth flow. If it is true, the first
|
||||||
dict contains the authenticated credentials of each stage.
|
dict contains the authenticated credentials of each stage.
|
||||||
|
|
||||||
@@ -75,7 +82,7 @@ class AuthHandler(BaseHandler):
|
|||||||
del clientdict['auth']
|
del clientdict['auth']
|
||||||
if 'session' in authdict:
|
if 'session' in authdict:
|
||||||
sid = authdict['session']
|
sid = authdict['session']
|
||||||
sess = self._get_session_info(sid)
|
session = self._get_session_info(sid)
|
||||||
|
|
||||||
if len(clientdict) > 0:
|
if len(clientdict) > 0:
|
||||||
# This was designed to allow the client to omit the parameters
|
# This was designed to allow the client to omit the parameters
|
||||||
@@ -85,20 +92,21 @@ class AuthHandler(BaseHandler):
|
|||||||
# email auth link on there). It's probably too open to abuse
|
# email auth link on there). It's probably too open to abuse
|
||||||
# because it lets unauthenticated clients store arbitrary objects
|
# because it lets unauthenticated clients store arbitrary objects
|
||||||
# on a home server.
|
# on a home server.
|
||||||
# sess['clientdict'] = clientdict
|
# Revisit: Assumimg the REST APIs do sensible validation, the data
|
||||||
# self._save_session(sess)
|
# isn't arbintrary.
|
||||||
pass
|
session['clientdict'] = clientdict
|
||||||
elif 'clientdict' in sess:
|
self._save_session(session)
|
||||||
clientdict = sess['clientdict']
|
elif 'clientdict' in session:
|
||||||
|
clientdict = session['clientdict']
|
||||||
|
|
||||||
if not authdict:
|
if not authdict:
|
||||||
defer.returnValue(
|
defer.returnValue(
|
||||||
(False, self._auth_dict_for_flows(flows, sess), clientdict)
|
(False, self._auth_dict_for_flows(flows, session), clientdict)
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'creds' not in sess:
|
if 'creds' not in session:
|
||||||
sess['creds'] = {}
|
session['creds'] = {}
|
||||||
creds = sess['creds']
|
creds = session['creds']
|
||||||
|
|
||||||
# check auth type currently being presented
|
# check auth type currently being presented
|
||||||
if 'type' in authdict:
|
if 'type' in authdict:
|
||||||
@@ -107,15 +115,15 @@ class AuthHandler(BaseHandler):
|
|||||||
result = yield self.checkers[authdict['type']](authdict, clientip)
|
result = yield self.checkers[authdict['type']](authdict, clientip)
|
||||||
if result:
|
if result:
|
||||||
creds[authdict['type']] = result
|
creds[authdict['type']] = result
|
||||||
self._save_session(sess)
|
self._save_session(session)
|
||||||
|
|
||||||
for f in flows:
|
for f in flows:
|
||||||
if len(set(f) - set(creds.keys())) == 0:
|
if len(set(f) - set(creds.keys())) == 0:
|
||||||
logger.info("Auth completed with creds: %r", creds)
|
logger.info("Auth completed with creds: %r", creds)
|
||||||
self._remove_session(sess)
|
self._remove_session(session)
|
||||||
defer.returnValue((True, creds, clientdict))
|
defer.returnValue((True, creds, clientdict))
|
||||||
|
|
||||||
ret = self._auth_dict_for_flows(flows, sess)
|
ret = self._auth_dict_for_flows(flows, session)
|
||||||
ret['completed'] = creds.keys()
|
ret['completed'] = creds.keys()
|
||||||
defer.returnValue((False, ret, clientdict))
|
defer.returnValue((False, ret, clientdict))
|
||||||
|
|
||||||
@@ -149,22 +157,14 @@ class AuthHandler(BaseHandler):
|
|||||||
if "user" not in authdict or "password" not in authdict:
|
if "user" not in authdict or "password" not in authdict:
|
||||||
raise LoginError(400, "", Codes.MISSING_PARAM)
|
raise LoginError(400, "", Codes.MISSING_PARAM)
|
||||||
|
|
||||||
user = authdict["user"]
|
user_id = authdict["user"]
|
||||||
password = authdict["password"]
|
password = authdict["password"]
|
||||||
if not user.startswith('@'):
|
if not user_id.startswith('@'):
|
||||||
user = UserID.create(user, self.hs.hostname).to_string()
|
user_id = UserID.create(user_id, self.hs.hostname).to_string()
|
||||||
|
|
||||||
user_info = yield self.store.get_user_by_id(user_id=user)
|
user_id, password_hash = yield self._find_user_id_and_pwd_hash(user_id)
|
||||||
if not user_info:
|
self._check_password(user_id, password, password_hash)
|
||||||
logger.warn("Attempted to login as %s but they do not exist", user)
|
defer.returnValue(user_id)
|
||||||
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
|
|
||||||
|
|
||||||
stored_hash = user_info[0]["password_hash"]
|
|
||||||
if bcrypt.checkpw(password, stored_hash):
|
|
||||||
defer.returnValue(user)
|
|
||||||
else:
|
|
||||||
logger.warn("Failed password login for user %s", user)
|
|
||||||
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _check_recaptcha(self, authdict, clientip):
|
def _check_recaptcha(self, authdict, clientip):
|
||||||
@@ -187,8 +187,8 @@ class AuthHandler(BaseHandler):
|
|||||||
# each request
|
# each request
|
||||||
try:
|
try:
|
||||||
client = SimpleHttpClient(self.hs)
|
client = SimpleHttpClient(self.hs)
|
||||||
data = yield client.post_urlencoded_get_json(
|
resp_body = yield client.post_urlencoded_get_json(
|
||||||
"https://www.google.com/recaptcha/api/siteverify",
|
self.hs.config.recaptcha_siteverify_api,
|
||||||
args={
|
args={
|
||||||
'secret': self.hs.config.recaptcha_private_key,
|
'secret': self.hs.config.recaptcha_private_key,
|
||||||
'response': user_response,
|
'response': user_response,
|
||||||
@@ -198,7 +198,8 @@ class AuthHandler(BaseHandler):
|
|||||||
except PartialDownloadError as pde:
|
except PartialDownloadError as pde:
|
||||||
# Twisted is silly
|
# Twisted is silly
|
||||||
data = pde.response
|
data = pde.response
|
||||||
resp_body = simplejson.loads(data)
|
resp_body = simplejson.loads(data)
|
||||||
|
|
||||||
if 'success' in resp_body and resp_body['success']:
|
if 'success' in resp_body and resp_body['success']:
|
||||||
defer.returnValue(True)
|
defer.returnValue(True)
|
||||||
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
|
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
|
||||||
@@ -267,6 +268,79 @@ class AuthHandler(BaseHandler):
|
|||||||
|
|
||||||
return self.sessions[session_id]
|
return self.sessions[session_id]
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def login_with_password(self, user_id, password):
|
||||||
|
"""
|
||||||
|
Authenticates the user with their username and password.
|
||||||
|
|
||||||
|
Used only by the v1 login API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id (str): User ID
|
||||||
|
password (str): Password
|
||||||
|
Returns:
|
||||||
|
The access token for the user's session.
|
||||||
|
Raises:
|
||||||
|
StoreError if there was a problem storing the token.
|
||||||
|
LoginError if there was an authentication problem.
|
||||||
|
"""
|
||||||
|
user_id, password_hash = yield self._find_user_id_and_pwd_hash(user_id)
|
||||||
|
self._check_password(user_id, password, password_hash)
|
||||||
|
|
||||||
|
reg_handler = self.hs.get_handlers().registration_handler
|
||||||
|
access_token = reg_handler.generate_token(user_id)
|
||||||
|
logger.info("Logging in user %s", user_id)
|
||||||
|
yield self.store.add_access_token_to_user(user_id, access_token)
|
||||||
|
defer.returnValue((user_id, access_token))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _find_user_id_and_pwd_hash(self, user_id):
|
||||||
|
"""Checks to see if a user with the given id exists. Will check case
|
||||||
|
insensitively, but will throw if there are multiple inexact matches.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: A 2-tuple of `(canonical_user_id, password_hash)`
|
||||||
|
"""
|
||||||
|
user_infos = yield self.store.get_users_by_id_case_insensitive(user_id)
|
||||||
|
if not user_infos:
|
||||||
|
logger.warn("Attempted to login as %s but they do not exist", user_id)
|
||||||
|
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
|
||||||
|
|
||||||
|
if len(user_infos) > 1:
|
||||||
|
if user_id not in user_infos:
|
||||||
|
logger.warn(
|
||||||
|
"Attempted to login as %s but it matches more than one user "
|
||||||
|
"inexactly: %r",
|
||||||
|
user_id, user_infos.keys()
|
||||||
|
)
|
||||||
|
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
|
||||||
|
|
||||||
|
defer.returnValue((user_id, user_infos[user_id]))
|
||||||
|
else:
|
||||||
|
defer.returnValue(user_infos.popitem())
|
||||||
|
|
||||||
|
def _check_password(self, user_id, password, stored_hash):
|
||||||
|
"""Checks that user_id has passed password, raises LoginError if not."""
|
||||||
|
if not bcrypt.checkpw(password, stored_hash):
|
||||||
|
logger.warn("Failed password login for user %s", user_id)
|
||||||
|
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def set_password(self, user_id, newpassword):
|
||||||
|
password_hash = bcrypt.hashpw(newpassword, bcrypt.gensalt())
|
||||||
|
|
||||||
|
yield self.store.user_set_password_hash(user_id, password_hash)
|
||||||
|
yield self.store.user_delete_access_tokens(user_id)
|
||||||
|
yield self.hs.get_pusherpool().remove_pushers_by_user(user_id)
|
||||||
|
yield self.store.flush_user(user_id)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def add_threepid(self, user_id, medium, address, validated_at):
|
||||||
|
yield self.store.user_add_threepid(
|
||||||
|
user_id, medium, address, validated_at,
|
||||||
|
self.hs.get_clock().time_msec()
|
||||||
|
)
|
||||||
|
|
||||||
def _save_session(self, session):
|
def _save_session(self, session):
|
||||||
# TODO: Persistent storage
|
# TODO: Persistent storage
|
||||||
logger.debug("Saving session %s", session)
|
logger.debug("Saving session %s", session)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from synapse.api.constants import EventTypes
|
|||||||
from synapse.types import RoomAlias
|
from synapse.types import RoomAlias
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import string
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -40,6 +41,10 @@ class DirectoryHandler(BaseHandler):
|
|||||||
def _create_association(self, room_alias, room_id, servers=None):
|
def _create_association(self, room_alias, room_id, servers=None):
|
||||||
# general association creation for both human users and app services
|
# general association creation for both human users and app services
|
||||||
|
|
||||||
|
for wchar in string.whitespace:
|
||||||
|
if wchar in room_alias.localpart:
|
||||||
|
raise SynapseError(400, "Invalid characters in room alias")
|
||||||
|
|
||||||
if not self.hs.is_mine(room_alias):
|
if not self.hs.is_mine(room_alias):
|
||||||
raise SynapseError(400, "Room alias must be local")
|
raise SynapseError(400, "Room alias must be local")
|
||||||
# TODO(erikj): Change this.
|
# TODO(erikj): Change this.
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.util.logcontext import PreserveLoggingContext
|
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
from synapse.types import UserID
|
from synapse.types import UserID
|
||||||
from synapse.events.utils import serialize_event
|
from synapse.events.utils import serialize_event
|
||||||
@@ -50,7 +49,12 @@ class EventStreamHandler(BaseHandler):
|
|||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def get_stream(self, auth_user_id, pagin_config, timeout=0,
|
def get_stream(self, auth_user_id, pagin_config, timeout=0,
|
||||||
as_client_event=True, affect_presence=True):
|
as_client_event=True, affect_presence=True,
|
||||||
|
only_room_events=False):
|
||||||
|
"""Fetches the events stream for a given user.
|
||||||
|
|
||||||
|
If `only_room_events` is `True` only room events will be returned.
|
||||||
|
"""
|
||||||
auth_user = UserID.from_string(auth_user_id)
|
auth_user = UserID.from_string(auth_user_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -71,7 +75,15 @@ class EventStreamHandler(BaseHandler):
|
|||||||
self._streams_per_user[auth_user] += 1
|
self._streams_per_user[auth_user] += 1
|
||||||
|
|
||||||
rm_handler = self.hs.get_handlers().room_member_handler
|
rm_handler = self.hs.get_handlers().room_member_handler
|
||||||
room_ids = yield rm_handler.get_joined_rooms_for_user(auth_user)
|
|
||||||
|
app_service = yield self.store.get_app_service_by_user_id(
|
||||||
|
auth_user.to_string()
|
||||||
|
)
|
||||||
|
if app_service:
|
||||||
|
rooms = yield self.store.get_app_service_rooms(app_service)
|
||||||
|
room_ids = set(r.room_id for r in rooms)
|
||||||
|
else:
|
||||||
|
room_ids = yield rm_handler.get_joined_rooms_for_user(auth_user)
|
||||||
|
|
||||||
if timeout:
|
if timeout:
|
||||||
# If they've set a timeout set a minimum limit.
|
# If they've set a timeout set a minimum limit.
|
||||||
@@ -81,10 +93,10 @@ class EventStreamHandler(BaseHandler):
|
|||||||
# thundering herds on restart.
|
# thundering herds on restart.
|
||||||
timeout = random.randint(int(timeout*0.9), int(timeout*1.1))
|
timeout = random.randint(int(timeout*0.9), int(timeout*1.1))
|
||||||
|
|
||||||
with PreserveLoggingContext():
|
events, tokens = yield self.notifier.get_events_for(
|
||||||
events, tokens = yield self.notifier.get_events_for(
|
auth_user, room_ids, pagin_config, timeout,
|
||||||
auth_user, room_ids, pagin_config, timeout
|
only_room_events=only_room_events
|
||||||
)
|
)
|
||||||
|
|
||||||
time_now = self.clock.time_msec()
|
time_now = self.clock.time_msec()
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,11 @@
|
|||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
|
|
||||||
from synapse.api.errors import (
|
from synapse.api.errors import (
|
||||||
AuthError, FederationError, StoreError,
|
AuthError, FederationError, StoreError, CodeMessageException, SynapseError,
|
||||||
)
|
)
|
||||||
from synapse.api.constants import EventTypes, Membership, RejectedReason
|
from synapse.api.constants import EventTypes, Membership, RejectedReason
|
||||||
|
from synapse.util import unwrapFirstError
|
||||||
|
from synapse.util.logcontext import PreserveLoggingContext
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
from synapse.util.async import run_on_reactor
|
from synapse.util.async import run_on_reactor
|
||||||
from synapse.util.frozenutils import unfreeze
|
from synapse.util.frozenutils import unfreeze
|
||||||
@@ -29,6 +31,10 @@ from synapse.crypto.event_signing import (
|
|||||||
)
|
)
|
||||||
from synapse.types import UserID
|
from synapse.types import UserID
|
||||||
|
|
||||||
|
from synapse.events.utils import prune_event
|
||||||
|
|
||||||
|
from synapse.util.retryutils import NotRetryingDestination
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
@@ -73,8 +79,6 @@ class FederationHandler(BaseHandler):
|
|||||||
# When joining a room we need to queue any events for that room up
|
# When joining a room we need to queue any events for that room up
|
||||||
self.room_queues = {}
|
self.room_queues = {}
|
||||||
|
|
||||||
@log_function
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def handle_new_event(self, event, destinations):
|
def handle_new_event(self, event, destinations):
|
||||||
""" Takes in an event from the client to server side, that has already
|
""" Takes in an event from the client to server side, that has already
|
||||||
been authed and handled by the state module, and sends it to any
|
been authed and handled by the state module, and sends it to any
|
||||||
@@ -89,9 +93,7 @@ class FederationHandler(BaseHandler):
|
|||||||
processing.
|
processing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
yield run_on_reactor()
|
return self.replication_layer.send_pdu(event, destinations)
|
||||||
|
|
||||||
self.replication_layer.send_pdu(event, destinations)
|
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@@ -138,29 +140,32 @@ class FederationHandler(BaseHandler):
|
|||||||
if state and auth_chain is not None:
|
if state and auth_chain is not None:
|
||||||
# If we have any state or auth_chain given to us by the replication
|
# If we have any state or auth_chain given to us by the replication
|
||||||
# layer, then we should handle them (if we haven't before.)
|
# layer, then we should handle them (if we haven't before.)
|
||||||
|
|
||||||
|
event_infos = []
|
||||||
|
|
||||||
for e in itertools.chain(auth_chain, state):
|
for e in itertools.chain(auth_chain, state):
|
||||||
if e.event_id in seen_ids:
|
if e.event_id in seen_ids:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
e.internal_metadata.outlier = True
|
e.internal_metadata.outlier = True
|
||||||
try:
|
auth_ids = [e_id for e_id, _ in e.auth_events]
|
||||||
auth_ids = [e_id for e_id, _ in e.auth_events]
|
auth = {
|
||||||
auth = {
|
(e.type, e.state_key): e for e in auth_chain
|
||||||
(e.type, e.state_key): e for e in auth_chain
|
if e.event_id in auth_ids
|
||||||
if e.event_id in auth_ids
|
}
|
||||||
}
|
event_infos.append({
|
||||||
yield self._handle_new_event(
|
"event": e,
|
||||||
origin, e, auth_events=auth
|
"auth_events": auth,
|
||||||
)
|
})
|
||||||
seen_ids.add(e.event_id)
|
seen_ids.add(e.event_id)
|
||||||
except:
|
|
||||||
logger.exception(
|
yield self._handle_new_events(
|
||||||
"Failed to handle state event %s",
|
origin,
|
||||||
e.event_id,
|
event_infos,
|
||||||
)
|
outliers=True
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield self._handle_new_event(
|
_, event_stream_id, max_stream_id = yield self._handle_new_event(
|
||||||
origin,
|
origin,
|
||||||
event,
|
event,
|
||||||
state=state,
|
state=state,
|
||||||
@@ -201,9 +206,11 @@ class FederationHandler(BaseHandler):
|
|||||||
target_user = UserID.from_string(target_user_id)
|
target_user = UserID.from_string(target_user_id)
|
||||||
extra_users.append(target_user)
|
extra_users.append(target_user)
|
||||||
|
|
||||||
d = self.notifier.on_new_room_event(
|
with PreserveLoggingContext():
|
||||||
event, extra_users=extra_users
|
d = self.notifier.on_new_room_event(
|
||||||
)
|
event, event_stream_id, max_stream_id,
|
||||||
|
extra_users=extra_users
|
||||||
|
)
|
||||||
|
|
||||||
def log_failure(f):
|
def log_failure(f):
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -220,38 +227,300 @@ class FederationHandler(BaseHandler):
|
|||||||
"user_joined_room", user=user, room_id=event.room_id
|
"user_joined_room", user=user, room_id=event.room_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _filter_events_for_server(self, server_name, room_id, events):
|
||||||
|
event_to_state = yield self.store.get_state_for_events(
|
||||||
|
room_id, frozenset(e.event_id for e in events),
|
||||||
|
types=(
|
||||||
|
(EventTypes.RoomHistoryVisibility, ""),
|
||||||
|
(EventTypes.Member, None),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def redact_disallowed(event, state):
|
||||||
|
if not state:
|
||||||
|
return event
|
||||||
|
|
||||||
|
history = state.get((EventTypes.RoomHistoryVisibility, ''), None)
|
||||||
|
if history:
|
||||||
|
visibility = history.content.get("history_visibility", "shared")
|
||||||
|
if visibility in ["invited", "joined"]:
|
||||||
|
# We now loop through all state events looking for
|
||||||
|
# membership states for the requesting server to determine
|
||||||
|
# if the server is either in the room or has been invited
|
||||||
|
# into the room.
|
||||||
|
for ev in state.values():
|
||||||
|
if ev.type != EventTypes.Member:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
domain = UserID.from_string(ev.state_key).domain
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if domain != server_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
memtype = ev.membership
|
||||||
|
if memtype == Membership.JOIN:
|
||||||
|
return event
|
||||||
|
elif memtype == Membership.INVITE:
|
||||||
|
if visibility == "invited":
|
||||||
|
return event
|
||||||
|
else:
|
||||||
|
return prune_event(event)
|
||||||
|
|
||||||
|
return event
|
||||||
|
|
||||||
|
defer.returnValue([
|
||||||
|
redact_disallowed(e, event_to_state[e.event_id])
|
||||||
|
for e in events
|
||||||
|
])
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def backfill(self, dest, room_id, limit):
|
def backfill(self, dest, room_id, limit, extremities=[]):
|
||||||
""" Trigger a backfill request to `dest` for the given `room_id`
|
""" Trigger a backfill request to `dest` for the given `room_id`
|
||||||
"""
|
"""
|
||||||
extremities = yield self.store.get_oldest_events_in_room(room_id)
|
if not extremities:
|
||||||
|
extremities = yield self.store.get_oldest_events_in_room(room_id)
|
||||||
|
|
||||||
pdus = yield self.replication_layer.backfill(
|
events = yield self.replication_layer.backfill(
|
||||||
dest,
|
dest,
|
||||||
room_id,
|
room_id,
|
||||||
limit,
|
limit=limit,
|
||||||
extremities=extremities,
|
extremities=extremities,
|
||||||
)
|
)
|
||||||
|
|
||||||
events = []
|
event_map = {e.event_id: e for e in events}
|
||||||
|
|
||||||
for pdu in pdus:
|
event_ids = set(e.event_id for e in events)
|
||||||
event = pdu
|
|
||||||
|
|
||||||
# FIXME (erikj): Not sure this actually works :/
|
edges = [
|
||||||
context = yield self.state_handler.compute_event_context(event)
|
ev.event_id
|
||||||
|
for ev in events
|
||||||
|
if set(e_id for e_id, _ in ev.prev_events) - event_ids
|
||||||
|
]
|
||||||
|
|
||||||
events.append((event, context))
|
logger.info(
|
||||||
|
"backfill: Got %d events with %d edges",
|
||||||
|
len(events), len(edges),
|
||||||
|
)
|
||||||
|
|
||||||
yield self.store.persist_event(
|
# For each edge get the current state.
|
||||||
event,
|
|
||||||
context=context,
|
auth_events = {}
|
||||||
backfilled=True
|
state_events = {}
|
||||||
|
events_to_state = {}
|
||||||
|
for e_id in edges:
|
||||||
|
state, auth = yield self.replication_layer.get_state_for_room(
|
||||||
|
destination=dest,
|
||||||
|
room_id=room_id,
|
||||||
|
event_id=e_id
|
||||||
)
|
)
|
||||||
|
auth_events.update({a.event_id: a for a in auth})
|
||||||
|
auth_events.update({s.event_id: s for s in state})
|
||||||
|
state_events.update({s.event_id: s for s in state})
|
||||||
|
events_to_state[e_id] = state
|
||||||
|
|
||||||
|
seen_events = yield self.store.have_events(
|
||||||
|
set(auth_events.keys()) | set(state_events.keys())
|
||||||
|
)
|
||||||
|
|
||||||
|
all_events = events + state_events.values() + auth_events.values()
|
||||||
|
required_auth = set(
|
||||||
|
a_id for event in all_events for a_id, _ in event.auth_events
|
||||||
|
)
|
||||||
|
|
||||||
|
missing_auth = required_auth - set(auth_events)
|
||||||
|
results = yield defer.gatherResults(
|
||||||
|
[
|
||||||
|
self.replication_layer.get_pdu(
|
||||||
|
[dest],
|
||||||
|
event_id,
|
||||||
|
outlier=True,
|
||||||
|
timeout=10000,
|
||||||
|
)
|
||||||
|
for event_id in missing_auth
|
||||||
|
],
|
||||||
|
consumeErrors=True
|
||||||
|
).addErrback(unwrapFirstError)
|
||||||
|
auth_events.update({a.event_id: a for a in results})
|
||||||
|
|
||||||
|
ev_infos = []
|
||||||
|
for a in auth_events.values():
|
||||||
|
if a.event_id in seen_events:
|
||||||
|
continue
|
||||||
|
ev_infos.append({
|
||||||
|
"event": a,
|
||||||
|
"auth_events": {
|
||||||
|
(auth_events[a_id].type, auth_events[a_id].state_key):
|
||||||
|
auth_events[a_id]
|
||||||
|
for a_id, _ in a.auth_events
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for e_id in events_to_state:
|
||||||
|
ev_infos.append({
|
||||||
|
"event": event_map[e_id],
|
||||||
|
"state": events_to_state[e_id],
|
||||||
|
"auth_events": {
|
||||||
|
(auth_events[a_id].type, auth_events[a_id].state_key):
|
||||||
|
auth_events[a_id]
|
||||||
|
for a_id, _ in event_map[e_id].auth_events
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
events.sort(key=lambda e: e.depth)
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
if event in events_to_state:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ev_infos.append({
|
||||||
|
"event": event,
|
||||||
|
})
|
||||||
|
|
||||||
|
yield self._handle_new_events(
|
||||||
|
dest, ev_infos,
|
||||||
|
backfilled=True,
|
||||||
|
)
|
||||||
|
|
||||||
defer.returnValue(events)
|
defer.returnValue(events)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def maybe_backfill(self, room_id, current_depth):
|
||||||
|
"""Checks the database to see if we should backfill before paginating,
|
||||||
|
and if so do.
|
||||||
|
"""
|
||||||
|
extremities = yield self.store.get_oldest_events_with_depth_in_room(
|
||||||
|
room_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not extremities:
|
||||||
|
logger.debug("Not backfilling as no extremeties found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if we reached a point where we should start backfilling.
|
||||||
|
sorted_extremeties_tuple = sorted(
|
||||||
|
extremities.items(),
|
||||||
|
key=lambda e: -int(e[1])
|
||||||
|
)
|
||||||
|
max_depth = sorted_extremeties_tuple[0][1]
|
||||||
|
|
||||||
|
if current_depth > max_depth:
|
||||||
|
logger.debug(
|
||||||
|
"Not backfilling as we don't need to. %d < %d",
|
||||||
|
max_depth, current_depth,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Now we need to decide which hosts to hit first.
|
||||||
|
|
||||||
|
# First we try hosts that are already in the room
|
||||||
|
# TODO: HEURISTIC ALERT.
|
||||||
|
|
||||||
|
curr_state = yield self.state_handler.get_current_state(room_id)
|
||||||
|
|
||||||
|
def get_domains_from_state(state):
|
||||||
|
joined_users = [
|
||||||
|
(state_key, int(event.depth))
|
||||||
|
for (e_type, state_key), event in state.items()
|
||||||
|
if e_type == EventTypes.Member
|
||||||
|
and event.membership == Membership.JOIN
|
||||||
|
]
|
||||||
|
|
||||||
|
joined_domains = {}
|
||||||
|
for u, d in joined_users:
|
||||||
|
try:
|
||||||
|
dom = UserID.from_string(u).domain
|
||||||
|
old_d = joined_domains.get(dom)
|
||||||
|
if old_d:
|
||||||
|
joined_domains[dom] = min(d, old_d)
|
||||||
|
else:
|
||||||
|
joined_domains[dom] = d
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return sorted(joined_domains.items(), key=lambda d: d[1])
|
||||||
|
|
||||||
|
curr_domains = get_domains_from_state(curr_state)
|
||||||
|
|
||||||
|
likely_domains = [
|
||||||
|
domain for domain, depth in curr_domains
|
||||||
|
if domain is not self.server_name
|
||||||
|
]
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def try_backfill(domains):
|
||||||
|
# TODO: Should we try multiple of these at a time?
|
||||||
|
for dom in domains:
|
||||||
|
try:
|
||||||
|
events = yield self.backfill(
|
||||||
|
dom, room_id,
|
||||||
|
limit=100,
|
||||||
|
extremities=[e for e in extremities.keys()]
|
||||||
|
)
|
||||||
|
except SynapseError:
|
||||||
|
logger.info(
|
||||||
|
"Failed to backfill from %s because %s",
|
||||||
|
dom, e,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
except CodeMessageException as e:
|
||||||
|
if 400 <= e.code < 500:
|
||||||
|
raise
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Failed to backfill from %s because %s",
|
||||||
|
dom, e,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
except NotRetryingDestination as e:
|
||||||
|
logger.info(e.message)
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(
|
||||||
|
"Failed to backfill from %s because %s",
|
||||||
|
dom, e,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if events:
|
||||||
|
defer.returnValue(True)
|
||||||
|
defer.returnValue(False)
|
||||||
|
|
||||||
|
success = yield try_backfill(likely_domains)
|
||||||
|
if success:
|
||||||
|
defer.returnValue(True)
|
||||||
|
|
||||||
|
# Huh, well *those* domains didn't work out. Lets try some domains
|
||||||
|
# from the time.
|
||||||
|
|
||||||
|
tried_domains = set(likely_domains)
|
||||||
|
tried_domains.add(self.server_name)
|
||||||
|
|
||||||
|
event_ids = list(extremities.keys())
|
||||||
|
|
||||||
|
states = yield defer.gatherResults([
|
||||||
|
self.state_handler.resolve_state_groups(room_id, [e])
|
||||||
|
for e in event_ids
|
||||||
|
])
|
||||||
|
states = dict(zip(event_ids, [s[1] for s in states]))
|
||||||
|
|
||||||
|
for e_id, _ in sorted_extremeties_tuple:
|
||||||
|
likely_domains = get_domains_from_state(states[e_id])
|
||||||
|
|
||||||
|
success = yield try_backfill([
|
||||||
|
dom for dom in likely_domains
|
||||||
|
if dom not in tried_domains
|
||||||
|
])
|
||||||
|
if success:
|
||||||
|
defer.returnValue(True)
|
||||||
|
|
||||||
|
tried_domains.update(likely_domains)
|
||||||
|
|
||||||
|
defer.returnValue(False)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def send_invite(self, target_host, event):
|
def send_invite(self, target_host, event):
|
||||||
""" Sends the invite to the remote server for signing.
|
""" Sends the invite to the remote server for signing.
|
||||||
@@ -380,46 +649,22 @@ class FederationHandler(BaseHandler):
|
|||||||
# FIXME
|
# FIXME
|
||||||
pass
|
pass
|
||||||
|
|
||||||
for e in auth_chain:
|
ev_infos = []
|
||||||
e.internal_metadata.outlier = True
|
for e in itertools.chain(state, auth_chain):
|
||||||
|
|
||||||
if e.event_id == event.event_id:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
auth_ids = [e_id for e_id, _ in e.auth_events]
|
|
||||||
auth = {
|
|
||||||
(e.type, e.state_key): e for e in auth_chain
|
|
||||||
if e.event_id in auth_ids
|
|
||||||
}
|
|
||||||
yield self._handle_new_event(
|
|
||||||
origin, e, auth_events=auth
|
|
||||||
)
|
|
||||||
except:
|
|
||||||
logger.exception(
|
|
||||||
"Failed to handle auth event %s",
|
|
||||||
e.event_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
for e in state:
|
|
||||||
if e.event_id == event.event_id:
|
if e.event_id == event.event_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
e.internal_metadata.outlier = True
|
e.internal_metadata.outlier = True
|
||||||
try:
|
auth_ids = [e_id for e_id, _ in e.auth_events]
|
||||||
auth_ids = [e_id for e_id, _ in e.auth_events]
|
ev_infos.append({
|
||||||
auth = {
|
"event": e,
|
||||||
|
"auth_events": {
|
||||||
(e.type, e.state_key): e for e in auth_chain
|
(e.type, e.state_key): e for e in auth_chain
|
||||||
if e.event_id in auth_ids
|
if e.event_id in auth_ids
|
||||||
}
|
}
|
||||||
yield self._handle_new_event(
|
})
|
||||||
origin, e, auth_events=auth
|
|
||||||
)
|
yield self._handle_new_events(origin, ev_infos, outliers=True)
|
||||||
except:
|
|
||||||
logger.exception(
|
|
||||||
"Failed to handle state event %s",
|
|
||||||
e.event_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
auth_ids = [e_id for e_id, _ in event.auth_events]
|
auth_ids = [e_id for e_id, _ in event.auth_events]
|
||||||
auth_events = {
|
auth_events = {
|
||||||
@@ -427,7 +672,7 @@ class FederationHandler(BaseHandler):
|
|||||||
if e.event_id in auth_ids
|
if e.event_id in auth_ids
|
||||||
}
|
}
|
||||||
|
|
||||||
yield self._handle_new_event(
|
_, event_stream_id, max_stream_id = yield self._handle_new_event(
|
||||||
origin,
|
origin,
|
||||||
new_event,
|
new_event,
|
||||||
state=state,
|
state=state,
|
||||||
@@ -435,9 +680,11 @@ class FederationHandler(BaseHandler):
|
|||||||
auth_events=auth_events,
|
auth_events=auth_events,
|
||||||
)
|
)
|
||||||
|
|
||||||
d = self.notifier.on_new_room_event(
|
with PreserveLoggingContext():
|
||||||
new_event, extra_users=[joinee]
|
d = self.notifier.on_new_room_event(
|
||||||
)
|
new_event, event_stream_id, max_stream_id,
|
||||||
|
extra_users=[joinee]
|
||||||
|
)
|
||||||
|
|
||||||
def log_failure(f):
|
def log_failure(f):
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -502,7 +749,9 @@ class FederationHandler(BaseHandler):
|
|||||||
|
|
||||||
event.internal_metadata.outlier = False
|
event.internal_metadata.outlier = False
|
||||||
|
|
||||||
context = yield self._handle_new_event(origin, event)
|
context, event_stream_id, max_stream_id = yield self._handle_new_event(
|
||||||
|
origin, event
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"on_send_join_request: After _handle_new_event: %s, sigs: %s",
|
"on_send_join_request: After _handle_new_event: %s, sigs: %s",
|
||||||
@@ -516,9 +765,10 @@ class FederationHandler(BaseHandler):
|
|||||||
target_user = UserID.from_string(target_user_id)
|
target_user = UserID.from_string(target_user_id)
|
||||||
extra_users.append(target_user)
|
extra_users.append(target_user)
|
||||||
|
|
||||||
d = self.notifier.on_new_room_event(
|
with PreserveLoggingContext():
|
||||||
event, extra_users=extra_users
|
d = self.notifier.on_new_room_event(
|
||||||
)
|
event, event_stream_id, max_stream_id, extra_users=extra_users
|
||||||
|
)
|
||||||
|
|
||||||
def log_failure(f):
|
def log_failure(f):
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -591,16 +841,18 @@ class FederationHandler(BaseHandler):
|
|||||||
|
|
||||||
context = yield self.state_handler.compute_event_context(event)
|
context = yield self.state_handler.compute_event_context(event)
|
||||||
|
|
||||||
yield self.store.persist_event(
|
event_stream_id, max_stream_id = yield self.store.persist_event(
|
||||||
event,
|
event,
|
||||||
context=context,
|
context=context,
|
||||||
backfilled=False,
|
backfilled=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
target_user = UserID.from_string(event.state_key)
|
target_user = UserID.from_string(event.state_key)
|
||||||
d = self.notifier.on_new_room_event(
|
with PreserveLoggingContext():
|
||||||
event, extra_users=[target_user],
|
d = self.notifier.on_new_room_event(
|
||||||
)
|
event, event_stream_id, max_stream_id,
|
||||||
|
extra_users=[target_user],
|
||||||
|
)
|
||||||
|
|
||||||
def log_failure(f):
|
def log_failure(f):
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -622,7 +874,7 @@ class FederationHandler(BaseHandler):
|
|||||||
raise AuthError(403, "Host not in room.")
|
raise AuthError(403, "Host not in room.")
|
||||||
|
|
||||||
state_groups = yield self.store.get_state_groups(
|
state_groups = yield self.store.get_state_groups(
|
||||||
[event_id]
|
room_id, [event_id]
|
||||||
)
|
)
|
||||||
|
|
||||||
if state_groups:
|
if state_groups:
|
||||||
@@ -669,6 +921,8 @@ class FederationHandler(BaseHandler):
|
|||||||
limit
|
limit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
events = yield self._filter_events_for_server(origin, room_id, events)
|
||||||
|
|
||||||
defer.returnValue(events)
|
defer.returnValue(events)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@@ -727,31 +981,72 @@ class FederationHandler(BaseHandler):
|
|||||||
def _handle_new_event(self, origin, event, state=None, backfilled=False,
|
def _handle_new_event(self, origin, event, state=None, backfilled=False,
|
||||||
current_state=None, auth_events=None):
|
current_state=None, auth_events=None):
|
||||||
|
|
||||||
logger.debug(
|
outlier = event.internal_metadata.is_outlier()
|
||||||
"_handle_new_event: %s, sigs: %s",
|
|
||||||
event.event_id, event.signatures,
|
context = yield self._prep_event(
|
||||||
|
origin, event,
|
||||||
|
state=state,
|
||||||
|
backfilled=backfilled,
|
||||||
|
current_state=current_state,
|
||||||
|
auth_events=auth_events,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
event_stream_id, max_stream_id = yield self.store.persist_event(
|
||||||
|
event,
|
||||||
|
context=context,
|
||||||
|
backfilled=backfilled,
|
||||||
|
is_new_state=(not outlier and not backfilled),
|
||||||
|
current_state=current_state,
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue((context, event_stream_id, max_stream_id))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _handle_new_events(self, origin, event_infos, backfilled=False,
|
||||||
|
outliers=False):
|
||||||
|
contexts = yield defer.gatherResults(
|
||||||
|
[
|
||||||
|
self._prep_event(
|
||||||
|
origin,
|
||||||
|
ev_info["event"],
|
||||||
|
state=ev_info.get("state"),
|
||||||
|
backfilled=backfilled,
|
||||||
|
auth_events=ev_info.get("auth_events"),
|
||||||
|
)
|
||||||
|
for ev_info in event_infos
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
yield self.store.persist_events(
|
||||||
|
[
|
||||||
|
(ev_info["event"], context)
|
||||||
|
for ev_info, context in itertools.izip(event_infos, contexts)
|
||||||
|
],
|
||||||
|
backfilled=backfilled,
|
||||||
|
is_new_state=(not outliers and not backfilled),
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _prep_event(self, origin, event, state=None, backfilled=False,
|
||||||
|
current_state=None, auth_events=None):
|
||||||
|
outlier = event.internal_metadata.is_outlier()
|
||||||
|
|
||||||
context = yield self.state_handler.compute_event_context(
|
context = yield self.state_handler.compute_event_context(
|
||||||
event, old_state=state
|
event, old_state=state, outlier=outlier,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not auth_events:
|
if not auth_events:
|
||||||
auth_events = context.current_state
|
auth_events = context.current_state
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
"_handle_new_event: %s, auth_events: %s",
|
|
||||||
event.event_id, auth_events,
|
|
||||||
)
|
|
||||||
|
|
||||||
is_new_state = not event.internal_metadata.is_outlier()
|
|
||||||
|
|
||||||
# This is a hack to fix some old rooms where the initial join event
|
# This is a hack to fix some old rooms where the initial join event
|
||||||
# didn't reference the create event in its auth events.
|
# didn't reference the create event in its auth events.
|
||||||
if event.type == EventTypes.Member and not event.auth_events:
|
if event.type == EventTypes.Member and not event.auth_events:
|
||||||
if len(event.prev_events) == 1:
|
if len(event.prev_events) == 1 and event.depth < 5:
|
||||||
c = yield self.store.get_event(event.prev_events[0][0])
|
c = yield self.store.get_event(
|
||||||
if c.type == EventTypes.Create:
|
event.prev_events[0][0],
|
||||||
|
allow_none=True,
|
||||||
|
)
|
||||||
|
if c and c.type == EventTypes.Create:
|
||||||
auth_events[(c.type, c.state_key)] = c
|
auth_events[(c.type, c.state_key)] = c
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -766,25 +1061,6 @@ class FederationHandler(BaseHandler):
|
|||||||
|
|
||||||
context.rejected = RejectedReason.AUTH_ERROR
|
context.rejected = RejectedReason.AUTH_ERROR
|
||||||
|
|
||||||
# FIXME: Don't store as rejected with AUTH_ERROR if we haven't
|
|
||||||
# seen all the auth events.
|
|
||||||
yield self.store.persist_event(
|
|
||||||
event,
|
|
||||||
context=context,
|
|
||||||
backfilled=backfilled,
|
|
||||||
is_new_state=False,
|
|
||||||
current_state=current_state,
|
|
||||||
)
|
|
||||||
raise
|
|
||||||
|
|
||||||
yield self.store.persist_event(
|
|
||||||
event,
|
|
||||||
context=context,
|
|
||||||
backfilled=backfilled,
|
|
||||||
is_new_state=(is_new_state and not backfilled),
|
|
||||||
current_state=current_state,
|
|
||||||
)
|
|
||||||
|
|
||||||
defer.returnValue(context)
|
defer.returnValue(context)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@@ -848,14 +1124,24 @@ class FederationHandler(BaseHandler):
|
|||||||
@log_function
|
@log_function
|
||||||
def do_auth(self, origin, event, context, auth_events):
|
def do_auth(self, origin, event, context, auth_events):
|
||||||
# Check if we have all the auth events.
|
# Check if we have all the auth events.
|
||||||
have_events = yield self.store.have_events(
|
current_state = set(e.event_id for e in auth_events.values())
|
||||||
[e_id for e_id, _ in event.auth_events]
|
|
||||||
)
|
|
||||||
|
|
||||||
event_auth_events = set(e_id for e_id, _ in event.auth_events)
|
event_auth_events = set(e_id for e_id, _ in event.auth_events)
|
||||||
|
|
||||||
|
if event_auth_events - current_state:
|
||||||
|
have_events = yield self.store.have_events(
|
||||||
|
event_auth_events - current_state
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
have_events = {}
|
||||||
|
|
||||||
|
have_events.update({
|
||||||
|
e.event_id: ""
|
||||||
|
for e in auth_events.values()
|
||||||
|
})
|
||||||
|
|
||||||
seen_events = set(have_events.keys())
|
seen_events = set(have_events.keys())
|
||||||
|
|
||||||
missing_auth = event_auth_events - seen_events
|
missing_auth = event_auth_events - seen_events - current_state
|
||||||
|
|
||||||
if missing_auth:
|
if missing_auth:
|
||||||
logger.info("Missing auth: %s", missing_auth)
|
logger.info("Missing auth: %s", missing_auth)
|
||||||
@@ -925,7 +1211,7 @@ class FederationHandler(BaseHandler):
|
|||||||
if d in have_events and not have_events[d]
|
if d in have_events and not have_events[d]
|
||||||
],
|
],
|
||||||
consumeErrors=True
|
consumeErrors=True
|
||||||
)
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
if different_events:
|
if different_events:
|
||||||
local_view = dict(auth_events)
|
local_view = dict(auth_events)
|
||||||
@@ -1170,3 +1456,52 @@ class FederationHandler(BaseHandler):
|
|||||||
},
|
},
|
||||||
"missing": [e.event_id for e in missing_locals],
|
"missing": [e.event_id for e in missing_locals],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _handle_auth_events(self, origin, auth_events):
|
||||||
|
auth_ids_to_deferred = {}
|
||||||
|
|
||||||
|
def process_auth_ev(ev):
|
||||||
|
auth_ids = [e_id for e_id, _ in ev.auth_events]
|
||||||
|
|
||||||
|
prev_ds = [
|
||||||
|
auth_ids_to_deferred[i]
|
||||||
|
for i in auth_ids
|
||||||
|
if i in auth_ids_to_deferred
|
||||||
|
]
|
||||||
|
|
||||||
|
d = defer.Deferred()
|
||||||
|
|
||||||
|
auth_ids_to_deferred[ev.event_id] = d
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def f(*_):
|
||||||
|
ev.internal_metadata.outlier = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
auth = {
|
||||||
|
(e.type, e.state_key): e for e in auth_events
|
||||||
|
if e.event_id in auth_ids
|
||||||
|
}
|
||||||
|
|
||||||
|
yield self._handle_new_event(
|
||||||
|
origin, ev, auth_events=auth
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
logger.exception(
|
||||||
|
"Failed to handle auth event %s",
|
||||||
|
ev.event_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
d.callback(None)
|
||||||
|
|
||||||
|
if prev_ds:
|
||||||
|
dx = defer.DeferredList(prev_ds)
|
||||||
|
dx.addBoth(f)
|
||||||
|
else:
|
||||||
|
f()
|
||||||
|
|
||||||
|
for e in auth_events:
|
||||||
|
process_auth_ev(e)
|
||||||
|
|
||||||
|
yield defer.DeferredList(auth_ids_to_deferred.values())
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class IdentityHandler(BaseHandler):
|
|||||||
http_client = SimpleHttpClient(self.hs)
|
http_client = SimpleHttpClient(self.hs)
|
||||||
# XXX: make this configurable!
|
# XXX: make this configurable!
|
||||||
# trustedIdServers = ['matrix.org', 'localhost:8090']
|
# trustedIdServers = ['matrix.org', 'localhost:8090']
|
||||||
trustedIdServers = ['matrix.org']
|
trustedIdServers = ['matrix.org', 'vector.im']
|
||||||
|
|
||||||
if 'id_server' in creds:
|
if 'id_server' in creds:
|
||||||
id_server = creds['id_server']
|
id_server = creds['id_server']
|
||||||
@@ -117,3 +117,28 @@ class IdentityHandler(BaseHandler):
|
|||||||
except CodeMessageException as e:
|
except CodeMessageException as e:
|
||||||
data = json.loads(e.msg)
|
data = json.loads(e.msg)
|
||||||
defer.returnValue(data)
|
defer.returnValue(data)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def requestEmailToken(self, id_server, email, client_secret, send_attempt, **kwargs):
|
||||||
|
yield run_on_reactor()
|
||||||
|
http_client = SimpleHttpClient(self.hs)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'email': email,
|
||||||
|
'client_secret': client_secret,
|
||||||
|
'send_attempt': send_attempt,
|
||||||
|
}
|
||||||
|
params.update(kwargs)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = yield http_client.post_urlencoded_get_json(
|
||||||
|
"https://%s%s" % (
|
||||||
|
id_server,
|
||||||
|
"/_matrix/identity/api/v1/validate/email/requestToken"
|
||||||
|
),
|
||||||
|
params
|
||||||
|
)
|
||||||
|
defer.returnValue(data)
|
||||||
|
except CodeMessageException as e:
|
||||||
|
logger.info("Proxied requestToken failed: %r", e)
|
||||||
|
raise e
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014, 2015 OpenMarket 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.
|
|
||||||
|
|
||||||
from twisted.internet import defer
|
|
||||||
|
|
||||||
from ._base import BaseHandler
|
|
||||||
from synapse.api.errors import LoginError, Codes
|
|
||||||
|
|
||||||
import bcrypt
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class LoginHandler(BaseHandler):
|
|
||||||
|
|
||||||
def __init__(self, hs):
|
|
||||||
super(LoginHandler, self).__init__(hs)
|
|
||||||
self.hs = hs
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def login(self, user, password):
|
|
||||||
"""Login as the specified user with the specified password.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user (str): The user ID.
|
|
||||||
password (str): The password.
|
|
||||||
Returns:
|
|
||||||
The newly allocated access token.
|
|
||||||
Raises:
|
|
||||||
StoreError if there was a problem storing the token.
|
|
||||||
LoginError if there was an authentication problem.
|
|
||||||
"""
|
|
||||||
# TODO do this better, it can't go in __init__ else it cyclic loops
|
|
||||||
if not hasattr(self, "reg_handler"):
|
|
||||||
self.reg_handler = self.hs.get_handlers().registration_handler
|
|
||||||
|
|
||||||
# pull out the hash for this user if they exist
|
|
||||||
user_info = yield self.store.get_user_by_id(user_id=user)
|
|
||||||
if not user_info:
|
|
||||||
logger.warn("Attempted to login as %s but they do not exist", user)
|
|
||||||
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
|
|
||||||
|
|
||||||
stored_hash = user_info["password_hash"]
|
|
||||||
if bcrypt.checkpw(password, stored_hash):
|
|
||||||
# generate an access token and store it.
|
|
||||||
token = self.reg_handler._generate_token(user)
|
|
||||||
logger.info("Adding token %s for user %s", token, user)
|
|
||||||
yield self.store.add_access_token_to_user(user, token)
|
|
||||||
defer.returnValue(token)
|
|
||||||
else:
|
|
||||||
logger.warn("Failed password login for user %s", user)
|
|
||||||
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def set_password(self, user_id, newpassword, token_id=None):
|
|
||||||
password_hash = bcrypt.hashpw(newpassword, bcrypt.gensalt())
|
|
||||||
|
|
||||||
yield self.store.user_set_password_hash(user_id, password_hash)
|
|
||||||
yield self.store.user_delete_access_tokens_apart_from(user_id, token_id)
|
|
||||||
yield self.hs.get_pusherpool().remove_pushers_by_user_access_token(
|
|
||||||
user_id, token_id
|
|
||||||
)
|
|
||||||
yield self.store.flush_user(user_id)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def add_threepid(self, user_id, medium, address, validated_at):
|
|
||||||
yield self.store.user_add_threepid(
|
|
||||||
user_id, medium, address, validated_at,
|
|
||||||
self.hs.get_clock().time_msec()
|
|
||||||
)
|
|
||||||
@@ -20,8 +20,9 @@ from synapse.api.errors import RoomError, SynapseError
|
|||||||
from synapse.streams.config import PaginationConfig
|
from synapse.streams.config import PaginationConfig
|
||||||
from synapse.events.utils import serialize_event
|
from synapse.events.utils import serialize_event
|
||||||
from synapse.events.validator import EventValidator
|
from synapse.events.validator import EventValidator
|
||||||
|
from synapse.util import unwrapFirstError
|
||||||
from synapse.util.logcontext import PreserveLoggingContext
|
from synapse.util.logcontext import PreserveLoggingContext
|
||||||
from synapse.types import UserID
|
from synapse.types import UserID, RoomStreamToken
|
||||||
|
|
||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
|
|
||||||
@@ -89,9 +90,19 @@ class MessageHandler(BaseHandler):
|
|||||||
|
|
||||||
if not pagin_config.from_token:
|
if not pagin_config.from_token:
|
||||||
pagin_config.from_token = (
|
pagin_config.from_token = (
|
||||||
yield self.hs.get_event_sources().get_current_token()
|
yield self.hs.get_event_sources().get_current_token(
|
||||||
|
direction='b'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
room_token = RoomStreamToken.parse(pagin_config.from_token.room_key)
|
||||||
|
if room_token.topological is None:
|
||||||
|
raise SynapseError(400, "Invalid token")
|
||||||
|
|
||||||
|
yield self.hs.get_handlers().federation_handler.maybe_backfill(
|
||||||
|
room_id, room_token.topological
|
||||||
|
)
|
||||||
|
|
||||||
user = UserID.from_string(user_id)
|
user = UserID.from_string(user_id)
|
||||||
|
|
||||||
events, next_key = yield data_source.get_pagination_rows(
|
events, next_key = yield data_source.get_pagination_rows(
|
||||||
@@ -102,11 +113,21 @@ class MessageHandler(BaseHandler):
|
|||||||
"room_key", next_key
|
"room_key", next_key
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not events:
|
||||||
|
defer.returnValue({
|
||||||
|
"chunk": [],
|
||||||
|
"start": pagin_config.from_token.to_string(),
|
||||||
|
"end": next_token.to_string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
events = yield self._filter_events_for_client(user_id, room_id, events)
|
||||||
|
|
||||||
time_now = self.clock.time_msec()
|
time_now = self.clock.time_msec()
|
||||||
|
|
||||||
chunk = {
|
chunk = {
|
||||||
"chunk": [
|
"chunk": [
|
||||||
serialize_event(e, time_now, as_client_event) for e in events
|
serialize_event(e, time_now, as_client_event)
|
||||||
|
for e in events
|
||||||
],
|
],
|
||||||
"start": pagin_config.from_token.to_string(),
|
"start": pagin_config.from_token.to_string(),
|
||||||
"end": next_token.to_string(),
|
"end": next_token.to_string(),
|
||||||
@@ -114,6 +135,52 @@ class MessageHandler(BaseHandler):
|
|||||||
|
|
||||||
defer.returnValue(chunk)
|
defer.returnValue(chunk)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _filter_events_for_client(self, user_id, room_id, events):
|
||||||
|
event_id_to_state = yield self.store.get_state_for_events(
|
||||||
|
room_id, frozenset(e.event_id for e in events),
|
||||||
|
types=(
|
||||||
|
(EventTypes.RoomHistoryVisibility, ""),
|
||||||
|
(EventTypes.Member, user_id),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def allowed(event, state):
|
||||||
|
if event.type == EventTypes.RoomHistoryVisibility:
|
||||||
|
return True
|
||||||
|
|
||||||
|
membership_ev = state.get((EventTypes.Member, user_id), None)
|
||||||
|
if membership_ev:
|
||||||
|
membership = membership_ev.membership
|
||||||
|
else:
|
||||||
|
membership = Membership.LEAVE
|
||||||
|
|
||||||
|
if membership == Membership.JOIN:
|
||||||
|
return True
|
||||||
|
|
||||||
|
history = state.get((EventTypes.RoomHistoryVisibility, ''), None)
|
||||||
|
if history:
|
||||||
|
visibility = history.content.get("history_visibility", "shared")
|
||||||
|
else:
|
||||||
|
visibility = "shared"
|
||||||
|
|
||||||
|
if visibility == "public":
|
||||||
|
return True
|
||||||
|
elif visibility == "shared":
|
||||||
|
return True
|
||||||
|
elif visibility == "joined":
|
||||||
|
return membership == Membership.JOIN
|
||||||
|
elif visibility == "invited":
|
||||||
|
return membership == Membership.INVITE
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
defer.returnValue([
|
||||||
|
event
|
||||||
|
for event in events
|
||||||
|
if allowed(event, event_id_to_state[event.event_id])
|
||||||
|
])
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def create_and_send_event(self, event_dict, ratelimit=True,
|
def create_and_send_event(self, event_dict, ratelimit=True,
|
||||||
client=None, txn_id=None):
|
client=None, txn_id=None):
|
||||||
@@ -250,47 +317,36 @@ class MessageHandler(BaseHandler):
|
|||||||
is joined on, may return a "messages" key with messages, depending
|
is joined on, may return a "messages" key with messages, depending
|
||||||
on the specified PaginationConfig.
|
on the specified PaginationConfig.
|
||||||
"""
|
"""
|
||||||
start_time = self.clock.time_msec()
|
|
||||||
|
|
||||||
def delta():
|
|
||||||
return self.clock.time_msec() - start_time
|
|
||||||
|
|
||||||
logger.info("initial_sync: start")
|
|
||||||
room_list = yield self.store.get_rooms_for_user_where_membership_is(
|
room_list = yield self.store.get_rooms_for_user_where_membership_is(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
membership_list=[Membership.INVITE, Membership.JOIN]
|
membership_list=[Membership.INVITE, Membership.JOIN]
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info("initial_sync: got_rooms %d", delta())
|
|
||||||
|
|
||||||
user = UserID.from_string(user_id)
|
user = UserID.from_string(user_id)
|
||||||
|
|
||||||
rooms_ret = []
|
rooms_ret = []
|
||||||
|
|
||||||
now_token = yield self.hs.get_event_sources().get_current_token()
|
now_token = yield self.hs.get_event_sources().get_current_token()
|
||||||
|
|
||||||
logger.info("initial_sync: now_token %d", delta())
|
|
||||||
|
|
||||||
presence_stream = self.hs.get_event_sources().sources["presence"]
|
presence_stream = self.hs.get_event_sources().sources["presence"]
|
||||||
pagination_config = PaginationConfig(from_token=now_token)
|
pagination_config = PaginationConfig(from_token=now_token)
|
||||||
presence, _ = yield presence_stream.get_pagination_rows(
|
presence, _ = yield presence_stream.get_pagination_rows(
|
||||||
user, pagination_config.get_source_config("presence"), None
|
user, pagination_config.get_source_config("presence"), None
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info("initial_sync: presence_done %d", delta())
|
receipt_stream = self.hs.get_event_sources().sources["receipt"]
|
||||||
|
receipt, _ = yield receipt_stream.get_pagination_rows(
|
||||||
|
user, pagination_config.get_source_config("receipt"), None
|
||||||
|
)
|
||||||
|
|
||||||
public_room_ids = yield self.store.get_public_room_ids()
|
public_room_ids = yield self.store.get_public_room_ids()
|
||||||
|
|
||||||
logger.info("initial_sync: public_rooms %d", delta())
|
|
||||||
|
|
||||||
limit = pagin_config.limit
|
limit = pagin_config.limit
|
||||||
if limit is None:
|
if limit is None:
|
||||||
limit = 10
|
limit = 10
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def handle_room(event):
|
def handle_room(event):
|
||||||
logger.info("initial_sync: start: %s %d", event.room_id, delta())
|
|
||||||
|
|
||||||
d = {
|
d = {
|
||||||
"room_id": event.room_id,
|
"room_id": event.room_id,
|
||||||
"membership": event.membership,
|
"membership": event.membership,
|
||||||
@@ -319,6 +375,10 @@ class MessageHandler(BaseHandler):
|
|||||||
event.room_id
|
event.room_id
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
|
messages = yield self._filter_events_for_client(
|
||||||
|
user_id, event.room_id, messages
|
||||||
)
|
)
|
||||||
|
|
||||||
start_token = now_token.copy_and_replace("room_key", token[0])
|
start_token = now_token.copy_and_replace("room_key", token[0])
|
||||||
@@ -341,19 +401,20 @@ class MessageHandler(BaseHandler):
|
|||||||
except:
|
except:
|
||||||
logger.exception("Failed to get snapshot")
|
logger.exception("Failed to get snapshot")
|
||||||
|
|
||||||
logger.info("initial_sync: end: %s %d", event.room_id, delta())
|
# Only do N rooms at once
|
||||||
|
n = 5
|
||||||
yield defer.gatherResults(
|
d_list = [handle_room(e) for e in room_list]
|
||||||
[handle_room(e) for e in room_list],
|
for i in range(0, len(d_list), n):
|
||||||
consumeErrors=True
|
yield defer.gatherResults(
|
||||||
)
|
d_list[i:i + n],
|
||||||
|
consumeErrors=True
|
||||||
logger.info("initial_sync: done", delta())
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
ret = {
|
ret = {
|
||||||
"rooms": rooms_ret,
|
"rooms": rooms_ret,
|
||||||
"presence": presence,
|
"presence": presence,
|
||||||
"end": now_token.to_string()
|
"receipts": receipt,
|
||||||
|
"end": now_token.to_string(),
|
||||||
}
|
}
|
||||||
|
|
||||||
defer.returnValue(ret)
|
defer.returnValue(ret)
|
||||||
@@ -389,15 +450,6 @@ class MessageHandler(BaseHandler):
|
|||||||
if limit is None:
|
if limit is None:
|
||||||
limit = 10
|
limit = 10
|
||||||
|
|
||||||
messages, token = yield self.store.get_recent_events_for_room(
|
|
||||||
room_id,
|
|
||||||
limit=limit,
|
|
||||||
end_token=now_token.room_key,
|
|
||||||
)
|
|
||||||
|
|
||||||
start_token = now_token.copy_and_replace("room_key", token[0])
|
|
||||||
end_token = now_token.copy_and_replace("room_key", token[1])
|
|
||||||
|
|
||||||
room_members = [
|
room_members = [
|
||||||
m for m in current_state.values()
|
m for m in current_state.values()
|
||||||
if m.type == EventTypes.Member
|
if m.type == EventTypes.Member
|
||||||
@@ -405,19 +457,39 @@ class MessageHandler(BaseHandler):
|
|||||||
]
|
]
|
||||||
|
|
||||||
presence_handler = self.hs.get_handlers().presence_handler
|
presence_handler = self.hs.get_handlers().presence_handler
|
||||||
presence = []
|
|
||||||
for m in room_members:
|
@defer.inlineCallbacks
|
||||||
try:
|
def get_presence():
|
||||||
member_presence = yield presence_handler.get_state(
|
states = yield presence_handler.get_states(
|
||||||
target_user=UserID.from_string(m.user_id),
|
target_users=[UserID.from_string(m.user_id) for m in room_members],
|
||||||
auth_user=auth_user,
|
auth_user=auth_user,
|
||||||
as_event=True,
|
as_event=True,
|
||||||
)
|
check_auth=False,
|
||||||
presence.append(member_presence)
|
)
|
||||||
except SynapseError:
|
|
||||||
logger.exception(
|
defer.returnValue(states.values())
|
||||||
"Failed to get member presence of %r", m.user_id
|
|
||||||
|
receipts_handler = self.hs.get_handlers().receipts_handler
|
||||||
|
|
||||||
|
presence, receipts, (messages, token) = yield defer.gatherResults(
|
||||||
|
[
|
||||||
|
get_presence(),
|
||||||
|
receipts_handler.get_receipts_for_room(room_id, now_token.receipt_key),
|
||||||
|
self.store.get_recent_events_for_room(
|
||||||
|
room_id,
|
||||||
|
limit=limit,
|
||||||
|
end_token=now_token.room_key,
|
||||||
)
|
)
|
||||||
|
],
|
||||||
|
consumeErrors=True,
|
||||||
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
|
messages = yield self._filter_events_for_client(
|
||||||
|
user_id, room_id, messages
|
||||||
|
)
|
||||||
|
|
||||||
|
start_token = now_token.copy_and_replace("room_key", token[0])
|
||||||
|
end_token = now_token.copy_and_replace("room_key", token[1])
|
||||||
|
|
||||||
time_now = self.clock.time_msec()
|
time_now = self.clock.time_msec()
|
||||||
|
|
||||||
@@ -430,5 +502,6 @@ class MessageHandler(BaseHandler):
|
|||||||
"end": end_token.to_string(),
|
"end": end_token.to_string(),
|
||||||
},
|
},
|
||||||
"state": state,
|
"state": state,
|
||||||
"presence": presence
|
"presence": presence,
|
||||||
|
"receipts": receipts,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ from twisted.internet import defer
|
|||||||
from synapse.api.errors import SynapseError, AuthError
|
from synapse.api.errors import SynapseError, AuthError
|
||||||
from synapse.api.constants import PresenceState
|
from synapse.api.constants import PresenceState
|
||||||
|
|
||||||
from synapse.util.logutils import log_function
|
|
||||||
from synapse.util.logcontext import PreserveLoggingContext
|
from synapse.util.logcontext import PreserveLoggingContext
|
||||||
|
from synapse.util.logutils import log_function
|
||||||
from synapse.types import UserID
|
from synapse.types import UserID
|
||||||
import synapse.metrics
|
import synapse.metrics
|
||||||
|
|
||||||
@@ -146,6 +146,10 @@ class PresenceHandler(BaseHandler):
|
|||||||
self._user_cachemap = {}
|
self._user_cachemap = {}
|
||||||
self._user_cachemap_latest_serial = 0
|
self._user_cachemap_latest_serial = 0
|
||||||
|
|
||||||
|
# map room_ids to the latest presence serial for a member of that
|
||||||
|
# room
|
||||||
|
self._room_serials = {}
|
||||||
|
|
||||||
metrics.register_callback(
|
metrics.register_callback(
|
||||||
"userCachemap:size",
|
"userCachemap:size",
|
||||||
lambda: len(self._user_cachemap),
|
lambda: len(self._user_cachemap),
|
||||||
@@ -187,24 +191,38 @@ class PresenceHandler(BaseHandler):
|
|||||||
defer.returnValue(False)
|
defer.returnValue(False)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_state(self, target_user, auth_user, as_event=False):
|
def get_state(self, target_user, auth_user, as_event=False, check_auth=True):
|
||||||
if self.hs.is_mine(target_user):
|
"""Get the current presence state of the given user.
|
||||||
visible = yield self.is_presence_visible(
|
|
||||||
observer_user=auth_user,
|
|
||||||
observed_user=target_user
|
|
||||||
)
|
|
||||||
|
|
||||||
if not visible:
|
Args:
|
||||||
raise SynapseError(404, "Presence information not visible")
|
target_user (UserID): The user whose presence we want
|
||||||
state = yield self.store.get_presence_state(target_user.localpart)
|
auth_user (UserID): The user requesting the presence, used for
|
||||||
if "mtime" in state:
|
checking if said user is allowed to see the persence of the
|
||||||
del state["mtime"]
|
`target_user`
|
||||||
state["presence"] = state.pop("state")
|
as_event (bool): Format the return as an event or not?
|
||||||
|
check_auth (bool): Perform the auth checks or not?
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: The presence state of the `target_user`, whose format depends
|
||||||
|
on the `as_event` argument.
|
||||||
|
"""
|
||||||
|
if self.hs.is_mine(target_user):
|
||||||
|
if check_auth:
|
||||||
|
visible = yield self.is_presence_visible(
|
||||||
|
observer_user=auth_user,
|
||||||
|
observed_user=target_user
|
||||||
|
)
|
||||||
|
|
||||||
|
if not visible:
|
||||||
|
raise SynapseError(404, "Presence information not visible")
|
||||||
|
|
||||||
if target_user in self._user_cachemap:
|
if target_user in self._user_cachemap:
|
||||||
cached_state = self._user_cachemap[target_user].get_state()
|
state = self._user_cachemap[target_user].get_state()
|
||||||
if "last_active" in cached_state:
|
else:
|
||||||
state["last_active"] = cached_state["last_active"]
|
state = yield self.store.get_presence_state(target_user.localpart)
|
||||||
|
if "mtime" in state:
|
||||||
|
del state["mtime"]
|
||||||
|
state["presence"] = state.pop("state")
|
||||||
else:
|
else:
|
||||||
# TODO(paul): Have remote server send us permissions set
|
# TODO(paul): Have remote server send us permissions set
|
||||||
state = self._get_or_offline_usercache(target_user).get_state()
|
state = self._get_or_offline_usercache(target_user).get_state()
|
||||||
@@ -228,6 +246,81 @@ class PresenceHandler(BaseHandler):
|
|||||||
else:
|
else:
|
||||||
defer.returnValue(state)
|
defer.returnValue(state)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_states(self, target_users, auth_user, as_event=False, check_auth=True):
|
||||||
|
"""A batched version of the `get_state` method that accepts a list of
|
||||||
|
`target_users`
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_users (list): The list of UserID's whose presence we want
|
||||||
|
auth_user (UserID): The user requesting the presence, used for
|
||||||
|
checking if said user is allowed to see the persence of the
|
||||||
|
`target_users`
|
||||||
|
as_event (bool): Format the return as an event or not?
|
||||||
|
check_auth (bool): Perform the auth checks or not?
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: A mapping from user -> presence_state
|
||||||
|
"""
|
||||||
|
local_users, remote_users = partitionbool(
|
||||||
|
target_users,
|
||||||
|
lambda u: self.hs.is_mine(u)
|
||||||
|
)
|
||||||
|
|
||||||
|
if check_auth:
|
||||||
|
for user in local_users:
|
||||||
|
visible = yield self.is_presence_visible(
|
||||||
|
observer_user=auth_user,
|
||||||
|
observed_user=user
|
||||||
|
)
|
||||||
|
|
||||||
|
if not visible:
|
||||||
|
raise SynapseError(404, "Presence information not visible")
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
if local_users:
|
||||||
|
for user in local_users:
|
||||||
|
if user in self._user_cachemap:
|
||||||
|
results[user] = self._user_cachemap[user].get_state()
|
||||||
|
|
||||||
|
local_to_user = {u.localpart: u for u in local_users}
|
||||||
|
|
||||||
|
states = yield self.store.get_presence_states(
|
||||||
|
[u.localpart for u in local_users if u not in results]
|
||||||
|
)
|
||||||
|
|
||||||
|
for local_part, state in states.items():
|
||||||
|
if state is None:
|
||||||
|
continue
|
||||||
|
res = {"presence": state["state"]}
|
||||||
|
if "status_msg" in state and state["status_msg"]:
|
||||||
|
res["status_msg"] = state["status_msg"]
|
||||||
|
results[local_to_user[local_part]] = res
|
||||||
|
|
||||||
|
for user in remote_users:
|
||||||
|
# TODO(paul): Have remote server send us permissions set
|
||||||
|
results[user] = self._get_or_offline_usercache(user).get_state()
|
||||||
|
|
||||||
|
for state in results.values():
|
||||||
|
if "last_active" in state:
|
||||||
|
state["last_active_ago"] = int(
|
||||||
|
self.clock.time_msec() - state.pop("last_active")
|
||||||
|
)
|
||||||
|
|
||||||
|
if as_event:
|
||||||
|
for user, state in results.items():
|
||||||
|
content = state
|
||||||
|
content["user_id"] = user.to_string()
|
||||||
|
|
||||||
|
if "last_active" in content:
|
||||||
|
content["last_active_ago"] = int(
|
||||||
|
self._clock.time_msec() - content.pop("last_active")
|
||||||
|
)
|
||||||
|
|
||||||
|
results[user] = {"type": "m.presence", "content": content}
|
||||||
|
|
||||||
|
defer.returnValue(results)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def set_state(self, target_user, auth_user, state):
|
def set_state(self, target_user, auth_user, state):
|
||||||
@@ -278,15 +371,14 @@ class PresenceHandler(BaseHandler):
|
|||||||
now_online = state["presence"] != PresenceState.OFFLINE
|
now_online = state["presence"] != PresenceState.OFFLINE
|
||||||
was_polling = target_user in self._user_cachemap
|
was_polling = target_user in self._user_cachemap
|
||||||
|
|
||||||
with PreserveLoggingContext():
|
if now_online and not was_polling:
|
||||||
if now_online and not was_polling:
|
self.start_polling_presence(target_user, state=state)
|
||||||
self.start_polling_presence(target_user, state=state)
|
elif not now_online and was_polling:
|
||||||
elif not now_online and was_polling:
|
self.stop_polling_presence(target_user)
|
||||||
self.stop_polling_presence(target_user)
|
|
||||||
|
|
||||||
# TODO(paul): perform a presence push as part of start/stop poll so
|
# TODO(paul): perform a presence push as part of start/stop poll so
|
||||||
# we don't have to do this all the time
|
# we don't have to do this all the time
|
||||||
self.changed_presencelike_data(target_user, state)
|
self.changed_presencelike_data(target_user, state)
|
||||||
|
|
||||||
def bump_presence_active_time(self, user, now=None):
|
def bump_presence_active_time(self, user, now=None):
|
||||||
if now is None:
|
if now is None:
|
||||||
@@ -298,13 +390,34 @@ class PresenceHandler(BaseHandler):
|
|||||||
|
|
||||||
self.changed_presencelike_data(user, {"last_active": now})
|
self.changed_presencelike_data(user, {"last_active": now})
|
||||||
|
|
||||||
|
def get_joined_rooms_for_user(self, user):
|
||||||
|
"""Get the list of rooms a user is joined to.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user(UserID): The user.
|
||||||
|
Returns:
|
||||||
|
A Deferred of a list of room id strings.
|
||||||
|
"""
|
||||||
|
rm_handler = self.homeserver.get_handlers().room_member_handler
|
||||||
|
return rm_handler.get_joined_rooms_for_user(user)
|
||||||
|
|
||||||
|
def get_joined_users_for_room_id(self, room_id):
|
||||||
|
rm_handler = self.homeserver.get_handlers().room_member_handler
|
||||||
|
return rm_handler.get_room_members(room_id)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
def changed_presencelike_data(self, user, state):
|
def changed_presencelike_data(self, user, state):
|
||||||
statuscache = self._get_or_make_usercache(user)
|
"""Updates the presence state of a local user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user(UserID): The user being updated.
|
||||||
|
state(dict): The new presence state for the user.
|
||||||
|
Returns:
|
||||||
|
A Deferred
|
||||||
|
"""
|
||||||
self._user_cachemap_latest_serial += 1
|
self._user_cachemap_latest_serial += 1
|
||||||
statuscache.update(state, serial=self._user_cachemap_latest_serial)
|
statuscache = yield self.update_presence_cache(user, state)
|
||||||
|
yield self.push_presence(user, statuscache=statuscache)
|
||||||
return self.push_presence(user, statuscache=statuscache)
|
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def started_user_eventstream(self, user):
|
def started_user_eventstream(self, user):
|
||||||
@@ -318,14 +431,21 @@ class PresenceHandler(BaseHandler):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def user_joined_room(self, user, room_id):
|
def user_joined_room(self, user, room_id):
|
||||||
if self.hs.is_mine(user):
|
"""Called via the distributor whenever a user joins a room.
|
||||||
statuscache = self._get_or_make_usercache(user)
|
Notifies the new member of the presence of the current members.
|
||||||
|
Notifies the current members of the room of the new member's presence.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user(UserID): The user who joined the room.
|
||||||
|
room_id(str): The room id the user joined.
|
||||||
|
"""
|
||||||
|
if self.hs.is_mine(user):
|
||||||
# No actual update but we need to bump the serial anyway for the
|
# No actual update but we need to bump the serial anyway for the
|
||||||
# event source
|
# event source
|
||||||
self._user_cachemap_latest_serial += 1
|
self._user_cachemap_latest_serial += 1
|
||||||
statuscache.update({}, serial=self._user_cachemap_latest_serial)
|
statuscache = yield self.update_presence_cache(
|
||||||
|
user, room_ids=[room_id]
|
||||||
|
)
|
||||||
self.push_update_to_local_and_remote(
|
self.push_update_to_local_and_remote(
|
||||||
observed_user=user,
|
observed_user=user,
|
||||||
room_ids=[room_id],
|
room_ids=[room_id],
|
||||||
@@ -333,18 +453,22 @@ class PresenceHandler(BaseHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# We also want to tell them about current presence of people.
|
# We also want to tell them about current presence of people.
|
||||||
rm_handler = self.homeserver.get_handlers().room_member_handler
|
curr_users = yield self.get_joined_users_for_room_id(room_id)
|
||||||
curr_users = yield rm_handler.get_room_members(room_id)
|
|
||||||
|
|
||||||
for local_user in [c for c in curr_users if self.hs.is_mine(c)]:
|
for local_user in [c for c in curr_users if self.hs.is_mine(c)]:
|
||||||
|
statuscache = yield self.update_presence_cache(
|
||||||
|
local_user, room_ids=[room_id], add_to_cache=False
|
||||||
|
)
|
||||||
|
|
||||||
self.push_update_to_local_and_remote(
|
self.push_update_to_local_and_remote(
|
||||||
observed_user=local_user,
|
observed_user=local_user,
|
||||||
users_to_push=[user],
|
users_to_push=[user],
|
||||||
statuscache=self._get_or_offline_usercache(local_user),
|
statuscache=statuscache,
|
||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def send_invite(self, observer_user, observed_user):
|
def send_invite(self, observer_user, observed_user):
|
||||||
|
"""Request the presence of a local or remote user for a local user"""
|
||||||
if not self.hs.is_mine(observer_user):
|
if not self.hs.is_mine(observer_user):
|
||||||
raise SynapseError(400, "User is not hosted on this Home Server")
|
raise SynapseError(400, "User is not hosted on this Home Server")
|
||||||
|
|
||||||
@@ -379,6 +503,15 @@ class PresenceHandler(BaseHandler):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def invite_presence(self, observed_user, observer_user):
|
def invite_presence(self, observed_user, observer_user):
|
||||||
|
"""Handles a m.presence_invite EDU. A remote or local user has
|
||||||
|
requested presence updates for a local user. If the invite is accepted
|
||||||
|
then allow the local or remote user to see the presence of the local
|
||||||
|
user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
observed_user(UserID): The local user whose presence is requested.
|
||||||
|
observer_user(UserID): The remote or local user requesting presence.
|
||||||
|
"""
|
||||||
accept = yield self._should_accept_invite(observed_user, observer_user)
|
accept = yield self._should_accept_invite(observed_user, observer_user)
|
||||||
|
|
||||||
if accept:
|
if accept:
|
||||||
@@ -405,16 +538,34 @@ class PresenceHandler(BaseHandler):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def accept_presence(self, observed_user, observer_user):
|
def accept_presence(self, observed_user, observer_user):
|
||||||
|
"""Handles a m.presence_accept EDU. Mark a presence invite from a
|
||||||
|
local or remote user as accepted in a local user's presence list.
|
||||||
|
Starts polling for presence updates from the local or remote user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
observed_user(UserID): The user to update in the presence list.
|
||||||
|
observer_user(UserID): The owner of the presence list to update.
|
||||||
|
"""
|
||||||
yield self.store.set_presence_list_accepted(
|
yield self.store.set_presence_list_accepted(
|
||||||
observer_user.localpart, observed_user.to_string()
|
observer_user.localpart, observed_user.to_string()
|
||||||
)
|
)
|
||||||
with PreserveLoggingContext():
|
|
||||||
self.start_polling_presence(
|
self.start_polling_presence(
|
||||||
observer_user, target_user=observed_user
|
observer_user, target_user=observed_user
|
||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def deny_presence(self, observed_user, observer_user):
|
def deny_presence(self, observed_user, observer_user):
|
||||||
|
"""Handle a m.presence_deny EDU. Removes a local or remote user from a
|
||||||
|
local user's presence list.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
observed_user(UserID): The local or remote user to remove from the
|
||||||
|
list.
|
||||||
|
observer_user(UserID): The local owner of the presence list.
|
||||||
|
Returns:
|
||||||
|
A Deferred.
|
||||||
|
"""
|
||||||
yield self.store.del_presence_list(
|
yield self.store.del_presence_list(
|
||||||
observer_user.localpart, observed_user.to_string()
|
observer_user.localpart, observed_user.to_string()
|
||||||
)
|
)
|
||||||
@@ -423,6 +574,16 @@ class PresenceHandler(BaseHandler):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def drop(self, observed_user, observer_user):
|
def drop(self, observed_user, observer_user):
|
||||||
|
"""Remove a local or remote user from a local user's presence list and
|
||||||
|
unsubscribe the local user from updates that user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
observed_user(UserId): The local or remote user to remove from the
|
||||||
|
list.
|
||||||
|
observer_user(UserId): The local owner of the presence list.
|
||||||
|
Returns:
|
||||||
|
A Deferred.
|
||||||
|
"""
|
||||||
if not self.hs.is_mine(observer_user):
|
if not self.hs.is_mine(observer_user):
|
||||||
raise SynapseError(400, "User is not hosted on this Home Server")
|
raise SynapseError(400, "User is not hosted on this Home Server")
|
||||||
|
|
||||||
@@ -430,34 +591,66 @@ class PresenceHandler(BaseHandler):
|
|||||||
observer_user.localpart, observed_user.to_string()
|
observer_user.localpart, observed_user.to_string()
|
||||||
)
|
)
|
||||||
|
|
||||||
with PreserveLoggingContext():
|
self.stop_polling_presence(
|
||||||
self.stop_polling_presence(
|
observer_user, target_user=observed_user
|
||||||
observer_user, target_user=observed_user
|
)
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_presence_list(self, observer_user, accepted=None):
|
def get_presence_list(self, observer_user, accepted=None):
|
||||||
|
"""Get the presence list for a local user. The retured list includes
|
||||||
|
the current presence state for each user listed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
observer_user(UserID): The local user whose presence list to fetch.
|
||||||
|
accepted(bool or None): If not none then only include users who
|
||||||
|
have or have not accepted the presence invite request.
|
||||||
|
Returns:
|
||||||
|
A Deferred list of presence state events.
|
||||||
|
"""
|
||||||
if not self.hs.is_mine(observer_user):
|
if not self.hs.is_mine(observer_user):
|
||||||
raise SynapseError(400, "User is not hosted on this Home Server")
|
raise SynapseError(400, "User is not hosted on this Home Server")
|
||||||
|
|
||||||
presence = yield self.store.get_presence_list(
|
presence_list = yield self.store.get_presence_list(
|
||||||
observer_user.localpart, accepted=accepted
|
observer_user.localpart, accepted=accepted
|
||||||
)
|
)
|
||||||
|
|
||||||
for p in presence:
|
results = []
|
||||||
observed_user = UserID.from_string(p.pop("observed_user_id"))
|
for row in presence_list:
|
||||||
p["observed_user"] = observed_user
|
observed_user = UserID.from_string(row["observed_user_id"])
|
||||||
p.update(self._get_or_offline_usercache(observed_user).get_state())
|
result = {
|
||||||
if "last_active" in p:
|
"observed_user": observed_user, "accepted": row["accepted"]
|
||||||
p["last_active_ago"] = int(
|
}
|
||||||
self.clock.time_msec() - p.pop("last_active")
|
result.update(
|
||||||
|
self._get_or_offline_usercache(observed_user).get_state()
|
||||||
|
)
|
||||||
|
if "last_active" in result:
|
||||||
|
result["last_active_ago"] = int(
|
||||||
|
self.clock.time_msec() - result.pop("last_active")
|
||||||
)
|
)
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
defer.returnValue(presence)
|
defer.returnValue(results)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def start_polling_presence(self, user, target_user=None, state=None):
|
def start_polling_presence(self, user, target_user=None, state=None):
|
||||||
|
"""Subscribe a local user to presence updates from a local or remote
|
||||||
|
user. If no target_user is supplied then subscribe to all users stored
|
||||||
|
in the presence list for the local user.
|
||||||
|
|
||||||
|
Additonally this pushes the current presence state of this user to all
|
||||||
|
target_users. That state can be provided directly or will be read from
|
||||||
|
the stored state for the local user.
|
||||||
|
|
||||||
|
Also this attempts to notify the local user of the current state of
|
||||||
|
any local target users.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user(UserID): The local user that whishes for presence updates.
|
||||||
|
target_user(UserID): The local or remote user whose updates are
|
||||||
|
wanted.
|
||||||
|
state(dict): Optional presence state for the local user.
|
||||||
|
"""
|
||||||
logger.debug("Start polling for presence from %s", user)
|
logger.debug("Start polling for presence from %s", user)
|
||||||
|
|
||||||
if target_user:
|
if target_user:
|
||||||
@@ -473,8 +666,7 @@ class PresenceHandler(BaseHandler):
|
|||||||
|
|
||||||
# Also include people in all my rooms
|
# Also include people in all my rooms
|
||||||
|
|
||||||
rm_handler = self.homeserver.get_handlers().room_member_handler
|
room_ids = yield self.get_joined_rooms_for_user(user)
|
||||||
room_ids = yield rm_handler.get_joined_rooms_for_user(user)
|
|
||||||
|
|
||||||
if state is None:
|
if state is None:
|
||||||
state = yield self.store.get_presence_state(user.localpart)
|
state = yield self.store.get_presence_state(user.localpart)
|
||||||
@@ -498,9 +690,7 @@ class PresenceHandler(BaseHandler):
|
|||||||
# We want to tell the person that just came online
|
# We want to tell the person that just came online
|
||||||
# presence state of people they are interested in?
|
# presence state of people they are interested in?
|
||||||
self.push_update_to_clients(
|
self.push_update_to_clients(
|
||||||
observed_user=target_user,
|
|
||||||
users_to_push=[user],
|
users_to_push=[user],
|
||||||
statuscache=self._get_or_offline_usercache(target_user),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
deferreds = []
|
deferreds = []
|
||||||
@@ -517,6 +707,12 @@ class PresenceHandler(BaseHandler):
|
|||||||
yield defer.DeferredList(deferreds, consumeErrors=True)
|
yield defer.DeferredList(deferreds, consumeErrors=True)
|
||||||
|
|
||||||
def _start_polling_local(self, user, target_user):
|
def _start_polling_local(self, user, target_user):
|
||||||
|
"""Subscribe a local user to presence updates for a local user
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user(UserId): The local user that wishes for updates.
|
||||||
|
target_user(UserId): The local users whose updates are wanted.
|
||||||
|
"""
|
||||||
target_localpart = target_user.localpart
|
target_localpart = target_user.localpart
|
||||||
|
|
||||||
if target_localpart not in self._local_pushmap:
|
if target_localpart not in self._local_pushmap:
|
||||||
@@ -525,6 +721,17 @@ class PresenceHandler(BaseHandler):
|
|||||||
self._local_pushmap[target_localpart].add(user)
|
self._local_pushmap[target_localpart].add(user)
|
||||||
|
|
||||||
def _start_polling_remote(self, user, domain, remoteusers):
|
def _start_polling_remote(self, user, domain, remoteusers):
|
||||||
|
"""Subscribe a local user to presence updates for remote users on a
|
||||||
|
given remote domain.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user(UserID): The local user that wishes for updates.
|
||||||
|
domain(str): The remote server the local user wants updates from.
|
||||||
|
remoteusers(UserID): The remote users that local user wants to be
|
||||||
|
told about.
|
||||||
|
Returns:
|
||||||
|
A Deferred.
|
||||||
|
"""
|
||||||
to_poll = set()
|
to_poll = set()
|
||||||
|
|
||||||
for u in remoteusers:
|
for u in remoteusers:
|
||||||
@@ -545,6 +752,17 @@ class PresenceHandler(BaseHandler):
|
|||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def stop_polling_presence(self, user, target_user=None):
|
def stop_polling_presence(self, user, target_user=None):
|
||||||
|
"""Unsubscribe a local user from presence updates from a local or
|
||||||
|
remote user. If no target user is supplied then unsubscribe the user
|
||||||
|
from all presence updates that the user had subscribed to.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user(UserID): The local user that no longer wishes for updates.
|
||||||
|
target_user(UserID or None): The user whose updates are no longer
|
||||||
|
wanted.
|
||||||
|
Returns:
|
||||||
|
A Deferred.
|
||||||
|
"""
|
||||||
logger.debug("Stop polling for presence from %s", user)
|
logger.debug("Stop polling for presence from %s", user)
|
||||||
|
|
||||||
if not target_user or self.hs.is_mine(target_user):
|
if not target_user or self.hs.is_mine(target_user):
|
||||||
@@ -573,6 +791,13 @@ class PresenceHandler(BaseHandler):
|
|||||||
return defer.DeferredList(deferreds, consumeErrors=True)
|
return defer.DeferredList(deferreds, consumeErrors=True)
|
||||||
|
|
||||||
def _stop_polling_local(self, user, target_user):
|
def _stop_polling_local(self, user, target_user):
|
||||||
|
"""Unsubscribe a local user from presence updates from a local user on
|
||||||
|
this server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user(UserID): The local user that no longer wishes for updates.
|
||||||
|
target_user(UserID): The user whose updates are no longer wanted.
|
||||||
|
"""
|
||||||
for localpart in self._local_pushmap.keys():
|
for localpart in self._local_pushmap.keys():
|
||||||
if target_user and localpart != target_user.localpart:
|
if target_user and localpart != target_user.localpart:
|
||||||
continue
|
continue
|
||||||
@@ -585,6 +810,17 @@ class PresenceHandler(BaseHandler):
|
|||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def _stop_polling_remote(self, user, domain, remoteusers):
|
def _stop_polling_remote(self, user, domain, remoteusers):
|
||||||
|
"""Unsubscribe a local user from presence updates from remote users on
|
||||||
|
a given domain.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user(UserID): The local user that no longer wishes for updates.
|
||||||
|
domain(str): The remote server to unsubscribe from.
|
||||||
|
remoteusers([UserID]): The users on that remote server that the
|
||||||
|
local user no longer wishes to be updated about.
|
||||||
|
Returns:
|
||||||
|
A Deferred.
|
||||||
|
"""
|
||||||
to_unpoll = set()
|
to_unpoll = set()
|
||||||
|
|
||||||
for u in remoteusers:
|
for u in remoteusers:
|
||||||
@@ -606,6 +842,19 @@ class PresenceHandler(BaseHandler):
|
|||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def push_presence(self, user, statuscache):
|
def push_presence(self, user, statuscache):
|
||||||
|
"""
|
||||||
|
Notify local and remote users of a change in presence of a local user.
|
||||||
|
Pushes the update to local clients and remote domains that are directly
|
||||||
|
subscribed to the presence of the local user.
|
||||||
|
Also pushes that update to any local user or remote domain that shares
|
||||||
|
a room with the local user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user(UserID): The local user whose presence was updated.
|
||||||
|
statuscache(UserPresenceCache): Cache of the user's presence state
|
||||||
|
Returns:
|
||||||
|
A Deferred.
|
||||||
|
"""
|
||||||
assert(self.hs.is_mine(user))
|
assert(self.hs.is_mine(user))
|
||||||
|
|
||||||
logger.debug("Pushing presence update from %s", user)
|
logger.debug("Pushing presence update from %s", user)
|
||||||
@@ -617,8 +866,7 @@ class PresenceHandler(BaseHandler):
|
|||||||
# and also user is informed of server-forced pushes
|
# and also user is informed of server-forced pushes
|
||||||
localusers.add(user)
|
localusers.add(user)
|
||||||
|
|
||||||
rm_handler = self.homeserver.get_handlers().room_member_handler
|
room_ids = yield self.get_joined_rooms_for_user(user)
|
||||||
room_ids = yield rm_handler.get_joined_rooms_for_user(user)
|
|
||||||
|
|
||||||
if not localusers and not room_ids:
|
if not localusers and not room_ids:
|
||||||
defer.returnValue(None)
|
defer.returnValue(None)
|
||||||
@@ -632,45 +880,24 @@ class PresenceHandler(BaseHandler):
|
|||||||
)
|
)
|
||||||
yield self.distributor.fire("user_presence_changed", user, statuscache)
|
yield self.distributor.fire("user_presence_changed", user, statuscache)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _push_presence_remote(self, user, destination, state=None):
|
|
||||||
if state is None:
|
|
||||||
state = yield self.store.get_presence_state(user.localpart)
|
|
||||||
del state["mtime"]
|
|
||||||
state["presence"] = state.pop("state")
|
|
||||||
|
|
||||||
if user in self._user_cachemap:
|
|
||||||
state["last_active"] = (
|
|
||||||
self._user_cachemap[user].get_state()["last_active"]
|
|
||||||
)
|
|
||||||
|
|
||||||
yield self.distributor.fire(
|
|
||||||
"collect_presencelike_data", user, state
|
|
||||||
)
|
|
||||||
|
|
||||||
if "last_active" in state:
|
|
||||||
state = dict(state)
|
|
||||||
state["last_active_ago"] = int(
|
|
||||||
self.clock.time_msec() - state.pop("last_active")
|
|
||||||
)
|
|
||||||
|
|
||||||
user_state = {
|
|
||||||
"user_id": user.to_string(),
|
|
||||||
}
|
|
||||||
user_state.update(**state)
|
|
||||||
|
|
||||||
yield self.federation.send_edu(
|
|
||||||
destination=destination,
|
|
||||||
edu_type="m.presence",
|
|
||||||
content={
|
|
||||||
"push": [
|
|
||||||
user_state,
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def incoming_presence(self, origin, content):
|
def incoming_presence(self, origin, content):
|
||||||
|
"""Handle an incoming m.presence EDU.
|
||||||
|
For each presence update in the "push" list update our local cache and
|
||||||
|
notify the appropriate local clients. Only clients that share a room
|
||||||
|
or are directly subscribed to the presence for a user should be
|
||||||
|
notified of the update.
|
||||||
|
For each subscription request in the "poll" list start pushing presence
|
||||||
|
updates to the remote server.
|
||||||
|
For unsubscribe request in the "unpoll" list stop pushing presence
|
||||||
|
updates to the remote server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
orgin(str): The source of this m.presence EDU.
|
||||||
|
content(dict): The content of this m.presence EDU.
|
||||||
|
Returns:
|
||||||
|
A Deferred.
|
||||||
|
"""
|
||||||
deferreds = []
|
deferreds = []
|
||||||
|
|
||||||
for push in content.get("push", []):
|
for push in content.get("push", []):
|
||||||
@@ -684,8 +911,7 @@ class PresenceHandler(BaseHandler):
|
|||||||
" | %d interested local observers %r", len(observers), observers
|
" | %d interested local observers %r", len(observers), observers
|
||||||
)
|
)
|
||||||
|
|
||||||
rm_handler = self.homeserver.get_handlers().room_member_handler
|
room_ids = yield self.get_joined_rooms_for_user(user)
|
||||||
room_ids = yield rm_handler.get_joined_rooms_for_user(user)
|
|
||||||
if room_ids:
|
if room_ids:
|
||||||
logger.debug(" | %d interested room IDs %r", len(room_ids), room_ids)
|
logger.debug(" | %d interested room IDs %r", len(room_ids), room_ids)
|
||||||
|
|
||||||
@@ -704,20 +930,15 @@ class PresenceHandler(BaseHandler):
|
|||||||
self.clock.time_msec() - state.pop("last_active_ago")
|
self.clock.time_msec() - state.pop("last_active_ago")
|
||||||
)
|
)
|
||||||
|
|
||||||
statuscache = self._get_or_make_usercache(user)
|
|
||||||
|
|
||||||
self._user_cachemap_latest_serial += 1
|
self._user_cachemap_latest_serial += 1
|
||||||
statuscache.update(state, serial=self._user_cachemap_latest_serial)
|
yield self.update_presence_cache(user, state, room_ids=room_ids)
|
||||||
|
|
||||||
if not observers and not room_ids:
|
if not observers and not room_ids:
|
||||||
logger.debug(" | no interested observers or room IDs")
|
logger.debug(" | no interested observers or room IDs")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.push_update_to_clients(
|
self.push_update_to_clients(
|
||||||
observed_user=user,
|
users_to_push=observers, room_ids=room_ids
|
||||||
users_to_push=observers,
|
|
||||||
room_ids=room_ids,
|
|
||||||
statuscache=statuscache,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
user_id = user.to_string()
|
user_id = user.to_string()
|
||||||
@@ -766,13 +987,58 @@ class PresenceHandler(BaseHandler):
|
|||||||
if not self._remote_sendmap[user]:
|
if not self._remote_sendmap[user]:
|
||||||
del self._remote_sendmap[user]
|
del self._remote_sendmap[user]
|
||||||
|
|
||||||
with PreserveLoggingContext():
|
yield defer.DeferredList(deferreds, consumeErrors=True)
|
||||||
yield defer.DeferredList(deferreds, consumeErrors=True)
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def update_presence_cache(self, user, state={}, room_ids=None,
|
||||||
|
add_to_cache=True):
|
||||||
|
"""Update the presence cache for a user with a new state and bump the
|
||||||
|
serial to the latest value.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user(UserID): The user being updated
|
||||||
|
state(dict): The presence state being updated
|
||||||
|
room_ids(None or list of str): A list of room_ids to update. If
|
||||||
|
room_ids is None then fetch the list of room_ids the user is
|
||||||
|
joined to.
|
||||||
|
add_to_cache: Whether to add an entry to the presence cache if the
|
||||||
|
user isn't already in the cache.
|
||||||
|
Returns:
|
||||||
|
A Deferred UserPresenceCache for the user being updated.
|
||||||
|
"""
|
||||||
|
if room_ids is None:
|
||||||
|
room_ids = yield self.get_joined_rooms_for_user(user)
|
||||||
|
|
||||||
|
for room_id in room_ids:
|
||||||
|
self._room_serials[room_id] = self._user_cachemap_latest_serial
|
||||||
|
if add_to_cache:
|
||||||
|
statuscache = self._get_or_make_usercache(user)
|
||||||
|
else:
|
||||||
|
statuscache = self._get_or_offline_usercache(user)
|
||||||
|
statuscache.update(state, serial=self._user_cachemap_latest_serial)
|
||||||
|
defer.returnValue(statuscache)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def push_update_to_local_and_remote(self, observed_user, statuscache,
|
def push_update_to_local_and_remote(self, observed_user, statuscache,
|
||||||
users_to_push=[], room_ids=[],
|
users_to_push=[], room_ids=[],
|
||||||
remote_domains=[]):
|
remote_domains=[]):
|
||||||
|
"""Notify local clients and remote servers of a change in the presence
|
||||||
|
of a user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
observed_user(UserID): The user to push the presence state for.
|
||||||
|
statuscache(UserPresenceCache): The cache for the presence state to
|
||||||
|
push.
|
||||||
|
users_to_push([UserID]): A list of local and remote users to
|
||||||
|
notify.
|
||||||
|
room_ids([str]): Notify the local and remote occupants of these
|
||||||
|
rooms.
|
||||||
|
remote_domains([str]): A list of remote servers to notify in
|
||||||
|
addition to those implied by the users_to_push and the
|
||||||
|
room_ids.
|
||||||
|
Returns:
|
||||||
|
A Deferred.
|
||||||
|
"""
|
||||||
|
|
||||||
localusers, remoteusers = partitionbool(
|
localusers, remoteusers = partitionbool(
|
||||||
users_to_push,
|
users_to_push,
|
||||||
@@ -782,10 +1048,7 @@ class PresenceHandler(BaseHandler):
|
|||||||
localusers = set(localusers)
|
localusers = set(localusers)
|
||||||
|
|
||||||
self.push_update_to_clients(
|
self.push_update_to_clients(
|
||||||
observed_user=observed_user,
|
users_to_push=localusers, room_ids=room_ids
|
||||||
users_to_push=localusers,
|
|
||||||
room_ids=room_ids,
|
|
||||||
statuscache=statuscache,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
remote_domains = set(remote_domains)
|
remote_domains = set(remote_domains)
|
||||||
@@ -810,11 +1073,65 @@ class PresenceHandler(BaseHandler):
|
|||||||
|
|
||||||
defer.returnValue((localusers, remote_domains))
|
defer.returnValue((localusers, remote_domains))
|
||||||
|
|
||||||
def push_update_to_clients(self, observed_user, users_to_push=[],
|
def push_update_to_clients(self, users_to_push=[], room_ids=[]):
|
||||||
room_ids=[], statuscache=None):
|
"""Notify clients of a new presence event.
|
||||||
self.notifier.on_new_user_event(
|
|
||||||
users_to_push,
|
Args:
|
||||||
room_ids,
|
users_to_push([UserID]): List of users to notify.
|
||||||
|
room_ids([str]): List of room_ids to notify.
|
||||||
|
"""
|
||||||
|
with PreserveLoggingContext():
|
||||||
|
self.notifier.on_new_event(
|
||||||
|
"presence_key",
|
||||||
|
self._user_cachemap_latest_serial,
|
||||||
|
users_to_push,
|
||||||
|
room_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _push_presence_remote(self, user, destination, state=None):
|
||||||
|
"""Push a user's presence to a remote server. If a presence state event
|
||||||
|
that event is sent. Otherwise a new state event is constructed from the
|
||||||
|
stored presence state.
|
||||||
|
The last_active is replaced with last_active_ago in case the wallclock
|
||||||
|
time on the remote server is different to the time on this server.
|
||||||
|
Sends an EDU to the remote server with the current presence state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user(UserID): The user to push the presence state for.
|
||||||
|
destination(str): The remote server to send state to.
|
||||||
|
state(dict): The state to push, or None to use the current stored
|
||||||
|
state.
|
||||||
|
Returns:
|
||||||
|
A Deferred.
|
||||||
|
"""
|
||||||
|
if state is None:
|
||||||
|
state = yield self.store.get_presence_state(user.localpart)
|
||||||
|
del state["mtime"]
|
||||||
|
state["presence"] = state.pop("state")
|
||||||
|
|
||||||
|
if user in self._user_cachemap:
|
||||||
|
state["last_active"] = (
|
||||||
|
self._user_cachemap[user].get_state()["last_active"]
|
||||||
|
)
|
||||||
|
|
||||||
|
yield self.distributor.fire(
|
||||||
|
"collect_presencelike_data", user, state
|
||||||
|
)
|
||||||
|
|
||||||
|
if "last_active" in state:
|
||||||
|
state = dict(state)
|
||||||
|
state["last_active_ago"] = int(
|
||||||
|
self.clock.time_msec() - state.pop("last_active")
|
||||||
|
)
|
||||||
|
|
||||||
|
user_state = {"user_id": user.to_string(), }
|
||||||
|
user_state.update(state)
|
||||||
|
|
||||||
|
yield self.federation.send_edu(
|
||||||
|
destination=destination,
|
||||||
|
edu_type="m.presence",
|
||||||
|
content={"push": [user_state, ], }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -823,39 +1140,11 @@ class PresenceEventSource(object):
|
|||||||
self.hs = hs
|
self.hs = hs
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def is_visible(self, observer_user, observed_user):
|
|
||||||
if observer_user == observed_user:
|
|
||||||
defer.returnValue(True)
|
|
||||||
|
|
||||||
presence = self.hs.get_handlers().presence_handler
|
|
||||||
|
|
||||||
if (yield presence.store.user_rooms_intersect(
|
|
||||||
[u.to_string() for u in observer_user, observed_user])):
|
|
||||||
defer.returnValue(True)
|
|
||||||
|
|
||||||
if self.hs.is_mine(observed_user):
|
|
||||||
pushmap = presence._local_pushmap
|
|
||||||
|
|
||||||
defer.returnValue(
|
|
||||||
observed_user.localpart in pushmap and
|
|
||||||
observer_user in pushmap[observed_user.localpart]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
recvmap = presence._remote_recvmap
|
|
||||||
|
|
||||||
defer.returnValue(
|
|
||||||
observed_user in recvmap and
|
|
||||||
observer_user in recvmap[observed_user]
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def get_new_events_for_user(self, user, from_key, limit):
|
def get_new_events_for_user(self, user, from_key, limit):
|
||||||
from_key = int(from_key)
|
from_key = int(from_key)
|
||||||
|
|
||||||
observer_user = user
|
|
||||||
|
|
||||||
presence = self.hs.get_handlers().presence_handler
|
presence = self.hs.get_handlers().presence_handler
|
||||||
cachemap = presence._user_cachemap
|
cachemap = presence._user_cachemap
|
||||||
|
|
||||||
@@ -864,17 +1153,27 @@ class PresenceEventSource(object):
|
|||||||
clock = self.clock
|
clock = self.clock
|
||||||
latest_serial = 0
|
latest_serial = 0
|
||||||
|
|
||||||
|
user_ids_to_check = {user}
|
||||||
|
presence_list = yield presence.store.get_presence_list(
|
||||||
|
user.localpart, accepted=True
|
||||||
|
)
|
||||||
|
if presence_list is not None:
|
||||||
|
user_ids_to_check |= set(
|
||||||
|
UserID.from_string(p["observed_user_id"]) for p in presence_list
|
||||||
|
)
|
||||||
|
room_ids = yield presence.get_joined_rooms_for_user(user)
|
||||||
|
for room_id in set(room_ids) & set(presence._room_serials):
|
||||||
|
if presence._room_serials[room_id] > from_key:
|
||||||
|
joined = yield presence.get_joined_users_for_room_id(room_id)
|
||||||
|
user_ids_to_check |= set(joined)
|
||||||
|
|
||||||
updates = []
|
updates = []
|
||||||
# TODO(paul): use a DeferredList ? How to limit concurrency.
|
for observed_user in user_ids_to_check & set(cachemap):
|
||||||
for observed_user in cachemap.keys():
|
|
||||||
cached = cachemap[observed_user]
|
cached = cachemap[observed_user]
|
||||||
|
|
||||||
if cached.serial <= from_key or cached.serial > max_serial:
|
if cached.serial <= from_key or cached.serial > max_serial:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not (yield self.is_visible(observer_user, observed_user)):
|
|
||||||
continue
|
|
||||||
|
|
||||||
latest_serial = max(cached.serial, latest_serial)
|
latest_serial = max(cached.serial, latest_serial)
|
||||||
updates.append(cached.make_event(user=observed_user, clock=clock))
|
updates.append(cached.make_event(user=observed_user, clock=clock))
|
||||||
|
|
||||||
@@ -911,8 +1210,6 @@ class PresenceEventSource(object):
|
|||||||
def get_pagination_rows(self, user, pagination_config, key):
|
def get_pagination_rows(self, user, pagination_config, key):
|
||||||
# TODO (erikj): Does this make sense? Ordering?
|
# TODO (erikj): Does this make sense? Ordering?
|
||||||
|
|
||||||
observer_user = user
|
|
||||||
|
|
||||||
from_key = int(pagination_config.from_key)
|
from_key = int(pagination_config.from_key)
|
||||||
|
|
||||||
if pagination_config.to_key:
|
if pagination_config.to_key:
|
||||||
@@ -923,14 +1220,26 @@ class PresenceEventSource(object):
|
|||||||
presence = self.hs.get_handlers().presence_handler
|
presence = self.hs.get_handlers().presence_handler
|
||||||
cachemap = presence._user_cachemap
|
cachemap = presence._user_cachemap
|
||||||
|
|
||||||
|
user_ids_to_check = {user}
|
||||||
|
presence_list = yield presence.store.get_presence_list(
|
||||||
|
user.localpart, accepted=True
|
||||||
|
)
|
||||||
|
if presence_list is not None:
|
||||||
|
user_ids_to_check |= set(
|
||||||
|
UserID.from_string(p["observed_user_id"]) for p in presence_list
|
||||||
|
)
|
||||||
|
room_ids = yield presence.get_joined_rooms_for_user(user)
|
||||||
|
for room_id in set(room_ids) & set(presence._room_serials):
|
||||||
|
if presence._room_serials[room_id] >= from_key:
|
||||||
|
joined = yield presence.get_joined_users_for_room_id(room_id)
|
||||||
|
user_ids_to_check |= set(joined)
|
||||||
|
|
||||||
updates = []
|
updates = []
|
||||||
# TODO(paul): use a DeferredList ? How to limit concurrency.
|
for observed_user in user_ids_to_check & set(cachemap):
|
||||||
for observed_user in cachemap.keys():
|
|
||||||
if not (to_key < cachemap[observed_user].serial <= from_key):
|
if not (to_key < cachemap[observed_user].serial <= from_key):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if (yield self.is_visible(observer_user, observed_user)):
|
updates.append((observed_user, cachemap[observed_user]))
|
||||||
updates.append((observed_user, cachemap[observed_user]))
|
|
||||||
|
|
||||||
# TODO(paul): limit
|
# TODO(paul): limit
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ from twisted.internet import defer
|
|||||||
|
|
||||||
from synapse.api.errors import SynapseError, AuthError, CodeMessageException
|
from synapse.api.errors import SynapseError, AuthError, CodeMessageException
|
||||||
from synapse.api.constants import EventTypes, Membership
|
from synapse.api.constants import EventTypes, Membership
|
||||||
from synapse.util.logcontext import PreserveLoggingContext
|
|
||||||
from synapse.types import UserID
|
from synapse.types import UserID
|
||||||
|
from synapse.util import unwrapFirstError
|
||||||
|
|
||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
|
|
||||||
@@ -88,6 +88,9 @@ class ProfileHandler(BaseHandler):
|
|||||||
if target_user != auth_user:
|
if target_user != auth_user:
|
||||||
raise AuthError(400, "Cannot set another user's displayname")
|
raise AuthError(400, "Cannot set another user's displayname")
|
||||||
|
|
||||||
|
if new_displayname == '':
|
||||||
|
new_displayname = None
|
||||||
|
|
||||||
yield self.store.set_profile_displayname(
|
yield self.store.set_profile_displayname(
|
||||||
target_user.localpart, new_displayname
|
target_user.localpart, new_displayname
|
||||||
)
|
)
|
||||||
@@ -154,14 +157,13 @@ class ProfileHandler(BaseHandler):
|
|||||||
if not self.hs.is_mine(user):
|
if not self.hs.is_mine(user):
|
||||||
defer.returnValue(None)
|
defer.returnValue(None)
|
||||||
|
|
||||||
with PreserveLoggingContext():
|
(displayname, avatar_url) = yield defer.gatherResults(
|
||||||
(displayname, avatar_url) = yield defer.gatherResults(
|
[
|
||||||
[
|
self.store.get_profile_displayname(user.localpart),
|
||||||
self.store.get_profile_displayname(user.localpart),
|
self.store.get_profile_avatar_url(user.localpart),
|
||||||
self.store.get_profile_avatar_url(user.localpart),
|
],
|
||||||
],
|
consumeErrors=True
|
||||||
consumeErrors=True
|
).addErrback(unwrapFirstError)
|
||||||
)
|
|
||||||
|
|
||||||
state["displayname"] = displayname
|
state["displayname"] = displayname
|
||||||
state["avatar_url"] = avatar_url
|
state["avatar_url"] = avatar_url
|
||||||
|
|||||||
210
synapse/handlers/receipts.py
Normal file
210
synapse/handlers/receipts.py
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 OpenMarket 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.
|
||||||
|
|
||||||
|
from ._base import BaseHandler
|
||||||
|
|
||||||
|
from twisted.internet import defer
|
||||||
|
|
||||||
|
from synapse.util.logcontext import PreserveLoggingContext
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ReceiptsHandler(BaseHandler):
|
||||||
|
def __init__(self, hs):
|
||||||
|
super(ReceiptsHandler, self).__init__(hs)
|
||||||
|
|
||||||
|
self.hs = hs
|
||||||
|
self.federation = hs.get_replication_layer()
|
||||||
|
self.federation.register_edu_handler(
|
||||||
|
"m.receipt", self._received_remote_receipt
|
||||||
|
)
|
||||||
|
self.clock = self.hs.get_clock()
|
||||||
|
|
||||||
|
self._receipt_cache = None
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def received_client_receipt(self, room_id, receipt_type, user_id,
|
||||||
|
event_id):
|
||||||
|
"""Called when a client tells us a local user has read up to the given
|
||||||
|
event_id in the room.
|
||||||
|
"""
|
||||||
|
receipt = {
|
||||||
|
"room_id": room_id,
|
||||||
|
"receipt_type": receipt_type,
|
||||||
|
"user_id": user_id,
|
||||||
|
"event_ids": [event_id],
|
||||||
|
"data": {
|
||||||
|
"ts": int(self.clock.time_msec()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is_new = yield self._handle_new_receipts([receipt])
|
||||||
|
|
||||||
|
if is_new:
|
||||||
|
self._push_remotes([receipt])
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _received_remote_receipt(self, origin, content):
|
||||||
|
"""Called when we receive an EDU of type m.receipt from a remote HS.
|
||||||
|
"""
|
||||||
|
receipts = [
|
||||||
|
{
|
||||||
|
"room_id": room_id,
|
||||||
|
"receipt_type": receipt_type,
|
||||||
|
"user_id": user_id,
|
||||||
|
"event_ids": user_values["event_ids"],
|
||||||
|
"data": user_values.get("data", {}),
|
||||||
|
}
|
||||||
|
for room_id, room_values in content.items()
|
||||||
|
for receipt_type, users in room_values.items()
|
||||||
|
for user_id, user_values in users.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
yield self._handle_new_receipts(receipts)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _handle_new_receipts(self, receipts):
|
||||||
|
"""Takes a list of receipts, stores them and informs the notifier.
|
||||||
|
"""
|
||||||
|
for receipt in receipts:
|
||||||
|
room_id = receipt["room_id"]
|
||||||
|
receipt_type = receipt["receipt_type"]
|
||||||
|
user_id = receipt["user_id"]
|
||||||
|
event_ids = receipt["event_ids"]
|
||||||
|
data = receipt["data"]
|
||||||
|
|
||||||
|
res = yield self.store.insert_receipt(
|
||||||
|
room_id, receipt_type, user_id, event_ids, data
|
||||||
|
)
|
||||||
|
|
||||||
|
if not res:
|
||||||
|
# res will be None if this read receipt is 'old'
|
||||||
|
defer.returnValue(False)
|
||||||
|
|
||||||
|
stream_id, max_persisted_id = res
|
||||||
|
|
||||||
|
with PreserveLoggingContext():
|
||||||
|
self.notifier.on_new_event(
|
||||||
|
"receipt_key", max_persisted_id, rooms=[room_id]
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue(True)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _push_remotes(self, receipts):
|
||||||
|
"""Given a list of receipts, works out which remote servers should be
|
||||||
|
poked and pokes them.
|
||||||
|
"""
|
||||||
|
# TODO: Some of this stuff should be coallesced.
|
||||||
|
for receipt in receipts:
|
||||||
|
room_id = receipt["room_id"]
|
||||||
|
receipt_type = receipt["receipt_type"]
|
||||||
|
user_id = receipt["user_id"]
|
||||||
|
event_ids = receipt["event_ids"]
|
||||||
|
data = receipt["data"]
|
||||||
|
|
||||||
|
remotedomains = set()
|
||||||
|
|
||||||
|
rm_handler = self.hs.get_handlers().room_member_handler
|
||||||
|
yield rm_handler.fetch_room_distributions_into(
|
||||||
|
room_id, localusers=None, remotedomains=remotedomains
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug("Sending receipt to: %r", remotedomains)
|
||||||
|
|
||||||
|
for domain in remotedomains:
|
||||||
|
self.federation.send_edu(
|
||||||
|
destination=domain,
|
||||||
|
edu_type="m.receipt",
|
||||||
|
content={
|
||||||
|
room_id: {
|
||||||
|
receipt_type: {
|
||||||
|
user_id: {
|
||||||
|
"event_ids": event_ids,
|
||||||
|
"data": data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_receipts_for_room(self, room_id, to_key):
|
||||||
|
"""Gets all receipts for a room, upto the given key.
|
||||||
|
"""
|
||||||
|
result = yield self.store.get_linearized_receipts_for_room(
|
||||||
|
room_id,
|
||||||
|
to_key=to_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
defer.returnValue([])
|
||||||
|
|
||||||
|
event = {
|
||||||
|
"type": "m.receipt",
|
||||||
|
"room_id": room_id,
|
||||||
|
"content": result,
|
||||||
|
}
|
||||||
|
|
||||||
|
defer.returnValue([event])
|
||||||
|
|
||||||
|
|
||||||
|
class ReceiptEventSource(object):
|
||||||
|
def __init__(self, hs):
|
||||||
|
self.store = hs.get_datastore()
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_new_events_for_user(self, user, from_key, limit):
|
||||||
|
from_key = int(from_key)
|
||||||
|
to_key = yield self.get_current_key()
|
||||||
|
|
||||||
|
if from_key == to_key:
|
||||||
|
defer.returnValue(([], to_key))
|
||||||
|
|
||||||
|
rooms = yield self.store.get_rooms_for_user(user.to_string())
|
||||||
|
rooms = [room.room_id for room in rooms]
|
||||||
|
events = yield self.store.get_linearized_receipts_for_rooms(
|
||||||
|
rooms,
|
||||||
|
from_key=from_key,
|
||||||
|
to_key=to_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue((events, to_key))
|
||||||
|
|
||||||
|
def get_current_key(self, direction='f'):
|
||||||
|
return self.store.get_max_receipt_stream_id()
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_pagination_rows(self, user, config, key):
|
||||||
|
to_key = int(config.from_key)
|
||||||
|
|
||||||
|
if config.to_key:
|
||||||
|
from_key = int(config.to_key)
|
||||||
|
else:
|
||||||
|
from_key = None
|
||||||
|
|
||||||
|
rooms = yield self.store.get_rooms_for_user(user.to_string())
|
||||||
|
rooms = [room.room_id for room in rooms]
|
||||||
|
events = yield self.store.get_linearized_receipts_for_rooms(
|
||||||
|
rooms,
|
||||||
|
from_key=from_key,
|
||||||
|
to_key=to_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue((events, to_key))
|
||||||
@@ -57,8 +57,8 @@ class RegistrationHandler(BaseHandler):
|
|||||||
|
|
||||||
yield self.check_user_id_is_valid(user_id)
|
yield self.check_user_id_is_valid(user_id)
|
||||||
|
|
||||||
u = yield self.store.get_user_by_id(user_id)
|
users = yield self.store.get_users_by_id_case_insensitive(user_id)
|
||||||
if u:
|
if users:
|
||||||
raise SynapseError(
|
raise SynapseError(
|
||||||
400,
|
400,
|
||||||
"User ID already taken.",
|
"User ID already taken.",
|
||||||
@@ -73,7 +73,8 @@ class RegistrationHandler(BaseHandler):
|
|||||||
localpart : The local part of the user ID to register. If None,
|
localpart : The local part of the user ID to register. If None,
|
||||||
one will be randomly generated.
|
one will be randomly generated.
|
||||||
password (str) : The password to assign to this user so they can
|
password (str) : The password to assign to this user so they can
|
||||||
login again.
|
login again. This can be None which means they cannot login again
|
||||||
|
via a password (e.g. the user is an application service user).
|
||||||
Returns:
|
Returns:
|
||||||
A tuple of (user_id, access_token).
|
A tuple of (user_id, access_token).
|
||||||
Raises:
|
Raises:
|
||||||
@@ -90,7 +91,7 @@ class RegistrationHandler(BaseHandler):
|
|||||||
user = UserID(localpart, self.hs.hostname)
|
user = UserID(localpart, self.hs.hostname)
|
||||||
user_id = user.to_string()
|
user_id = user.to_string()
|
||||||
|
|
||||||
token = self._generate_token(user_id)
|
token = self.generate_token(user_id)
|
||||||
yield self.store.register(
|
yield self.store.register(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
token=token,
|
token=token,
|
||||||
@@ -110,7 +111,7 @@ class RegistrationHandler(BaseHandler):
|
|||||||
user_id = user.to_string()
|
user_id = user.to_string()
|
||||||
yield self.check_user_id_is_valid(user_id)
|
yield self.check_user_id_is_valid(user_id)
|
||||||
|
|
||||||
token = self._generate_token(user_id)
|
token = self.generate_token(user_id)
|
||||||
yield self.store.register(
|
yield self.store.register(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
token=token,
|
token=token,
|
||||||
@@ -160,7 +161,7 @@ class RegistrationHandler(BaseHandler):
|
|||||||
400, "Invalid user localpart for this application service.",
|
400, "Invalid user localpart for this application service.",
|
||||||
errcode=Codes.EXCLUSIVE
|
errcode=Codes.EXCLUSIVE
|
||||||
)
|
)
|
||||||
token = self._generate_token(user_id)
|
token = self.generate_token(user_id)
|
||||||
yield self.store.register(
|
yield self.store.register(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
token=token,
|
token=token,
|
||||||
@@ -192,6 +193,35 @@ class RegistrationHandler(BaseHandler):
|
|||||||
else:
|
else:
|
||||||
logger.info("Valid captcha entered from %s", ip)
|
logger.info("Valid captcha entered from %s", ip)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def register_saml2(self, localpart):
|
||||||
|
"""
|
||||||
|
Registers email_id as SAML2 Based Auth.
|
||||||
|
"""
|
||||||
|
if urllib.quote(localpart) != localpart:
|
||||||
|
raise SynapseError(
|
||||||
|
400,
|
||||||
|
"User ID must only contain characters which do not"
|
||||||
|
" require URL encoding."
|
||||||
|
)
|
||||||
|
user = UserID(localpart, self.hs.hostname)
|
||||||
|
user_id = user.to_string()
|
||||||
|
|
||||||
|
yield self.check_user_id_is_valid(user_id)
|
||||||
|
token = self.generate_token(user_id)
|
||||||
|
try:
|
||||||
|
yield self.store.register(
|
||||||
|
user_id=user_id,
|
||||||
|
token=token,
|
||||||
|
password_hash=None
|
||||||
|
)
|
||||||
|
yield self.distributor.fire("registered_user", user)
|
||||||
|
except Exception, e:
|
||||||
|
yield self.store.add_access_token_to_user(user_id, token)
|
||||||
|
# Ignore Registration errors
|
||||||
|
logger.exception(e)
|
||||||
|
defer.returnValue((user_id, token))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def register_email(self, threepidCreds):
|
def register_email(self, threepidCreds):
|
||||||
"""
|
"""
|
||||||
@@ -243,7 +273,7 @@ class RegistrationHandler(BaseHandler):
|
|||||||
errcode=Codes.EXCLUSIVE
|
errcode=Codes.EXCLUSIVE
|
||||||
)
|
)
|
||||||
|
|
||||||
def _generate_token(self, user_id):
|
def generate_token(self, user_id):
|
||||||
# urlsafe variant uses _ and - so use . as the separator and replace
|
# urlsafe variant uses _ and - so use . as the separator and replace
|
||||||
# all =s with .s so http clients don't quote =s when it is used as
|
# all =s with .s so http clients don't quote =s when it is used as
|
||||||
# query params.
|
# query params.
|
||||||
|
|||||||
@@ -19,19 +19,36 @@ from twisted.internet import defer
|
|||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
|
|
||||||
from synapse.types import UserID, RoomAlias, RoomID
|
from synapse.types import UserID, RoomAlias, RoomID
|
||||||
from synapse.api.constants import EventTypes, Membership, JoinRules
|
from synapse.api.constants import (
|
||||||
|
EventTypes, Membership, JoinRules, RoomCreationPreset,
|
||||||
|
)
|
||||||
from synapse.api.errors import StoreError, SynapseError
|
from synapse.api.errors import StoreError, SynapseError
|
||||||
from synapse.util import stringutils
|
from synapse.util import stringutils, unwrapFirstError
|
||||||
from synapse.util.async import run_on_reactor
|
from synapse.util.async import run_on_reactor
|
||||||
from synapse.events.utils import serialize_event
|
from synapse.events.utils import serialize_event
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
import logging
|
import logging
|
||||||
|
import string
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RoomCreationHandler(BaseHandler):
|
class RoomCreationHandler(BaseHandler):
|
||||||
|
|
||||||
|
PRESETS_DICT = {
|
||||||
|
RoomCreationPreset.PRIVATE_CHAT: {
|
||||||
|
"join_rules": JoinRules.INVITE,
|
||||||
|
"history_visibility": "invited",
|
||||||
|
"original_invitees_have_ops": False,
|
||||||
|
},
|
||||||
|
RoomCreationPreset.PUBLIC_CHAT: {
|
||||||
|
"join_rules": JoinRules.PUBLIC,
|
||||||
|
"history_visibility": "shared",
|
||||||
|
"original_invitees_have_ops": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def create_room(self, user_id, room_id, config):
|
def create_room(self, user_id, room_id, config):
|
||||||
""" Creates a new room.
|
""" Creates a new room.
|
||||||
@@ -50,6 +67,10 @@ class RoomCreationHandler(BaseHandler):
|
|||||||
self.ratelimit(user_id)
|
self.ratelimit(user_id)
|
||||||
|
|
||||||
if "room_alias_name" in config:
|
if "room_alias_name" in config:
|
||||||
|
for wchar in string.whitespace:
|
||||||
|
if wchar in config["room_alias_name"]:
|
||||||
|
raise SynapseError(400, "Invalid characters in room alias")
|
||||||
|
|
||||||
room_alias = RoomAlias.create(
|
room_alias = RoomAlias.create(
|
||||||
config["room_alias_name"],
|
config["room_alias_name"],
|
||||||
self.hs.hostname,
|
self.hs.hostname,
|
||||||
@@ -116,9 +137,25 @@ class RoomCreationHandler(BaseHandler):
|
|||||||
servers=[self.hs.hostname],
|
servers=[self.hs.hostname],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
preset_config = config.get(
|
||||||
|
"preset",
|
||||||
|
RoomCreationPreset.PUBLIC_CHAT
|
||||||
|
if is_public
|
||||||
|
else RoomCreationPreset.PRIVATE_CHAT
|
||||||
|
)
|
||||||
|
|
||||||
|
raw_initial_state = config.get("initial_state", [])
|
||||||
|
|
||||||
|
initial_state = OrderedDict()
|
||||||
|
for val in raw_initial_state:
|
||||||
|
initial_state[(val["type"], val.get("state_key", ""))] = val["content"]
|
||||||
|
|
||||||
user = UserID.from_string(user_id)
|
user = UserID.from_string(user_id)
|
||||||
creation_events = self._create_events_for_new_room(
|
creation_events = self._create_events_for_new_room(
|
||||||
user, room_id, is_public=is_public
|
user, room_id,
|
||||||
|
preset_config=preset_config,
|
||||||
|
invite_list=invite_list,
|
||||||
|
initial_state=initial_state,
|
||||||
)
|
)
|
||||||
|
|
||||||
msg_handler = self.hs.get_handlers().message_handler
|
msg_handler = self.hs.get_handlers().message_handler
|
||||||
@@ -165,7 +202,10 @@ class RoomCreationHandler(BaseHandler):
|
|||||||
|
|
||||||
defer.returnValue(result)
|
defer.returnValue(result)
|
||||||
|
|
||||||
def _create_events_for_new_room(self, creator, room_id, is_public=False):
|
def _create_events_for_new_room(self, creator, room_id, preset_config,
|
||||||
|
invite_list, initial_state):
|
||||||
|
config = RoomCreationHandler.PRESETS_DICT[preset_config]
|
||||||
|
|
||||||
creator_id = creator.to_string()
|
creator_id = creator.to_string()
|
||||||
|
|
||||||
event_keys = {
|
event_keys = {
|
||||||
@@ -198,16 +238,20 @@ class RoomCreationHandler(BaseHandler):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
power_levels_event = create(
|
returned_events = [creation_event, join_event]
|
||||||
etype=EventTypes.PowerLevels,
|
|
||||||
content={
|
if (EventTypes.PowerLevels, '') not in initial_state:
|
||||||
|
power_level_content = {
|
||||||
"users": {
|
"users": {
|
||||||
creator.to_string(): 100,
|
creator.to_string(): 100,
|
||||||
},
|
},
|
||||||
"users_default": 0,
|
"users_default": 0,
|
||||||
"events": {
|
"events": {
|
||||||
EventTypes.Name: 100,
|
EventTypes.Name: 50,
|
||||||
EventTypes.PowerLevels: 100,
|
EventTypes.PowerLevels: 100,
|
||||||
|
EventTypes.RoomHistoryVisibility: 100,
|
||||||
|
EventTypes.CanonicalAlias: 50,
|
||||||
|
EventTypes.RoomAvatar: 50,
|
||||||
},
|
},
|
||||||
"events_default": 0,
|
"events_default": 0,
|
||||||
"state_default": 50,
|
"state_default": 50,
|
||||||
@@ -215,21 +259,43 @@ class RoomCreationHandler(BaseHandler):
|
|||||||
"kick": 50,
|
"kick": 50,
|
||||||
"redact": 50,
|
"redact": 50,
|
||||||
"invite": 0,
|
"invite": 0,
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
|
||||||
join_rule = JoinRules.PUBLIC if is_public else JoinRules.INVITE
|
if config["original_invitees_have_ops"]:
|
||||||
join_rules_event = create(
|
for invitee in invite_list:
|
||||||
etype=EventTypes.JoinRules,
|
power_level_content["users"][invitee] = 100
|
||||||
content={"join_rule": join_rule},
|
|
||||||
)
|
|
||||||
|
|
||||||
return [
|
power_levels_event = create(
|
||||||
creation_event,
|
etype=EventTypes.PowerLevels,
|
||||||
join_event,
|
content=power_level_content,
|
||||||
power_levels_event,
|
)
|
||||||
join_rules_event,
|
|
||||||
]
|
returned_events.append(power_levels_event)
|
||||||
|
|
||||||
|
if (EventTypes.JoinRules, '') not in initial_state:
|
||||||
|
join_rules_event = create(
|
||||||
|
etype=EventTypes.JoinRules,
|
||||||
|
content={"join_rule": config["join_rules"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
returned_events.append(join_rules_event)
|
||||||
|
|
||||||
|
if (EventTypes.RoomHistoryVisibility, '') not in initial_state:
|
||||||
|
history_event = create(
|
||||||
|
etype=EventTypes.RoomHistoryVisibility,
|
||||||
|
content={"history_visibility": config["history_visibility"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
returned_events.append(history_event)
|
||||||
|
|
||||||
|
for (etype, state_key), content in initial_state.items():
|
||||||
|
returned_events.append(create(
|
||||||
|
etype=etype,
|
||||||
|
state_key=state_key,
|
||||||
|
content=content,
|
||||||
|
))
|
||||||
|
|
||||||
|
return returned_events
|
||||||
|
|
||||||
|
|
||||||
class RoomMemberHandler(BaseHandler):
|
class RoomMemberHandler(BaseHandler):
|
||||||
@@ -493,15 +559,9 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
"""Returns a list of roomids that the user has any of the given
|
"""Returns a list of roomids that the user has any of the given
|
||||||
membership states in."""
|
membership states in."""
|
||||||
|
|
||||||
app_service = yield self.store.get_app_service_by_user_id(
|
rooms = yield self.store.get_rooms_for_user(
|
||||||
user.to_string()
|
user.to_string(),
|
||||||
)
|
)
|
||||||
if app_service:
|
|
||||||
rooms = yield self.store.get_app_service_rooms(app_service)
|
|
||||||
else:
|
|
||||||
rooms = yield self.store.get_rooms_for_user(
|
|
||||||
user.to_string(),
|
|
||||||
)
|
|
||||||
|
|
||||||
# For some reason the list of events contains duplicates
|
# For some reason the list of events contains duplicates
|
||||||
# TODO(paul): work out why because I really don't think it should
|
# TODO(paul): work out why because I really don't think it should
|
||||||
@@ -529,11 +589,17 @@ class RoomListHandler(BaseHandler):
|
|||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_public_room_list(self):
|
def get_public_room_list(self):
|
||||||
chunk = yield self.store.get_rooms(is_public=True)
|
chunk = yield self.store.get_rooms(is_public=True)
|
||||||
for room in chunk:
|
results = yield defer.gatherResults(
|
||||||
joined_users = yield self.store.get_users_in_room(
|
[
|
||||||
room_id=room["room_id"],
|
self.store.get_users_in_room(room["room_id"])
|
||||||
)
|
for room in chunk
|
||||||
room["num_joined_members"] = len(joined_users)
|
],
|
||||||
|
consumeErrors=True,
|
||||||
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
|
for i, room in enumerate(chunk):
|
||||||
|
room["num_joined_members"] = len(results[i])
|
||||||
|
|
||||||
# FIXME (erikj): START is no longer a valid value
|
# FIXME (erikj): START is no longer a valid value
|
||||||
defer.returnValue({"start": "START", "end": "END", "chunk": chunk})
|
defer.returnValue({"start": "START", "end": "END", "chunk": chunk})
|
||||||
|
|
||||||
@@ -569,8 +635,8 @@ class RoomEventSource(object):
|
|||||||
|
|
||||||
defer.returnValue((events, end_key))
|
defer.returnValue((events, end_key))
|
||||||
|
|
||||||
def get_current_key(self):
|
def get_current_key(self, direction='f'):
|
||||||
return self.store.get_room_events_max_id()
|
return self.store.get_room_events_max_id(direction)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_pagination_rows(self, user, config, key):
|
def get_pagination_rows(self, user, config, key):
|
||||||
|
|||||||
@@ -92,13 +92,22 @@ class SyncHandler(BaseHandler):
|
|||||||
result = yield self.current_sync_for_user(sync_config, since_token)
|
result = yield self.current_sync_for_user(sync_config, since_token)
|
||||||
defer.returnValue(result)
|
defer.returnValue(result)
|
||||||
else:
|
else:
|
||||||
def current_sync_callback():
|
def current_sync_callback(before_token, after_token):
|
||||||
return self.current_sync_for_user(sync_config, since_token)
|
return self.current_sync_for_user(sync_config, since_token)
|
||||||
|
|
||||||
rm_handler = self.hs.get_handlers().room_member_handler
|
rm_handler = self.hs.get_handlers().room_member_handler
|
||||||
room_ids = yield rm_handler.get_joined_rooms_for_user(
|
|
||||||
sync_config.user
|
app_service = yield self.store.get_app_service_by_user_id(
|
||||||
|
sync_config.user.to_string()
|
||||||
)
|
)
|
||||||
|
if app_service:
|
||||||
|
rooms = yield self.store.get_app_service_rooms(app_service)
|
||||||
|
room_ids = set(r.room_id for r in rooms)
|
||||||
|
else:
|
||||||
|
room_ids = yield rm_handler.get_joined_rooms_for_user(
|
||||||
|
sync_config.user
|
||||||
|
)
|
||||||
|
|
||||||
result = yield self.notifier.wait_for_events(
|
result = yield self.notifier.wait_for_events(
|
||||||
sync_config.user, room_ids,
|
sync_config.user, room_ids,
|
||||||
sync_config.filter, timeout, current_sync_callback
|
sync_config.filter, timeout, current_sync_callback
|
||||||
@@ -229,7 +238,16 @@ class SyncHandler(BaseHandler):
|
|||||||
logger.debug("Typing %r", typing_by_room)
|
logger.debug("Typing %r", typing_by_room)
|
||||||
|
|
||||||
rm_handler = self.hs.get_handlers().room_member_handler
|
rm_handler = self.hs.get_handlers().room_member_handler
|
||||||
room_ids = yield rm_handler.get_joined_rooms_for_user(sync_config.user)
|
app_service = yield self.store.get_app_service_by_user_id(
|
||||||
|
sync_config.user.to_string()
|
||||||
|
)
|
||||||
|
if app_service:
|
||||||
|
rooms = yield self.store.get_app_service_rooms(app_service)
|
||||||
|
room_ids = set(r.room_id for r in rooms)
|
||||||
|
else:
|
||||||
|
room_ids = yield rm_handler.get_joined_rooms_for_user(
|
||||||
|
sync_config.user
|
||||||
|
)
|
||||||
|
|
||||||
# TODO (mjark): Does public mean "published"?
|
# TODO (mjark): Does public mean "published"?
|
||||||
published_rooms = yield self.store.get_rooms(is_public=True)
|
published_rooms = yield self.store.get_rooms(is_public=True)
|
||||||
@@ -292,6 +310,52 @@ class SyncHandler(BaseHandler):
|
|||||||
next_batch=now_token,
|
next_batch=now_token,
|
||||||
))
|
))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _filter_events_for_client(self, user_id, room_id, events):
|
||||||
|
event_id_to_state = yield self.store.get_state_for_events(
|
||||||
|
room_id, frozenset(e.event_id for e in events),
|
||||||
|
types=(
|
||||||
|
(EventTypes.RoomHistoryVisibility, ""),
|
||||||
|
(EventTypes.Member, user_id),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def allowed(event, state):
|
||||||
|
if event.type == EventTypes.RoomHistoryVisibility:
|
||||||
|
return True
|
||||||
|
|
||||||
|
membership_ev = state.get((EventTypes.Member, user_id), None)
|
||||||
|
if membership_ev:
|
||||||
|
membership = membership_ev.membership
|
||||||
|
else:
|
||||||
|
membership = Membership.LEAVE
|
||||||
|
|
||||||
|
if membership == Membership.JOIN:
|
||||||
|
return True
|
||||||
|
|
||||||
|
history = state.get((EventTypes.RoomHistoryVisibility, ''), None)
|
||||||
|
if history:
|
||||||
|
visibility = history.content.get("history_visibility", "shared")
|
||||||
|
else:
|
||||||
|
visibility = "shared"
|
||||||
|
|
||||||
|
if visibility == "public":
|
||||||
|
return True
|
||||||
|
elif visibility == "shared":
|
||||||
|
return True
|
||||||
|
elif visibility == "joined":
|
||||||
|
return membership == Membership.JOIN
|
||||||
|
elif visibility == "invited":
|
||||||
|
return membership == Membership.INVITE
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
defer.returnValue([
|
||||||
|
event
|
||||||
|
for event in events
|
||||||
|
if allowed(event, event_id_to_state[event.event_id])
|
||||||
|
])
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def load_filtered_recents(self, room_id, sync_config, now_token,
|
def load_filtered_recents(self, room_id, sync_config, now_token,
|
||||||
since_token=None):
|
since_token=None):
|
||||||
@@ -313,6 +377,9 @@ class SyncHandler(BaseHandler):
|
|||||||
(room_key, _) = keys
|
(room_key, _) = keys
|
||||||
end_key = "s" + room_key.split('-')[-1]
|
end_key = "s" + room_key.split('-')[-1]
|
||||||
loaded_recents = sync_config.filter.filter_room_events(events)
|
loaded_recents = sync_config.filter.filter_room_events(events)
|
||||||
|
loaded_recents = yield self._filter_events_for_client(
|
||||||
|
sync_config.user.to_string(), room_id, loaded_recents,
|
||||||
|
)
|
||||||
loaded_recents.extend(recents)
|
loaded_recents.extend(recents)
|
||||||
recents = loaded_recents
|
recents = loaded_recents
|
||||||
if len(events) <= load_limit:
|
if len(events) <= load_limit:
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from twisted.internet import defer
|
|||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
|
|
||||||
from synapse.api.errors import SynapseError, AuthError
|
from synapse.api.errors import SynapseError, AuthError
|
||||||
|
from synapse.util.logcontext import PreserveLoggingContext
|
||||||
from synapse.types import UserID
|
from synapse.types import UserID
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -203,20 +204,19 @@ class TypingNotificationHandler(BaseHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _push_update_local(self, room_id, user, typing):
|
def _push_update_local(self, room_id, user, typing):
|
||||||
if room_id not in self._room_serials:
|
room_set = self._room_typing.setdefault(room_id, set())
|
||||||
self._room_serials[room_id] = 0
|
|
||||||
self._room_typing[room_id] = set()
|
|
||||||
|
|
||||||
room_set = self._room_typing[room_id]
|
|
||||||
if typing:
|
if typing:
|
||||||
room_set.add(user)
|
room_set.add(user)
|
||||||
elif user in room_set:
|
else:
|
||||||
room_set.remove(user)
|
room_set.discard(user)
|
||||||
|
|
||||||
self._latest_room_serial += 1
|
self._latest_room_serial += 1
|
||||||
self._room_serials[room_id] = self._latest_room_serial
|
self._room_serials[room_id] = self._latest_room_serial
|
||||||
|
|
||||||
self.notifier.on_new_user_event(rooms=[room_id])
|
with PreserveLoggingContext():
|
||||||
|
self.notifier.on_new_event(
|
||||||
|
"typing_key", self._latest_room_serial, rooms=[room_id]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TypingNotificationEventSource(object):
|
class TypingNotificationEventSource(object):
|
||||||
@@ -256,8 +256,8 @@ class TypingNotificationEventSource(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
events = []
|
events = []
|
||||||
for room_id in handler._room_serials:
|
for room_id in joined_room_ids:
|
||||||
if room_id not in joined_room_ids:
|
if room_id not in handler._room_serials:
|
||||||
continue
|
continue
|
||||||
if handler._room_serials[room_id] <= from_key:
|
if handler._room_serials[room_id] <= from_key:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -14,12 +14,14 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from synapse.api.errors import CodeMessageException
|
from synapse.api.errors import CodeMessageException
|
||||||
|
from synapse.util.logcontext import preserve_context_over_fn
|
||||||
from syutil.jsonutil import encode_canonical_json
|
from syutil.jsonutil import encode_canonical_json
|
||||||
import synapse.metrics
|
import synapse.metrics
|
||||||
|
|
||||||
from twisted.internet import defer, reactor
|
from twisted.internet import defer, reactor
|
||||||
from twisted.web.client import (
|
from twisted.web.client import (
|
||||||
Agent, readBody, FileBodyProducer, PartialDownloadError
|
Agent, readBody, FileBodyProducer, PartialDownloadError,
|
||||||
|
HTTPConnectionPool,
|
||||||
)
|
)
|
||||||
from twisted.web.http_headers import Headers
|
from twisted.web.http_headers import Headers
|
||||||
|
|
||||||
@@ -54,21 +56,36 @@ class SimpleHttpClient(object):
|
|||||||
# The default context factory in Twisted 14.0.0 (which we require) is
|
# The default context factory in Twisted 14.0.0 (which we require) is
|
||||||
# BrowserLikePolicyForHTTPS which will do regular cert validation
|
# BrowserLikePolicyForHTTPS which will do regular cert validation
|
||||||
# 'like a browser'
|
# 'like a browser'
|
||||||
self.agent = Agent(reactor)
|
pool = HTTPConnectionPool(reactor)
|
||||||
|
pool.maxPersistentPerHost = 10
|
||||||
|
self.agent = Agent(reactor, pool=pool)
|
||||||
self.version_string = hs.version_string
|
self.version_string = hs.version_string
|
||||||
|
|
||||||
def request(self, method, *args, **kwargs):
|
def request(self, method, uri, *args, **kwargs):
|
||||||
# A small wrapper around self.agent.request() so we can easily attach
|
# A small wrapper around self.agent.request() so we can easily attach
|
||||||
# counters to it
|
# counters to it
|
||||||
outgoing_requests_counter.inc(method)
|
outgoing_requests_counter.inc(method)
|
||||||
d = self.agent.request(method, *args, **kwargs)
|
d = preserve_context_over_fn(
|
||||||
|
self.agent.request,
|
||||||
|
method, uri, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Sending request %s %s", method, uri)
|
||||||
|
|
||||||
def _cb(response):
|
def _cb(response):
|
||||||
incoming_responses_counter.inc(method, response.code)
|
incoming_responses_counter.inc(method, response.code)
|
||||||
|
logger.info(
|
||||||
|
"Received response to %s %s: %s",
|
||||||
|
method, uri, response.code
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def _eb(failure):
|
def _eb(failure):
|
||||||
incoming_responses_counter.inc(method, "ERR")
|
incoming_responses_counter.inc(method, "ERR")
|
||||||
|
logger.info(
|
||||||
|
"Error sending request to %s %s: %s %s",
|
||||||
|
method, uri, failure.type, failure.getErrorMessage()
|
||||||
|
)
|
||||||
return failure
|
return failure
|
||||||
|
|
||||||
d.addCallbacks(_cb, _eb)
|
d.addCallbacks(_cb, _eb)
|
||||||
@@ -77,7 +94,9 @@ class SimpleHttpClient(object):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def post_urlencoded_get_json(self, uri, args={}):
|
def post_urlencoded_get_json(self, uri, args={}):
|
||||||
|
# TODO: Do we ever want to log message contents?
|
||||||
logger.debug("post_urlencoded_get_json args: %s", args)
|
logger.debug("post_urlencoded_get_json args: %s", args)
|
||||||
|
|
||||||
query_bytes = urllib.urlencode(args, True)
|
query_bytes = urllib.urlencode(args, True)
|
||||||
|
|
||||||
response = yield self.request(
|
response = yield self.request(
|
||||||
@@ -90,7 +109,7 @@ class SimpleHttpClient(object):
|
|||||||
bodyProducer=FileBodyProducer(StringIO(query_bytes))
|
bodyProducer=FileBodyProducer(StringIO(query_bytes))
|
||||||
)
|
)
|
||||||
|
|
||||||
body = yield readBody(response)
|
body = yield preserve_context_over_fn(readBody, response)
|
||||||
|
|
||||||
defer.returnValue(json.loads(body))
|
defer.returnValue(json.loads(body))
|
||||||
|
|
||||||
@@ -98,7 +117,7 @@ class SimpleHttpClient(object):
|
|||||||
def post_json_get_json(self, uri, post_json):
|
def post_json_get_json(self, uri, post_json):
|
||||||
json_str = encode_canonical_json(post_json)
|
json_str = encode_canonical_json(post_json)
|
||||||
|
|
||||||
logger.info("HTTP POST %s -> %s", json_str, uri)
|
logger.debug("HTTP POST %s -> %s", json_str, uri)
|
||||||
|
|
||||||
response = yield self.request(
|
response = yield self.request(
|
||||||
"POST",
|
"POST",
|
||||||
@@ -109,7 +128,7 @@ class SimpleHttpClient(object):
|
|||||||
bodyProducer=FileBodyProducer(StringIO(json_str))
|
bodyProducer=FileBodyProducer(StringIO(json_str))
|
||||||
)
|
)
|
||||||
|
|
||||||
body = yield readBody(response)
|
body = yield preserve_context_over_fn(readBody, response)
|
||||||
|
|
||||||
defer.returnValue(json.loads(body))
|
defer.returnValue(json.loads(body))
|
||||||
|
|
||||||
@@ -142,7 +161,7 @@ class SimpleHttpClient(object):
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
body = yield readBody(response)
|
body = yield preserve_context_over_fn(readBody, response)
|
||||||
|
|
||||||
if 200 <= response.code < 300:
|
if 200 <= response.code < 300:
|
||||||
defer.returnValue(json.loads(body))
|
defer.returnValue(json.loads(body))
|
||||||
@@ -185,7 +204,7 @@ class SimpleHttpClient(object):
|
|||||||
bodyProducer=FileBodyProducer(StringIO(json_str))
|
bodyProducer=FileBodyProducer(StringIO(json_str))
|
||||||
)
|
)
|
||||||
|
|
||||||
body = yield readBody(response)
|
body = yield preserve_context_over_fn(readBody, response)
|
||||||
|
|
||||||
if 200 <= response.code < 300:
|
if 200 <= response.code < 300:
|
||||||
defer.returnValue(json.loads(body))
|
defer.returnValue(json.loads(body))
|
||||||
@@ -219,7 +238,7 @@ class CaptchaServerHttpClient(SimpleHttpClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
body = yield readBody(response)
|
body = yield preserve_context_over_fn(readBody, response)
|
||||||
defer.returnValue(body)
|
defer.returnValue(body)
|
||||||
except PartialDownloadError as e:
|
except PartialDownloadError as e:
|
||||||
# twisted dislikes google's response, no content length.
|
# twisted dislikes google's response, no content length.
|
||||||
|
|||||||
@@ -16,13 +16,13 @@
|
|||||||
|
|
||||||
from twisted.internet import defer, reactor, protocol
|
from twisted.internet import defer, reactor, protocol
|
||||||
from twisted.internet.error import DNSLookupError
|
from twisted.internet.error import DNSLookupError
|
||||||
from twisted.web.client import readBody, _AgentBase, _URI
|
from twisted.web.client import readBody, HTTPConnectionPool, Agent
|
||||||
from twisted.web.http_headers import Headers
|
from twisted.web.http_headers import Headers
|
||||||
from twisted.web._newclient import ResponseDone
|
from twisted.web._newclient import ResponseDone
|
||||||
|
|
||||||
from synapse.http.endpoint import matrix_federation_endpoint
|
from synapse.http.endpoint import matrix_federation_endpoint
|
||||||
from synapse.util.async import sleep
|
from synapse.util.async import sleep
|
||||||
from synapse.util.logcontext import PreserveLoggingContext
|
from synapse.util.logcontext import preserve_context_over_fn
|
||||||
import synapse.metrics
|
import synapse.metrics
|
||||||
|
|
||||||
from syutil.jsonutil import encode_canonical_json
|
from syutil.jsonutil import encode_canonical_json
|
||||||
@@ -35,11 +35,13 @@ from syutil.crypto.jsonsign import sign_json
|
|||||||
|
|
||||||
import simplejson as json
|
import simplejson as json
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
import urllib
|
import urllib
|
||||||
import urlparse
|
import urlparse
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
outbound_logger = logging.getLogger("synapse.http.outbound")
|
||||||
|
|
||||||
metrics = synapse.metrics.get_metrics_for(__name__)
|
metrics = synapse.metrics.get_metrics_for(__name__)
|
||||||
|
|
||||||
@@ -53,41 +55,17 @@ incoming_responses_counter = metrics.register_counter(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MatrixFederationHttpAgent(_AgentBase):
|
class MatrixFederationEndpointFactory(object):
|
||||||
|
def __init__(self, hs):
|
||||||
|
self.tls_context_factory = hs.tls_context_factory
|
||||||
|
|
||||||
def __init__(self, reactor, pool=None):
|
def endpointForURI(self, uri):
|
||||||
_AgentBase.__init__(self, reactor, pool)
|
destination = uri.netloc
|
||||||
|
|
||||||
def request(self, destination, endpoint, method, path, params, query,
|
return matrix_federation_endpoint(
|
||||||
headers, body_producer):
|
reactor, destination, timeout=10,
|
||||||
|
ssl_context_factory=self.tls_context_factory
|
||||||
outgoing_requests_counter.inc(method)
|
)
|
||||||
|
|
||||||
host = b""
|
|
||||||
port = 0
|
|
||||||
fragment = b""
|
|
||||||
|
|
||||||
parsed_URI = _URI(b"http", destination, host, port, path, params,
|
|
||||||
query, fragment)
|
|
||||||
|
|
||||||
# Set the connection pool key to be the destination.
|
|
||||||
key = destination
|
|
||||||
|
|
||||||
d = self._requestWithEndpoint(key, endpoint, method, parsed_URI,
|
|
||||||
headers, body_producer,
|
|
||||||
parsed_URI.originForm)
|
|
||||||
|
|
||||||
def _cb(response):
|
|
||||||
incoming_responses_counter.inc(method, response.code)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def _eb(failure):
|
|
||||||
incoming_responses_counter.inc(method, "ERR")
|
|
||||||
return failure
|
|
||||||
|
|
||||||
d.addCallbacks(_cb, _eb)
|
|
||||||
|
|
||||||
return d
|
|
||||||
|
|
||||||
|
|
||||||
class MatrixFederationHttpClient(object):
|
class MatrixFederationHttpClient(object):
|
||||||
@@ -103,105 +81,123 @@ class MatrixFederationHttpClient(object):
|
|||||||
self.hs = hs
|
self.hs = hs
|
||||||
self.signing_key = hs.config.signing_key[0]
|
self.signing_key = hs.config.signing_key[0]
|
||||||
self.server_name = hs.hostname
|
self.server_name = hs.hostname
|
||||||
self.agent = MatrixFederationHttpAgent(reactor)
|
pool = HTTPConnectionPool(reactor)
|
||||||
|
pool.maxPersistentPerHost = 10
|
||||||
|
self.agent = Agent.usingEndpointFactory(
|
||||||
|
reactor, MatrixFederationEndpointFactory(hs), pool=pool
|
||||||
|
)
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
self.version_string = hs.version_string
|
self.version_string = hs.version_string
|
||||||
|
self._next_id = 1
|
||||||
|
|
||||||
|
def _create_url(self, destination, path_bytes, param_bytes, query_bytes):
|
||||||
|
return urlparse.urlunparse(
|
||||||
|
("matrix", destination, path_bytes, param_bytes, query_bytes, "")
|
||||||
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _create_request(self, destination, method, path_bytes,
|
def _create_request(self, destination, method, path_bytes,
|
||||||
body_callback, headers_dict={}, param_bytes=b"",
|
body_callback, headers_dict={}, param_bytes=b"",
|
||||||
query_bytes=b"", retry_on_dns_fail=True):
|
query_bytes=b"", retry_on_dns_fail=True,
|
||||||
|
timeout=None):
|
||||||
""" Creates and sends a request to the given url
|
""" Creates and sends a request to the given url
|
||||||
"""
|
"""
|
||||||
headers_dict[b"User-Agent"] = [self.version_string]
|
headers_dict[b"User-Agent"] = [self.version_string]
|
||||||
headers_dict[b"Host"] = [destination]
|
headers_dict[b"Host"] = [destination]
|
||||||
|
|
||||||
url_bytes = urlparse.urlunparse(
|
url_bytes = self._create_url(
|
||||||
("", "", path_bytes, param_bytes, query_bytes, "",)
|
destination, path_bytes, param_bytes, query_bytes
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info("Sending request to %s: %s %s",
|
txn_id = "%s-O-%s" % (method, self._next_id)
|
||||||
destination, method, url_bytes)
|
self._next_id = (self._next_id + 1) % (sys.maxint - 1)
|
||||||
|
|
||||||
logger.debug(
|
outbound_logger.info(
|
||||||
"Types: %s",
|
"{%s} [%s] Sending request: %s %s",
|
||||||
[
|
txn_id, destination, method, url_bytes
|
||||||
type(destination), type(method), type(path_bytes),
|
|
||||||
type(param_bytes),
|
|
||||||
type(query_bytes)
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# XXX: Would be much nicer to retry only at the transaction-layer
|
# XXX: Would be much nicer to retry only at the transaction-layer
|
||||||
# (once we have reliable transactions in place)
|
# (once we have reliable transactions in place)
|
||||||
retries_left = 5
|
retries_left = 5
|
||||||
|
|
||||||
endpoint = self._getEndpoint(reactor, destination)
|
http_url_bytes = urlparse.urlunparse(
|
||||||
|
("", "", path_bytes, param_bytes, query_bytes, "")
|
||||||
while True:
|
|
||||||
producer = None
|
|
||||||
if body_callback:
|
|
||||||
producer = body_callback(method, url_bytes, headers_dict)
|
|
||||||
|
|
||||||
try:
|
|
||||||
with PreserveLoggingContext():
|
|
||||||
request_deferred = self.agent.request(
|
|
||||||
destination,
|
|
||||||
endpoint,
|
|
||||||
method,
|
|
||||||
path_bytes,
|
|
||||||
param_bytes,
|
|
||||||
query_bytes,
|
|
||||||
Headers(headers_dict),
|
|
||||||
producer
|
|
||||||
)
|
|
||||||
|
|
||||||
response = yield self.clock.time_bound_deferred(
|
|
||||||
request_deferred,
|
|
||||||
time_out=60,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.debug("Got response to %s", method)
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
if not retry_on_dns_fail and isinstance(e, DNSLookupError):
|
|
||||||
logger.warn(
|
|
||||||
"DNS Lookup failed to %s with %s",
|
|
||||||
destination,
|
|
||||||
e
|
|
||||||
)
|
|
||||||
raise
|
|
||||||
|
|
||||||
logger.warn(
|
|
||||||
"Sending request failed to %s: %s %s: %s - %s",
|
|
||||||
destination,
|
|
||||||
method,
|
|
||||||
url_bytes,
|
|
||||||
type(e).__name__,
|
|
||||||
_flatten_response_never_received(e),
|
|
||||||
)
|
|
||||||
|
|
||||||
if retries_left:
|
|
||||||
yield sleep(2 ** (5 - retries_left))
|
|
||||||
retries_left -= 1
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
"Received response %d %s for %s: %s %s",
|
|
||||||
response.code,
|
|
||||||
response.phrase,
|
|
||||||
destination,
|
|
||||||
method,
|
|
||||||
url_bytes
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
log_result = None
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
producer = None
|
||||||
|
if body_callback:
|
||||||
|
producer = body_callback(method, http_url_bytes, headers_dict)
|
||||||
|
|
||||||
|
try:
|
||||||
|
def send_request():
|
||||||
|
request_deferred = preserve_context_over_fn(
|
||||||
|
self.agent.request,
|
||||||
|
method,
|
||||||
|
url_bytes,
|
||||||
|
Headers(headers_dict),
|
||||||
|
producer
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.clock.time_bound_deferred(
|
||||||
|
request_deferred,
|
||||||
|
time_out=timeout/1000. if timeout else 60,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = yield preserve_context_over_fn(
|
||||||
|
send_request,
|
||||||
|
)
|
||||||
|
|
||||||
|
log_result = "%d %s" % (response.code, response.phrase,)
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
if not retry_on_dns_fail and isinstance(e, DNSLookupError):
|
||||||
|
logger.warn(
|
||||||
|
"DNS Lookup failed to %s with %s",
|
||||||
|
destination,
|
||||||
|
e
|
||||||
|
)
|
||||||
|
log_result = "DNS Lookup failed to %s with %s" % (
|
||||||
|
destination, e
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
logger.warn(
|
||||||
|
"{%s} Sending request failed to %s: %s %s: %s - %s",
|
||||||
|
txn_id,
|
||||||
|
destination,
|
||||||
|
method,
|
||||||
|
url_bytes,
|
||||||
|
type(e).__name__,
|
||||||
|
_flatten_response_never_received(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
log_result = "%s - %s" % (
|
||||||
|
type(e).__name__, _flatten_response_never_received(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
if retries_left and not timeout:
|
||||||
|
yield sleep(2 ** (5 - retries_left))
|
||||||
|
retries_left -= 1
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
outbound_logger.info(
|
||||||
|
"{%s} [%s] Result: %s",
|
||||||
|
txn_id,
|
||||||
|
destination,
|
||||||
|
log_result,
|
||||||
|
)
|
||||||
|
|
||||||
if 200 <= response.code < 300:
|
if 200 <= response.code < 300:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# :'(
|
# :'(
|
||||||
# Update transactions table?
|
# Update transactions table?
|
||||||
body = yield readBody(response)
|
body = yield preserve_context_over_fn(readBody, response)
|
||||||
raise HttpResponseException(
|
raise HttpResponseException(
|
||||||
response.code, response.phrase, body
|
response.code, response.phrase, body
|
||||||
)
|
)
|
||||||
@@ -281,10 +277,7 @@ class MatrixFederationHttpClient(object):
|
|||||||
"Content-Type not application/json"
|
"Content-Type not application/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug("Getting resp body")
|
body = yield preserve_context_over_fn(readBody, response)
|
||||||
body = yield readBody(response)
|
|
||||||
logger.debug("Got resp body")
|
|
||||||
|
|
||||||
defer.returnValue(json.loads(body))
|
defer.returnValue(json.loads(body))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@@ -327,14 +320,13 @@ class MatrixFederationHttpClient(object):
|
|||||||
"Content-Type not application/json"
|
"Content-Type not application/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug("Getting resp body")
|
body = yield preserve_context_over_fn(readBody, response)
|
||||||
body = yield readBody(response)
|
|
||||||
logger.debug("Got resp body")
|
|
||||||
|
|
||||||
defer.returnValue(json.loads(body))
|
defer.returnValue(json.loads(body))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_json(self, destination, path, args={}, retry_on_dns_fail=True):
|
def get_json(self, destination, path, args={}, retry_on_dns_fail=True,
|
||||||
|
timeout=None):
|
||||||
""" GETs some json from the given host homeserver and path
|
""" GETs some json from the given host homeserver and path
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -343,6 +335,9 @@ class MatrixFederationHttpClient(object):
|
|||||||
path (str): The HTTP path.
|
path (str): The HTTP path.
|
||||||
args (dict): A dictionary used to create query strings, defaults to
|
args (dict): A dictionary used to create query strings, defaults to
|
||||||
None.
|
None.
|
||||||
|
timeout (int): How long to try (in ms) the destination for before
|
||||||
|
giving up. None indicates no timeout and that the request will
|
||||||
|
be retried.
|
||||||
Returns:
|
Returns:
|
||||||
Deferred: Succeeds when we get *any* HTTP response.
|
Deferred: Succeeds when we get *any* HTTP response.
|
||||||
|
|
||||||
@@ -370,7 +365,8 @@ class MatrixFederationHttpClient(object):
|
|||||||
path.encode("ascii"),
|
path.encode("ascii"),
|
||||||
query_bytes=query_bytes,
|
query_bytes=query_bytes,
|
||||||
body_callback=body_callback,
|
body_callback=body_callback,
|
||||||
retry_on_dns_fail=retry_on_dns_fail
|
retry_on_dns_fail=retry_on_dns_fail,
|
||||||
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
if 200 <= response.code < 300:
|
if 200 <= response.code < 300:
|
||||||
@@ -382,9 +378,7 @@ class MatrixFederationHttpClient(object):
|
|||||||
"Content-Type not application/json"
|
"Content-Type not application/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug("Getting resp body")
|
body = yield preserve_context_over_fn(readBody, response)
|
||||||
body = yield readBody(response)
|
|
||||||
logger.debug("Got resp body")
|
|
||||||
|
|
||||||
defer.returnValue(json.loads(body))
|
defer.returnValue(json.loads(body))
|
||||||
|
|
||||||
@@ -427,19 +421,16 @@ class MatrixFederationHttpClient(object):
|
|||||||
headers = dict(response.headers.getAllRawHeaders())
|
headers = dict(response.headers.getAllRawHeaders())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
length = yield _readBodyToFile(response, output_stream, max_size)
|
length = yield preserve_context_over_fn(
|
||||||
|
_readBodyToFile,
|
||||||
|
response, output_stream, max_size
|
||||||
|
)
|
||||||
except:
|
except:
|
||||||
logger.exception("Failed to download body")
|
logger.exception("Failed to download body")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
defer.returnValue((length, headers))
|
defer.returnValue((length, headers))
|
||||||
|
|
||||||
def _getEndpoint(self, reactor, destination):
|
|
||||||
return matrix_federation_endpoint(
|
|
||||||
reactor, destination, timeout=10,
|
|
||||||
ssl_context_factory=self.hs.tls_context_factory
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class _ReadBodyToFileProtocol(protocol.Protocol):
|
class _ReadBodyToFileProtocol(protocol.Protocol):
|
||||||
def __init__(self, stream, deferred, max_size):
|
def __init__(self, stream, deferred, max_size):
|
||||||
|
|||||||
@@ -17,11 +17,12 @@
|
|||||||
from synapse.api.errors import (
|
from synapse.api.errors import (
|
||||||
cs_exception, SynapseError, CodeMessageException, UnrecognizedRequestError
|
cs_exception, SynapseError, CodeMessageException, UnrecognizedRequestError
|
||||||
)
|
)
|
||||||
from synapse.util.logcontext import LoggingContext
|
from synapse.util.logcontext import LoggingContext, PreserveLoggingContext
|
||||||
import synapse.metrics
|
import synapse.metrics
|
||||||
|
import synapse.events
|
||||||
|
|
||||||
from syutil.jsonutil import (
|
from syutil.jsonutil import (
|
||||||
encode_canonical_json, encode_pretty_printed_json
|
encode_canonical_json, encode_pretty_printed_json, encode_json
|
||||||
)
|
)
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
@@ -78,51 +79,39 @@ def request_handler(request_handler):
|
|||||||
_next_request_id += 1
|
_next_request_id += 1
|
||||||
with LoggingContext(request_id) as request_context:
|
with LoggingContext(request_id) as request_context:
|
||||||
request_context.request = request_id
|
request_context.request = request_id
|
||||||
code = None
|
with request.processing():
|
||||||
start = self.clock.time_msec()
|
try:
|
||||||
try:
|
d = request_handler(self, request)
|
||||||
logger.info(
|
with PreserveLoggingContext():
|
||||||
"Received request: %s %s",
|
yield d
|
||||||
request.method, request.path
|
except CodeMessageException as e:
|
||||||
)
|
code = e.code
|
||||||
yield request_handler(self, request)
|
if isinstance(e, SynapseError):
|
||||||
code = request.code
|
logger.info(
|
||||||
except CodeMessageException as e:
|
"%s SynapseError: %s - %s", request, code, e.msg
|
||||||
code = e.code
|
)
|
||||||
if isinstance(e, SynapseError):
|
else:
|
||||||
logger.info(
|
logger.exception(e)
|
||||||
"%s SynapseError: %s - %s", request, code, e.msg
|
outgoing_responses_counter.inc(request.method, str(code))
|
||||||
|
respond_with_json(
|
||||||
|
request, code, cs_exception(e), send_cors=True,
|
||||||
|
pretty_print=_request_user_agent_is_curl(request),
|
||||||
|
version_string=self.version_string,
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
logger.exception(
|
||||||
|
"Failed handle request %s.%s on %r: %r",
|
||||||
|
request_handler.__module__,
|
||||||
|
request_handler.__name__,
|
||||||
|
self,
|
||||||
|
request
|
||||||
|
)
|
||||||
|
respond_with_json(
|
||||||
|
request,
|
||||||
|
500,
|
||||||
|
{"error": "Internal server error"},
|
||||||
|
send_cors=True
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
logger.exception(e)
|
|
||||||
outgoing_responses_counter.inc(request.method, str(code))
|
|
||||||
respond_with_json(
|
|
||||||
request, code, cs_exception(e), send_cors=True,
|
|
||||||
pretty_print=_request_user_agent_is_curl(request),
|
|
||||||
version_string=self.version_string,
|
|
||||||
)
|
|
||||||
except:
|
|
||||||
code = 500
|
|
||||||
logger.exception(
|
|
||||||
"Failed handle request %s.%s on %r: %r",
|
|
||||||
request_handler.__module__,
|
|
||||||
request_handler.__name__,
|
|
||||||
self,
|
|
||||||
request
|
|
||||||
)
|
|
||||||
respond_with_json(
|
|
||||||
request,
|
|
||||||
500,
|
|
||||||
{"error": "Internal server error"},
|
|
||||||
send_cors=True
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
code = str(code) if code else "-"
|
|
||||||
end = self.clock.time_msec()
|
|
||||||
logger.info(
|
|
||||||
"Processed request: %dms %s %s %s",
|
|
||||||
end-start, code, request.method, request.path
|
|
||||||
)
|
|
||||||
return wrapped_request_handler
|
return wrapped_request_handler
|
||||||
|
|
||||||
|
|
||||||
@@ -166,9 +155,10 @@ class JsonResource(HttpServer, resource.Resource):
|
|||||||
|
|
||||||
_PathEntry = collections.namedtuple("_PathEntry", ["pattern", "callback"])
|
_PathEntry = collections.namedtuple("_PathEntry", ["pattern", "callback"])
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs, canonical_json=True):
|
||||||
resource.Resource.__init__(self)
|
resource.Resource.__init__(self)
|
||||||
|
|
||||||
|
self.canonical_json = canonical_json
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
self.path_regexs = {}
|
self.path_regexs = {}
|
||||||
self.version_string = hs.version_string
|
self.version_string = hs.version_string
|
||||||
@@ -217,7 +207,7 @@ class JsonResource(HttpServer, resource.Resource):
|
|||||||
incoming_requests_counter.inc(request.method, servlet_classname)
|
incoming_requests_counter.inc(request.method, servlet_classname)
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
urllib.unquote(u).decode("UTF-8") for u in m.groups()
|
urllib.unquote(u).decode("UTF-8") if u else u for u in m.groups()
|
||||||
]
|
]
|
||||||
|
|
||||||
callback_return = yield callback(request, *args)
|
callback_return = yield callback(request, *args)
|
||||||
@@ -254,6 +244,7 @@ class JsonResource(HttpServer, resource.Resource):
|
|||||||
response_code_message=response_code_message,
|
response_code_message=response_code_message,
|
||||||
pretty_print=_request_user_agent_is_curl(request),
|
pretty_print=_request_user_agent_is_curl(request),
|
||||||
version_string=self.version_string,
|
version_string=self.version_string,
|
||||||
|
canonical_json=self.canonical_json,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -275,11 +266,16 @@ class RootRedirect(resource.Resource):
|
|||||||
|
|
||||||
def respond_with_json(request, code, json_object, send_cors=False,
|
def respond_with_json(request, code, json_object, send_cors=False,
|
||||||
response_code_message=None, pretty_print=False,
|
response_code_message=None, pretty_print=False,
|
||||||
version_string=""):
|
version_string="", canonical_json=True):
|
||||||
if pretty_print:
|
if pretty_print:
|
||||||
json_bytes = encode_pretty_printed_json(json_object) + "\n"
|
json_bytes = encode_pretty_printed_json(json_object) + "\n"
|
||||||
else:
|
else:
|
||||||
json_bytes = encode_canonical_json(json_object)
|
if canonical_json:
|
||||||
|
json_bytes = encode_canonical_json(json_object)
|
||||||
|
else:
|
||||||
|
json_bytes = encode_json(
|
||||||
|
json_object, using_frozen_dicts=synapse.events.USE_FROZEN_DICTS
|
||||||
|
)
|
||||||
|
|
||||||
return respond_with_json_bytes(
|
return respond_with_json_bytes(
|
||||||
request, code, json_bytes,
|
request, code, json_bytes,
|
||||||
|
|||||||
@@ -18,8 +18,12 @@ from __future__ import absolute_import
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from resource import getrusage, getpagesize, RUSAGE_SELF
|
from resource import getrusage, getpagesize, RUSAGE_SELF
|
||||||
|
import functools
|
||||||
import os
|
import os
|
||||||
import stat
|
import stat
|
||||||
|
import time
|
||||||
|
|
||||||
|
from twisted.internet import reactor
|
||||||
|
|
||||||
from .metric import (
|
from .metric import (
|
||||||
CounterMetric, CallbackMetric, DistributionMetric, CacheMetric
|
CounterMetric, CallbackMetric, DistributionMetric, CacheMetric
|
||||||
@@ -144,3 +148,50 @@ def _process_fds():
|
|||||||
return counts
|
return counts
|
||||||
|
|
||||||
get_metrics_for("process").register_callback("fds", _process_fds, labels=["type"])
|
get_metrics_for("process").register_callback("fds", _process_fds, labels=["type"])
|
||||||
|
|
||||||
|
reactor_metrics = get_metrics_for("reactor")
|
||||||
|
tick_time = reactor_metrics.register_distribution("tick_time")
|
||||||
|
pending_calls_metric = reactor_metrics.register_distribution("pending_calls")
|
||||||
|
|
||||||
|
|
||||||
|
def runUntilCurrentTimer(func):
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
def f(*args, **kwargs):
|
||||||
|
now = reactor.seconds()
|
||||||
|
num_pending = 0
|
||||||
|
|
||||||
|
# _newTimedCalls is one long list of *all* pending calls. Below loop
|
||||||
|
# is based off of impl of reactor.runUntilCurrent
|
||||||
|
for delayed_call in reactor._newTimedCalls:
|
||||||
|
if delayed_call.time > now:
|
||||||
|
break
|
||||||
|
|
||||||
|
if delayed_call.delayed_time > 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
num_pending += 1
|
||||||
|
|
||||||
|
num_pending += len(reactor.threadCallQueue)
|
||||||
|
|
||||||
|
start = time.time() * 1000
|
||||||
|
ret = func(*args, **kwargs)
|
||||||
|
end = time.time() * 1000
|
||||||
|
tick_time.inc_by(end - start)
|
||||||
|
pending_calls_metric.inc_by(num_pending)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Ensure the reactor has all the attributes we expect
|
||||||
|
reactor.runUntilCurrent
|
||||||
|
reactor._newTimedCalls
|
||||||
|
reactor.threadCallQueue
|
||||||
|
|
||||||
|
# runUntilCurrent is called when we have pending calls. It is called once
|
||||||
|
# per iteratation after fd polling.
|
||||||
|
reactor.runUntilCurrent = runUntilCurrentTimer(reactor.runUntilCurrent)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -16,8 +16,7 @@
|
|||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
from synapse.util.logcontext import PreserveLoggingContext
|
from synapse.util.async import run_on_reactor, ObservableDeferred
|
||||||
from synapse.util.async import run_on_reactor
|
|
||||||
from synapse.types import StreamToken
|
from synapse.types import StreamToken
|
||||||
import synapse.metrics
|
import synapse.metrics
|
||||||
|
|
||||||
@@ -43,62 +42,78 @@ def count(func, l):
|
|||||||
|
|
||||||
class _NotificationListener(object):
|
class _NotificationListener(object):
|
||||||
""" This represents a single client connection to the events stream.
|
""" This represents a single client connection to the events stream.
|
||||||
|
|
||||||
The events stream handler will have yielded to the deferred, so to
|
The events stream handler will have yielded to the deferred, so to
|
||||||
notify the handler it is sufficient to resolve the deferred.
|
notify the handler it is sufficient to resolve the deferred.
|
||||||
|
"""
|
||||||
|
__slots__ = ["deferred"]
|
||||||
|
|
||||||
|
def __init__(self, deferred):
|
||||||
|
self.deferred = deferred
|
||||||
|
|
||||||
|
|
||||||
|
class _NotifierUserStream(object):
|
||||||
|
"""This represents a user connected to the event stream.
|
||||||
|
It tracks the most recent stream token for that user.
|
||||||
|
At a given point a user may have a number of streams listening for
|
||||||
|
events.
|
||||||
|
|
||||||
This listener will also keep track of which rooms it is listening in
|
This listener will also keep track of which rooms it is listening in
|
||||||
so that it can remove itself from the indexes in the Notifier class.
|
so that it can remove itself from the indexes in the Notifier class.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, user, rooms, from_token, limit, timeout, deferred,
|
def __init__(self, user, rooms, current_token, time_now_ms,
|
||||||
appservice=None):
|
appservice=None):
|
||||||
self.user = user
|
self.user = str(user)
|
||||||
self.appservice = appservice
|
self.appservice = appservice
|
||||||
self.from_token = from_token
|
self.rooms = set(rooms)
|
||||||
self.limit = limit
|
self.current_token = current_token
|
||||||
self.timeout = timeout
|
self.last_notified_ms = time_now_ms
|
||||||
self.deferred = deferred
|
|
||||||
self.rooms = rooms
|
|
||||||
self.timer = None
|
|
||||||
|
|
||||||
def notified(self):
|
self.notify_deferred = ObservableDeferred(defer.Deferred())
|
||||||
return self.deferred.called
|
|
||||||
|
|
||||||
def notify(self, notifier, events, start_token, end_token):
|
def notify(self, stream_key, stream_id, time_now_ms):
|
||||||
""" Inform whoever is listening about the new events. This will
|
"""Notify any listeners for this user of a new event from an
|
||||||
also remove this listener from all the indexes in the Notifier
|
event source.
|
||||||
|
Args:
|
||||||
|
stream_key(str): The stream the event came from.
|
||||||
|
stream_id(str): The new id for the stream the event came from.
|
||||||
|
time_now_ms(int): The current time in milliseconds.
|
||||||
|
"""
|
||||||
|
self.current_token = self.current_token.copy_and_advance(
|
||||||
|
stream_key, stream_id
|
||||||
|
)
|
||||||
|
self.last_notified_ms = time_now_ms
|
||||||
|
noify_deferred = self.notify_deferred
|
||||||
|
self.notify_deferred = ObservableDeferred(defer.Deferred())
|
||||||
|
noify_deferred.callback(self.current_token)
|
||||||
|
|
||||||
|
def remove(self, notifier):
|
||||||
|
""" Remove this listener from all the indexes in the Notifier
|
||||||
it knows about.
|
it knows about.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
result = (events, (start_token, end_token))
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.deferred.callback(result)
|
|
||||||
notified_events_counter.inc_by(len(events))
|
|
||||||
except defer.AlreadyCalledError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Should the following be done be using intrusively linked lists?
|
|
||||||
# -- erikj
|
|
||||||
|
|
||||||
for room in self.rooms:
|
for room in self.rooms:
|
||||||
lst = notifier.room_to_listeners.get(room, set())
|
lst = notifier.room_to_user_streams.get(room, set())
|
||||||
lst.discard(self)
|
lst.discard(self)
|
||||||
|
|
||||||
notifier.user_to_listeners.get(self.user, set()).discard(self)
|
notifier.user_to_user_stream.pop(self.user)
|
||||||
|
|
||||||
if self.appservice:
|
if self.appservice:
|
||||||
notifier.appservice_to_listeners.get(
|
notifier.appservice_to_user_streams.get(
|
||||||
self.appservice, set()
|
self.appservice, set()
|
||||||
).discard(self)
|
).discard(self)
|
||||||
|
|
||||||
# Cancel the timeout for this notifer if one exists.
|
def count_listeners(self):
|
||||||
if self.timer is not None:
|
return len(self.notify_deferred.observers())
|
||||||
try:
|
|
||||||
notifier.clock.cancel_call_later(self.timer)
|
def new_listener(self, token):
|
||||||
except:
|
"""Returns a deferred that is resolved when there is a new token
|
||||||
logger.warn("Failed to cancel notifier timer")
|
greater than the given token.
|
||||||
|
"""
|
||||||
|
if self.current_token.is_after(token):
|
||||||
|
return _NotificationListener(defer.succeed(self.current_token))
|
||||||
|
else:
|
||||||
|
return _NotificationListener(self.notify_deferred.observe())
|
||||||
|
|
||||||
|
|
||||||
class Notifier(object):
|
class Notifier(object):
|
||||||
@@ -108,14 +123,18 @@ class Notifier(object):
|
|||||||
Primarily used from the /events stream.
|
Primarily used from the /events stream.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
UNUSED_STREAM_EXPIRY_MS = 10 * 60 * 1000
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
self.hs = hs
|
self.hs = hs
|
||||||
|
|
||||||
self.room_to_listeners = {}
|
self.user_to_user_stream = {}
|
||||||
self.user_to_listeners = {}
|
self.room_to_user_streams = {}
|
||||||
self.appservice_to_listeners = {}
|
self.appservice_to_user_streams = {}
|
||||||
|
|
||||||
self.event_sources = hs.get_event_sources()
|
self.event_sources = hs.get_event_sources()
|
||||||
|
self.store = hs.get_datastore()
|
||||||
|
self.pending_new_room_events = []
|
||||||
|
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
|
|
||||||
@@ -123,370 +142,272 @@ class Notifier(object):
|
|||||||
"user_joined_room", self._user_joined_room
|
"user_joined_room", self._user_joined_room
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.clock.looping_call(
|
||||||
|
self.remove_expired_streams, self.UNUSED_STREAM_EXPIRY_MS
|
||||||
|
)
|
||||||
|
|
||||||
# This is not a very cheap test to perform, but it's only executed
|
# This is not a very cheap test to perform, but it's only executed
|
||||||
# when rendering the metrics page, which is likely once per minute at
|
# when rendering the metrics page, which is likely once per minute at
|
||||||
# most when scraping it.
|
# most when scraping it.
|
||||||
def count_listeners():
|
def count_listeners():
|
||||||
all_listeners = set()
|
all_user_streams = set()
|
||||||
|
|
||||||
for x in self.room_to_listeners.values():
|
for x in self.room_to_user_streams.values():
|
||||||
all_listeners |= x
|
all_user_streams |= x
|
||||||
for x in self.user_to_listeners.values():
|
for x in self.user_to_user_stream.values():
|
||||||
all_listeners |= x
|
all_user_streams.add(x)
|
||||||
for x in self.appservice_to_listeners.values():
|
for x in self.appservice_to_user_streams.values():
|
||||||
all_listeners |= x
|
all_user_streams |= x
|
||||||
|
|
||||||
return len(all_listeners)
|
return sum(stream.count_listeners() for stream in all_user_streams)
|
||||||
metrics.register_callback("listeners", count_listeners)
|
metrics.register_callback("listeners", count_listeners)
|
||||||
|
|
||||||
metrics.register_callback(
|
metrics.register_callback(
|
||||||
"rooms",
|
"rooms",
|
||||||
lambda: count(bool, self.room_to_listeners.values()),
|
lambda: count(bool, self.room_to_user_streams.values()),
|
||||||
)
|
)
|
||||||
metrics.register_callback(
|
metrics.register_callback(
|
||||||
"users",
|
"users",
|
||||||
lambda: count(bool, self.user_to_listeners.values()),
|
lambda: len(self.user_to_user_stream),
|
||||||
)
|
)
|
||||||
metrics.register_callback(
|
metrics.register_callback(
|
||||||
"appservices",
|
"appservices",
|
||||||
lambda: count(bool, self.appservice_to_listeners.values()),
|
lambda: count(bool, self.appservice_to_user_streams.values()),
|
||||||
)
|
)
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_new_room_event(self, event, extra_users=[]):
|
def on_new_room_event(self, event, room_stream_id, max_room_stream_id,
|
||||||
|
extra_users=[]):
|
||||||
""" Used by handlers to inform the notifier something has happened
|
""" Used by handlers to inform the notifier something has happened
|
||||||
in the room, room event wise.
|
in the room, room event wise.
|
||||||
|
|
||||||
This triggers the notifier to wake up any listeners that are
|
This triggers the notifier to wake up any listeners that are
|
||||||
listening to the room, and any listeners for the users in the
|
listening to the room, and any listeners for the users in the
|
||||||
`extra_users` param.
|
`extra_users` param.
|
||||||
|
|
||||||
|
The events can be peristed out of order. The notifier will wait
|
||||||
|
until all previous events have been persisted before notifying
|
||||||
|
the client streams.
|
||||||
"""
|
"""
|
||||||
yield run_on_reactor()
|
yield run_on_reactor()
|
||||||
|
|
||||||
|
self.pending_new_room_events.append((
|
||||||
|
room_stream_id, event, extra_users
|
||||||
|
))
|
||||||
|
self._notify_pending_new_room_events(max_room_stream_id)
|
||||||
|
|
||||||
|
def _notify_pending_new_room_events(self, max_room_stream_id):
|
||||||
|
"""Notify for the room events that were queued waiting for a previous
|
||||||
|
event to be persisted.
|
||||||
|
Args:
|
||||||
|
max_room_stream_id(int): The highest stream_id below which all
|
||||||
|
events have been persisted.
|
||||||
|
"""
|
||||||
|
pending = self.pending_new_room_events
|
||||||
|
self.pending_new_room_events = []
|
||||||
|
for room_stream_id, event, extra_users in pending:
|
||||||
|
if room_stream_id > max_room_stream_id:
|
||||||
|
self.pending_new_room_events.append((
|
||||||
|
room_stream_id, event, extra_users
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
self._on_new_room_event(event, room_stream_id, extra_users)
|
||||||
|
|
||||||
|
def _on_new_room_event(self, event, room_stream_id, extra_users=[]):
|
||||||
|
"""Notify any user streams that are interested in this room event"""
|
||||||
# poke any interested application service.
|
# poke any interested application service.
|
||||||
self.hs.get_handlers().appservice_handler.notify_interested_services(
|
self.hs.get_handlers().appservice_handler.notify_interested_services(
|
||||||
event
|
event
|
||||||
)
|
)
|
||||||
|
|
||||||
room_id = event.room_id
|
app_streams = set()
|
||||||
|
|
||||||
room_source = self.event_sources.sources["room"]
|
for appservice in self.appservice_to_user_streams:
|
||||||
|
|
||||||
room_listeners = self.room_to_listeners.get(room_id, set())
|
|
||||||
|
|
||||||
_discard_if_notified(room_listeners)
|
|
||||||
|
|
||||||
listeners = room_listeners.copy()
|
|
||||||
|
|
||||||
for user in extra_users:
|
|
||||||
user_listeners = self.user_to_listeners.get(user, set())
|
|
||||||
|
|
||||||
_discard_if_notified(user_listeners)
|
|
||||||
|
|
||||||
listeners |= user_listeners
|
|
||||||
|
|
||||||
for appservice in self.appservice_to_listeners:
|
|
||||||
# TODO (kegan): Redundant appservice listener checks?
|
# TODO (kegan): Redundant appservice listener checks?
|
||||||
# App services will already be in the room_to_listeners set, but
|
# App services will already be in the room_to_user_streams set, but
|
||||||
# that isn't enough. They need to be checked here in order to
|
# that isn't enough. They need to be checked here in order to
|
||||||
# receive *invites* for users they are interested in. Does this
|
# receive *invites* for users they are interested in. Does this
|
||||||
# make the room_to_listeners check somewhat obselete?
|
# make the room_to_user_streams check somewhat obselete?
|
||||||
if appservice.is_interested(event):
|
if appservice.is_interested(event):
|
||||||
app_listeners = self.appservice_to_listeners.get(
|
app_user_streams = self.appservice_to_user_streams.get(
|
||||||
appservice, set()
|
appservice, set()
|
||||||
)
|
)
|
||||||
|
app_streams |= app_user_streams
|
||||||
|
|
||||||
_discard_if_notified(app_listeners)
|
self.on_new_event(
|
||||||
|
"room_key", room_stream_id,
|
||||||
listeners |= app_listeners
|
users=extra_users,
|
||||||
|
rooms=[event.room_id],
|
||||||
logger.debug("on_new_room_event listeners %s", listeners)
|
extra_streams=app_streams,
|
||||||
|
)
|
||||||
# TODO (erikj): Can we make this more efficient by hitting the
|
|
||||||
# db once?
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def notify(listener):
|
|
||||||
events, end_key = yield room_source.get_new_events_for_user(
|
|
||||||
listener.user,
|
|
||||||
listener.from_token.room_key,
|
|
||||||
listener.limit,
|
|
||||||
)
|
|
||||||
|
|
||||||
if events:
|
|
||||||
end_token = listener.from_token.copy_and_replace(
|
|
||||||
"room_key", end_key
|
|
||||||
)
|
|
||||||
|
|
||||||
listener.notify(
|
|
||||||
self, events, listener.from_token, end_token
|
|
||||||
)
|
|
||||||
|
|
||||||
def eb(failure):
|
|
||||||
logger.exception("Failed to notify listener", failure)
|
|
||||||
|
|
||||||
with PreserveLoggingContext():
|
|
||||||
yield defer.DeferredList(
|
|
||||||
[notify(l).addErrback(eb) for l in listeners],
|
|
||||||
consumeErrors=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def on_new_user_event(self, users=[], rooms=[]):
|
def on_new_event(self, stream_key, new_token, users=[], rooms=[],
|
||||||
""" Used to inform listeners that something has happend
|
extra_streams=set()):
|
||||||
presence/user event wise.
|
""" Used to inform listeners that something has happend event wise.
|
||||||
|
|
||||||
Will wake up all listeners for the given users and rooms.
|
Will wake up all listeners for the given users and rooms.
|
||||||
"""
|
"""
|
||||||
yield run_on_reactor()
|
yield run_on_reactor()
|
||||||
|
user_streams = set()
|
||||||
# TODO(paul): This is horrible, having to manually list every event
|
|
||||||
# source here individually
|
|
||||||
presence_source = self.event_sources.sources["presence"]
|
|
||||||
typing_source = self.event_sources.sources["typing"]
|
|
||||||
|
|
||||||
listeners = set()
|
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
user_listeners = self.user_to_listeners.get(user, set())
|
user_stream = self.user_to_user_stream.get(str(user))
|
||||||
|
if user_stream is not None:
|
||||||
_discard_if_notified(user_listeners)
|
user_streams.add(user_stream)
|
||||||
|
|
||||||
listeners |= user_listeners
|
|
||||||
|
|
||||||
for room in rooms:
|
for room in rooms:
|
||||||
room_listeners = self.room_to_listeners.get(room, set())
|
user_streams |= self.room_to_user_streams.get(room, set())
|
||||||
|
|
||||||
_discard_if_notified(room_listeners)
|
time_now_ms = self.clock.time_msec()
|
||||||
|
for user_stream in user_streams:
|
||||||
listeners |= room_listeners
|
try:
|
||||||
|
user_stream.notify(stream_key, new_token, time_now_ms)
|
||||||
@defer.inlineCallbacks
|
except:
|
||||||
def notify(listener):
|
logger.exception("Failed to notify listener")
|
||||||
presence_events, presence_end_key = (
|
|
||||||
yield presence_source.get_new_events_for_user(
|
|
||||||
listener.user,
|
|
||||||
listener.from_token.presence_key,
|
|
||||||
listener.limit,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
typing_events, typing_end_key = (
|
|
||||||
yield typing_source.get_new_events_for_user(
|
|
||||||
listener.user,
|
|
||||||
listener.from_token.typing_key,
|
|
||||||
listener.limit,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if presence_events or typing_events:
|
|
||||||
end_token = listener.from_token.copy_and_replace(
|
|
||||||
"presence_key", presence_end_key
|
|
||||||
).copy_and_replace(
|
|
||||||
"typing_key", typing_end_key
|
|
||||||
)
|
|
||||||
|
|
||||||
listener.notify(
|
|
||||||
self,
|
|
||||||
presence_events + typing_events,
|
|
||||||
listener.from_token,
|
|
||||||
end_token
|
|
||||||
)
|
|
||||||
|
|
||||||
def eb(failure):
|
|
||||||
logger.error(
|
|
||||||
"Failed to notify listener",
|
|
||||||
exc_info=(
|
|
||||||
failure.type,
|
|
||||||
failure.value,
|
|
||||||
failure.getTracebackObject())
|
|
||||||
)
|
|
||||||
|
|
||||||
with PreserveLoggingContext():
|
|
||||||
yield defer.DeferredList(
|
|
||||||
[notify(l).addErrback(eb) for l in listeners],
|
|
||||||
consumeErrors=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def wait_for_events(self, user, rooms, filter, timeout, callback):
|
def wait_for_events(self, user, rooms, timeout, callback,
|
||||||
|
from_token=StreamToken("s0", "0", "0", "0")):
|
||||||
"""Wait until the callback returns a non empty response or the
|
"""Wait until the callback returns a non empty response or the
|
||||||
timeout fires.
|
timeout fires.
|
||||||
"""
|
"""
|
||||||
|
user = str(user)
|
||||||
|
user_stream = self.user_to_user_stream.get(user)
|
||||||
|
if user_stream is None:
|
||||||
|
appservice = yield self.store.get_app_service_by_user_id(user)
|
||||||
|
current_token = yield self.event_sources.get_current_token()
|
||||||
|
rooms = yield self.store.get_rooms_for_user(user)
|
||||||
|
rooms = [room.room_id for room in rooms]
|
||||||
|
user_stream = _NotifierUserStream(
|
||||||
|
user=user,
|
||||||
|
rooms=rooms,
|
||||||
|
appservice=appservice,
|
||||||
|
current_token=current_token,
|
||||||
|
time_now_ms=self.clock.time_msec(),
|
||||||
|
)
|
||||||
|
self._register_with_keys(user_stream)
|
||||||
|
|
||||||
deferred = defer.Deferred()
|
result = None
|
||||||
|
|
||||||
from_token = StreamToken("s0", "0", "0")
|
|
||||||
|
|
||||||
listener = [_NotificationListener(
|
|
||||||
user=user,
|
|
||||||
rooms=rooms,
|
|
||||||
from_token=from_token,
|
|
||||||
limit=1,
|
|
||||||
timeout=timeout,
|
|
||||||
deferred=deferred,
|
|
||||||
)]
|
|
||||||
|
|
||||||
if timeout:
|
if timeout:
|
||||||
self._register_with_keys(listener[0])
|
# Will be set to a _NotificationListener that we'll be waiting on.
|
||||||
|
# Allows us to cancel it.
|
||||||
|
listener = None
|
||||||
|
|
||||||
result = yield callback()
|
def timed_out():
|
||||||
timer = [None]
|
if listener:
|
||||||
|
listener.deferred.cancel()
|
||||||
|
timer = self.clock.call_later(timeout/1000., timed_out)
|
||||||
|
|
||||||
if timeout:
|
prev_token = from_token
|
||||||
timed_out = [False]
|
while not result:
|
||||||
|
try:
|
||||||
|
current_token = user_stream.current_token
|
||||||
|
|
||||||
def _timeout_listener():
|
result = yield callback(prev_token, current_token)
|
||||||
timed_out[0] = True
|
if result:
|
||||||
timer[0] = None
|
break
|
||||||
listener[0].notify(self, [], from_token, from_token)
|
|
||||||
|
|
||||||
# We create multiple notification listeners so we have to manage
|
# Now we wait for the _NotifierUserStream to be told there
|
||||||
# canceling the timeout ourselves.
|
# is a new token.
|
||||||
timer[0] = self.clock.call_later(timeout/1000., _timeout_listener)
|
# We need to supply the token we supplied to callback so
|
||||||
|
# that we don't miss any current_token updates.
|
||||||
|
prev_token = current_token
|
||||||
|
listener = user_stream.new_listener(prev_token)
|
||||||
|
yield listener.deferred
|
||||||
|
except defer.CancelledError:
|
||||||
|
break
|
||||||
|
|
||||||
while not result and not timed_out[0]:
|
self.clock.cancel_call_later(timer, ignore_errs=True)
|
||||||
yield deferred
|
else:
|
||||||
deferred = defer.Deferred()
|
current_token = user_stream.current_token
|
||||||
listener[0] = _NotificationListener(
|
result = yield callback(from_token, current_token)
|
||||||
user=user,
|
|
||||||
rooms=rooms,
|
|
||||||
from_token=from_token,
|
|
||||||
limit=1,
|
|
||||||
timeout=timeout,
|
|
||||||
deferred=deferred,
|
|
||||||
)
|
|
||||||
self._register_with_keys(listener[0])
|
|
||||||
result = yield callback()
|
|
||||||
|
|
||||||
if timer[0] is not None:
|
|
||||||
try:
|
|
||||||
self.clock.cancel_call_later(timer[0])
|
|
||||||
except:
|
|
||||||
logger.exception("Failed to cancel notifer timer")
|
|
||||||
|
|
||||||
defer.returnValue(result)
|
defer.returnValue(result)
|
||||||
|
|
||||||
def get_events_for(self, user, rooms, pagination_config, timeout):
|
@defer.inlineCallbacks
|
||||||
|
def get_events_for(self, user, rooms, pagination_config, timeout,
|
||||||
|
only_room_events=False):
|
||||||
""" For the given user and rooms, return any new events for them. If
|
""" For the given user and rooms, return any new events for them. If
|
||||||
there are no new events wait for up to `timeout` milliseconds for any
|
there are no new events wait for up to `timeout` milliseconds for any
|
||||||
new events to happen before returning.
|
new events to happen before returning.
|
||||||
|
|
||||||
|
If `only_room_events` is `True` only room events will be returned.
|
||||||
"""
|
"""
|
||||||
deferred = defer.Deferred()
|
from_token = pagination_config.from_token
|
||||||
|
|
||||||
self._get_events(
|
|
||||||
deferred, user, rooms, pagination_config.from_token,
|
|
||||||
pagination_config.limit, timeout
|
|
||||||
).addErrback(deferred.errback)
|
|
||||||
|
|
||||||
return deferred
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _get_events(self, deferred, user, rooms, from_token, limit, timeout):
|
|
||||||
if not from_token:
|
if not from_token:
|
||||||
from_token = yield self.event_sources.get_current_token()
|
from_token = yield self.event_sources.get_current_token()
|
||||||
|
|
||||||
appservice = yield self.hs.get_datastore().get_app_service_by_user_id(
|
limit = pagination_config.limit
|
||||||
user.to_string()
|
|
||||||
)
|
|
||||||
|
|
||||||
listener = _NotificationListener(
|
@defer.inlineCallbacks
|
||||||
user,
|
def check_for_updates(before_token, after_token):
|
||||||
rooms,
|
if not after_token.is_after(before_token):
|
||||||
from_token,
|
defer.returnValue(None)
|
||||||
limit,
|
|
||||||
timeout,
|
|
||||||
deferred,
|
|
||||||
appservice=appservice
|
|
||||||
)
|
|
||||||
|
|
||||||
def _timeout_listener():
|
events = []
|
||||||
# TODO (erikj): We should probably set to_token to the current
|
end_token = from_token
|
||||||
# max rather than reusing from_token.
|
for name, source in self.event_sources.sources.items():
|
||||||
# Remove the timer from the listener so we don't try to cancel it.
|
keyname = "%s_key" % name
|
||||||
listener.timer = None
|
before_id = getattr(before_token, keyname)
|
||||||
listener.notify(
|
after_id = getattr(after_token, keyname)
|
||||||
self,
|
if before_id == after_id:
|
||||||
[],
|
continue
|
||||||
listener.from_token,
|
if only_room_events and name != "room":
|
||||||
listener.from_token,
|
continue
|
||||||
)
|
new_events, new_key = yield source.get_new_events_for_user(
|
||||||
|
user, getattr(from_token, keyname), limit,
|
||||||
if timeout:
|
|
||||||
self._register_with_keys(listener)
|
|
||||||
|
|
||||||
yield self._check_for_updates(listener)
|
|
||||||
|
|
||||||
if not timeout:
|
|
||||||
_timeout_listener()
|
|
||||||
else:
|
|
||||||
# Only add the timer if the listener hasn't been notified
|
|
||||||
if not listener.notified():
|
|
||||||
listener.timer = self.clock.call_later(
|
|
||||||
timeout/1000.0, _timeout_listener
|
|
||||||
)
|
)
|
||||||
return
|
events.extend(new_events)
|
||||||
|
end_token = end_token.copy_and_replace(keyname, new_key)
|
||||||
|
|
||||||
|
if events:
|
||||||
|
defer.returnValue((events, (from_token, end_token)))
|
||||||
|
else:
|
||||||
|
defer.returnValue(None)
|
||||||
|
|
||||||
|
result = yield self.wait_for_events(
|
||||||
|
user, rooms, timeout, check_for_updates, from_token=from_token
|
||||||
|
)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
result = ([], (from_token, from_token))
|
||||||
|
|
||||||
|
defer.returnValue(result)
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def _register_with_keys(self, listener):
|
def remove_expired_streams(self):
|
||||||
for room in listener.rooms:
|
time_now_ms = self.clock.time_msec()
|
||||||
s = self.room_to_listeners.setdefault(room, set())
|
expired_streams = []
|
||||||
s.add(listener)
|
expire_before_ts = time_now_ms - self.UNUSED_STREAM_EXPIRY_MS
|
||||||
|
for stream in self.user_to_user_stream.values():
|
||||||
|
if stream.count_listeners():
|
||||||
|
continue
|
||||||
|
if stream.last_notified_ms < expire_before_ts:
|
||||||
|
expired_streams.append(stream)
|
||||||
|
|
||||||
self.user_to_listeners.setdefault(listener.user, set()).add(listener)
|
for expired_stream in expired_streams:
|
||||||
|
expired_stream.remove(self)
|
||||||
|
|
||||||
if listener.appservice:
|
|
||||||
self.appservice_to_listeners.setdefault(
|
|
||||||
listener.appservice, set()
|
|
||||||
).add(listener)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
@log_function
|
@log_function
|
||||||
def _check_for_updates(self, listener):
|
def _register_with_keys(self, user_stream):
|
||||||
# TODO (erikj): We need to think about limits across multiple sources
|
self.user_to_user_stream[user_stream.user] = user_stream
|
||||||
events = []
|
|
||||||
|
|
||||||
from_token = listener.from_token
|
for room in user_stream.rooms:
|
||||||
limit = listener.limit
|
s = self.room_to_user_streams.setdefault(room, set())
|
||||||
|
s.add(user_stream)
|
||||||
|
|
||||||
# TODO (erikj): DeferredList?
|
if user_stream.appservice:
|
||||||
for name, source in self.event_sources.sources.items():
|
self.appservice_to_user_stream.setdefault(
|
||||||
keyname = "%s_key" % name
|
user_stream.appservice, set()
|
||||||
|
).add(user_stream)
|
||||||
stuff, new_key = yield source.get_new_events_for_user(
|
|
||||||
listener.user,
|
|
||||||
getattr(from_token, keyname),
|
|
||||||
limit,
|
|
||||||
)
|
|
||||||
|
|
||||||
events.extend(stuff)
|
|
||||||
|
|
||||||
from_token = from_token.copy_and_replace(keyname, new_key)
|
|
||||||
|
|
||||||
end_token = from_token
|
|
||||||
|
|
||||||
if events:
|
|
||||||
listener.notify(self, events, listener.from_token, end_token)
|
|
||||||
|
|
||||||
defer.returnValue(listener)
|
|
||||||
|
|
||||||
def _user_joined_room(self, user, room_id):
|
def _user_joined_room(self, user, room_id):
|
||||||
new_listeners = self.user_to_listeners.get(user, set())
|
user = str(user)
|
||||||
|
new_user_stream = self.user_to_user_stream.get(user)
|
||||||
listeners = self.room_to_listeners.setdefault(room_id, set())
|
if new_user_stream is not None:
|
||||||
listeners |= new_listeners
|
room_streams = self.room_to_user_streams.setdefault(room_id, set())
|
||||||
|
room_streams.add(new_user_stream)
|
||||||
for l in new_listeners:
|
new_user_stream.rooms.add(room_id)
|
||||||
l.rooms.add(room_id)
|
|
||||||
|
|
||||||
|
|
||||||
def _discard_if_notified(listener_set):
|
|
||||||
"""Remove any 'stale' listeners from the given set.
|
|
||||||
"""
|
|
||||||
to_discard = set()
|
|
||||||
for l in listener_set:
|
|
||||||
if l.notified():
|
|
||||||
to_discard.add(l)
|
|
||||||
|
|
||||||
listener_set -= to_discard
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import baserules
|
|||||||
import logging
|
import logging
|
||||||
import simplejson as json
|
import simplejson as json
|
||||||
import re
|
import re
|
||||||
|
import random
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -74,35 +75,33 @@ class Pusher(object):
|
|||||||
|
|
||||||
rawrules = yield self.store.get_push_rules_for_user(self.user_name)
|
rawrules = yield self.store.get_push_rules_for_user(self.user_name)
|
||||||
|
|
||||||
for r in rawrules:
|
rules = []
|
||||||
r['conditions'] = json.loads(r['conditions'])
|
for rawrule in rawrules:
|
||||||
r['actions'] = json.loads(r['actions'])
|
rule = dict(rawrule)
|
||||||
|
rule['conditions'] = json.loads(rawrule['conditions'])
|
||||||
|
rule['actions'] = json.loads(rawrule['actions'])
|
||||||
|
rules.append(rule)
|
||||||
|
|
||||||
enabled_map = yield self.store.get_push_rules_enabled_for_user(self.user_name)
|
enabled_map = yield self.store.get_push_rules_enabled_for_user(self.user_name)
|
||||||
|
|
||||||
user = UserID.from_string(self.user_name)
|
user = UserID.from_string(self.user_name)
|
||||||
|
|
||||||
rules = baserules.list_with_base_rules(rawrules, user)
|
rules = baserules.list_with_base_rules(rules, user)
|
||||||
|
|
||||||
|
room_id = ev['room_id']
|
||||||
|
|
||||||
# get *our* member event for display name matching
|
# get *our* member event for display name matching
|
||||||
member_events_for_room = yield self.store.get_current_state(
|
|
||||||
room_id=ev['room_id'],
|
|
||||||
event_type='m.room.member',
|
|
||||||
state_key=None
|
|
||||||
)
|
|
||||||
my_display_name = None
|
my_display_name = None
|
||||||
room_member_count = 0
|
our_member_event = yield self.store.get_current_state(
|
||||||
for mev in member_events_for_room:
|
room_id=room_id,
|
||||||
if mev.content['membership'] != 'join':
|
event_type='m.room.member',
|
||||||
continue
|
state_key=self.user_name,
|
||||||
|
)
|
||||||
|
if our_member_event:
|
||||||
|
my_display_name = our_member_event[0].content.get("displayname")
|
||||||
|
|
||||||
# This loop does two things:
|
room_members = yield self.store.get_users_in_room(room_id)
|
||||||
# 1) Find our current display name
|
room_member_count = len(room_members)
|
||||||
if mev.state_key == self.user_name and 'displayname' in mev.content:
|
|
||||||
my_display_name = mev.content['displayname']
|
|
||||||
|
|
||||||
# and 2) Get the number of people in that room
|
|
||||||
room_member_count += 1
|
|
||||||
|
|
||||||
for r in rules:
|
for r in rules:
|
||||||
if r['rule_id'] in enabled_map:
|
if r['rule_id'] in enabled_map:
|
||||||
@@ -250,7 +249,9 @@ class Pusher(object):
|
|||||||
# we fail to dispatch the push)
|
# we fail to dispatch the push)
|
||||||
config = PaginationConfig(from_token=None, limit='1')
|
config = PaginationConfig(from_token=None, limit='1')
|
||||||
chunk = yield self.evStreamHandler.get_stream(
|
chunk = yield self.evStreamHandler.get_stream(
|
||||||
self.user_name, config, timeout=0)
|
self.user_name, config, timeout=0, affect_presence=False,
|
||||||
|
only_room_events=True
|
||||||
|
)
|
||||||
self.last_token = chunk['end']
|
self.last_token = chunk['end']
|
||||||
self.store.update_pusher_last_token(
|
self.store.update_pusher_last_token(
|
||||||
self.app_id, self.pushkey, self.user_name, self.last_token
|
self.app_id, self.pushkey, self.user_name, self.last_token
|
||||||
@@ -258,132 +259,160 @@ class Pusher(object):
|
|||||||
logger.info("Pusher %s for user %s starting from token %s",
|
logger.info("Pusher %s for user %s starting from token %s",
|
||||||
self.pushkey, self.user_name, self.last_token)
|
self.pushkey, self.user_name, self.last_token)
|
||||||
|
|
||||||
|
wait = 0
|
||||||
while self.alive:
|
while self.alive:
|
||||||
from_tok = StreamToken.from_string(self.last_token)
|
try:
|
||||||
config = PaginationConfig(from_token=from_tok, limit='1')
|
if wait > 0:
|
||||||
chunk = yield self.evStreamHandler.get_stream(
|
yield synapse.util.async.sleep(wait)
|
||||||
self.user_name, config,
|
yield self.get_and_dispatch()
|
||||||
timeout=100*365*24*60*60*1000, affect_presence=False
|
wait = 0
|
||||||
)
|
except:
|
||||||
|
if wait == 0:
|
||||||
# limiting to 1 may get 1 event plus 1 presence event, so
|
wait = 1
|
||||||
# pick out the actual event
|
else:
|
||||||
single_event = None
|
wait = min(wait * 2, 1800)
|
||||||
for c in chunk['chunk']:
|
logger.exception(
|
||||||
if 'event_id' in c: # Hmmm...
|
"Exception in pusher loop for pushkey %s. Pausing for %ds",
|
||||||
single_event = c
|
self.pushkey, wait
|
||||||
break
|
|
||||||
if not single_event:
|
|
||||||
self.last_token = chunk['end']
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not self.alive:
|
|
||||||
continue
|
|
||||||
|
|
||||||
processed = False
|
|
||||||
actions = yield self._actions_for_event(single_event)
|
|
||||||
tweaks = _tweaks_for_actions(actions)
|
|
||||||
|
|
||||||
if len(actions) == 0:
|
|
||||||
logger.warn("Empty actions! Using default action.")
|
|
||||||
actions = Pusher.DEFAULT_ACTIONS
|
|
||||||
if 'notify' not in actions and 'dont_notify' not in actions:
|
|
||||||
logger.warn("Neither notify nor dont_notify in actions: adding default")
|
|
||||||
actions.extend(Pusher.DEFAULT_ACTIONS)
|
|
||||||
if 'dont_notify' in actions:
|
|
||||||
logger.debug(
|
|
||||||
"%s for %s: dont_notify",
|
|
||||||
single_event['event_id'], self.user_name
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_and_dispatch(self):
|
||||||
|
from_tok = StreamToken.from_string(self.last_token)
|
||||||
|
config = PaginationConfig(from_token=from_tok, limit='1')
|
||||||
|
timeout = (300 + random.randint(-60, 60)) * 1000
|
||||||
|
chunk = yield self.evStreamHandler.get_stream(
|
||||||
|
self.user_name, config, timeout=timeout, affect_presence=False,
|
||||||
|
only_room_events=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# limiting to 1 may get 1 event plus 1 presence event, so
|
||||||
|
# pick out the actual event
|
||||||
|
single_event = None
|
||||||
|
for c in chunk['chunk']:
|
||||||
|
if 'event_id' in c: # Hmmm...
|
||||||
|
single_event = c
|
||||||
|
break
|
||||||
|
if not single_event:
|
||||||
|
self.last_token = chunk['end']
|
||||||
|
logger.debug("Event stream timeout for pushkey %s", self.pushkey)
|
||||||
|
yield self.store.update_pusher_last_token(
|
||||||
|
self.app_id,
|
||||||
|
self.pushkey,
|
||||||
|
self.user_name,
|
||||||
|
self.last_token
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.alive:
|
||||||
|
return
|
||||||
|
|
||||||
|
processed = False
|
||||||
|
actions = yield self._actions_for_event(single_event)
|
||||||
|
tweaks = _tweaks_for_actions(actions)
|
||||||
|
|
||||||
|
if len(actions) == 0:
|
||||||
|
logger.warn("Empty actions! Using default action.")
|
||||||
|
actions = Pusher.DEFAULT_ACTIONS
|
||||||
|
|
||||||
|
if 'notify' not in actions and 'dont_notify' not in actions:
|
||||||
|
logger.warn("Neither notify nor dont_notify in actions: adding default")
|
||||||
|
actions.extend(Pusher.DEFAULT_ACTIONS)
|
||||||
|
|
||||||
|
if 'dont_notify' in actions:
|
||||||
|
logger.debug(
|
||||||
|
"%s for %s: dont_notify",
|
||||||
|
single_event['event_id'], self.user_name
|
||||||
|
)
|
||||||
|
processed = True
|
||||||
|
else:
|
||||||
|
rejected = yield self.dispatch_push(single_event, tweaks)
|
||||||
|
self.has_unread = True
|
||||||
|
if isinstance(rejected, list) or isinstance(rejected, tuple):
|
||||||
processed = True
|
processed = True
|
||||||
else:
|
for pk in rejected:
|
||||||
rejected = yield self.dispatch_push(single_event, tweaks)
|
if pk != self.pushkey:
|
||||||
self.has_unread = True
|
# for sanity, we only remove the pushkey if it
|
||||||
if isinstance(rejected, list) or isinstance(rejected, tuple):
|
# was the one we actually sent...
|
||||||
processed = True
|
logger.warn(
|
||||||
for pk in rejected:
|
("Ignoring rejected pushkey %s because we"
|
||||||
if pk != self.pushkey:
|
" didn't send it"), pk
|
||||||
# for sanity, we only remove the pushkey if it
|
)
|
||||||
# was the one we actually sent...
|
else:
|
||||||
logger.warn(
|
logger.info(
|
||||||
("Ignoring rejected pushkey %s because we"
|
"Pushkey %s was rejected: removing",
|
||||||
" didn't send it"), pk
|
pk
|
||||||
)
|
)
|
||||||
else:
|
yield self.hs.get_pusherpool().remove_pusher(
|
||||||
logger.info(
|
self.app_id, pk, self.user_name
|
||||||
"Pushkey %s was rejected: removing",
|
)
|
||||||
pk
|
|
||||||
)
|
|
||||||
yield self.hs.get_pusherpool().remove_pusher(
|
|
||||||
self.app_id, pk, self.user_name
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self.alive:
|
if not self.alive:
|
||||||
continue
|
return
|
||||||
|
|
||||||
if processed:
|
if processed:
|
||||||
self.backoff_delay = Pusher.INITIAL_BACKOFF
|
self.backoff_delay = Pusher.INITIAL_BACKOFF
|
||||||
self.last_token = chunk['end']
|
self.last_token = chunk['end']
|
||||||
self.store.update_pusher_last_token_and_success(
|
yield self.store.update_pusher_last_token_and_success(
|
||||||
|
self.app_id,
|
||||||
|
self.pushkey,
|
||||||
|
self.user_name,
|
||||||
|
self.last_token,
|
||||||
|
self.clock.time_msec()
|
||||||
|
)
|
||||||
|
if self.failing_since:
|
||||||
|
self.failing_since = None
|
||||||
|
yield self.store.update_pusher_failing_since(
|
||||||
self.app_id,
|
self.app_id,
|
||||||
self.pushkey,
|
self.pushkey,
|
||||||
self.user_name,
|
self.user_name,
|
||||||
self.last_token,
|
self.failing_since)
|
||||||
self.clock.time_msec()
|
else:
|
||||||
|
if not self.failing_since:
|
||||||
|
self.failing_since = self.clock.time_msec()
|
||||||
|
yield self.store.update_pusher_failing_since(
|
||||||
|
self.app_id,
|
||||||
|
self.pushkey,
|
||||||
|
self.user_name,
|
||||||
|
self.failing_since
|
||||||
|
)
|
||||||
|
|
||||||
|
if (self.failing_since and
|
||||||
|
self.failing_since <
|
||||||
|
self.clock.time_msec() - Pusher.GIVE_UP_AFTER):
|
||||||
|
# we really only give up so that if the URL gets
|
||||||
|
# fixed, we don't suddenly deliver a load
|
||||||
|
# of old notifications.
|
||||||
|
logger.warn("Giving up on a notification to user %s, "
|
||||||
|
"pushkey %s",
|
||||||
|
self.user_name, self.pushkey)
|
||||||
|
self.backoff_delay = Pusher.INITIAL_BACKOFF
|
||||||
|
self.last_token = chunk['end']
|
||||||
|
yield self.store.update_pusher_last_token(
|
||||||
|
self.app_id,
|
||||||
|
self.pushkey,
|
||||||
|
self.user_name,
|
||||||
|
self.last_token
|
||||||
|
)
|
||||||
|
|
||||||
|
self.failing_since = None
|
||||||
|
yield self.store.update_pusher_failing_since(
|
||||||
|
self.app_id,
|
||||||
|
self.pushkey,
|
||||||
|
self.user_name,
|
||||||
|
self.failing_since
|
||||||
)
|
)
|
||||||
if self.failing_since:
|
|
||||||
self.failing_since = None
|
|
||||||
self.store.update_pusher_failing_since(
|
|
||||||
self.app_id,
|
|
||||||
self.pushkey,
|
|
||||||
self.user_name,
|
|
||||||
self.failing_since)
|
|
||||||
else:
|
else:
|
||||||
if not self.failing_since:
|
logger.warn("Failed to dispatch push for user %s "
|
||||||
self.failing_since = self.clock.time_msec()
|
"(failing for %dms)."
|
||||||
self.store.update_pusher_failing_since(
|
"Trying again in %dms",
|
||||||
self.app_id,
|
self.user_name,
|
||||||
self.pushkey,
|
self.clock.time_msec() - self.failing_since,
|
||||||
self.user_name,
|
self.backoff_delay)
|
||||||
self.failing_since
|
yield synapse.util.async.sleep(self.backoff_delay / 1000.0)
|
||||||
)
|
self.backoff_delay *= 2
|
||||||
|
if self.backoff_delay > Pusher.MAX_BACKOFF:
|
||||||
if (self.failing_since and
|
self.backoff_delay = Pusher.MAX_BACKOFF
|
||||||
self.failing_since <
|
|
||||||
self.clock.time_msec() - Pusher.GIVE_UP_AFTER):
|
|
||||||
# we really only give up so that if the URL gets
|
|
||||||
# fixed, we don't suddenly deliver a load
|
|
||||||
# of old notifications.
|
|
||||||
logger.warn("Giving up on a notification to user %s, "
|
|
||||||
"pushkey %s",
|
|
||||||
self.user_name, self.pushkey)
|
|
||||||
self.backoff_delay = Pusher.INITIAL_BACKOFF
|
|
||||||
self.last_token = chunk['end']
|
|
||||||
self.store.update_pusher_last_token(
|
|
||||||
self.app_id,
|
|
||||||
self.pushkey,
|
|
||||||
self.user_name,
|
|
||||||
self.last_token
|
|
||||||
)
|
|
||||||
|
|
||||||
self.failing_since = None
|
|
||||||
self.store.update_pusher_failing_since(
|
|
||||||
self.app_id,
|
|
||||||
self.pushkey,
|
|
||||||
self.user_name,
|
|
||||||
self.failing_since
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.warn("Failed to dispatch push for user %s "
|
|
||||||
"(failing for %dms)."
|
|
||||||
"Trying again in %dms",
|
|
||||||
self.user_name,
|
|
||||||
self.clock.time_msec() - self.failing_since,
|
|
||||||
self.backoff_delay)
|
|
||||||
yield synapse.util.async.sleep(self.backoff_delay / 1000.0)
|
|
||||||
self.backoff_delay *= 2
|
|
||||||
if self.backoff_delay > Pusher.MAX_BACKOFF:
|
|
||||||
self.backoff_delay = Pusher.MAX_BACKOFF
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.alive = False
|
self.alive = False
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ def make_base_append_underride_rules(user):
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'rule_id': 'global/override/.m.rule.contains_display_name',
|
'rule_id': 'global/underride/.m.rule.contains_display_name',
|
||||||
'conditions': [
|
'conditions': [
|
||||||
{
|
{
|
||||||
'kind': 'contains_display_name'
|
'kind': 'contains_display_name'
|
||||||
|
|||||||
@@ -94,17 +94,14 @@ class PusherPool:
|
|||||||
self.remove_pusher(p['app_id'], p['pushkey'], p['user_name'])
|
self.remove_pusher(p['app_id'], p['pushkey'], p['user_name'])
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def remove_pushers_by_user_access_token(self, user_id, not_access_token_id):
|
def remove_pushers_by_user(self, user_id):
|
||||||
all = yield self.store.get_all_pushers()
|
all = yield self.store.get_all_pushers()
|
||||||
logger.info(
|
logger.info(
|
||||||
"Removing all pushers for user %s except access token %s",
|
"Removing all pushers for user %s",
|
||||||
user_id, not_access_token_id
|
user_id,
|
||||||
)
|
)
|
||||||
for p in all:
|
for p in all:
|
||||||
if (
|
if p['user_name'] == user_id:
|
||||||
p['user_name'] == user_id and
|
|
||||||
p['access_token'] != not_access_token_id
|
|
||||||
):
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Removing pusher for app id %s, pushkey %s, user %s",
|
"Removing pusher for app id %s, pushkey %s, user %s",
|
||||||
p['app_id'], p['pushkey'], p['user_name']
|
p['app_id'], p['pushkey'], p['user_name']
|
||||||
|
|||||||
@@ -18,30 +18,33 @@ from distutils.version import LooseVersion
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = {
|
REQUIREMENTS = {
|
||||||
"syutil>=0.0.6": ["syutil>=0.0.6"],
|
"syutil>=0.0.7": ["syutil>=0.0.7"],
|
||||||
"Twisted==14.0.2": ["twisted==14.0.2"],
|
"Twisted>=15.1.0": ["twisted>=15.1.0"],
|
||||||
"service_identity>=1.0.0": ["service_identity>=1.0.0"],
|
"service_identity>=1.0.0": ["service_identity>=1.0.0"],
|
||||||
"pyopenssl>=0.14": ["OpenSSL>=0.14"],
|
"pyopenssl>=0.14": ["OpenSSL>=0.14"],
|
||||||
"pyyaml": ["yaml"],
|
"pyyaml": ["yaml"],
|
||||||
"pyasn1": ["pyasn1"],
|
"pyasn1": ["pyasn1"],
|
||||||
"pynacl": ["nacl"],
|
"pynacl>=0.0.3": ["nacl>=0.0.3"],
|
||||||
"daemonize": ["daemonize"],
|
"daemonize": ["daemonize"],
|
||||||
"py-bcrypt": ["bcrypt"],
|
"py-bcrypt": ["bcrypt"],
|
||||||
"frozendict>=0.4": ["frozendict"],
|
"frozendict>=0.4": ["frozendict"],
|
||||||
"pillow": ["PIL"],
|
"pillow": ["PIL"],
|
||||||
"pydenticon": ["pydenticon"],
|
"pydenticon": ["pydenticon"],
|
||||||
|
"ujson": ["ujson"],
|
||||||
|
"blist": ["blist"],
|
||||||
|
"pysaml2": ["saml2"],
|
||||||
}
|
}
|
||||||
CONDITIONAL_REQUIREMENTS = {
|
CONDITIONAL_REQUIREMENTS = {
|
||||||
"web_client": {
|
"web_client": {
|
||||||
"matrix_angular_sdk>=0.6.5": ["syweb>=0.6.5"],
|
"matrix_angular_sdk>=0.6.6": ["syweb>=0.6.6"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def requirements(config=None, include_conditional=False):
|
def requirements(config=None, include_conditional=False):
|
||||||
reqs = REQUIREMENTS.copy()
|
reqs = REQUIREMENTS.copy()
|
||||||
for key, req in CONDITIONAL_REQUIREMENTS.items():
|
if include_conditional:
|
||||||
if (config and getattr(config, key)) or include_conditional:
|
for _, req in CONDITIONAL_REQUIREMENTS.items():
|
||||||
reqs.update(req)
|
reqs.update(req)
|
||||||
return reqs
|
return reqs
|
||||||
|
|
||||||
@@ -49,23 +52,18 @@ def requirements(config=None, include_conditional=False):
|
|||||||
def github_link(project, version, egg):
|
def github_link(project, version, egg):
|
||||||
return "https://github.com/%s/tarball/%s/#egg=%s" % (project, version, egg)
|
return "https://github.com/%s/tarball/%s/#egg=%s" % (project, version, egg)
|
||||||
|
|
||||||
DEPENDENCY_LINKS = [
|
DEPENDENCY_LINKS = {
|
||||||
github_link(
|
"syutil": github_link(
|
||||||
project="pyca/pynacl",
|
|
||||||
version="d4d3175589b892f6ea7c22f466e0e223853516fa",
|
|
||||||
egg="pynacl-0.3.0",
|
|
||||||
),
|
|
||||||
github_link(
|
|
||||||
project="matrix-org/syutil",
|
project="matrix-org/syutil",
|
||||||
version="v0.0.6",
|
version="v0.0.7",
|
||||||
egg="syutil-0.0.6",
|
egg="syutil-0.0.7",
|
||||||
),
|
),
|
||||||
github_link(
|
"matrix-angular-sdk": github_link(
|
||||||
project="matrix-org/matrix-angular-sdk",
|
project="matrix-org/matrix-angular-sdk",
|
||||||
version="v0.6.5",
|
version="v0.6.6",
|
||||||
egg="matrix_angular_sdk-0.6.5",
|
egg="matrix_angular_sdk-0.6.6",
|
||||||
),
|
),
|
||||||
]
|
}
|
||||||
|
|
||||||
|
|
||||||
class MissingRequirementError(Exception):
|
class MissingRequirementError(Exception):
|
||||||
@@ -133,7 +131,7 @@ def check_requirements(config=None):
|
|||||||
def list_requirements():
|
def list_requirements():
|
||||||
result = []
|
result = []
|
||||||
linked = []
|
linked = []
|
||||||
for link in DEPENDENCY_LINKS:
|
for link in DEPENDENCY_LINKS.values():
|
||||||
egg = link.split("#egg=")[1]
|
egg = link.split("#egg=")[1]
|
||||||
linked.append(egg.split('-')[0])
|
linked.append(egg.split('-')[0])
|
||||||
result.append(link)
|
result.append(link)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class ClientV1RestResource(JsonResource):
|
|||||||
"""A resource for version 1 of the matrix client API."""
|
"""A resource for version 1 of the matrix client API."""
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
JsonResource.__init__(self, hs)
|
JsonResource.__init__(self, hs, canonical_json=False)
|
||||||
self.register_servlets(self, hs)
|
self.register_servlets(self, hs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -20,14 +20,32 @@ from synapse.types import UserID
|
|||||||
from base import ClientV1RestServlet, client_path_pattern
|
from base import ClientV1RestServlet, client_path_pattern
|
||||||
|
|
||||||
import simplejson as json
|
import simplejson as json
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from saml2 import BINDING_HTTP_POST
|
||||||
|
from saml2 import config
|
||||||
|
from saml2.client import Saml2Client
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class LoginRestServlet(ClientV1RestServlet):
|
class LoginRestServlet(ClientV1RestServlet):
|
||||||
PATTERN = client_path_pattern("/login$")
|
PATTERN = client_path_pattern("/login$")
|
||||||
PASS_TYPE = "m.login.password"
|
PASS_TYPE = "m.login.password"
|
||||||
|
SAML2_TYPE = "m.login.saml2"
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
super(LoginRestServlet, self).__init__(hs)
|
||||||
|
self.idp_redirect_url = hs.config.saml2_idp_redirect_url
|
||||||
|
self.saml2_enabled = hs.config.saml2_enabled
|
||||||
|
|
||||||
def on_GET(self, request):
|
def on_GET(self, request):
|
||||||
return (200, {"flows": [{"type": LoginRestServlet.PASS_TYPE}]})
|
flows = [{"type": LoginRestServlet.PASS_TYPE}]
|
||||||
|
if self.saml2_enabled:
|
||||||
|
flows.append({"type": LoginRestServlet.SAML2_TYPE})
|
||||||
|
return (200, {"flows": flows})
|
||||||
|
|
||||||
def on_OPTIONS(self, request):
|
def on_OPTIONS(self, request):
|
||||||
return (200, {})
|
return (200, {})
|
||||||
@@ -39,6 +57,16 @@ class LoginRestServlet(ClientV1RestServlet):
|
|||||||
if login_submission["type"] == LoginRestServlet.PASS_TYPE:
|
if login_submission["type"] == LoginRestServlet.PASS_TYPE:
|
||||||
result = yield self.do_password_login(login_submission)
|
result = yield self.do_password_login(login_submission)
|
||||||
defer.returnValue(result)
|
defer.returnValue(result)
|
||||||
|
elif self.saml2_enabled and (login_submission["type"] ==
|
||||||
|
LoginRestServlet.SAML2_TYPE):
|
||||||
|
relay_state = ""
|
||||||
|
if "relay_state" in login_submission:
|
||||||
|
relay_state = "&RelayState="+urllib.quote(
|
||||||
|
login_submission["relay_state"])
|
||||||
|
result = {
|
||||||
|
"uri": "%s%s" % (self.idp_redirect_url, relay_state)
|
||||||
|
}
|
||||||
|
defer.returnValue((200, result))
|
||||||
else:
|
else:
|
||||||
raise SynapseError(400, "Bad login type.")
|
raise SynapseError(400, "Bad login type.")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -46,17 +74,24 @@ class LoginRestServlet(ClientV1RestServlet):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def do_password_login(self, login_submission):
|
def do_password_login(self, login_submission):
|
||||||
if not login_submission["user"].startswith('@'):
|
if 'medium' in login_submission and 'address' in login_submission:
|
||||||
login_submission["user"] = UserID.create(
|
user_id = yield self.hs.get_datastore().get_user_id_by_threepid(
|
||||||
login_submission["user"], self.hs.hostname).to_string()
|
login_submission['medium'], login_submission['address']
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
user_id = login_submission['user']
|
||||||
|
|
||||||
handler = self.handlers.login_handler
|
if not user_id.startswith('@'):
|
||||||
token = yield handler.login(
|
user_id = UserID.create(
|
||||||
user=login_submission["user"],
|
user_id, self.hs.hostname
|
||||||
|
).to_string()
|
||||||
|
|
||||||
|
user_id, token = yield self.handlers.auth_handler.login_with_password(
|
||||||
|
user_id=user_id,
|
||||||
password=login_submission["password"])
|
password=login_submission["password"])
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"user_id": login_submission["user"], # may have changed
|
"user_id": user_id, # may have changed
|
||||||
"access_token": token,
|
"access_token": token,
|
||||||
"home_server": self.hs.hostname,
|
"home_server": self.hs.hostname,
|
||||||
}
|
}
|
||||||
@@ -94,6 +129,49 @@ class PasswordResetRestServlet(ClientV1RestServlet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SAML2RestServlet(ClientV1RestServlet):
|
||||||
|
PATTERN = client_path_pattern("/login/saml2")
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
super(SAML2RestServlet, self).__init__(hs)
|
||||||
|
self.sp_config = hs.config.saml2_config_path
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_POST(self, request):
|
||||||
|
saml2_auth = None
|
||||||
|
try:
|
||||||
|
conf = config.SPConfig()
|
||||||
|
conf.load_file(self.sp_config)
|
||||||
|
SP = Saml2Client(conf)
|
||||||
|
saml2_auth = SP.parse_authn_request_response(
|
||||||
|
request.args['SAMLResponse'][0], BINDING_HTTP_POST)
|
||||||
|
except Exception, e: # Not authenticated
|
||||||
|
logger.exception(e)
|
||||||
|
if saml2_auth and saml2_auth.status_ok() and not saml2_auth.not_signed:
|
||||||
|
username = saml2_auth.name_id.text
|
||||||
|
handler = self.handlers.registration_handler
|
||||||
|
(user_id, token) = yield handler.register_saml2(username)
|
||||||
|
# Forward to the RelayState callback along with ava
|
||||||
|
if 'RelayState' in request.args:
|
||||||
|
request.redirect(urllib.unquote(
|
||||||
|
request.args['RelayState'][0]) +
|
||||||
|
'?status=authenticated&access_token=' +
|
||||||
|
token + '&user_id=' + user_id + '&ava=' +
|
||||||
|
urllib.quote(json.dumps(saml2_auth.ava)))
|
||||||
|
request.finish()
|
||||||
|
defer.returnValue(None)
|
||||||
|
defer.returnValue((200, {"status": "authenticated",
|
||||||
|
"user_id": user_id, "token": token,
|
||||||
|
"ava": saml2_auth.ava}))
|
||||||
|
elif 'RelayState' in request.args:
|
||||||
|
request.redirect(urllib.unquote(
|
||||||
|
request.args['RelayState'][0]) +
|
||||||
|
'?status=not_authenticated')
|
||||||
|
request.finish()
|
||||||
|
defer.returnValue(None)
|
||||||
|
defer.returnValue((200, {"status": "not_authenticated"}))
|
||||||
|
|
||||||
|
|
||||||
def _parse_json(request):
|
def _parse_json(request):
|
||||||
try:
|
try:
|
||||||
content = json.loads(request.content.read())
|
content = json.loads(request.content.read())
|
||||||
@@ -106,4 +184,6 @@ def _parse_json(request):
|
|||||||
|
|
||||||
def register_servlets(hs, http_server):
|
def register_servlets(hs, http_server):
|
||||||
LoginRestServlet(hs).register(http_server)
|
LoginRestServlet(hs).register(http_server)
|
||||||
|
if hs.config.saml2_enabled:
|
||||||
|
SAML2RestServlet(hs).register(http_server)
|
||||||
# TODO PasswordResetRestServlet(hs).register(http_server)
|
# TODO PasswordResetRestServlet(hs).register(http_server)
|
||||||
|
|||||||
@@ -118,11 +118,14 @@ class PushRuleRestServlet(ClientV1RestServlet):
|
|||||||
user.to_string()
|
user.to_string()
|
||||||
)
|
)
|
||||||
|
|
||||||
for r in rawrules:
|
ruleslist = []
|
||||||
r["conditions"] = json.loads(r["conditions"])
|
for rawrule in rawrules:
|
||||||
r["actions"] = json.loads(r["actions"])
|
rule = dict(rawrule)
|
||||||
|
rule["conditions"] = json.loads(rawrule["conditions"])
|
||||||
|
rule["actions"] = json.loads(rawrule["actions"])
|
||||||
|
ruleslist.append(rule)
|
||||||
|
|
||||||
ruleslist = baserules.list_with_base_rules(rawrules, user)
|
ruleslist = baserules.list_with_base_rules(ruleslist, user)
|
||||||
|
|
||||||
rules = {'global': {}, 'device': {}}
|
rules = {'global': {}, 'device': {}}
|
||||||
|
|
||||||
|
|||||||
@@ -412,6 +412,8 @@ class RoomMembershipRestServlet(ClientV1RestServlet):
|
|||||||
if "user_id" not in content:
|
if "user_id" not in content:
|
||||||
raise SynapseError(400, "Missing user_id key.")
|
raise SynapseError(400, "Missing user_id key.")
|
||||||
state_key = content["user_id"]
|
state_key = content["user_id"]
|
||||||
|
# make sure it looks like a user ID; it'll throw if it's invalid.
|
||||||
|
UserID.from_string(state_key)
|
||||||
|
|
||||||
if membership_action == "kick":
|
if membership_action == "kick":
|
||||||
membership_action = "leave"
|
membership_action = "leave"
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ class HttpTransactionStore(object):
|
|||||||
A tuple of (HTTP response code, response content) or None.
|
A tuple of (HTTP response code, response content) or None.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logger.debug("get_response Key: %s TxnId: %s", key, txn_id)
|
logger.debug("get_response TxnId: %s", txn_id)
|
||||||
(last_txn_id, response) = self.transactions[key]
|
(last_txn_id, response) = self.transactions[key]
|
||||||
if txn_id == last_txn_id:
|
if txn_id == last_txn_id:
|
||||||
logger.info("get_response: Returning a response for %s", key)
|
logger.info("get_response: Returning a response for %s", txn_id)
|
||||||
return response
|
return response
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
@@ -58,7 +58,7 @@ class HttpTransactionStore(object):
|
|||||||
txn_id (str): The transaction ID for this request.
|
txn_id (str): The transaction ID for this request.
|
||||||
response (tuple): A tuple of (HTTP response code, response content)
|
response (tuple): A tuple of (HTTP response code, response content)
|
||||||
"""
|
"""
|
||||||
logger.debug("store_response Key: %s TxnId: %s", key, txn_id)
|
logger.debug("store_response TxnId: %s", txn_id)
|
||||||
self.transactions[key] = (txn_id, response)
|
self.transactions[key] = (txn_id, response)
|
||||||
|
|
||||||
def store_client_transaction(self, request, txn_id, response):
|
def store_client_transaction(self, request, txn_id, response):
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ from . import (
|
|||||||
filter,
|
filter,
|
||||||
account,
|
account,
|
||||||
register,
|
register,
|
||||||
auth
|
auth,
|
||||||
|
receipts,
|
||||||
|
keys,
|
||||||
)
|
)
|
||||||
|
|
||||||
from synapse.http.server import JsonResource
|
from synapse.http.server import JsonResource
|
||||||
@@ -28,7 +30,7 @@ class ClientV2AlphaRestResource(JsonResource):
|
|||||||
"""A resource for version 2 alpha of the matrix client API."""
|
"""A resource for version 2 alpha of the matrix client API."""
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
JsonResource.__init__(self, hs)
|
JsonResource.__init__(self, hs, canonical_json=False)
|
||||||
self.register_servlets(self, hs)
|
self.register_servlets(self, hs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -38,3 +40,5 @@ class ClientV2AlphaRestResource(JsonResource):
|
|||||||
account.register_servlets(hs, client_resource)
|
account.register_servlets(hs, client_resource)
|
||||||
register.register_servlets(hs, client_resource)
|
register.register_servlets(hs, client_resource)
|
||||||
auth.register_servlets(hs, client_resource)
|
auth.register_servlets(hs, client_resource)
|
||||||
|
receipts.register_servlets(hs, client_resource)
|
||||||
|
keys.register_servlets(hs, client_resource)
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ class PasswordRestServlet(RestServlet):
|
|||||||
self.hs = hs
|
self.hs = hs
|
||||||
self.auth = hs.get_auth()
|
self.auth = hs.get_auth()
|
||||||
self.auth_handler = hs.get_handlers().auth_handler
|
self.auth_handler = hs.get_handlers().auth_handler
|
||||||
self.login_handler = hs.get_handlers().login_handler
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_POST(self, request):
|
def on_POST(self, request):
|
||||||
@@ -47,7 +46,7 @@ class PasswordRestServlet(RestServlet):
|
|||||||
authed, result, params = yield self.auth_handler.check_auth([
|
authed, result, params = yield self.auth_handler.check_auth([
|
||||||
[LoginType.PASSWORD],
|
[LoginType.PASSWORD],
|
||||||
[LoginType.EMAIL_IDENTITY]
|
[LoginType.EMAIL_IDENTITY]
|
||||||
], body)
|
], body, self.hs.get_ip_from_request(request))
|
||||||
|
|
||||||
if not authed:
|
if not authed:
|
||||||
defer.returnValue((401, result))
|
defer.returnValue((401, result))
|
||||||
@@ -65,12 +64,12 @@ class PasswordRestServlet(RestServlet):
|
|||||||
if 'medium' not in threepid or 'address' not in threepid:
|
if 'medium' not in threepid or 'address' not in threepid:
|
||||||
raise SynapseError(500, "Malformed threepid")
|
raise SynapseError(500, "Malformed threepid")
|
||||||
# if using email, we must know about the email they're authing with!
|
# if using email, we must know about the email they're authing with!
|
||||||
threepid_user = yield self.hs.get_datastore().get_user_by_threepid(
|
threepid_user_id = yield self.hs.get_datastore().get_user_id_by_threepid(
|
||||||
threepid['medium'], threepid['address']
|
threepid['medium'], threepid['address']
|
||||||
)
|
)
|
||||||
if not threepid_user:
|
if not threepid_user_id:
|
||||||
raise SynapseError(404, "Email address not found", Codes.NOT_FOUND)
|
raise SynapseError(404, "Email address not found", Codes.NOT_FOUND)
|
||||||
user_id = threepid_user
|
user_id = threepid_user_id
|
||||||
else:
|
else:
|
||||||
logger.error("Auth succeeded but no known type!", result.keys())
|
logger.error("Auth succeeded but no known type!", result.keys())
|
||||||
raise SynapseError(500, "", Codes.UNKNOWN)
|
raise SynapseError(500, "", Codes.UNKNOWN)
|
||||||
@@ -79,8 +78,8 @@ class PasswordRestServlet(RestServlet):
|
|||||||
raise SynapseError(400, "", Codes.MISSING_PARAM)
|
raise SynapseError(400, "", Codes.MISSING_PARAM)
|
||||||
new_password = params['new_password']
|
new_password = params['new_password']
|
||||||
|
|
||||||
yield self.login_handler.set_password(
|
yield self.auth_handler.set_password(
|
||||||
user_id, new_password, None
|
user_id, new_password
|
||||||
)
|
)
|
||||||
|
|
||||||
defer.returnValue((200, {}))
|
defer.returnValue((200, {}))
|
||||||
@@ -95,7 +94,6 @@ class ThreepidRestServlet(RestServlet):
|
|||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
super(ThreepidRestServlet, self).__init__()
|
super(ThreepidRestServlet, self).__init__()
|
||||||
self.hs = hs
|
self.hs = hs
|
||||||
self.login_handler = hs.get_handlers().login_handler
|
|
||||||
self.identity_handler = hs.get_handlers().identity_handler
|
self.identity_handler = hs.get_handlers().identity_handler
|
||||||
self.auth = hs.get_auth()
|
self.auth = hs.get_auth()
|
||||||
|
|
||||||
@@ -135,7 +133,7 @@ class ThreepidRestServlet(RestServlet):
|
|||||||
logger.warn("Couldn't add 3pid: invalid response from ID sevrer")
|
logger.warn("Couldn't add 3pid: invalid response from ID sevrer")
|
||||||
raise SynapseError(500, "Invalid response from ID Server")
|
raise SynapseError(500, "Invalid response from ID Server")
|
||||||
|
|
||||||
yield self.login_handler.add_threepid(
|
yield self.auth_handler.add_threepid(
|
||||||
auth_user.to_string(),
|
auth_user.to_string(),
|
||||||
threepid['medium'],
|
threepid['medium'],
|
||||||
threepid['address'],
|
threepid['address'],
|
||||||
|
|||||||
316
synapse/rest/client/v2_alpha/keys.py
Normal file
316
synapse/rest/client/v2_alpha/keys.py
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 OpenMarket 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.
|
||||||
|
|
||||||
|
from twisted.internet import defer
|
||||||
|
|
||||||
|
from synapse.api.errors import SynapseError
|
||||||
|
from synapse.http.servlet import RestServlet
|
||||||
|
from synapse.types import UserID
|
||||||
|
from syutil.jsonutil import encode_canonical_json
|
||||||
|
|
||||||
|
from ._base import client_v2_pattern
|
||||||
|
|
||||||
|
import simplejson as json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class KeyUploadServlet(RestServlet):
|
||||||
|
"""
|
||||||
|
POST /keys/upload/<device_id> HTTP/1.1
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"device_keys": {
|
||||||
|
"user_id": "<user_id>",
|
||||||
|
"device_id": "<device_id>",
|
||||||
|
"valid_until_ts": <millisecond_timestamp>,
|
||||||
|
"algorithms": [
|
||||||
|
"m.olm.curve25519-aes-sha256",
|
||||||
|
]
|
||||||
|
"keys": {
|
||||||
|
"<algorithm>:<device_id>": "<key_base64>",
|
||||||
|
},
|
||||||
|
"signatures:" {
|
||||||
|
"<user_id>" {
|
||||||
|
"<algorithm>:<device_id>": "<signature_base64>"
|
||||||
|
} } },
|
||||||
|
"one_time_keys": {
|
||||||
|
"<algorithm>:<key_id>": "<key_base64>"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
PATTERN = client_v2_pattern("/keys/upload/(?P<device_id>[^/]*)")
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
super(KeyUploadServlet, self).__init__()
|
||||||
|
self.store = hs.get_datastore()
|
||||||
|
self.clock = hs.get_clock()
|
||||||
|
self.auth = hs.get_auth()
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_POST(self, request, device_id):
|
||||||
|
auth_user, client_info = yield self.auth.get_user_by_req(request)
|
||||||
|
user_id = auth_user.to_string()
|
||||||
|
# TODO: Check that the device_id matches that in the authentication
|
||||||
|
# or derive the device_id from the authentication instead.
|
||||||
|
try:
|
||||||
|
body = json.loads(request.content.read())
|
||||||
|
except:
|
||||||
|
raise SynapseError(400, "Invalid key JSON")
|
||||||
|
time_now = self.clock.time_msec()
|
||||||
|
|
||||||
|
# TODO: Validate the JSON to make sure it has the right keys.
|
||||||
|
device_keys = body.get("device_keys", None)
|
||||||
|
if device_keys:
|
||||||
|
logger.info(
|
||||||
|
"Updating device_keys for device %r for user %r at %d",
|
||||||
|
device_id, auth_user, time_now
|
||||||
|
)
|
||||||
|
# TODO: Sign the JSON with the server key
|
||||||
|
yield self.store.set_e2e_device_keys(
|
||||||
|
user_id, device_id, time_now,
|
||||||
|
encode_canonical_json(device_keys)
|
||||||
|
)
|
||||||
|
|
||||||
|
one_time_keys = body.get("one_time_keys", None)
|
||||||
|
if one_time_keys:
|
||||||
|
logger.info(
|
||||||
|
"Adding %d one_time_keys for device %r for user %r at %d",
|
||||||
|
len(one_time_keys), device_id, user_id, time_now
|
||||||
|
)
|
||||||
|
key_list = []
|
||||||
|
for key_id, key_json in one_time_keys.items():
|
||||||
|
algorithm, key_id = key_id.split(":")
|
||||||
|
key_list.append((
|
||||||
|
algorithm, key_id, encode_canonical_json(key_json)
|
||||||
|
))
|
||||||
|
|
||||||
|
yield self.store.add_e2e_one_time_keys(
|
||||||
|
user_id, device_id, time_now, key_list
|
||||||
|
)
|
||||||
|
|
||||||
|
result = yield self.store.count_e2e_one_time_keys(user_id, device_id)
|
||||||
|
defer.returnValue((200, {"one_time_key_counts": result}))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_GET(self, request, device_id):
|
||||||
|
auth_user, client_info = yield self.auth.get_user_by_req(request)
|
||||||
|
user_id = auth_user.to_string()
|
||||||
|
|
||||||
|
result = yield self.store.count_e2e_one_time_keys(user_id, device_id)
|
||||||
|
defer.returnValue((200, {"one_time_key_counts": result}))
|
||||||
|
|
||||||
|
|
||||||
|
class KeyQueryServlet(RestServlet):
|
||||||
|
"""
|
||||||
|
GET /keys/query/<user_id> HTTP/1.1
|
||||||
|
|
||||||
|
GET /keys/query/<user_id>/<device_id> HTTP/1.1
|
||||||
|
|
||||||
|
POST /keys/query HTTP/1.1
|
||||||
|
Content-Type: application/json
|
||||||
|
{
|
||||||
|
"device_keys": {
|
||||||
|
"<user_id>": ["<device_id>"]
|
||||||
|
} }
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
{
|
||||||
|
"device_keys": {
|
||||||
|
"<user_id>": {
|
||||||
|
"<device_id>": {
|
||||||
|
"user_id": "<user_id>", // Duplicated to be signed
|
||||||
|
"device_id": "<device_id>", // Duplicated to be signed
|
||||||
|
"valid_until_ts": <millisecond_timestamp>,
|
||||||
|
"algorithms": [ // List of supported algorithms
|
||||||
|
"m.olm.curve25519-aes-sha256",
|
||||||
|
],
|
||||||
|
"keys": { // Must include a ed25519 signing key
|
||||||
|
"<algorithm>:<key_id>": "<key_base64>",
|
||||||
|
},
|
||||||
|
"signatures:" {
|
||||||
|
// Must be signed with device's ed25519 key
|
||||||
|
"<user_id>/<device_id>": {
|
||||||
|
"<algorithm>:<key_id>": "<signature_base64>"
|
||||||
|
}
|
||||||
|
// Must be signed by this server.
|
||||||
|
"<server_name>": {
|
||||||
|
"<algorithm>:<key_id>": "<signature_base64>"
|
||||||
|
} } } } } }
|
||||||
|
"""
|
||||||
|
|
||||||
|
PATTERN = client_v2_pattern(
|
||||||
|
"/keys/query(?:"
|
||||||
|
"/(?P<user_id>[^/]*)(?:"
|
||||||
|
"/(?P<device_id>[^/]*)"
|
||||||
|
")?"
|
||||||
|
")?"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
super(KeyQueryServlet, self).__init__()
|
||||||
|
self.store = hs.get_datastore()
|
||||||
|
self.auth = hs.get_auth()
|
||||||
|
self.federation = hs.get_replication_layer()
|
||||||
|
self.is_mine = hs.is_mine
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_POST(self, request, user_id, device_id):
|
||||||
|
yield self.auth.get_user_by_req(request)
|
||||||
|
try:
|
||||||
|
body = json.loads(request.content.read())
|
||||||
|
except:
|
||||||
|
raise SynapseError(400, "Invalid key JSON")
|
||||||
|
result = yield self.handle_request(body)
|
||||||
|
defer.returnValue(result)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_GET(self, request, user_id, device_id):
|
||||||
|
auth_user, client_info = yield self.auth.get_user_by_req(request)
|
||||||
|
auth_user_id = auth_user.to_string()
|
||||||
|
user_id = user_id if user_id else auth_user_id
|
||||||
|
device_ids = [device_id] if device_id else []
|
||||||
|
result = yield self.handle_request(
|
||||||
|
{"device_keys": {user_id: device_ids}}
|
||||||
|
)
|
||||||
|
defer.returnValue(result)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def handle_request(self, body):
|
||||||
|
local_query = []
|
||||||
|
remote_queries = {}
|
||||||
|
for user_id, device_ids in body.get("device_keys", {}).items():
|
||||||
|
user = UserID.from_string(user_id)
|
||||||
|
if self.is_mine(user):
|
||||||
|
if not device_ids:
|
||||||
|
local_query.append((user_id, None))
|
||||||
|
else:
|
||||||
|
for device_id in device_ids:
|
||||||
|
local_query.append((user_id, device_id))
|
||||||
|
else:
|
||||||
|
remote_queries.setdefault(user.domain, {})[user_id] = list(
|
||||||
|
device_ids
|
||||||
|
)
|
||||||
|
results = yield self.store.get_e2e_device_keys(local_query)
|
||||||
|
|
||||||
|
json_result = {}
|
||||||
|
for user_id, device_keys in results.items():
|
||||||
|
for device_id, json_bytes in device_keys.items():
|
||||||
|
json_result.setdefault(user_id, {})[device_id] = json.loads(
|
||||||
|
json_bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
for destination, device_keys in remote_queries.items():
|
||||||
|
remote_result = yield self.federation.query_client_keys(
|
||||||
|
destination, {"device_keys": device_keys}
|
||||||
|
)
|
||||||
|
for user_id, keys in remote_result["device_keys"].items():
|
||||||
|
if user_id in device_keys:
|
||||||
|
json_result[user_id] = keys
|
||||||
|
defer.returnValue((200, {"device_keys": json_result}))
|
||||||
|
|
||||||
|
|
||||||
|
class OneTimeKeyServlet(RestServlet):
|
||||||
|
"""
|
||||||
|
GET /keys/claim/<user-id>/<device-id>/<algorithm> HTTP/1.1
|
||||||
|
|
||||||
|
POST /keys/claim HTTP/1.1
|
||||||
|
{
|
||||||
|
"one_time_keys": {
|
||||||
|
"<user_id>": {
|
||||||
|
"<device_id>": "<algorithm>"
|
||||||
|
} } }
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
{
|
||||||
|
"one_time_keys": {
|
||||||
|
"<user_id>": {
|
||||||
|
"<device_id>": {
|
||||||
|
"<algorithm>:<key_id>": "<key_base64>"
|
||||||
|
} } } }
|
||||||
|
|
||||||
|
"""
|
||||||
|
PATTERN = client_v2_pattern(
|
||||||
|
"/keys/claim(?:/?|(?:/"
|
||||||
|
"(?P<user_id>[^/]*)/(?P<device_id>[^/]*)/(?P<algorithm>[^/]*)"
|
||||||
|
")?)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
super(OneTimeKeyServlet, self).__init__()
|
||||||
|
self.store = hs.get_datastore()
|
||||||
|
self.auth = hs.get_auth()
|
||||||
|
self.clock = hs.get_clock()
|
||||||
|
self.federation = hs.get_replication_layer()
|
||||||
|
self.is_mine = hs.is_mine
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_GET(self, request, user_id, device_id, algorithm):
|
||||||
|
yield self.auth.get_user_by_req(request)
|
||||||
|
result = yield self.handle_request(
|
||||||
|
{"one_time_keys": {user_id: {device_id: algorithm}}}
|
||||||
|
)
|
||||||
|
defer.returnValue(result)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_POST(self, request, user_id, device_id, algorithm):
|
||||||
|
yield self.auth.get_user_by_req(request)
|
||||||
|
try:
|
||||||
|
body = json.loads(request.content.read())
|
||||||
|
except:
|
||||||
|
raise SynapseError(400, "Invalid key JSON")
|
||||||
|
result = yield self.handle_request(body)
|
||||||
|
defer.returnValue(result)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def handle_request(self, body):
|
||||||
|
local_query = []
|
||||||
|
remote_queries = {}
|
||||||
|
for user_id, device_keys in body.get("one_time_keys", {}).items():
|
||||||
|
user = UserID.from_string(user_id)
|
||||||
|
if self.is_mine(user):
|
||||||
|
for device_id, algorithm in device_keys.items():
|
||||||
|
local_query.append((user_id, device_id, algorithm))
|
||||||
|
else:
|
||||||
|
remote_queries.setdefault(user.domain, {})[user_id] = (
|
||||||
|
device_keys
|
||||||
|
)
|
||||||
|
results = yield self.store.claim_e2e_one_time_keys(local_query)
|
||||||
|
|
||||||
|
json_result = {}
|
||||||
|
for user_id, device_keys in results.items():
|
||||||
|
for device_id, keys in device_keys.items():
|
||||||
|
for key_id, json_bytes in keys.items():
|
||||||
|
json_result.setdefault(user_id, {})[device_id] = {
|
||||||
|
key_id: json.loads(json_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
for destination, device_keys in remote_queries.items():
|
||||||
|
remote_result = yield self.federation.claim_client_keys(
|
||||||
|
destination, {"one_time_keys": device_keys}
|
||||||
|
)
|
||||||
|
for user_id, keys in remote_result["one_time_keys"].items():
|
||||||
|
if user_id in device_keys:
|
||||||
|
json_result[user_id] = keys
|
||||||
|
|
||||||
|
defer.returnValue((200, {"one_time_keys": json_result}))
|
||||||
|
|
||||||
|
|
||||||
|
def register_servlets(hs, http_server):
|
||||||
|
KeyUploadServlet(hs).register(http_server)
|
||||||
|
KeyQueryServlet(hs).register(http_server)
|
||||||
|
OneTimeKeyServlet(hs).register(http_server)
|
||||||
55
synapse/rest/client/v2_alpha/receipts.py
Normal file
55
synapse/rest/client/v2_alpha/receipts.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 OpenMarket 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.
|
||||||
|
|
||||||
|
from twisted.internet import defer
|
||||||
|
|
||||||
|
from synapse.http.servlet import RestServlet
|
||||||
|
from ._base import client_v2_pattern
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ReceiptRestServlet(RestServlet):
|
||||||
|
PATTERN = client_v2_pattern(
|
||||||
|
"/rooms/(?P<room_id>[^/]*)"
|
||||||
|
"/receipt/(?P<receipt_type>[^/]*)"
|
||||||
|
"/(?P<event_id>[^/]*)$"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
super(ReceiptRestServlet, self).__init__()
|
||||||
|
self.hs = hs
|
||||||
|
self.auth = hs.get_auth()
|
||||||
|
self.receipts_handler = hs.get_handlers().receipts_handler
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_POST(self, request, room_id, receipt_type, event_id):
|
||||||
|
user, client = yield self.auth.get_user_by_req(request)
|
||||||
|
|
||||||
|
yield self.receipts_handler.received_client_receipt(
|
||||||
|
room_id,
|
||||||
|
receipt_type,
|
||||||
|
user_id=user.to_string(),
|
||||||
|
event_id=event_id
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue((200, {}))
|
||||||
|
|
||||||
|
|
||||||
|
def register_servlets(hs, http_server):
|
||||||
|
ReceiptRestServlet(hs).register(http_server)
|
||||||
@@ -19,7 +19,7 @@ from synapse.api.constants import LoginType
|
|||||||
from synapse.api.errors import SynapseError, Codes
|
from synapse.api.errors import SynapseError, Codes
|
||||||
from synapse.http.servlet import RestServlet
|
from synapse.http.servlet import RestServlet
|
||||||
|
|
||||||
from ._base import client_v2_pattern, parse_request_allow_empty
|
from ._base import client_v2_pattern, parse_json_dict_from_request
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import hmac
|
import hmac
|
||||||
@@ -50,26 +50,64 @@ class RegisterRestServlet(RestServlet):
|
|||||||
self.auth_handler = hs.get_handlers().auth_handler
|
self.auth_handler = hs.get_handlers().auth_handler
|
||||||
self.registration_handler = hs.get_handlers().registration_handler
|
self.registration_handler = hs.get_handlers().registration_handler
|
||||||
self.identity_handler = hs.get_handlers().identity_handler
|
self.identity_handler = hs.get_handlers().identity_handler
|
||||||
self.login_handler = hs.get_handlers().login_handler
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_POST(self, request):
|
def on_POST(self, request):
|
||||||
yield run_on_reactor()
|
yield run_on_reactor()
|
||||||
|
|
||||||
body = parse_request_allow_empty(request)
|
if '/register/email/requestToken' in request.path:
|
||||||
if 'password' not in body:
|
ret = yield self.onEmailTokenRequest(request)
|
||||||
raise SynapseError(400, "", Codes.MISSING_PARAM)
|
defer.returnValue(ret)
|
||||||
|
|
||||||
|
body = parse_json_dict_from_request(request)
|
||||||
|
|
||||||
|
# we do basic sanity checks here because the auth layer will store these
|
||||||
|
# in sessions. Pull out the username/password provided to us.
|
||||||
|
desired_password = None
|
||||||
|
if 'password' in body:
|
||||||
|
if (not isinstance(body['password'], basestring) or
|
||||||
|
len(body['password']) > 512):
|
||||||
|
raise SynapseError(400, "Invalid password")
|
||||||
|
desired_password = body["password"]
|
||||||
|
|
||||||
|
desired_username = None
|
||||||
if 'username' in body:
|
if 'username' in body:
|
||||||
|
if (not isinstance(body['username'], basestring) or
|
||||||
|
len(body['username']) > 512):
|
||||||
|
raise SynapseError(400, "Invalid username")
|
||||||
desired_username = body['username']
|
desired_username = body['username']
|
||||||
yield self.registration_handler.check_username(desired_username)
|
|
||||||
|
|
||||||
is_using_shared_secret = False
|
appservice = None
|
||||||
is_application_server = False
|
|
||||||
|
|
||||||
service = None
|
|
||||||
if 'access_token' in request.args:
|
if 'access_token' in request.args:
|
||||||
service = yield self.auth.get_appservice_by_req(request)
|
appservice = yield self.auth.get_appservice_by_req(request)
|
||||||
|
|
||||||
|
# fork off as soon as possible for ASes and shared secret auth which
|
||||||
|
# have completely different registration flows to normal users
|
||||||
|
|
||||||
|
# == Application Service Registration ==
|
||||||
|
if appservice:
|
||||||
|
result = yield self._do_appservice_registration(
|
||||||
|
desired_username, request.args["access_token"][0]
|
||||||
|
)
|
||||||
|
defer.returnValue((200, result)) # we throw for non 200 responses
|
||||||
|
return
|
||||||
|
|
||||||
|
# == Shared Secret Registration == (e.g. create new user scripts)
|
||||||
|
if 'mac' in body:
|
||||||
|
# FIXME: Should we really be determining if this is shared secret
|
||||||
|
# auth based purely on the 'mac' key?
|
||||||
|
result = yield self._do_shared_secret_registration(
|
||||||
|
desired_username, desired_password, body["mac"]
|
||||||
|
)
|
||||||
|
defer.returnValue((200, result)) # we throw for non 200 responses
|
||||||
|
return
|
||||||
|
|
||||||
|
# == Normal User Registration == (everyone else)
|
||||||
|
if self.hs.config.disable_registration:
|
||||||
|
raise SynapseError(403, "Registration has been disabled")
|
||||||
|
|
||||||
|
if desired_username is not None:
|
||||||
|
yield self.registration_handler.check_username(desired_username)
|
||||||
|
|
||||||
if self.hs.config.enable_registration_captcha:
|
if self.hs.config.enable_registration_captcha:
|
||||||
flows = [
|
flows = [
|
||||||
@@ -82,50 +120,34 @@ class RegisterRestServlet(RestServlet):
|
|||||||
[LoginType.EMAIL_IDENTITY]
|
[LoginType.EMAIL_IDENTITY]
|
||||||
]
|
]
|
||||||
|
|
||||||
if service:
|
authed, result, params = yield self.auth_handler.check_auth(
|
||||||
is_application_server = True
|
flows, body, self.hs.get_ip_from_request(request)
|
||||||
elif 'mac' in body:
|
|
||||||
# Check registration-specific shared secret auth
|
|
||||||
if 'username' not in body:
|
|
||||||
raise SynapseError(400, "", Codes.MISSING_PARAM)
|
|
||||||
self._check_shared_secret_auth(
|
|
||||||
body['username'], body['mac']
|
|
||||||
)
|
|
||||||
is_using_shared_secret = True
|
|
||||||
else:
|
|
||||||
authed, result, params = yield self.auth_handler.check_auth(
|
|
||||||
flows, body, self.hs.get_ip_from_request(request)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not authed:
|
|
||||||
defer.returnValue((401, result))
|
|
||||||
|
|
||||||
can_register = (
|
|
||||||
not self.hs.config.disable_registration
|
|
||||||
or is_application_server
|
|
||||||
or is_using_shared_secret
|
|
||||||
)
|
)
|
||||||
if not can_register:
|
|
||||||
raise SynapseError(403, "Registration has been disabled")
|
|
||||||
|
|
||||||
|
if not authed:
|
||||||
|
defer.returnValue((401, result))
|
||||||
|
return
|
||||||
|
|
||||||
|
# NB: This may be from the auth handler and NOT from the POST
|
||||||
if 'password' not in params:
|
if 'password' not in params:
|
||||||
raise SynapseError(400, "", Codes.MISSING_PARAM)
|
raise SynapseError(400, "Missing password.", Codes.MISSING_PARAM)
|
||||||
desired_username = params['username'] if 'username' in params else None
|
|
||||||
new_password = params['password']
|
desired_username = params.get("username", None)
|
||||||
|
new_password = params.get("password", None)
|
||||||
|
|
||||||
(user_id, token) = yield self.registration_handler.register(
|
(user_id, token) = yield self.registration_handler.register(
|
||||||
localpart=desired_username,
|
localpart=desired_username,
|
||||||
password=new_password
|
password=new_password
|
||||||
)
|
)
|
||||||
|
|
||||||
if LoginType.EMAIL_IDENTITY in result:
|
if result and LoginType.EMAIL_IDENTITY in result:
|
||||||
threepid = result[LoginType.EMAIL_IDENTITY]
|
threepid = result[LoginType.EMAIL_IDENTITY]
|
||||||
|
|
||||||
for reqd in ['medium', 'address', 'validated_at']:
|
for reqd in ['medium', 'address', 'validated_at']:
|
||||||
if reqd not in threepid:
|
if reqd not in threepid:
|
||||||
logger.info("Can't add incomplete 3pid")
|
logger.info("Can't add incomplete 3pid")
|
||||||
else:
|
else:
|
||||||
yield self.login_handler.add_threepid(
|
yield self.auth_handler.add_threepid(
|
||||||
user_id,
|
user_id,
|
||||||
threepid['medium'],
|
threepid['medium'],
|
||||||
threepid['address'],
|
threepid['address'],
|
||||||
@@ -144,18 +166,21 @@ class RegisterRestServlet(RestServlet):
|
|||||||
else:
|
else:
|
||||||
logger.info("bind_email not specified: not binding email")
|
logger.info("bind_email not specified: not binding email")
|
||||||
|
|
||||||
result = {
|
result = self._create_registration_details(user_id, token)
|
||||||
"user_id": user_id,
|
|
||||||
"access_token": token,
|
|
||||||
"home_server": self.hs.hostname,
|
|
||||||
}
|
|
||||||
|
|
||||||
defer.returnValue((200, result))
|
defer.returnValue((200, result))
|
||||||
|
|
||||||
def on_OPTIONS(self, _):
|
def on_OPTIONS(self, _):
|
||||||
return 200, {}
|
return 200, {}
|
||||||
|
|
||||||
def _check_shared_secret_auth(self, username, mac):
|
@defer.inlineCallbacks
|
||||||
|
def _do_appservice_registration(self, username, as_token):
|
||||||
|
(user_id, token) = yield self.registration_handler.appservice_register(
|
||||||
|
username, as_token
|
||||||
|
)
|
||||||
|
defer.returnValue(self._create_registration_details(user_id, token))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _do_shared_secret_registration(self, username, password, mac):
|
||||||
if not self.hs.config.registration_shared_secret:
|
if not self.hs.config.registration_shared_secret:
|
||||||
raise SynapseError(400, "Shared secret registration is not enabled")
|
raise SynapseError(400, "Shared secret registration is not enabled")
|
||||||
|
|
||||||
@@ -171,13 +196,46 @@ class RegisterRestServlet(RestServlet):
|
|||||||
digestmod=sha1,
|
digestmod=sha1,
|
||||||
).hexdigest()
|
).hexdigest()
|
||||||
|
|
||||||
if compare_digest(want_mac, got_mac):
|
if not compare_digest(want_mac, got_mac):
|
||||||
return True
|
|
||||||
else:
|
|
||||||
raise SynapseError(
|
raise SynapseError(
|
||||||
403, "HMAC incorrect",
|
403, "HMAC incorrect",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
(user_id, token) = yield self.registration_handler.register(
|
||||||
|
localpart=username, password=password
|
||||||
|
)
|
||||||
|
defer.returnValue(self._create_registration_details(user_id, token))
|
||||||
|
|
||||||
|
def _create_registration_details(self, user_id, token):
|
||||||
|
return {
|
||||||
|
"user_id": user_id,
|
||||||
|
"access_token": token,
|
||||||
|
"home_server": self.hs.hostname,
|
||||||
|
}
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def onEmailTokenRequest(self, request):
|
||||||
|
body = parse_json_dict_from_request(request)
|
||||||
|
|
||||||
|
required = ['id_server', 'client_secret', 'email', 'send_attempt']
|
||||||
|
absent = []
|
||||||
|
for k in required:
|
||||||
|
if k not in body:
|
||||||
|
absent.append(k)
|
||||||
|
|
||||||
|
if len(absent) > 0:
|
||||||
|
raise SynapseError(400, "Missing params: %r" % absent, Codes.MISSING_PARAM)
|
||||||
|
|
||||||
|
existingUid = yield self.hs.get_datastore().get_user_id_by_threepid(
|
||||||
|
'email', body['email']
|
||||||
|
)
|
||||||
|
|
||||||
|
if existingUid is not None:
|
||||||
|
raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE)
|
||||||
|
|
||||||
|
ret = yield self.identity_handler.requestEmailToken(**body)
|
||||||
|
defer.returnValue((200, ret))
|
||||||
|
|
||||||
|
|
||||||
def register_servlets(hs, http_server):
|
def register_servlets(hs, http_server):
|
||||||
RegisterRestServlet(hs).register(http_server)
|
RegisterRestServlet(hs).register(http_server)
|
||||||
|
|||||||
@@ -15,29 +15,42 @@
|
|||||||
|
|
||||||
from .thumbnailer import Thumbnailer
|
from .thumbnailer import Thumbnailer
|
||||||
|
|
||||||
|
from synapse.http.matrixfederationclient import MatrixFederationHttpClient
|
||||||
from synapse.http.server import respond_with_json
|
from synapse.http.server import respond_with_json
|
||||||
from synapse.util.stringutils import random_string
|
from synapse.util.stringutils import random_string
|
||||||
from synapse.api.errors import (
|
from synapse.api.errors import (
|
||||||
cs_error, Codes, SynapseError
|
cs_error, Codes, SynapseError
|
||||||
)
|
)
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer, threads
|
||||||
from twisted.web.resource import Resource
|
from twisted.web.resource import Resource
|
||||||
from twisted.protocols.basic import FileSender
|
from twisted.protocols.basic import FileSender
|
||||||
|
|
||||||
from synapse.util.async import create_observer
|
from synapse.util.async import ObservableDeferred
|
||||||
|
from synapse.util.stringutils import is_ascii
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import cgi
|
||||||
import logging
|
import logging
|
||||||
|
import urllib
|
||||||
|
import urlparse
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def parse_media_id(request):
|
def parse_media_id(request):
|
||||||
try:
|
try:
|
||||||
server_name, media_id = request.postpath
|
# This allows users to append e.g. /test.png to the URL. Useful for
|
||||||
return (server_name, media_id)
|
# clients that parse the URL to see content type.
|
||||||
|
server_name, media_id = request.postpath[:2]
|
||||||
|
file_name = None
|
||||||
|
if len(request.postpath) > 2:
|
||||||
|
try:
|
||||||
|
file_name = urlparse.unquote(request.postpath[-1]).decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
pass
|
||||||
|
return server_name, media_id, file_name
|
||||||
except:
|
except:
|
||||||
raise SynapseError(
|
raise SynapseError(
|
||||||
404,
|
404,
|
||||||
@@ -52,7 +65,7 @@ class BaseMediaResource(Resource):
|
|||||||
def __init__(self, hs, filepaths):
|
def __init__(self, hs, filepaths):
|
||||||
Resource.__init__(self)
|
Resource.__init__(self)
|
||||||
self.auth = hs.get_auth()
|
self.auth = hs.get_auth()
|
||||||
self.client = hs.get_http_client()
|
self.client = MatrixFederationHttpClient(hs)
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
self.server_name = hs.hostname
|
self.server_name = hs.hostname
|
||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
@@ -61,6 +74,8 @@ class BaseMediaResource(Resource):
|
|||||||
self.filepaths = filepaths
|
self.filepaths = filepaths
|
||||||
self.version_string = hs.version_string
|
self.version_string = hs.version_string
|
||||||
self.downloads = {}
|
self.downloads = {}
|
||||||
|
self.dynamic_thumbnails = hs.config.dynamic_thumbnails
|
||||||
|
self.thumbnail_requirements = hs.config.thumbnail_requirements
|
||||||
|
|
||||||
def _respond_404(self, request):
|
def _respond_404(self, request):
|
||||||
respond_with_json(
|
respond_with_json(
|
||||||
@@ -83,13 +98,17 @@ class BaseMediaResource(Resource):
|
|||||||
download = self.downloads.get(key)
|
download = self.downloads.get(key)
|
||||||
if download is None:
|
if download is None:
|
||||||
download = self._get_remote_media_impl(server_name, media_id)
|
download = self._get_remote_media_impl(server_name, media_id)
|
||||||
|
download = ObservableDeferred(
|
||||||
|
download,
|
||||||
|
consumeErrors=True
|
||||||
|
)
|
||||||
self.downloads[key] = download
|
self.downloads[key] = download
|
||||||
|
|
||||||
@download.addBoth
|
@download.addBoth
|
||||||
def callback(media_info):
|
def callback(media_info):
|
||||||
del self.downloads[key]
|
del self.downloads[key]
|
||||||
return media_info
|
return media_info
|
||||||
return create_observer(download)
|
return download.observe()
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _get_remote_media_impl(self, server_name, media_id):
|
def _get_remote_media_impl(self, server_name, media_id):
|
||||||
@@ -123,12 +142,31 @@ class BaseMediaResource(Resource):
|
|||||||
media_type = headers["Content-Type"][0]
|
media_type = headers["Content-Type"][0]
|
||||||
time_now_ms = self.clock.time_msec()
|
time_now_ms = self.clock.time_msec()
|
||||||
|
|
||||||
|
content_disposition = headers.get("Content-Disposition", None)
|
||||||
|
if content_disposition:
|
||||||
|
_, params = cgi.parse_header(content_disposition[0],)
|
||||||
|
upload_name = params.get("filename", None)
|
||||||
|
if upload_name and not is_ascii(upload_name):
|
||||||
|
upload_name = None
|
||||||
|
else:
|
||||||
|
upload_name_utf8 = params.get("filename*", None)
|
||||||
|
if upload_name_utf8.lower().startswith("utf-8''"):
|
||||||
|
upload_name = upload_name_utf8[7:]
|
||||||
|
if upload_name:
|
||||||
|
upload_name = urlparse.unquote(upload_name)
|
||||||
|
try:
|
||||||
|
upload_name = upload_name.decode("utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
upload_name = None
|
||||||
|
else:
|
||||||
|
upload_name = None
|
||||||
|
|
||||||
yield self.store.store_cached_remote_media(
|
yield self.store.store_cached_remote_media(
|
||||||
origin=server_name,
|
origin=server_name,
|
||||||
media_id=media_id,
|
media_id=media_id,
|
||||||
media_type=media_type,
|
media_type=media_type,
|
||||||
time_now_ms=self.clock.time_msec(),
|
time_now_ms=self.clock.time_msec(),
|
||||||
upload_name=None,
|
upload_name=upload_name,
|
||||||
media_length=length,
|
media_length=length,
|
||||||
filesystem_id=file_id,
|
filesystem_id=file_id,
|
||||||
)
|
)
|
||||||
@@ -139,7 +177,7 @@ class BaseMediaResource(Resource):
|
|||||||
media_info = {
|
media_info = {
|
||||||
"media_type": media_type,
|
"media_type": media_type,
|
||||||
"media_length": length,
|
"media_length": length,
|
||||||
"upload_name": None,
|
"upload_name": upload_name,
|
||||||
"created_ts": time_now_ms,
|
"created_ts": time_now_ms,
|
||||||
"filesystem_id": file_id,
|
"filesystem_id": file_id,
|
||||||
}
|
}
|
||||||
@@ -152,11 +190,26 @@ class BaseMediaResource(Resource):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _respond_with_file(self, request, media_type, file_path,
|
def _respond_with_file(self, request, media_type, file_path,
|
||||||
file_size=None):
|
file_size=None, upload_name=None):
|
||||||
logger.debug("Responding with %r", file_path)
|
logger.debug("Responding with %r", file_path)
|
||||||
|
|
||||||
if os.path.isfile(file_path):
|
if os.path.isfile(file_path):
|
||||||
request.setHeader(b"Content-Type", media_type.encode("UTF-8"))
|
request.setHeader(b"Content-Type", media_type.encode("UTF-8"))
|
||||||
|
if upload_name:
|
||||||
|
if is_ascii(upload_name):
|
||||||
|
request.setHeader(
|
||||||
|
b"Content-Disposition",
|
||||||
|
b"inline; filename=%s" % (
|
||||||
|
urllib.quote(upload_name.encode("utf-8")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
request.setHeader(
|
||||||
|
b"Content-Disposition",
|
||||||
|
b"inline; filename*=utf-8''%s" % (
|
||||||
|
urllib.quote(upload_name.encode("utf-8")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# cache for at least a day.
|
# cache for at least a day.
|
||||||
# XXX: we might want to turn this off for data we don't want to
|
# XXX: we might want to turn this off for data we don't want to
|
||||||
@@ -182,22 +235,74 @@ class BaseMediaResource(Resource):
|
|||||||
self._respond_404(request)
|
self._respond_404(request)
|
||||||
|
|
||||||
def _get_thumbnail_requirements(self, media_type):
|
def _get_thumbnail_requirements(self, media_type):
|
||||||
if media_type == "image/jpeg":
|
return self.thumbnail_requirements.get(media_type, ())
|
||||||
return (
|
|
||||||
(32, 32, "crop", "image/jpeg"),
|
def _generate_thumbnail(self, input_path, t_path, t_width, t_height,
|
||||||
(96, 96, "crop", "image/jpeg"),
|
t_method, t_type):
|
||||||
(320, 240, "scale", "image/jpeg"),
|
thumbnailer = Thumbnailer(input_path)
|
||||||
(640, 480, "scale", "image/jpeg"),
|
m_width = thumbnailer.width
|
||||||
)
|
m_height = thumbnailer.height
|
||||||
elif (media_type == "image/png") or (media_type == "image/gif"):
|
|
||||||
return (
|
if m_width * m_height >= self.max_image_pixels:
|
||||||
(32, 32, "crop", "image/png"),
|
logger.info(
|
||||||
(96, 96, "crop", "image/png"),
|
"Image too large to thumbnail %r x %r > %r",
|
||||||
(320, 240, "scale", "image/png"),
|
m_width, m_height, self.max_image_pixels
|
||||||
(640, 480, "scale", "image/png"),
|
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if t_method == "crop":
|
||||||
|
t_len = thumbnailer.crop(t_path, t_width, t_height, t_type)
|
||||||
|
elif t_method == "scale":
|
||||||
|
t_len = thumbnailer.scale(t_path, t_width, t_height, t_type)
|
||||||
else:
|
else:
|
||||||
return ()
|
t_len = None
|
||||||
|
|
||||||
|
return t_len
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _generate_local_exact_thumbnail(self, media_id, t_width, t_height,
|
||||||
|
t_method, t_type):
|
||||||
|
input_path = self.filepaths.local_media_filepath(media_id)
|
||||||
|
|
||||||
|
t_path = self.filepaths.local_media_thumbnail(
|
||||||
|
media_id, t_width, t_height, t_type, t_method
|
||||||
|
)
|
||||||
|
self._makedirs(t_path)
|
||||||
|
|
||||||
|
t_len = yield threads.deferToThread(
|
||||||
|
self._generate_thumbnail,
|
||||||
|
input_path, t_path, t_width, t_height, t_method, t_type
|
||||||
|
)
|
||||||
|
|
||||||
|
if t_len:
|
||||||
|
yield self.store.store_local_thumbnail(
|
||||||
|
media_id, t_width, t_height, t_type, t_method, t_len
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue(t_path)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _generate_remote_exact_thumbnail(self, server_name, file_id, media_id,
|
||||||
|
t_width, t_height, t_method, t_type):
|
||||||
|
input_path = self.filepaths.remote_media_filepath(server_name, file_id)
|
||||||
|
|
||||||
|
t_path = self.filepaths.remote_media_thumbnail(
|
||||||
|
server_name, file_id, t_width, t_height, t_type, t_method
|
||||||
|
)
|
||||||
|
self._makedirs(t_path)
|
||||||
|
|
||||||
|
t_len = yield threads.deferToThread(
|
||||||
|
self._generate_thumbnail,
|
||||||
|
input_path, t_path, t_width, t_height, t_method, t_type
|
||||||
|
)
|
||||||
|
|
||||||
|
if t_len:
|
||||||
|
yield self.store.store_remote_media_thumbnail(
|
||||||
|
server_name, media_id, file_id,
|
||||||
|
t_width, t_height, t_type, t_method, t_len
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue(t_path)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _generate_local_thumbnails(self, media_id, media_info):
|
def _generate_local_thumbnails(self, media_id, media_info):
|
||||||
@@ -218,43 +323,52 @@ class BaseMediaResource(Resource):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
scales = set()
|
local_thumbnails = []
|
||||||
crops = set()
|
|
||||||
for r_width, r_height, r_method, r_type in requirements:
|
def generate_thumbnails():
|
||||||
if r_method == "scale":
|
scales = set()
|
||||||
t_width, t_height = thumbnailer.aspect(r_width, r_height)
|
crops = set()
|
||||||
scales.add((
|
for r_width, r_height, r_method, r_type in requirements:
|
||||||
min(m_width, t_width), min(m_height, t_height), r_type,
|
if r_method == "scale":
|
||||||
|
t_width, t_height = thumbnailer.aspect(r_width, r_height)
|
||||||
|
scales.add((
|
||||||
|
min(m_width, t_width), min(m_height, t_height), r_type,
|
||||||
|
))
|
||||||
|
elif r_method == "crop":
|
||||||
|
crops.add((r_width, r_height, r_type))
|
||||||
|
|
||||||
|
for t_width, t_height, t_type in scales:
|
||||||
|
t_method = "scale"
|
||||||
|
t_path = self.filepaths.local_media_thumbnail(
|
||||||
|
media_id, t_width, t_height, t_type, t_method
|
||||||
|
)
|
||||||
|
self._makedirs(t_path)
|
||||||
|
t_len = thumbnailer.scale(t_path, t_width, t_height, t_type)
|
||||||
|
|
||||||
|
local_thumbnails.append((
|
||||||
|
media_id, t_width, t_height, t_type, t_method, t_len
|
||||||
))
|
))
|
||||||
elif r_method == "crop":
|
|
||||||
crops.add((r_width, r_height, r_type))
|
|
||||||
|
|
||||||
for t_width, t_height, t_type in scales:
|
for t_width, t_height, t_type in crops:
|
||||||
t_method = "scale"
|
if (t_width, t_height, t_type) in scales:
|
||||||
t_path = self.filepaths.local_media_thumbnail(
|
# If the aspect ratio of the cropped thumbnail matches a purely
|
||||||
media_id, t_width, t_height, t_type, t_method
|
# scaled one then there is no point in calculating a separate
|
||||||
)
|
# thumbnail.
|
||||||
self._makedirs(t_path)
|
continue
|
||||||
t_len = thumbnailer.scale(t_path, t_width, t_height, t_type)
|
t_method = "crop"
|
||||||
yield self.store.store_local_thumbnail(
|
t_path = self.filepaths.local_media_thumbnail(
|
||||||
media_id, t_width, t_height, t_type, t_method, t_len
|
media_id, t_width, t_height, t_type, t_method
|
||||||
)
|
)
|
||||||
|
self._makedirs(t_path)
|
||||||
|
t_len = thumbnailer.crop(t_path, t_width, t_height, t_type)
|
||||||
|
local_thumbnails.append((
|
||||||
|
media_id, t_width, t_height, t_type, t_method, t_len
|
||||||
|
))
|
||||||
|
|
||||||
for t_width, t_height, t_type in crops:
|
yield threads.deferToThread(generate_thumbnails)
|
||||||
if (t_width, t_height, t_type) in scales:
|
|
||||||
# If the aspect ratio of the cropped thumbnail matches a purely
|
for l in local_thumbnails:
|
||||||
# scaled one then there is no point in calculating a separate
|
yield self.store.store_local_thumbnail(*l)
|
||||||
# thumbnail.
|
|
||||||
continue
|
|
||||||
t_method = "crop"
|
|
||||||
t_path = self.filepaths.local_media_thumbnail(
|
|
||||||
media_id, t_width, t_height, t_type, t_method
|
|
||||||
)
|
|
||||||
self._makedirs(t_path)
|
|
||||||
t_len = thumbnailer.crop(t_path, t_width, t_height, t_type)
|
|
||||||
yield self.store.store_local_thumbnail(
|
|
||||||
media_id, t_width, t_height, t_type, t_method, t_len
|
|
||||||
)
|
|
||||||
|
|
||||||
defer.returnValue({
|
defer.returnValue({
|
||||||
"width": m_width,
|
"width": m_width,
|
||||||
@@ -269,57 +383,65 @@ class BaseMediaResource(Resource):
|
|||||||
if not requirements:
|
if not requirements:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
remote_thumbnails = []
|
||||||
|
|
||||||
input_path = self.filepaths.remote_media_filepath(server_name, file_id)
|
input_path = self.filepaths.remote_media_filepath(server_name, file_id)
|
||||||
thumbnailer = Thumbnailer(input_path)
|
thumbnailer = Thumbnailer(input_path)
|
||||||
m_width = thumbnailer.width
|
m_width = thumbnailer.width
|
||||||
m_height = thumbnailer.height
|
m_height = thumbnailer.height
|
||||||
|
|
||||||
if m_width * m_height >= self.max_image_pixels:
|
def generate_thumbnails():
|
||||||
logger.info(
|
if m_width * m_height >= self.max_image_pixels:
|
||||||
"Image too large to thumbnail %r x %r > %r",
|
logger.info(
|
||||||
m_width, m_height, self.max_image_pixels
|
"Image too large to thumbnail %r x %r > %r",
|
||||||
)
|
m_width, m_height, self.max_image_pixels
|
||||||
return
|
)
|
||||||
|
return
|
||||||
|
|
||||||
scales = set()
|
scales = set()
|
||||||
crops = set()
|
crops = set()
|
||||||
for r_width, r_height, r_method, r_type in requirements:
|
for r_width, r_height, r_method, r_type in requirements:
|
||||||
if r_method == "scale":
|
if r_method == "scale":
|
||||||
t_width, t_height = thumbnailer.aspect(r_width, r_height)
|
t_width, t_height = thumbnailer.aspect(r_width, r_height)
|
||||||
scales.add((
|
scales.add((
|
||||||
min(m_width, t_width), min(m_height, t_height), r_type,
|
min(m_width, t_width), min(m_height, t_height), r_type,
|
||||||
))
|
))
|
||||||
elif r_method == "crop":
|
elif r_method == "crop":
|
||||||
crops.add((r_width, r_height, r_type))
|
crops.add((r_width, r_height, r_type))
|
||||||
|
|
||||||
for t_width, t_height, t_type in scales:
|
for t_width, t_height, t_type in scales:
|
||||||
t_method = "scale"
|
t_method = "scale"
|
||||||
t_path = self.filepaths.remote_media_thumbnail(
|
t_path = self.filepaths.remote_media_thumbnail(
|
||||||
server_name, file_id, t_width, t_height, t_type, t_method
|
server_name, file_id, t_width, t_height, t_type, t_method
|
||||||
)
|
)
|
||||||
self._makedirs(t_path)
|
self._makedirs(t_path)
|
||||||
t_len = thumbnailer.scale(t_path, t_width, t_height, t_type)
|
t_len = thumbnailer.scale(t_path, t_width, t_height, t_type)
|
||||||
yield self.store.store_remote_media_thumbnail(
|
remote_thumbnails.append([
|
||||||
server_name, media_id, file_id,
|
server_name, media_id, file_id,
|
||||||
t_width, t_height, t_type, t_method, t_len
|
t_width, t_height, t_type, t_method, t_len
|
||||||
)
|
])
|
||||||
|
|
||||||
for t_width, t_height, t_type in crops:
|
for t_width, t_height, t_type in crops:
|
||||||
if (t_width, t_height, t_type) in scales:
|
if (t_width, t_height, t_type) in scales:
|
||||||
# If the aspect ratio of the cropped thumbnail matches a purely
|
# If the aspect ratio of the cropped thumbnail matches a purely
|
||||||
# scaled one then there is no point in calculating a separate
|
# scaled one then there is no point in calculating a separate
|
||||||
# thumbnail.
|
# thumbnail.
|
||||||
continue
|
continue
|
||||||
t_method = "crop"
|
t_method = "crop"
|
||||||
t_path = self.filepaths.remote_media_thumbnail(
|
t_path = self.filepaths.remote_media_thumbnail(
|
||||||
server_name, file_id, t_width, t_height, t_type, t_method
|
server_name, file_id, t_width, t_height, t_type, t_method
|
||||||
)
|
)
|
||||||
self._makedirs(t_path)
|
self._makedirs(t_path)
|
||||||
t_len = thumbnailer.crop(t_path, t_width, t_height, t_type)
|
t_len = thumbnailer.crop(t_path, t_width, t_height, t_type)
|
||||||
yield self.store.store_remote_media_thumbnail(
|
remote_thumbnails.append([
|
||||||
server_name, media_id, file_id,
|
server_name, media_id, file_id,
|
||||||
t_width, t_height, t_type, t_method, t_len
|
t_width, t_height, t_type, t_method, t_len
|
||||||
)
|
])
|
||||||
|
|
||||||
|
yield threads.deferToThread(generate_thumbnails)
|
||||||
|
|
||||||
|
for r in remote_thumbnails:
|
||||||
|
yield self.store.store_remote_media_thumbnail(*r)
|
||||||
|
|
||||||
defer.returnValue({
|
defer.returnValue({
|
||||||
"width": m_width,
|
"width": m_width,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user