mirror of
https://github.com/element-hq/synapse.git
synced 2025-12-09 01:30:18 +00:00
Compare commits
1071 Commits
erikj/rest
...
v0.13.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
907c1faf1e | ||
|
|
6c3126d950 | ||
|
|
6e89e69d08 | ||
|
|
e66d0bd03a | ||
|
|
4a2ace1857 | ||
|
|
5189bfdef4 | ||
|
|
24f00a6c33 | ||
|
|
8e49892b21 | ||
|
|
e557dc80b8 | ||
|
|
4eb8f9ca8a | ||
|
|
f7ef5c1d57 | ||
|
|
00c9ad49df | ||
|
|
9777c5f49a | ||
|
|
0214745239 | ||
|
|
46a02ff15b | ||
|
|
6ad9586c84 | ||
|
|
78a5482267 | ||
|
|
7b0d846407 | ||
|
|
f28cc45183 | ||
|
|
e664e9737c | ||
|
|
13ba8d878c | ||
|
|
78d6c1b5be | ||
|
|
feb294d552 | ||
|
|
70a8608749 | ||
|
|
7e3b586c1e | ||
|
|
eff12e838c | ||
|
|
82631c5f94 | ||
|
|
b58a8b1ee0 | ||
|
|
5b75b637b8 | ||
|
|
9ac9b75bc4 | ||
|
|
ebaa999f92 | ||
|
|
6c558ee8bc | ||
|
|
31a2b892d8 | ||
|
|
9daa4e2a85 | ||
|
|
3e2fcd67b2 | ||
|
|
241b71852e | ||
|
|
97294ef2fd | ||
|
|
549698b1e0 | ||
|
|
c486b7b41c | ||
|
|
f078ecbc8f | ||
|
|
2bb5f035af | ||
|
|
cca5c06679 | ||
|
|
0897357993 | ||
|
|
2c1fbea531 | ||
|
|
13e6262659 | ||
|
|
1d19a5ec0f | ||
|
|
77c7ed0e93 | ||
|
|
b052621f67 | ||
|
|
8f1031586f | ||
|
|
79a1c0574b | ||
|
|
489f92e0e5 | ||
|
|
737c4223ef | ||
|
|
db0da033eb | ||
|
|
6a9f1209df | ||
|
|
34dda7cc7f | ||
|
|
4d36e73230 | ||
|
|
709e09e1c3 | ||
|
|
aa4af94c69 | ||
|
|
b84d59c5f0 | ||
|
|
33c71c3a4b | ||
|
|
c8e4d5de7f | ||
|
|
156cea5b45 | ||
|
|
8450114098 | ||
|
|
24277fbb97 | ||
|
|
66bb255fcd | ||
|
|
5054806ec1 | ||
|
|
430e496050 | ||
|
|
d4f72a5bfb | ||
|
|
f8aae79a72 | ||
|
|
9cd80a7b5c | ||
|
|
772b45c745 | ||
|
|
5f280837a6 | ||
|
|
6f52e90065 | ||
|
|
771528ab13 | ||
|
|
b32121a5d1 | ||
|
|
2e36689df3 | ||
|
|
2df6114bc4 | ||
|
|
a644ac6d2c | ||
|
|
de11b5b9b5 | ||
|
|
d83d004ccd | ||
|
|
43e13dbd4d | ||
|
|
8a391e33ae | ||
|
|
477b1ed6cf | ||
|
|
04ad93e6fd | ||
|
|
65e92eca49 | ||
|
|
9039904f4c | ||
|
|
69214ea671 | ||
|
|
b023995538 | ||
|
|
793369791a | ||
|
|
7a8ea7e78b | ||
|
|
854ca32f10 | ||
|
|
d7ac861d3b | ||
|
|
89b40b225c | ||
|
|
4bf448be25 | ||
|
|
fa48020a52 | ||
|
|
1ef7cae41b | ||
|
|
2d3837bec7 | ||
|
|
498c2e60fd | ||
|
|
ceb6b8680a | ||
|
|
b264b9548f | ||
|
|
d98a9f2583 | ||
|
|
226a9a5fa6 | ||
|
|
25c311eaf6 | ||
|
|
cc9c97e0dc | ||
|
|
e70165039c | ||
|
|
c1de91aca4 | ||
|
|
b55b90bfb4 | ||
|
|
8da95b6f1b | ||
|
|
b91baae09d | ||
|
|
13724569ec | ||
|
|
4a6eb5eb45 | ||
|
|
6927d0e091 | ||
|
|
b5dbced938 | ||
|
|
f2d5ff5bf2 | ||
|
|
3d60686c0c | ||
|
|
45488e0ffa | ||
|
|
f67d60496a | ||
|
|
18579534ea | ||
|
|
47374a33fc | ||
|
|
0fcafbece8 | ||
|
|
96bb4bf38a | ||
|
|
fd142c29d9 | ||
|
|
ebc5f00efe | ||
|
|
ea320d3464 | ||
|
|
5687a00e4e | ||
|
|
b18114e19e | ||
|
|
02a9c3be6c | ||
|
|
4fce59f274 | ||
|
|
fb7299800f | ||
|
|
c046630c33 | ||
|
|
a30364c1f9 | ||
|
|
766526e114 | ||
|
|
50e18938a9 | ||
|
|
f3af1840cb | ||
|
|
467c27a1f9 | ||
|
|
3f5dd18bd4 | ||
|
|
40431251cb | ||
|
|
82cf3a8043 | ||
|
|
03b2c2577c | ||
|
|
0663c5bd52 | ||
|
|
35981c8b71 | ||
|
|
8fe8951a8d | ||
|
|
fdca8ec418 | ||
|
|
45cf827c8f | ||
|
|
00cb3eb24b | ||
|
|
c23a8c7833 | ||
|
|
e1941442d4 | ||
|
|
49c328a892 | ||
|
|
0935802f1e | ||
|
|
19fd425928 | ||
|
|
167d1df699 | ||
|
|
7ed2bbeb11 | ||
|
|
9101193242 | ||
|
|
571a566399 | ||
|
|
3c6518ddbf | ||
|
|
4e7948b47a | ||
|
|
ba8931829b | ||
|
|
61eaa6ec64 | ||
|
|
c5e7c0e436 | ||
|
|
e26390ca46 | ||
|
|
a6477d5933 | ||
|
|
5cba88ea7c | ||
|
|
5fc9b17518 | ||
|
|
fa90c180ee | ||
|
|
5610880003 | ||
|
|
e7febf4fbb | ||
|
|
aca3193efb | ||
|
|
b97f6626b6 | ||
|
|
f93ecf8783 | ||
|
|
0487c9441f | ||
|
|
a955cbfa49 | ||
|
|
8c97b49886 | ||
|
|
2152b320c5 | ||
|
|
d6d60b4d6c | ||
|
|
d6c831bd3d | ||
|
|
97b364cb25 | ||
|
|
03f4569dc3 | ||
|
|
8c94833b72 | ||
|
|
9fda8b5193 | ||
|
|
e4e33c743e | ||
|
|
87f9477b10 | ||
|
|
9959d9ece8 | ||
|
|
27b9775073 | ||
|
|
766c24b2e6 | ||
|
|
7179fdd550 | ||
|
|
c887c4cbd5 | ||
|
|
e18257f0e5 | ||
|
|
8431f62ebb | ||
|
|
f091b73e69 | ||
|
|
ce6fbbea94 | ||
|
|
aea5da0ef6 | ||
|
|
3a75159832 | ||
|
|
1ebf5e3d03 | ||
|
|
dc2647cd3d | ||
|
|
86896408b0 | ||
|
|
53cb173663 | ||
|
|
d59c58bc95 | ||
|
|
ddd25def01 | ||
|
|
8c6012a4af | ||
|
|
42deca50c2 | ||
|
|
d685ae73b4 | ||
|
|
4021f95261 | ||
|
|
f92fe15897 | ||
|
|
3fe8c56736 | ||
|
|
60965bd7e5 | ||
|
|
0e0e441b33 | ||
|
|
b4a41aa542 | ||
|
|
db6e26bb8c | ||
|
|
88baa3865e | ||
|
|
74f49f99f9 | ||
|
|
7065b75bfd | ||
|
|
7959e8b764 | ||
|
|
52bdd1b834 | ||
|
|
7a3fe48ba4 | ||
|
|
7cd418d38e | ||
|
|
cd80019eec | ||
|
|
d552861346 | ||
|
|
10f76dc5da | ||
|
|
5b142788d2 | ||
|
|
eaa836e8ca | ||
|
|
42eae4634f | ||
|
|
8acc5cb60f | ||
|
|
31a051b677 | ||
|
|
8f9c74e9f1 | ||
|
|
975903ae17 | ||
|
|
4efcaa43c8 | ||
|
|
330be18ec5 | ||
|
|
f1f8122120 | ||
|
|
297eded261 | ||
|
|
0e07f2e15d | ||
|
|
82b46f556d | ||
|
|
8f66fe6392 | ||
|
|
3a00f13436 | ||
|
|
c6549117a2 | ||
|
|
ed1d189e10 | ||
|
|
dfe1273d14 | ||
|
|
91a222c66d | ||
|
|
0503bdb316 | ||
|
|
930ba003f8 | ||
|
|
d54005059c | ||
|
|
d7c85ad916 | ||
|
|
c1a3021771 | ||
|
|
d049e81b10 | ||
|
|
c43b6dcc75 | ||
|
|
367cfab4e6 | ||
|
|
69adf8c384 | ||
|
|
73ca8e5834 | ||
|
|
b088291f14 | ||
|
|
a2ae01cc0f | ||
|
|
da417aa56d | ||
|
|
d4315bbf6b | ||
|
|
3fa344c037 | ||
|
|
7cc047455e | ||
|
|
d726597737 | ||
|
|
2309450a76 | ||
|
|
ea5eea2424 | ||
|
|
746f6e0eb3 | ||
|
|
7441d8cc0c | ||
|
|
ccf9387d57 | ||
|
|
d4cefb6289 | ||
|
|
259d1ecd1d | ||
|
|
191070123d | ||
|
|
afb7b377f2 | ||
|
|
af30140621 | ||
|
|
ac2842ff1e | ||
|
|
892ee473d9 | ||
|
|
40d9765123 | ||
|
|
2818a000aa | ||
|
|
fb5d8e58ff | ||
|
|
5a7d1ecffc | ||
|
|
d056a0a3d8 | ||
|
|
7a079adc8f | ||
|
|
b8518ffe65 | ||
|
|
9654ee0848 | ||
|
|
7ecd211163 | ||
|
|
05f78b3b52 | ||
|
|
f5fc8f2928 | ||
|
|
9a8949f022 | ||
|
|
3adcc4c86a | ||
|
|
47e7963e50 | ||
|
|
88af7bb48b | ||
|
|
0d241e1114 | ||
|
|
f750a442f7 | ||
|
|
003853e702 | ||
|
|
a284ad4092 | ||
|
|
47f82e4408 | ||
|
|
5cd2126a6a | ||
|
|
29c353c553 | ||
|
|
808a8aedab | ||
|
|
74474a6d63 | ||
|
|
d16dcf642e | ||
|
|
7dd14e5d1c | ||
|
|
866fe27e78 | ||
|
|
d1f56f732e | ||
|
|
0e39dcd135 | ||
|
|
345ff2196a | ||
|
|
2c176e02ae | ||
|
|
63485b3029 | ||
|
|
f59b564507 | ||
|
|
2068678b8c | ||
|
|
cc66a9a5e3 | ||
|
|
5de1563997 | ||
|
|
ac5a4477ad | ||
|
|
c049f60d4a | ||
|
|
b5ce4f0427 | ||
|
|
ac12b6d332 | ||
|
|
5819b7a78c | ||
|
|
5bf1a3d6dc | ||
|
|
3f8db3d597 | ||
|
|
a50013fd99 | ||
|
|
2978053d16 | ||
|
|
2c760372d6 | ||
|
|
2680043bc6 | ||
|
|
8db451f652 | ||
|
|
430d3d74f6 | ||
|
|
7ee1879ed4 | ||
|
|
d14fcfd24a | ||
|
|
27927463a1 | ||
|
|
fcb6df45e5 | ||
|
|
a7927c13fd | ||
|
|
339c8f0133 | ||
|
|
bce602eb4e | ||
|
|
939cbd7057 | ||
|
|
12623c99b6 | ||
|
|
2655d61d70 | ||
|
|
fcb05b4c82 | ||
|
|
806bae1ee7 | ||
|
|
f6fcff3602 | ||
|
|
244b356a37 | ||
|
|
49f33f6438 | ||
|
|
93afb40cd4 | ||
|
|
9c1f853d58 | ||
|
|
7d09ab8915 | ||
|
|
d9db819e23 | ||
|
|
37716d55ed | ||
|
|
4399684582 | ||
|
|
44b4fc5f50 | ||
|
|
f4dad9f639 | ||
|
|
8740e4e94a | ||
|
|
c0a279e808 | ||
|
|
96e400fee5 | ||
|
|
72ba26679b | ||
|
|
ea47760bd8 | ||
|
|
70dfe4dc96 | ||
|
|
a8e9e0b916 | ||
|
|
31de2953a3 | ||
|
|
fd5c28dc52 | ||
|
|
42aa1f3f33 | ||
|
|
2110e35fd6 | ||
|
|
b5d33a656f | ||
|
|
fe56138142 | ||
|
|
c110eb92f8 | ||
|
|
8f8b884430 | ||
|
|
a8cd1eb996 | ||
|
|
29e595e5d4 | ||
|
|
c232780081 | ||
|
|
7c816de442 | ||
|
|
8677b7d698 | ||
|
|
33bef689c1 | ||
|
|
fcbe63eaad | ||
|
|
5727922106 | ||
|
|
5dc5e29b9c | ||
|
|
3deffcdb1e | ||
|
|
c9ae1d1ee5 | ||
|
|
daadcf36c0 | ||
|
|
823b679232 | ||
|
|
6c28ac260c | ||
|
|
49c34dfd36 | ||
|
|
4106477e7f | ||
|
|
7ac6ca7311 | ||
|
|
11a974da21 | ||
|
|
09dc9854cd | ||
|
|
442fcc02f7 | ||
|
|
b6a585348a | ||
|
|
c582f178b7 | ||
|
|
4cec90a260 | ||
|
|
0e48f7f245 | ||
|
|
392773ccb2 | ||
|
|
bf32922e5a | ||
|
|
797691f908 | ||
|
|
e5ea4fad78 | ||
|
|
5880de186b | ||
|
|
992928304f | ||
|
|
ae1262a241 | ||
|
|
c79f221192 | ||
|
|
8ce5679813 | ||
|
|
eb03625626 | ||
|
|
87d577e023 | ||
|
|
2ef6de928d | ||
|
|
29e131df43 | ||
|
|
cfd07aafff | ||
|
|
a178eb1bc8 | ||
|
|
8737ead008 | ||
|
|
90921981be | ||
|
|
acb19068d0 | ||
|
|
d74c4e90d4 | ||
|
|
85ca8cb90c | ||
|
|
c3ea36304b | ||
|
|
1b5642604b | ||
|
|
07c33eff43 | ||
|
|
4eb7b950c8 | ||
|
|
dc65d0ae9d | ||
|
|
b18b99eb14 | ||
|
|
5680be70ae | ||
|
|
c77e7e60fc | ||
|
|
d74c6ace24 | ||
|
|
f1b67730fa | ||
|
|
92a1e74b20 | ||
|
|
c914d67cda | ||
|
|
f35f8d06ea | ||
|
|
d2709a5389 | ||
|
|
928c575c6f | ||
|
|
3051c9d002 | ||
|
|
34c09f33da | ||
|
|
cf3282d103 | ||
|
|
32d9fd0b26 | ||
|
|
c6e79c84de | ||
|
|
8d6dde7825 | ||
|
|
d12c00bdc3 | ||
|
|
ba39d3d5d7 | ||
|
|
f3948e001f | ||
|
|
7fa71e3267 | ||
|
|
517fb9a023 | ||
|
|
9ac417fa88 | ||
|
|
d2a92c6bde | ||
|
|
d79e90f078 | ||
|
|
9b4cd0cd0f | ||
|
|
140a50f641 | ||
|
|
5645d9747b | ||
|
|
3fbb031745 | ||
|
|
4c8f6a7e42 | ||
|
|
7df276d219 | ||
|
|
0ee0138325 | ||
|
|
77f06856b6 | ||
|
|
65c451cb38 | ||
|
|
251aafccca | ||
|
|
c058625959 | ||
|
|
cdd04f7055 | ||
|
|
542ab0f886 | ||
|
|
e525b46f12 | ||
|
|
b9b4466d0d | ||
|
|
c3fff251a9 | ||
|
|
45a9e0ae0c | ||
|
|
489a4cd1cf | ||
|
|
2a2b2ef834 | ||
|
|
2e2eeb43a6 | ||
|
|
7f3148865c | ||
|
|
bb9c7f2dd9 | ||
|
|
9036d2d6a8 | ||
|
|
c061b47c57 | ||
|
|
f73f154ec2 | ||
|
|
64b6606824 | ||
|
|
42a7a09eea | ||
|
|
091c545c4f | ||
|
|
a6ba41e078 | ||
|
|
f85949bde0 | ||
|
|
2f871ad143 | ||
|
|
c8ea2d5b1f | ||
|
|
b131fb1fe2 | ||
|
|
413d0d6a24 | ||
|
|
0a2d73fd60 | ||
|
|
ce4999268a | ||
|
|
633ceb9bb1 | ||
|
|
64374bda5b | ||
|
|
772ad4f715 | ||
|
|
bdacee476d | ||
|
|
10f82b4bea | ||
|
|
8c5f252edb | ||
|
|
8b9f471d27 | ||
|
|
a64f9bbfe0 | ||
|
|
42ad49f5b7 | ||
|
|
2b0f8a9482 | ||
|
|
af4422c42a | ||
|
|
0311612ce9 | ||
|
|
5fc03449c8 | ||
|
|
4fab578b43 | ||
|
|
661b76615b | ||
|
|
dcfc70e8ed | ||
|
|
63fdd9fe0b | ||
|
|
910956b0ec | ||
|
|
d3ac8fd87d | ||
|
|
e98e00558a | ||
|
|
3ddf0b9722 | ||
|
|
2acae8300f | ||
|
|
dbe7892e03 | ||
|
|
28c5181dfe | ||
|
|
15e9885197 | ||
|
|
8505a4ddc3 | ||
|
|
6051266924 | ||
|
|
070e28e203 | ||
|
|
834924248f | ||
|
|
98dfa7d24f | ||
|
|
338c0a8a69 | ||
|
|
a874c0894a | ||
|
|
f382a3bb7e | ||
|
|
76e69cc8de | ||
|
|
fde412b240 | ||
|
|
bfc52a2342 | ||
|
|
deeebbfcb7 | ||
|
|
1ee7280c4c | ||
|
|
cde49d3d2b | ||
|
|
e738920156 | ||
|
|
0065e554e0 | ||
|
|
5a3e4e43d8 | ||
|
|
d9a5c56930 | ||
|
|
51fb590c0e | ||
|
|
5577a61090 | ||
|
|
5e909c73d7 | ||
|
|
e0c9f30efa | ||
|
|
515548a47a | ||
|
|
aa667ee396 | ||
|
|
7d6b313312 | ||
|
|
a84a693327 | ||
|
|
99afb4b750 | ||
|
|
21f135ba76 | ||
|
|
d7ee7b589f | ||
|
|
a8589d1ff3 | ||
|
|
dd9430e758 | ||
|
|
05f6cb42db | ||
|
|
5bdb93c2a6 | ||
|
|
613748804a | ||
|
|
86345a511f | ||
|
|
a24eedada7 | ||
|
|
4a728beba1 | ||
|
|
019597555f | ||
|
|
e4bfe50e8f | ||
|
|
0f826b0b0d | ||
|
|
7c2ff8c889 | ||
|
|
7a8ba4c9a0 | ||
|
|
219027f580 | ||
|
|
6a5ff5f223 | ||
|
|
f7a1cdbbc6 | ||
|
|
d547afeae0 | ||
|
|
dd108286df | ||
|
|
266df8a9b8 | ||
|
|
9c9b2829ae | ||
|
|
50e5886de1 | ||
|
|
ba1d740239 | ||
|
|
a190b2e85e | ||
|
|
3dd1630848 | ||
|
|
07d18dcab1 | ||
|
|
41905784f7 | ||
|
|
4013216fcc | ||
|
|
84f2ad5dea | ||
|
|
44b2bf91be | ||
|
|
660dee94af | ||
|
|
262a97f02b | ||
|
|
bd0fa9e2d2 | ||
|
|
d57c5cda71 | ||
|
|
99e1d6777f | ||
|
|
3c85a317d6 | ||
|
|
5231737369 | ||
|
|
d6059bdd2a | ||
|
|
48a2526d62 | ||
|
|
b29d2fd7f8 | ||
|
|
edfcb83473 | ||
|
|
478b4e3ed4 | ||
|
|
b8680b82c3 | ||
|
|
ac213c2e08 | ||
|
|
e880164c59 | ||
|
|
526bc33e02 | ||
|
|
181616deed | ||
|
|
e515b48929 | ||
|
|
8810eb8c39 | ||
|
|
748c0f5efa | ||
|
|
491f3d16dc | ||
|
|
872c134807 | ||
|
|
f721fdbf87 | ||
|
|
976cb5aaa8 | ||
|
|
b2def42bfd | ||
|
|
b9acef5301 | ||
|
|
58d0927767 | ||
|
|
7dd6e5efca | ||
|
|
5dc09e82c4 | ||
|
|
c2c70f7daf | ||
|
|
477da77b46 | ||
|
|
37b2d69bbc | ||
|
|
addb248e0b | ||
|
|
4b1281f9b7 | ||
|
|
dede14f689 | ||
|
|
5eb4d13aaa | ||
|
|
c30cdb0d68 | ||
|
|
2a0ec3b89d | ||
|
|
03b2a6a8aa | ||
|
|
9fbd504b4e | ||
|
|
9670f226e3 | ||
|
|
6863466653 | ||
|
|
3d5c5e8be5 | ||
|
|
65a9bf2dd5 | ||
|
|
ae9f8cda7e | ||
|
|
5d321e4b9a | ||
|
|
a9526831a4 | ||
|
|
ed0f79bdc5 | ||
|
|
98ee629d00 | ||
|
|
f73ea0bda2 | ||
|
|
c533f69d38 | ||
|
|
a2922bb944 | ||
|
|
95f30ecd1f | ||
|
|
f487355364 | ||
|
|
6e70979973 | ||
|
|
14d7acfad4 | ||
|
|
27c5e1b374 | ||
|
|
af96c6f4d3 | ||
|
|
d32db0bc45 | ||
|
|
7b593af7e1 | ||
|
|
3d3da2b460 | ||
|
|
31069ecf6a | ||
|
|
71578e2bf2 | ||
|
|
2430fcd462 | ||
|
|
f593a6e5f8 | ||
|
|
c91a05776f | ||
|
|
5f9a2cb337 | ||
|
|
8c902431ba | ||
|
|
a33c0748e3 | ||
|
|
306415391d | ||
|
|
d0f28b46cd | ||
|
|
da7dd58641 | ||
|
|
bde8d78b8a | ||
|
|
4dcaa42b6d | ||
|
|
76936f43ae | ||
|
|
f280726037 | ||
|
|
6cd595e438 | ||
|
|
17dd5071ef | ||
|
|
df7cf6c0eb | ||
|
|
3e573a5c6b | ||
|
|
7dfa455508 | ||
|
|
924d85a75e | ||
|
|
91695150cc | ||
|
|
3dd09a8795 | ||
|
|
d7739c4e37 | ||
|
|
c6a15f5026 | ||
|
|
2ca01ed747 | ||
|
|
1b64cb019e | ||
|
|
8c3af5bc62 | ||
|
|
0eabfa55f6 | ||
|
|
6408541075 | ||
|
|
13130c2c9f | ||
|
|
7680ae16c9 | ||
|
|
2c1bc4392f | ||
|
|
93f7bb8dd5 | ||
|
|
1d9c1d4166 | ||
|
|
3f151da314 | ||
|
|
6b95a79724 | ||
|
|
e3dae653e8 | ||
|
|
9de1f328ad | ||
|
|
506874cca9 | ||
|
|
2f2bbb4d06 | ||
|
|
95c3306798 | ||
|
|
df6824a008 | ||
|
|
dd11bf8a79 | ||
|
|
1cfda3d2d8 | ||
|
|
8b5349c7bc | ||
|
|
37de8a7f4a | ||
|
|
7a802ec0ff | ||
|
|
d2ecde2cbb | ||
|
|
248cfd5eb3 | ||
|
|
9da4c5340d | ||
|
|
f9d9bd6aa0 | ||
|
|
5fcef78c6a | ||
|
|
c104fd3494 | ||
|
|
57a76c9aee | ||
|
|
06f74068f4 | ||
|
|
e5d91b8e57 | ||
|
|
f6e092f6cc | ||
|
|
24ae0eee8e | ||
|
|
3c3fc6b268 | ||
|
|
b361440738 | ||
|
|
f0ee1d515b | ||
|
|
628ba81a77 | ||
|
|
bed7889703 | ||
|
|
03204f54ac | ||
|
|
037ce4c68f | ||
|
|
2fcd9819ac | ||
|
|
162e2c1ce5 | ||
|
|
1fe973fa5a | ||
|
|
d153f482dd | ||
|
|
1c960fbb80 | ||
|
|
915e56e1af | ||
|
|
fbb76a4d5d | ||
|
|
8bae98b314 | ||
|
|
fe51b3628e | ||
|
|
ba26eb3d5d | ||
|
|
cf844e2ad6 | ||
|
|
b697a842a5 | ||
|
|
bd3de8f39a | ||
|
|
cbf3cd6151 | ||
|
|
a9770e5d24 | ||
|
|
cf4ef5f3c7 | ||
|
|
afdfd12bdf | ||
|
|
d3861b4442 | ||
|
|
391f2aa56c | ||
|
|
bceec65913 | ||
|
|
9eff52d1a6 | ||
|
|
0186aef814 | ||
|
|
e503848990 | ||
|
|
90b3a98df7 | ||
|
|
d34990141e | ||
|
|
f20d064e05 | ||
|
|
f5e25c5f35 | ||
|
|
f4db76692f | ||
|
|
09bb5cf02f | ||
|
|
3b90df21d5 | ||
|
|
1654d3b329 | ||
|
|
aca6e5bf46 | ||
|
|
4fbe6ca401 | ||
|
|
233af7c74b | ||
|
|
641420c5e0 | ||
|
|
6fed9fd697 | ||
|
|
9c3f4f8dfd | ||
|
|
0644f0eb7d | ||
|
|
da3dd4867d | ||
|
|
e4d622aaaf | ||
|
|
fddedd51d9 | ||
|
|
5ab4b0afe8 | ||
|
|
5dea4d37d1 | ||
|
|
fc27ca9006 | ||
|
|
468a2ed4ec | ||
|
|
018b504f5b | ||
|
|
49f1758d74 | ||
|
|
c0b3554401 | ||
|
|
3de46c7755 | ||
|
|
8fd8e72cec | ||
|
|
78f6010207 | ||
|
|
06bfd0a3c0 | ||
|
|
764e79d051 | ||
|
|
0d08670f61 | ||
|
|
320408ef47 | ||
|
|
fb7e260a20 | ||
|
|
14a9d805b9 | ||
|
|
39de87869c | ||
|
|
473a239d83 | ||
|
|
0a93df5f9c | ||
|
|
8ea5dccea1 | ||
|
|
50f1afbd5b | ||
|
|
2fc81af06a | ||
|
|
6a9c4cfd0b | ||
|
|
884e601683 | ||
|
|
63b28c7816 | ||
|
|
e327327174 | ||
|
|
aa3ab6c6a0 | ||
|
|
04034d0b56 | ||
|
|
6341be45c6 | ||
|
|
e93d550b79 | ||
|
|
e21cef9bb5 | ||
|
|
e1627388d1 | ||
|
|
f15ba926cc | ||
|
|
5d098a32c9 | ||
|
|
ffdc8e5e1c | ||
|
|
940a161192 | ||
|
|
2b779af10f | ||
|
|
dd2eb49385 | ||
|
|
cf437900e0 | ||
|
|
466b4ec01d | ||
|
|
38d82edf0e | ||
|
|
90b503216c | ||
|
|
36c58b18a3 | ||
|
|
a412b9a465 | ||
|
|
82e8a2d763 | ||
|
|
2ede7aa8a1 | ||
|
|
889388f105 | ||
|
|
c7db2068c8 | ||
|
|
0d63dc3ec9 | ||
|
|
c6a01f2ed0 | ||
|
|
9107ed23b7 | ||
|
|
b1953a9627 | ||
|
|
bbe10e8be7 | ||
|
|
c4135d85e1 | ||
|
|
dd40fb68e4 | ||
|
|
767c20a869 | ||
|
|
5335bf9c34 | ||
|
|
f2c4ee41b9 | ||
|
|
0da4b11efb | ||
|
|
0b31223c7a | ||
|
|
fece2f5c77 | ||
|
|
545a7b291a | ||
|
|
f23af34729 | ||
|
|
6be1b4b113 | ||
|
|
3a02a13e38 | ||
|
|
66d36b8e41 | ||
|
|
2aa98ff3bc | ||
|
|
5ee070d21f | ||
|
|
f1dcaf3296 | ||
|
|
2cebe53545 | ||
|
|
32fc0737d6 | ||
|
|
4df491b922 | ||
|
|
1ad6222ebf | ||
|
|
5bc690408d | ||
|
|
3640ddfbf6 | ||
|
|
729ea933ea | ||
|
|
347146be29 | ||
|
|
7a5ea067e2 | ||
|
|
7301e05122 | ||
|
|
ca2f90742d | ||
|
|
414a4a71b4 | ||
|
|
7a369e8a55 | ||
|
|
45f1827fb7 | ||
|
|
05c326d445 | ||
|
|
4e62ffdb21 | ||
|
|
f522f50a08 | ||
|
|
1758187715 | ||
|
|
33b3e04049 | ||
|
|
23cfd32e64 | ||
|
|
285d056629 | ||
|
|
c452dabc3d | ||
|
|
f74f48e9e6 | ||
|
|
6a3a840b19 | ||
|
|
a3bfef35fd | ||
|
|
6797fcd9ab | ||
|
|
97d792b28f | ||
|
|
7ce264ce5f | ||
|
|
8a0407c7e6 | ||
|
|
06986e46a3 | ||
|
|
5897e773fd | ||
|
|
2657140c58 | ||
|
|
eacb068ac2 | ||
|
|
57be722c46 | ||
|
|
771ca56c88 | ||
|
|
ddd8566f41 | ||
|
|
192241cf2a | ||
|
|
3eb62873f6 | ||
|
|
0e36756383 | ||
|
|
fb46937413 | ||
|
|
79b65f3875 | ||
|
|
621e84d9a0 | ||
|
|
fdf73c6855 | ||
|
|
0f432ba551 | ||
|
|
d58edd98e9 | ||
|
|
5cf22f0596 | ||
|
|
f6e6f3d87a | ||
|
|
f40b0ed5e1 | ||
|
|
5d80dad99e | ||
|
|
e83c4b8e3e | ||
|
|
a2e5f7f3d8 | ||
|
|
2f6ad79a80 | ||
|
|
a89b86dc47 | ||
|
|
892e70ec84 | ||
|
|
56dbcd1524 | ||
|
|
234d6f9f3e | ||
|
|
5cb298c934 | ||
|
|
d0b1968a4c | ||
|
|
c79c4f9b14 | ||
|
|
f69a5c9134 | ||
|
|
a299fede9d | ||
|
|
f73de2004e | ||
|
|
cea2039b56 | ||
|
|
f7e14bb535 | ||
|
|
87961d8dcf | ||
|
|
fa1cf5ef34 | ||
|
|
3f0a57eb9b | ||
|
|
4cf633d5e9 | ||
|
|
b8e37ed944 | ||
|
|
0c36098c1f | ||
|
|
216c976399 | ||
|
|
259d10f0e4 | ||
|
|
b051781ddb | ||
|
|
53c679b59b | ||
|
|
4e05aab4f7 | ||
|
|
671ac699f1 | ||
|
|
9b6f3bc742 | ||
|
|
2980136d75 | ||
|
|
fb0fecd0b9 | ||
|
|
61547106f5 | ||
|
|
232beb3a3c | ||
|
|
ba02bba88c | ||
|
|
1fc2d11a14 | ||
|
|
b0ac0a9438 | ||
|
|
8a98f0dc5b | ||
|
|
c9c82e8f4d | ||
|
|
e60dad86ba | ||
|
|
f142898f52 | ||
|
|
3993d6ecc2 | ||
|
|
4d25bc6c92 | ||
|
|
87da71bace | ||
|
|
3ce1b8c705 | ||
|
|
5025ba959f | ||
|
|
13a6e9beaf | ||
|
|
5201c66108 | ||
|
|
e94ffd89d6 | ||
|
|
d63a0ca34b | ||
|
|
e3d75f564a | ||
|
|
8627048787 | ||
|
|
4dec901c76 | ||
|
|
5c41224a89 | ||
|
|
c8baada94a | ||
|
|
ede07434e0 | ||
|
|
44e2933bf8 | ||
|
|
7be06680ed | ||
|
|
87deec824a | ||
|
|
45cd2b0233 | ||
|
|
f510586372 | ||
|
|
137fafce4e | ||
|
|
b02a342750 | ||
|
|
51d03e65b2 | ||
|
|
3c7d6202ea | ||
|
|
9ed784098a | ||
|
|
531e3aa75e | ||
|
|
68b7fc3e2b | ||
|
|
9261ef3a15 | ||
|
|
a8795c9644 | ||
|
|
07b58a431f | ||
|
|
0aab34004b | ||
|
|
e0bf0258ee | ||
|
|
aff4d850bd | ||
|
|
ae3082dd31 | ||
|
|
243a79d291 | ||
|
|
9371a35e89 | ||
|
|
0e5239ffc3 | ||
|
|
b19b9535f6 | ||
|
|
46d39343d9 | ||
|
|
f2d698cb52 | ||
|
|
33646eb000 | ||
|
|
524b708f98 | ||
|
|
380f148db7 | ||
|
|
fc012aa8dc | ||
|
|
e5acc8a47b | ||
|
|
d4b5621e0a | ||
|
|
23ed7dc0e7 | ||
|
|
315b03b58d | ||
|
|
c225d63e9e | ||
|
|
b8dd5b1a2d | ||
|
|
366af6b73a | ||
|
|
f2f031fd57 | ||
|
|
12122bfc36 | ||
|
|
edb998ba23 | ||
|
|
5df54de801 | ||
|
|
b62da463e1 | ||
|
|
3cf9948b8d | ||
|
|
73260ad01f | ||
|
|
22a8c91448 | ||
|
|
a8945d24d1 | ||
|
|
6296590bf7 | ||
|
|
bcfb653816 | ||
|
|
e46cdc08cc | ||
|
|
8189c4e3fd | ||
|
|
6ffbcf45c6 | ||
|
|
643b5fcdc8 | ||
|
|
f38df51e8d | ||
|
|
1a934e8bfd | ||
|
|
5338220d3a | ||
|
|
a059760954 | ||
|
|
d7c70d09f0 | ||
|
|
c185c1c413 | ||
|
|
f50c43464c | ||
|
|
f45aaf0e35 | ||
|
|
8c9df8774e | ||
|
|
99c7fbfef7 | ||
|
|
1d9e109820 | ||
|
|
d25b0f65ea | ||
|
|
858634e1d0 | ||
|
|
474274583f | ||
|
|
d82c5f7b5c | ||
|
|
0c38e8637f | ||
|
|
1941eb315d | ||
|
|
9020860479 | ||
|
|
14edea1aff | ||
|
|
b68db61222 | ||
|
|
bb407cd624 | ||
|
|
32d66738b0 | ||
|
|
95e53ac535 | ||
|
|
7639c3d9e5 | ||
|
|
7ecd11accb | ||
|
|
17dffef5ec | ||
|
|
3e2a1297b5 | ||
|
|
323d3e506d | ||
|
|
ff2b66f42e | ||
|
|
8897781558 | ||
|
|
2fa9e23e04 | ||
|
|
cacf0688c6 | ||
|
|
88971fd034 | ||
|
|
7ec9be9c53 | ||
|
|
17c80c8a3d | ||
|
|
cfd39d6b55 | ||
|
|
32a453d7ba | ||
|
|
f9340ea0d5 | ||
|
|
ec398af41c | ||
|
|
54414221e4 | ||
|
|
40b6a5aad1 | ||
|
|
ab9cf73258 | ||
|
|
30c2783d2f | ||
|
|
1a40afa756 | ||
|
|
f96b480670 | ||
|
|
956509dfec | ||
|
|
586beb8318 | ||
|
|
427943907f | ||
|
|
739464fbc5 | ||
|
|
ca53ad7425 | ||
|
|
f6fde343a1 | ||
|
|
927004e349 | ||
|
|
83b464e4f7 | ||
|
|
ab7f9bb861 | ||
|
|
54cb509d64 | ||
|
|
885301486c | ||
|
|
7f8fdc9814 | ||
|
|
01a5f1991c | ||
|
|
76421c496d | ||
|
|
7845f62c22 | ||
|
|
ae72e247fa | ||
|
|
61561b9df7 | ||
|
|
782f7fb489 | ||
|
|
a80ef851f7 | ||
|
|
347aa3c225 | ||
|
|
95f7661170 | ||
|
|
a9c299c0be | ||
|
|
e52f4dc599 | ||
|
|
625e13bfde | ||
|
|
22112f8d14 | ||
|
|
c33f5c1a24 | ||
|
|
1a46daf621 | ||
|
|
987803781e | ||
|
|
0a96a9a023 | ||
|
|
af7b214476 | ||
|
|
1b9802a0d9 | ||
|
|
c15cf6ac06 | ||
|
|
c85c912562 | ||
|
|
ce19fc0f11 | ||
|
|
51ef725647 | ||
|
|
dc72021748 | ||
|
|
dfef2b41aa | ||
|
|
91482cd6a0 | ||
|
|
e3d3205cd9 | ||
|
|
7c809abe86 | ||
|
|
db6e1e1fe3 | ||
|
|
61ee72517c | ||
|
|
1cacc71050 | ||
|
|
fac990a656 | ||
|
|
fcd9ba8802 | ||
|
|
93cc60e805 | ||
|
|
d4bb28c59b | ||
|
|
ca6496c27c | ||
|
|
492beb62a8 | ||
|
|
e0b466bcfd | ||
|
|
287c81abf3 | ||
|
|
c05b5ef7b0 | ||
|
|
ddd079c8f8 | ||
|
|
b28c7da0a4 | ||
|
|
34d26d3687 | ||
|
|
471555b3a8 | ||
|
|
58e6a58eb7 | ||
|
|
8fc52bc56a | ||
|
|
49ebd472fa | ||
|
|
40017a9a11 | ||
|
|
a086b7aa00 | ||
|
|
9c311dfce5 | ||
|
|
a38d36ccd0 | ||
|
|
d5e081c7ae | ||
|
|
5879edbb09 | ||
|
|
f31014b18f | ||
|
|
5b3e9713dd | ||
|
|
b43930d4c9 | ||
|
|
bad780a197 | ||
|
|
0a4b7226fc | ||
|
|
0ec78b360c | ||
|
|
ecd0c0dfc5 | ||
|
|
83892d0d30 | ||
|
|
9d39615b7d | ||
|
|
a14665bde7 | ||
|
|
257fa1c53e | ||
|
|
afe475e9be | ||
|
|
51b2448e05 | ||
|
|
c34ffd2736 | ||
|
|
54e688277a | ||
|
|
3a01901d6c | ||
|
|
744e7d2790 | ||
|
|
a3e332af19 | ||
|
|
4678055173 | ||
|
|
d59acb8c5b | ||
|
|
49ae42bbe1 | ||
|
|
9b05ef6f39 | ||
|
|
187320b019 | ||
|
|
b345853918 | ||
|
|
a88e16152f |
11
AUTHORS.rst
11
AUTHORS.rst
@@ -44,4 +44,13 @@ Eric Myhre <hash at exultant.us>
|
|||||||
repository API.
|
repository API.
|
||||||
|
|
||||||
Muthu Subramanian <muthu.subramanian.karunanidhi at ericsson.com>
|
Muthu Subramanian <muthu.subramanian.karunanidhi at ericsson.com>
|
||||||
* Add SAML2 support for registration and logins.
|
* Add SAML2 support for registration and login.
|
||||||
|
|
||||||
|
Steven Hammerton <steven.hammerton at openmarket.com>
|
||||||
|
* Add CAS support for registration and login.
|
||||||
|
|
||||||
|
Mads Robin Christensen <mads at v42 dot dk>
|
||||||
|
* CentOS 7 installation instructions.
|
||||||
|
|
||||||
|
Florent Violleau <floviolleau at gmail dot com>
|
||||||
|
* Add Raspberry Pi installation instructions and general troubleshooting items
|
||||||
213
CHANGES.rst
213
CHANGES.rst
@@ -1,3 +1,216 @@
|
|||||||
|
Changes in synapse v0.13.0 (2016-02-10)
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
This version includes an upgrade of the schema, specifically adding an index to
|
||||||
|
the ``events`` table. This may cause synapse to pause for several minutes the
|
||||||
|
first time it is started after the upgrade.
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
* Improve general performance (PR #540, #543. #544, #54, #549, #567)
|
||||||
|
* Change guest user ids to be incrementing integers (PR #550)
|
||||||
|
* Improve performance of public room list API (PR #552)
|
||||||
|
* Change profile API to omit keys rather than return null (PR #557)
|
||||||
|
* Add ``/media/r0`` endpoint prefix, which is equivalent to ``/media/v1/``
|
||||||
|
(PR #595)
|
||||||
|
|
||||||
|
Bug fixes:
|
||||||
|
|
||||||
|
* Fix bug with upgrading guest accounts where it would fail if you opened the
|
||||||
|
registration email on a different device (PR #547)
|
||||||
|
* Fix bug where unread count could be wrong (PR #568)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Changes in synapse v0.12.1-rc1 (2016-01-29)
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
* Add unread notification counts in ``/sync`` (PR #456)
|
||||||
|
* Add support for inviting 3pids in ``/createRoom`` (PR #460)
|
||||||
|
* Add ability for guest accounts to upgrade (PR #462)
|
||||||
|
* Add ``/versions`` API (PR #468)
|
||||||
|
* Add ``event`` to ``/context`` API (PR #492)
|
||||||
|
* Add specific error code for invalid user names in ``/register`` (PR #499)
|
||||||
|
* Add support for push badge counts (PR #507)
|
||||||
|
* Add support for non-guest users to peek in rooms using ``/events`` (PR #510)
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
* Change ``/sync`` so that guest users only get rooms they've joined (PR #469)
|
||||||
|
* Change to require unbanning before other membership changes (PR #501)
|
||||||
|
* Change default push rules to notify for all messages (PR #486)
|
||||||
|
* Change default push rules to not notify on membership changes (PR #514)
|
||||||
|
* Change default push rules in one to one rooms to only notify for events that
|
||||||
|
are messages (PR #529)
|
||||||
|
* Change ``/sync`` to reject requests with a ``from`` query param (PR #512)
|
||||||
|
* Change server manhole to use SSH rather than telnet (PR #473)
|
||||||
|
* Change server to require AS users to be registered before use (PR #487)
|
||||||
|
* Change server not to start when ASes are invalidly configured (PR #494)
|
||||||
|
* Change server to require ID and ``as_token`` to be unique for AS's (PR #496)
|
||||||
|
* Change maximum pagination limit to 1000 (PR #497)
|
||||||
|
|
||||||
|
Bug fixes:
|
||||||
|
|
||||||
|
* Fix bug where ``/sync`` didn't return when something under the leave key
|
||||||
|
changed (PR #461)
|
||||||
|
* Fix bug where we returned smaller rather than larger than requested
|
||||||
|
thumbnails when ``method=crop`` (PR #464)
|
||||||
|
* Fix thumbnails API to only return cropped thumbnails when asking for a
|
||||||
|
cropped thumbnail (PR #475)
|
||||||
|
* Fix bug where we occasionally still logged access tokens (PR #477)
|
||||||
|
* Fix bug where ``/events`` would always return immediately for guest users
|
||||||
|
(PR #480)
|
||||||
|
* Fix bug where ``/sync`` unexpectedly returned old left rooms (PR #481)
|
||||||
|
* Fix enabling and disabling push rules (PR #498)
|
||||||
|
* Fix bug where ``/register`` returned 500 when given unicode username
|
||||||
|
(PR #513)
|
||||||
|
|
||||||
|
Changes in synapse v0.12.0 (2016-01-04)
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
* Expose ``/login`` under ``r0`` (PR #459)
|
||||||
|
|
||||||
|
Changes in synapse v0.12.0-rc3 (2015-12-23)
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
* Allow guest accounts access to ``/sync`` (PR #455)
|
||||||
|
* Allow filters to include/exclude rooms at the room level
|
||||||
|
rather than just from the components of the sync for each
|
||||||
|
room. (PR #454)
|
||||||
|
* Include urls for room avatars in the response to ``/publicRooms`` (PR #453)
|
||||||
|
* Don't set a identicon as the avatar for a user when they register (PR #450)
|
||||||
|
* Add a ``display_name`` to third-party invites (PR #449)
|
||||||
|
* Send more information to the identity server for third-party invites so that
|
||||||
|
it can send richer messages to the invitee (PR #446)
|
||||||
|
* Cache the responses to ``/initialSync`` for 5 minutes. If a client
|
||||||
|
retries a request to ``/initialSync`` before the a response was computed
|
||||||
|
to the first request then the same response is used for both requests
|
||||||
|
(PR #457)
|
||||||
|
* Fix a bug where synapse would always request the signing keys of
|
||||||
|
remote servers even when the key was cached locally (PR #452)
|
||||||
|
* Fix 500 when pagination search results (PR #447)
|
||||||
|
* Fix a bug where synapse was leaking raw email address in third-party invites
|
||||||
|
(PR #448)
|
||||||
|
|
||||||
|
Changes in synapse v0.12.0-rc2 (2015-12-14)
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
* Add caches for whether rooms have been forgotten by a user (PR #434)
|
||||||
|
* Remove instructions to use ``--process-dependency-link`` since all of the
|
||||||
|
dependencies of synapse are on PyPI (PR #436)
|
||||||
|
* Parallelise the processing of ``/sync`` requests (PR #437)
|
||||||
|
* Fix race updating presence in ``/events`` (PR #444)
|
||||||
|
* Fix bug back-populating search results (PR #441)
|
||||||
|
* Fix bug calculating state in ``/sync`` requests (PR #442)
|
||||||
|
|
||||||
|
Changes in synapse v0.12.0-rc1 (2015-12-10)
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
* Host the client APIs released as r0 by
|
||||||
|
https://matrix.org/docs/spec/r0.0.0/client_server.html
|
||||||
|
on paths prefixed by ``/_matrix/client/r0``. (PR #430, PR #415, PR #400)
|
||||||
|
* Updates the client APIs to match r0 of the matrix specification.
|
||||||
|
|
||||||
|
* All APIs return events in the new event format, old APIs also include
|
||||||
|
the fields needed to parse the event using the old format for
|
||||||
|
compatibility. (PR #402)
|
||||||
|
* Search results are now given as a JSON array rather than
|
||||||
|
a JSON object (PR #405)
|
||||||
|
* Miscellaneous changes to search (PR #403, PR #406, PR #412)
|
||||||
|
* Filter JSON objects may now be passed as query parameters to ``/sync``
|
||||||
|
(PR #431)
|
||||||
|
* Fix implementation of ``/admin/whois`` (PR #418)
|
||||||
|
* Only include the rooms that user has left in ``/sync`` if the client
|
||||||
|
requests them in the filter (PR #423)
|
||||||
|
* Don't push for ``m.room.message`` by default (PR #411)
|
||||||
|
* Add API for setting per account user data (PR #392)
|
||||||
|
* Allow users to forget rooms (PR #385)
|
||||||
|
|
||||||
|
* Performance improvements and monitoring:
|
||||||
|
|
||||||
|
* Add per-request counters for CPU time spent on the main python thread.
|
||||||
|
(PR #421, PR #420)
|
||||||
|
* Add per-request counters for time spent in the database (PR #429)
|
||||||
|
* Make state updates in the C+S API idempotent (PR #416)
|
||||||
|
* Only fire ``user_joined_room`` if the user has actually joined. (PR #410)
|
||||||
|
* Reuse a single http client, rather than creating new ones (PR #413)
|
||||||
|
|
||||||
|
* Fixed a bug upgrading from older versions of synapse on postgresql (PR #417)
|
||||||
|
|
||||||
|
Changes in synapse v0.11.1 (2015-11-20)
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
* Add extra options to search API (PR #394)
|
||||||
|
* Fix bug where we did not correctly cap federation retry timers. This meant it
|
||||||
|
could take several hours for servers to start talking to ressurected servers,
|
||||||
|
even when they were receiving traffic from them (PR #393)
|
||||||
|
* Don't advertise login token flow unless CAS is enabled. This caused issues
|
||||||
|
where some clients would always use the fallback API if they did not
|
||||||
|
recognize all login flows (PR #391)
|
||||||
|
* Change /v2 sync API to rename ``private_user_data`` to ``account_data``
|
||||||
|
(PR #386)
|
||||||
|
* Change /v2 sync API to remove the ``event_map`` and rename keys in ``rooms``
|
||||||
|
object (PR #389)
|
||||||
|
|
||||||
|
Changes in synapse v0.11.0-r2 (2015-11-19)
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
* Fix bug in database port script (PR #387)
|
||||||
|
|
||||||
|
Changes in synapse v0.11.0-r1 (2015-11-18)
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
* Retry and fail federation requests more aggressively for requests that block
|
||||||
|
client side requests (PR #384)
|
||||||
|
|
||||||
|
Changes in synapse v0.11.0 (2015-11-17)
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
* Change CAS login API (PR #349)
|
||||||
|
|
||||||
|
Changes in synapse v0.11.0-rc2 (2015-11-13)
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
* Various changes to /sync API response format (PR #373)
|
||||||
|
* Fix regression when setting display name in newly joined room over
|
||||||
|
federation (PR #368)
|
||||||
|
* Fix problem where /search was slow when using SQLite (PR #366)
|
||||||
|
|
||||||
|
Changes in synapse v0.11.0-rc1 (2015-11-11)
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
* Add Search API (PR #307, #324, #327, #336, #350, #359)
|
||||||
|
* Add 'archived' state to v2 /sync API (PR #316)
|
||||||
|
* Add ability to reject invites (PR #317)
|
||||||
|
* Add config option to disable password login (PR #322)
|
||||||
|
* Add the login fallback API (PR #330)
|
||||||
|
* Add room context API (PR #334)
|
||||||
|
* Add room tagging support (PR #335)
|
||||||
|
* Update v2 /sync API to match spec (PR #305, #316, #321, #332, #337, #341)
|
||||||
|
* Change retry schedule for application services (PR #320)
|
||||||
|
* Change retry schedule for remote servers (PR #340)
|
||||||
|
* Fix bug where we hosted static content in the incorrect place (PR #329)
|
||||||
|
* Fix bug where we didn't increment retry interval for remote servers (PR #343)
|
||||||
|
|
||||||
|
Changes in synapse v0.10.1-rc1 (2015-10-15)
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
* Add support for CAS, thanks to Steven Hammerton (PR #295, #296)
|
||||||
|
* Add support for using macaroons for ``access_token`` (PR #256, #229)
|
||||||
|
* Add support for ``m.room.canonical_alias`` (PR #287)
|
||||||
|
* Add support for viewing the history of rooms that they have left. (PR #276,
|
||||||
|
#294)
|
||||||
|
* Add support for refresh tokens (PR #240)
|
||||||
|
* Add flag on creation which disables federation of the room (PR #279)
|
||||||
|
* Add some room state to invites. (PR #275)
|
||||||
|
* Atomically persist events when joining a room over federation (PR #283)
|
||||||
|
* Change default history visibility for private rooms (PR #271)
|
||||||
|
* Allow users to redact their own sent events (PR #262)
|
||||||
|
* Use tox for tests (PR #247)
|
||||||
|
* Split up syutil into separate libraries (PR #243)
|
||||||
|
|
||||||
Changes in synapse v0.10.0-r2 (2015-09-16)
|
Changes in synapse v0.10.0-r2 (2015-09-16)
|
||||||
==========================================
|
==========================================
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,11 @@ recursive-include scripts *
|
|||||||
recursive-include scripts-dev *
|
recursive-include scripts-dev *
|
||||||
recursive-include tests *.py
|
recursive-include tests *.py
|
||||||
|
|
||||||
recursive-include static *.css
|
recursive-include synapse/static *.css
|
||||||
recursive-include static *.html
|
recursive-include synapse/static *.gif
|
||||||
recursive-include static *.js
|
recursive-include synapse/static *.html
|
||||||
|
recursive-include synapse/static *.js
|
||||||
|
|
||||||
|
exclude jenkins.sh
|
||||||
|
|
||||||
prune demo/etc
|
prune demo/etc
|
||||||
|
|||||||
103
README.rst
103
README.rst
@@ -20,8 +20,8 @@ The overall architecture is::
|
|||||||
https://somewhere.org/_matrix https://elsewhere.net/_matrix
|
https://somewhere.org/_matrix https://elsewhere.net/_matrix
|
||||||
|
|
||||||
``#matrix:matrix.org`` is the official support room for Matrix, and can be
|
``#matrix:matrix.org`` is the official support room for Matrix, and can be
|
||||||
accessed by the web client at http://matrix.org/beta or via an IRC bridge at
|
accessed by any client from https://matrix.org/blog/try-matrix-now or via IRC
|
||||||
irc://irc.freenode.net/matrix.
|
bridge at 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!
|
||||||
@@ -77,14 +77,14 @@ Meanwhile, iOS and Android SDKs and clients are available from:
|
|||||||
- https://github.com/matrix-org/matrix-android-sdk
|
- https://github.com/matrix-org/matrix-android-sdk
|
||||||
|
|
||||||
We'd like to invite you to join #matrix:matrix.org (via
|
We'd like to invite you to join #matrix:matrix.org (via
|
||||||
https://matrix.org/beta), run a homeserver, take a look at the Matrix spec at
|
https://matrix.org/blog/try-matrix-now), run a homeserver, take a look at the
|
||||||
https://matrix.org/docs/spec and API docs at https://matrix.org/docs/api,
|
Matrix spec at https://matrix.org/docs/spec and API docs at
|
||||||
experiment with the APIs and the demo clients, and report any bugs via
|
https://matrix.org/docs/api, experiment with the APIs and the demo clients, and
|
||||||
https://matrix.org/jira.
|
report any bugs via https://matrix.org/jira.
|
||||||
|
|
||||||
Thanks for using Matrix!
|
Thanks for using Matrix!
|
||||||
|
|
||||||
[1] End-to-end encryption is currently in development
|
[1] End-to-end encryption is currently in development - see https://matrix.org/git/olm
|
||||||
|
|
||||||
Synapse Installation
|
Synapse Installation
|
||||||
====================
|
====================
|
||||||
@@ -111,18 +111,35 @@ 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 CentOS 7::
|
||||||
|
|
||||||
|
sudo yum install libtiff-devel libjpeg-devel libzip-devel freetype-devel \
|
||||||
|
lcms2-devel libwebp-devel tcl-devel tk-devel \
|
||||||
|
python-virtualenv libffi-devel openssl-devel
|
||||||
|
sudo yum groupinstall "Development Tools"
|
||||||
|
|
||||||
|
|
||||||
Installing prerequisites on Mac OS X::
|
Installing prerequisites on Mac OS X::
|
||||||
|
|
||||||
xcode-select --install
|
xcode-select --install
|
||||||
sudo easy_install pip
|
sudo easy_install pip
|
||||||
sudo pip install virtualenv
|
sudo pip install virtualenv
|
||||||
|
|
||||||
|
Installing prerequisites on Raspbian::
|
||||||
|
|
||||||
|
sudo apt-get install build-essential python2.7-dev libffi-dev \
|
||||||
|
python-pip python-setuptools sqlite3 \
|
||||||
|
libssl-dev python-virtualenv libjpeg-dev
|
||||||
|
sudo pip install --upgrade pip
|
||||||
|
sudo pip install --upgrade ndg-httpsclient
|
||||||
|
sudo pip install --upgrade virtualenv
|
||||||
|
|
||||||
To install the synapse homeserver run::
|
To install the synapse homeserver run::
|
||||||
|
|
||||||
virtualenv -p python2.7 ~/.synapse
|
virtualenv -p python2.7 ~/.synapse
|
||||||
source ~/.synapse/bin/activate
|
source ~/.synapse/bin/activate
|
||||||
pip install --upgrade setuptools
|
pip install --upgrade setuptools
|
||||||
pip install --process-dependency-links https://github.com/matrix-org/synapse/tarball/master
|
pip install 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``. Feel free to pick a different directory
|
environment under ``~/.synapse``. Feel free to pick a different directory
|
||||||
@@ -133,15 +150,25 @@ 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/.
|
||||||
|
|
||||||
|
Another alternative is to install via apt from http://matrix.org/packages/debian/.
|
||||||
|
Note that these packages do not include a client - choose one from
|
||||||
|
https://matrix.org/blog/try-matrix-now/ (or build your own with
|
||||||
|
https://github.com/matrix-org/matrix-js-sdk/).
|
||||||
|
|
||||||
|
Finally, Martin Giess has created an auto-deployment process with vagrant/ansible,
|
||||||
|
tested with VirtualBox/AWS/DigitalOcean - see https://github.com/EMnify/matrix-synapse-auto-deploy
|
||||||
|
for details.
|
||||||
|
|
||||||
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 \
|
||||||
|
--report-stats=[yes|no]
|
||||||
|
|
||||||
Substituting your host and domain name as appropriate.
|
...substituting your host and domain name as appropriate.
|
||||||
|
|
||||||
This will generate you a config file that you can then customise, but it will
|
This will generate you a config file that you can then customise, but it will
|
||||||
also generate a set of keys for you. These keys will allow your Home Server to
|
also generate a set of keys for you. These keys will allow your Home Server to
|
||||||
@@ -149,15 +176,15 @@ identify itself to other Home Servers, so don't lose or delete them. It would be
|
|||||||
wise to back them up somewhere safe. If, for whatever reason, you do need to
|
wise to back them up somewhere safe. If, for whatever reason, you do need to
|
||||||
change your Home Server's keys, you may find that other Home Servers have the
|
change your Home Server's keys, you may find that other Home Servers have the
|
||||||
old key cached. If you update the signing key, you should change the name of the
|
old key cached. If you update the signing key, you should change the name of the
|
||||||
key in the <server name>.signing.key file (the second word, which by default is
|
key in the <server name>.signing.key file (the second word) to something different.
|
||||||
, 'auto') to something different.
|
|
||||||
|
|
||||||
By default, registration of new users is disabled. You can either enable
|
By default, registration of new users is disabled. You can either enable
|
||||||
registration in the config by specifying ``enable_registration: true``
|
registration in the config by specifying ``enable_registration: true``
|
||||||
(it is then recommended to also set up CAPTCHA), or
|
(it is then recommended to also set up CAPTCHA - see docs/CAPTCHA_SETUP), or
|
||||||
you can use the command line to register new users::
|
you can use the command line to register new users::
|
||||||
|
|
||||||
$ source ~/.synapse/bin/activate
|
$ source ~/.synapse/bin/activate
|
||||||
|
$ synctl start # if not already running
|
||||||
$ register_new_matrix_user -c homeserver.yaml https://localhost:8448
|
$ register_new_matrix_user -c homeserver.yaml https://localhost:8448
|
||||||
New user localpart: erikj
|
New user localpart: erikj
|
||||||
Password:
|
Password:
|
||||||
@@ -167,6 +194,16 @@ you can use the command line to register new users::
|
|||||||
For reliable VoIP calls to be routed via this homeserver, you MUST configure
|
For reliable VoIP calls to be routed via this homeserver, you MUST configure
|
||||||
a TURN server. See docs/turn-howto.rst for details.
|
a TURN server. See docs/turn-howto.rst for details.
|
||||||
|
|
||||||
|
Running Synapse
|
||||||
|
===============
|
||||||
|
|
||||||
|
To actually run your new homeserver, pick a working directory for Synapse to
|
||||||
|
run (e.g. ``~/.synapse``), and::
|
||||||
|
|
||||||
|
cd ~/.synapse
|
||||||
|
source ./bin/activate
|
||||||
|
synctl start
|
||||||
|
|
||||||
Using PostgreSQL
|
Using PostgreSQL
|
||||||
================
|
================
|
||||||
|
|
||||||
@@ -189,16 +226,6 @@ may have a few regressions relative to SQLite.
|
|||||||
For information on how to install and use PostgreSQL, please see
|
For information on how to install and use PostgreSQL, please see
|
||||||
`docs/postgres.rst <docs/postgres.rst>`_.
|
`docs/postgres.rst <docs/postgres.rst>`_.
|
||||||
|
|
||||||
Running Synapse
|
|
||||||
===============
|
|
||||||
|
|
||||||
To actually run your new homeserver, pick a working directory for Synapse to
|
|
||||||
run (e.g. ``~/.synapse``), and::
|
|
||||||
|
|
||||||
cd ~/.synapse
|
|
||||||
source ./bin/activate
|
|
||||||
synctl start
|
|
||||||
|
|
||||||
Platform Specific Instructions
|
Platform Specific Instructions
|
||||||
==============================
|
==============================
|
||||||
|
|
||||||
@@ -220,8 +247,7 @@ pip may be outdated (6.0.7-1 and needs to be upgraded to 6.0.8-1 )::
|
|||||||
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 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
|
||||||
@@ -241,6 +267,14 @@ During setup of Synapse you need to call python2.7 directly again::
|
|||||||
|
|
||||||
...substituting your host and domain name as appropriate.
|
...substituting your host and domain name as appropriate.
|
||||||
|
|
||||||
|
FreeBSD
|
||||||
|
-------
|
||||||
|
|
||||||
|
Synapse can be installed via FreeBSD Ports or Packages:
|
||||||
|
|
||||||
|
- Ports: ``cd /usr/ports/net/py-matrix-synapse && make install clean``
|
||||||
|
- Packages: ``pkg install py27-matrix-synapse``
|
||||||
|
|
||||||
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:
|
||||||
@@ -280,12 +314,23 @@ 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 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
|
||||||
|
|
||||||
|
Installing may fail with ``Could not find any downloads that satisfy the requirement pymacaroons-pynacl (from matrix-synapse==0.12.0)``.
|
||||||
|
You can fix this by manually upgrading pip and virtualenv::
|
||||||
|
|
||||||
|
sudo pip install --upgrade virtualenv
|
||||||
|
|
||||||
|
You can next rerun ``virtualenv -p python2.7 synapse`` to update the virtual env.
|
||||||
|
|
||||||
|
Installing may fail during installing virtualenv with ``InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning.``
|
||||||
|
You can fix this by manually installing ndg-httpsclient::
|
||||||
|
|
||||||
|
pip install --upgrade ndg-httpsclient
|
||||||
|
|
||||||
Installing may fail with ``mock requires setuptools>=17.1. Aborting installation``.
|
Installing may fail with ``mock requires setuptools>=17.1. Aborting installation``.
|
||||||
You can fix this by upgrading setuptools::
|
You can fix this by upgrading setuptools::
|
||||||
|
|
||||||
@@ -425,6 +470,10 @@ SRV record, as that is the name other machines will expect it to have::
|
|||||||
python -m synapse.app.homeserver --config-path homeserver.yaml
|
python -m synapse.app.homeserver --config-path homeserver.yaml
|
||||||
|
|
||||||
|
|
||||||
|
If you've already generated the config file, you need to edit the "server_name"
|
||||||
|
in you ```homeserver.yaml``` file. If you've already started Synapse and a
|
||||||
|
database has been created, you will have to recreate the database.
|
||||||
|
|
||||||
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
|
||||||
increase the verbosity of logging output; at least for initial testing.
|
increase the verbosity of logging output; at least for initial testing.
|
||||||
|
|
||||||
|
|||||||
13
UPGRADE.rst
13
UPGRADE.rst
@@ -30,6 +30,19 @@ running:
|
|||||||
python synapse/python_dependencies.py | xargs -n1 pip install
|
python synapse/python_dependencies.py | xargs -n1 pip install
|
||||||
|
|
||||||
|
|
||||||
|
Upgrading to v0.11.0
|
||||||
|
====================
|
||||||
|
|
||||||
|
This release includes the option to send anonymous usage stats to matrix.org,
|
||||||
|
and requires that administrators explictly opt in or out by setting the
|
||||||
|
``report_stats`` option to either ``true`` or ``false``.
|
||||||
|
|
||||||
|
We would really appreciate it if you could help our project out by reporting
|
||||||
|
anonymized usage statistics from your homeserver. Only very basic aggregate
|
||||||
|
data (e.g. number of users) will be reported, but it helps us to track the
|
||||||
|
growth of the Matrix community, and helps us to make Matrix a success, as well
|
||||||
|
as to convince other networks that they should peer with us.
|
||||||
|
|
||||||
|
|
||||||
Upgrading to v0.9.0
|
Upgrading to v0.9.0
|
||||||
===================
|
===================
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
151
contrib/graph/graph3.py
Normal file
151
contrib/graph/graph3.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# Copyright 2016 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.
|
||||||
|
|
||||||
|
|
||||||
|
import pydot
|
||||||
|
import cgi
|
||||||
|
import simplejson as json
|
||||||
|
import datetime
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from synapse.events import FrozenEvent
|
||||||
|
from synapse.util.frozenutils import unfreeze
|
||||||
|
|
||||||
|
|
||||||
|
def make_graph(file_name, room_id, file_prefix, limit):
|
||||||
|
print "Reading lines"
|
||||||
|
with open(file_name) as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
print "Read lines"
|
||||||
|
|
||||||
|
events = [FrozenEvent(json.loads(line)) for line in lines]
|
||||||
|
|
||||||
|
print "Loaded events."
|
||||||
|
|
||||||
|
events.sort(key=lambda e: e.depth)
|
||||||
|
|
||||||
|
print "Sorted events"
|
||||||
|
|
||||||
|
if limit:
|
||||||
|
events = events[-int(limit):]
|
||||||
|
|
||||||
|
node_map = {}
|
||||||
|
|
||||||
|
graph = pydot.Dot(graph_name="Test")
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
t = datetime.datetime.fromtimestamp(
|
||||||
|
float(event.origin_server_ts) / 1000
|
||||||
|
).strftime('%Y-%m-%d %H:%M:%S,%f')
|
||||||
|
|
||||||
|
content = json.dumps(unfreeze(event.get_dict()["content"]), indent=4)
|
||||||
|
content = content.replace("\n", "<br/>\n")
|
||||||
|
|
||||||
|
print content
|
||||||
|
content = []
|
||||||
|
for key, value in unfreeze(event.get_dict()["content"]).items():
|
||||||
|
if value is None:
|
||||||
|
value = "<null>"
|
||||||
|
elif isinstance(value, basestring):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
value = json.dumps(value)
|
||||||
|
|
||||||
|
content.append(
|
||||||
|
"<b>%s</b>: %s," % (
|
||||||
|
cgi.escape(key, quote=True).encode("ascii", 'xmlcharrefreplace'),
|
||||||
|
cgi.escape(value, quote=True).encode("ascii", 'xmlcharrefreplace'),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
content = "<br/>\n".join(content)
|
||||||
|
|
||||||
|
print content
|
||||||
|
|
||||||
|
label = (
|
||||||
|
"<"
|
||||||
|
"<b>%(name)s </b><br/>"
|
||||||
|
"Type: <b>%(type)s </b><br/>"
|
||||||
|
"State key: <b>%(state_key)s </b><br/>"
|
||||||
|
"Content: <b>%(content)s </b><br/>"
|
||||||
|
"Time: <b>%(time)s </b><br/>"
|
||||||
|
"Depth: <b>%(depth)s </b><br/>"
|
||||||
|
">"
|
||||||
|
) % {
|
||||||
|
"name": event.event_id,
|
||||||
|
"type": event.type,
|
||||||
|
"state_key": event.get("state_key", None),
|
||||||
|
"content": content,
|
||||||
|
"time": t,
|
||||||
|
"depth": event.depth,
|
||||||
|
}
|
||||||
|
|
||||||
|
node = pydot.Node(
|
||||||
|
name=event.event_id,
|
||||||
|
label=label,
|
||||||
|
)
|
||||||
|
|
||||||
|
node_map[event.event_id] = node
|
||||||
|
graph.add_node(node)
|
||||||
|
|
||||||
|
print "Created Nodes"
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
for prev_id, _ in event.prev_events:
|
||||||
|
try:
|
||||||
|
end_node = node_map[prev_id]
|
||||||
|
except:
|
||||||
|
end_node = pydot.Node(
|
||||||
|
name=prev_id,
|
||||||
|
label="<<b>%s</b>>" % (prev_id,),
|
||||||
|
)
|
||||||
|
|
||||||
|
node_map[prev_id] = end_node
|
||||||
|
graph.add_node(end_node)
|
||||||
|
|
||||||
|
edge = pydot.Edge(node_map[event.event_id], end_node)
|
||||||
|
graph.add_edge(edge)
|
||||||
|
|
||||||
|
print "Created edges"
|
||||||
|
|
||||||
|
graph.write('%s.dot' % file_prefix, format='raw', prog='dot')
|
||||||
|
|
||||||
|
print "Created Dot"
|
||||||
|
|
||||||
|
graph.write_svg("%s.svg" % file_prefix, prog='dot')
|
||||||
|
|
||||||
|
print "Created svg"
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Generate a PDU graph for a given room by reading "
|
||||||
|
"from a file with line deliminated events. \n"
|
||||||
|
"Requires pydot."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p", "--prefix", dest="prefix",
|
||||||
|
help="String to prefix output files with",
|
||||||
|
default="graph_output"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-l", "--limit",
|
||||||
|
help="Only retrieve the last N events.",
|
||||||
|
)
|
||||||
|
parser.add_argument('event_file')
|
||||||
|
parser.add_argument('room')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
make_graph(args.event_file, args.room, args.prefix, args.limit)
|
||||||
@@ -38,6 +38,13 @@ for port in 8080 8081 8082; do
|
|||||||
|
|
||||||
perl -p -i -e 's/^enable_registration:.*/enable_registration: true/g' $DIR/etc/$port.config
|
perl -p -i -e 's/^enable_registration:.*/enable_registration: true/g' $DIR/etc/$port.config
|
||||||
|
|
||||||
|
if ! grep -F "full_twisted_stacktraces" -q $DIR/etc/$port.config; then
|
||||||
|
echo "full_twisted_stacktraces: true" >> $DIR/etc/$port.config
|
||||||
|
fi
|
||||||
|
if ! grep -F "report_stats" -q $DIR/etc/$port.config ; then
|
||||||
|
echo "report_stats: false" >> $DIR/etc/$port.config
|
||||||
|
fi
|
||||||
|
|
||||||
python -m synapse.app.homeserver \
|
python -m synapse.app.homeserver \
|
||||||
--config-path "$DIR/etc/$port.config" \
|
--config-path "$DIR/etc/$port.config" \
|
||||||
-D \
|
-D \
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ encoding use, e.g.::
|
|||||||
This would create an appropriate database named ``synapse`` owned by the
|
This would create an appropriate database named ``synapse`` owned by the
|
||||||
``synapse_user`` user (which must already exist).
|
``synapse_user`` user (which must already exist).
|
||||||
|
|
||||||
Set up client
|
Set up client in Debian/Ubuntu
|
||||||
=============
|
===========================
|
||||||
|
|
||||||
Postgres support depends on the postgres python connector ``psycopg2``. In the
|
Postgres support depends on the postgres python connector ``psycopg2``. In the
|
||||||
virtual env::
|
virtual env::
|
||||||
@@ -27,6 +27,19 @@ virtual env::
|
|||||||
sudo apt-get install libpq-dev
|
sudo apt-get install libpq-dev
|
||||||
pip install psycopg2
|
pip install psycopg2
|
||||||
|
|
||||||
|
Set up client in RHEL/CentOs 7
|
||||||
|
==============================
|
||||||
|
|
||||||
|
Make sure you have the appropriate version of postgres-devel installed. For a
|
||||||
|
postgres 9.4, use the postgres 9.4 packages from
|
||||||
|
[here](https://wiki.postgresql.org/wiki/YUM_Installation).
|
||||||
|
|
||||||
|
As with Debian/Ubuntu, postgres support depends on the postgres python connector
|
||||||
|
``psycopg2``. In the virtual env::
|
||||||
|
|
||||||
|
sudo yum install postgresql-devel libpqxx-devel.x86_64
|
||||||
|
export PATH=/usr/pgsql-9.4/bin/:$PATH
|
||||||
|
pip install psycopg2
|
||||||
|
|
||||||
Synapse config
|
Synapse config
|
||||||
==============
|
==============
|
||||||
|
|||||||
81
jenkins.sh
Executable file
81
jenkins.sh
Executable file
@@ -0,0 +1,81 @@
|
|||||||
|
#!/bin/bash -eu
|
||||||
|
|
||||||
|
export PYTHONDONTWRITEBYTECODE=yep
|
||||||
|
|
||||||
|
# Output test results as junit xml
|
||||||
|
export TRIAL_FLAGS="--reporter=subunit"
|
||||||
|
export TOXSUFFIX="| subunit-1to2 | subunit2junitxml --no-passthrough --output-to=results.xml"
|
||||||
|
# Write coverage reports to a separate file for each process
|
||||||
|
export COVERAGE_OPTS="-p"
|
||||||
|
export DUMP_COVERAGE_COMMAND="coverage help"
|
||||||
|
|
||||||
|
# Output flake8 violations to violations.flake8.log
|
||||||
|
# Don't exit with non-0 status code on Jenkins,
|
||||||
|
# so that the build steps continue and a later step can decided whether to
|
||||||
|
# UNSTABLE or FAILURE this build.
|
||||||
|
export PEP8SUFFIX="--output-file=violations.flake8.log || echo flake8 finished with status code \$?"
|
||||||
|
|
||||||
|
rm .coverage* || echo "No coverage files to remove"
|
||||||
|
|
||||||
|
tox
|
||||||
|
|
||||||
|
: ${GIT_BRANCH:="origin/$(git rev-parse --abbrev-ref HEAD)"}
|
||||||
|
|
||||||
|
TOX_BIN=$WORKSPACE/.tox/py27/bin
|
||||||
|
|
||||||
|
if [[ ! -e .sytest-base ]]; then
|
||||||
|
git clone https://github.com/matrix-org/sytest.git .sytest-base --mirror
|
||||||
|
else
|
||||||
|
(cd .sytest-base; git fetch -p)
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf sytest
|
||||||
|
git clone .sytest-base sytest --shared
|
||||||
|
cd sytest
|
||||||
|
|
||||||
|
git checkout "${GIT_BRANCH}" || (echo >&2 "No ref ${GIT_BRANCH} found, falling back to develop" ; git checkout develop)
|
||||||
|
|
||||||
|
: ${PERL5LIB:=$WORKSPACE/perl5/lib/perl5}
|
||||||
|
: ${PERL_MB_OPT:=--install_base=$WORKSPACE/perl5}
|
||||||
|
: ${PERL_MM_OPT:=INSTALL_BASE=$WORKSPACE/perl5}
|
||||||
|
export PERL5LIB PERL_MB_OPT PERL_MM_OPT
|
||||||
|
|
||||||
|
./install-deps.pl
|
||||||
|
|
||||||
|
: ${PORT_BASE:=8000}
|
||||||
|
|
||||||
|
echo >&2 "Running sytest with SQLite3";
|
||||||
|
./run-tests.pl --coverage -O tap --synapse-directory $WORKSPACE \
|
||||||
|
--python $TOX_BIN/python --all --port-base $PORT_BASE > results-sqlite3.tap
|
||||||
|
|
||||||
|
RUN_POSTGRES=""
|
||||||
|
|
||||||
|
for port in $(($PORT_BASE + 1)) $(($PORT_BASE + 2)); do
|
||||||
|
if psql synapse_jenkins_$port <<< ""; then
|
||||||
|
RUN_POSTGRES="$RUN_POSTGRES:$port"
|
||||||
|
cat > localhost-$port/database.yaml << EOF
|
||||||
|
name: psycopg2
|
||||||
|
args:
|
||||||
|
database: synapse_jenkins_$port
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Run if both postgresql databases exist
|
||||||
|
if test "$RUN_POSTGRES" = ":$(($PORT_BASE + 1)):$(($PORT_BASE + 2))"; then
|
||||||
|
echo >&2 "Running sytest with PostgreSQL";
|
||||||
|
$TOX_BIN/pip install psycopg2
|
||||||
|
./run-tests.pl --coverage -O tap --synapse-directory $WORKSPACE \
|
||||||
|
--python $TOX_BIN/python --all --port-base $PORT_BASE > results-postgresql.tap
|
||||||
|
else
|
||||||
|
echo >&2 "Skipping running sytest with PostgreSQL, $RUN_POSTGRES"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
cp sytest/.coverage.* .
|
||||||
|
|
||||||
|
# Combine the coverage reports
|
||||||
|
echo "Combining:" .coverage.*
|
||||||
|
$TOX_BIN/python -m coverage combine
|
||||||
|
# Output coverage to coverage.xml
|
||||||
|
$TOX_BIN/coverage xml -o coverage.xml
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/perl -pi
|
#!/usr/bin/perl -pi
|
||||||
# Copyright 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
$copyright = <<EOT;
|
$copyright = <<EOT;
|
||||||
/* Copyright 2015 OpenMarket Ltd
|
/* Copyright 2016 OpenMarket Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/perl -pi
|
#!/usr/bin/perl -pi
|
||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
$copyright = <<EOT;
|
$copyright = <<EOT;
|
||||||
# Copyright 2015 OpenMarket Ltd
|
# Copyright 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -79,16 +79,16 @@ def defined_names(prefix, defs, names):
|
|||||||
defined_names(prefix + name + ".", funcs, names)
|
defined_names(prefix + name + ".", funcs, names)
|
||||||
|
|
||||||
|
|
||||||
def used_names(prefix, defs, names):
|
def used_names(prefix, item, defs, names):
|
||||||
for name, funcs in defs.get('def', {}).items():
|
for name, funcs in defs.get('def', {}).items():
|
||||||
used_names(prefix + name + ".", funcs, names)
|
used_names(prefix + name + ".", name, funcs, names)
|
||||||
|
|
||||||
for name, funcs in defs.get('class', {}).items():
|
for name, funcs in defs.get('class', {}).items():
|
||||||
used_names(prefix + name + ".", funcs, names)
|
used_names(prefix + name + ".", name, funcs, names)
|
||||||
|
|
||||||
for used in defs.get('uses', ()):
|
for used in defs.get('uses', ()):
|
||||||
if used in names:
|
if used in names:
|
||||||
names[used].setdefault('used', []).append(prefix.rstrip('.'))
|
names[used].setdefault('used', {}).setdefault(item, []).append(prefix.rstrip('.'))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
@@ -109,6 +109,14 @@ if __name__ == '__main__':
|
|||||||
"directories", nargs='+', metavar="DIR",
|
"directories", nargs='+', metavar="DIR",
|
||||||
help="Directories to search for definitions"
|
help="Directories to search for definitions"
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--referrers", default=0, type=int,
|
||||||
|
help="Include referrers up to the given depth"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--format", default="yaml",
|
||||||
|
help="Output format, one of 'yaml' or 'dot'"
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
definitions = {}
|
definitions = {}
|
||||||
@@ -124,7 +132,7 @@ if __name__ == '__main__':
|
|||||||
defined_names(filepath + ":", defs, names)
|
defined_names(filepath + ":", defs, names)
|
||||||
|
|
||||||
for filepath, defs in definitions.items():
|
for filepath, defs in definitions.items():
|
||||||
used_names(filepath + ":", defs, names)
|
used_names(filepath + ":", None, defs, names)
|
||||||
|
|
||||||
patterns = [re.compile(pattern) for pattern in args.pattern or ()]
|
patterns = [re.compile(pattern) for pattern in args.pattern or ()]
|
||||||
ignore = [re.compile(pattern) for pattern in args.ignore or ()]
|
ignore = [re.compile(pattern) for pattern in args.ignore or ()]
|
||||||
@@ -139,4 +147,29 @@ if __name__ == '__main__':
|
|||||||
continue
|
continue
|
||||||
result[name] = definition
|
result[name] = definition
|
||||||
|
|
||||||
|
referrer_depth = args.referrers
|
||||||
|
referrers = set()
|
||||||
|
while referrer_depth:
|
||||||
|
referrer_depth -= 1
|
||||||
|
for entry in result.values():
|
||||||
|
for used_by in entry.get("used", ()):
|
||||||
|
referrers.add(used_by)
|
||||||
|
for name, definition in names.items():
|
||||||
|
if not name in referrers:
|
||||||
|
continue
|
||||||
|
if ignore and any(pattern.match(name) for pattern in ignore):
|
||||||
|
continue
|
||||||
|
result[name] = definition
|
||||||
|
|
||||||
|
if args.format == 'yaml':
|
||||||
yaml.dump(result, sys.stdout, default_flow_style=False)
|
yaml.dump(result, sys.stdout, default_flow_style=False)
|
||||||
|
elif args.format == 'dot':
|
||||||
|
print "digraph {"
|
||||||
|
for name, entry in result.items():
|
||||||
|
print name
|
||||||
|
for used_by in entry.get("used", ()):
|
||||||
|
if used_by in result:
|
||||||
|
print used_by, "->", name
|
||||||
|
print "}"
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown format %r" % (args.format))
|
||||||
|
|||||||
24
scripts-dev/dump_macaroon.py
Executable file
24
scripts-dev/dump_macaroon.py
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env python2
|
||||||
|
|
||||||
|
import pymacaroons
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if len(sys.argv) == 1:
|
||||||
|
sys.stderr.write("usage: %s macaroon [key]\n" % (sys.argv[0],))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
macaroon_string = sys.argv[1]
|
||||||
|
key = sys.argv[2] if len(sys.argv) > 2 else None
|
||||||
|
|
||||||
|
macaroon = pymacaroons.Macaroon.deserialize(macaroon_string)
|
||||||
|
print macaroon.inspect()
|
||||||
|
|
||||||
|
print ""
|
||||||
|
|
||||||
|
verifier = pymacaroons.Verifier()
|
||||||
|
verifier.satisfy_general(lambda c: True)
|
||||||
|
try:
|
||||||
|
verifier.verify(macaroon, key)
|
||||||
|
print "Signature is correct"
|
||||||
|
except Exception as e:
|
||||||
|
print e.message
|
||||||
62
scripts-dev/list_url_patterns.py
Executable file
62
scripts-dev/list_url_patterns.py
Executable file
@@ -0,0 +1,62 @@
|
|||||||
|
#! /usr/bin/python
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
PATTERNS_V1 = []
|
||||||
|
PATTERNS_V2 = []
|
||||||
|
|
||||||
|
RESULT = {
|
||||||
|
"v1": PATTERNS_V1,
|
||||||
|
"v2": PATTERNS_V2,
|
||||||
|
}
|
||||||
|
|
||||||
|
class CallVisitor(ast.NodeVisitor):
|
||||||
|
def visit_Call(self, node):
|
||||||
|
if isinstance(node.func, ast.Name):
|
||||||
|
name = node.func.id
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
if name == "client_path_patterns":
|
||||||
|
PATTERNS_V1.append(node.args[0].s)
|
||||||
|
elif name == "client_v2_patterns":
|
||||||
|
PATTERNS_V2.append(node.args[0].s)
|
||||||
|
|
||||||
|
|
||||||
|
def find_patterns_in_code(input_code):
|
||||||
|
input_ast = ast.parse(input_code)
|
||||||
|
visitor = CallVisitor()
|
||||||
|
visitor.visit(input_ast)
|
||||||
|
|
||||||
|
|
||||||
|
def find_patterns_in_file(filepath):
|
||||||
|
with open(filepath) as f:
|
||||||
|
find_patterns_in_code(f.read())
|
||||||
|
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='Find url patterns.')
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"directories", nargs='+', metavar="DIR",
|
||||||
|
help="Directories to search for definitions"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
for directory in args.directories:
|
||||||
|
for root, dirs, files in os.walk(directory):
|
||||||
|
for filename in files:
|
||||||
|
if filename.endswith(".py"):
|
||||||
|
filepath = os.path.join(root, filename)
|
||||||
|
find_patterns_in_file(filepath)
|
||||||
|
|
||||||
|
PATTERNS_V1.sort()
|
||||||
|
PATTERNS_V2.sort()
|
||||||
|
|
||||||
|
yaml.dump(RESULT, sys.stdout, default_flow_style=False)
|
||||||
1
scripts/gen_password
Normal file
1
scripts/gen_password
Normal file
@@ -0,0 +1 @@
|
|||||||
|
perl -MCrypt::Random -MCrypt::Eksblowfish::Bcrypt -e 'print Crypt::Eksblowfish::Bcrypt::bcrypt("secret", "\$2\$12\$" . Crypt::Eksblowfish::Bcrypt::en_base64(Crypt::Random::makerandom_octet(Length=>16)))."\n"'
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -68,6 +68,7 @@ APPEND_ONLY_TABLES = [
|
|||||||
"state_groups_state",
|
"state_groups_state",
|
||||||
"event_to_state_groups",
|
"event_to_state_groups",
|
||||||
"rejections",
|
"rejections",
|
||||||
|
"event_search",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -229,6 +230,38 @@ class Porter(object):
|
|||||||
if rows:
|
if rows:
|
||||||
next_chunk = rows[-1][0] + 1
|
next_chunk = rows[-1][0] + 1
|
||||||
|
|
||||||
|
if table == "event_search":
|
||||||
|
# We have to treat event_search differently since it has a
|
||||||
|
# different structure in the two different databases.
|
||||||
|
def insert(txn):
|
||||||
|
sql = (
|
||||||
|
"INSERT INTO event_search (event_id, room_id, key, sender, vector)"
|
||||||
|
" VALUES (?,?,?,?,to_tsvector('english', ?))"
|
||||||
|
)
|
||||||
|
|
||||||
|
rows_dict = [
|
||||||
|
dict(zip(headers, row))
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
txn.executemany(sql, [
|
||||||
|
(
|
||||||
|
row["event_id"],
|
||||||
|
row["room_id"],
|
||||||
|
row["key"],
|
||||||
|
row["sender"],
|
||||||
|
row["value"],
|
||||||
|
)
|
||||||
|
for row in rows_dict
|
||||||
|
])
|
||||||
|
|
||||||
|
self.postgres_store._simple_update_one_txn(
|
||||||
|
txn,
|
||||||
|
table="port_from_sqlite3",
|
||||||
|
keyvalues={"table_name": table},
|
||||||
|
updatevalues={"rowid": next_chunk},
|
||||||
|
)
|
||||||
|
else:
|
||||||
self._convert_rows(table, headers, rows)
|
self._convert_rows(table, headers, rows)
|
||||||
|
|
||||||
def insert(txn):
|
def insert(txn):
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ ignore =
|
|||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
max-line-length = 90
|
max-line-length = 90
|
||||||
|
ignore = W503 ; W503 requires that binary operators be at the end, not start, of lines. Erik doesn't like it.
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -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.10.0-r2"
|
__version__ = "0.13.0"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014 - 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -14,13 +14,18 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
"""This module contains classes for authenticating the user."""
|
"""This module contains classes for authenticating the user."""
|
||||||
|
from canonicaljson import encode_canonical_json
|
||||||
|
from signedjson.key import decode_verify_key_bytes
|
||||||
|
from signedjson.sign import verify_signed_json, SignatureVerifyException
|
||||||
|
|
||||||
from twisted.internet import defer
|
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, EventSizeError
|
||||||
|
from synapse.types import Requester, RoomID, UserID, EventID
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
from synapse.types import UserID, EventID
|
from synapse.util.logcontext import preserve_context_over_fn
|
||||||
|
from unpaddedbase64 import decode_base64
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import pymacaroons
|
import pymacaroons
|
||||||
@@ -31,6 +36,7 @@ logger = logging.getLogger(__name__)
|
|||||||
AuthEventTypes = (
|
AuthEventTypes = (
|
||||||
EventTypes.Create, EventTypes.Member, EventTypes.PowerLevels,
|
EventTypes.Create, EventTypes.Member, EventTypes.PowerLevels,
|
||||||
EventTypes.JoinRules, EventTypes.RoomHistoryVisibility,
|
EventTypes.JoinRules, EventTypes.RoomHistoryVisibility,
|
||||||
|
EventTypes.ThirdPartyInvite,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -43,6 +49,7 @@ class Auth(object):
|
|||||||
self.TOKEN_NOT_FOUND_HTTP_STATUS = 401
|
self.TOKEN_NOT_FOUND_HTTP_STATUS = 401
|
||||||
self._KNOWN_CAVEAT_PREFIXES = set([
|
self._KNOWN_CAVEAT_PREFIXES = set([
|
||||||
"gen = ",
|
"gen = ",
|
||||||
|
"guest = ",
|
||||||
"type = ",
|
"type = ",
|
||||||
"time < ",
|
"time < ",
|
||||||
"user_id = ",
|
"user_id = ",
|
||||||
@@ -59,6 +66,8 @@ class Auth(object):
|
|||||||
Returns:
|
Returns:
|
||||||
True if the auth checks pass.
|
True if the auth checks pass.
|
||||||
"""
|
"""
|
||||||
|
self.check_size_limits(event)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not hasattr(event, "room_id"):
|
if not hasattr(event, "room_id"):
|
||||||
raise AuthError(500, "Event has no room_id: %s" % event)
|
raise AuthError(500, "Event has no room_id: %s" % event)
|
||||||
@@ -80,6 +89,15 @@ class Auth(object):
|
|||||||
"Room %r does not exist" % (event.room_id,)
|
"Room %r does not exist" % (event.room_id,)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
creating_domain = RoomID.from_string(event.room_id).domain
|
||||||
|
originating_domain = UserID.from_string(event.sender).domain
|
||||||
|
if creating_domain != originating_domain:
|
||||||
|
if not self.can_federate(event, auth_events):
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"This room has been marked as unfederatable."
|
||||||
|
)
|
||||||
|
|
||||||
# FIXME: Temp hack
|
# FIXME: Temp hack
|
||||||
if event.type == EventTypes.Aliases:
|
if event.type == EventTypes.Aliases:
|
||||||
return True
|
return True
|
||||||
@@ -117,6 +135,23 @@ class Auth(object):
|
|||||||
logger.info("Denying! %s", event)
|
logger.info("Denying! %s", event)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def check_size_limits(self, event):
|
||||||
|
def too_big(field):
|
||||||
|
raise EventSizeError("%s too large" % (field,))
|
||||||
|
|
||||||
|
if len(event.user_id) > 255:
|
||||||
|
too_big("user_id")
|
||||||
|
if len(event.room_id) > 255:
|
||||||
|
too_big("room_id")
|
||||||
|
if event.is_state() and len(event.state_key) > 255:
|
||||||
|
too_big("state_key")
|
||||||
|
if len(event.type) > 255:
|
||||||
|
too_big("type")
|
||||||
|
if len(event.event_id) > 255:
|
||||||
|
too_big("event_id")
|
||||||
|
if len(encode_canonical_json(event.get_pdu_json())) > 65536:
|
||||||
|
too_big("event")
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def check_joined_room(self, room_id, user_id, current_state=None):
|
def check_joined_room(self, room_id, user_id, current_state=None):
|
||||||
"""Check if the user is currently joined in the room
|
"""Check if the user is currently joined in the room
|
||||||
@@ -149,15 +184,11 @@ class Auth(object):
|
|||||||
defer.returnValue(member)
|
defer.returnValue(member)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def check_user_was_in_room(self, room_id, user_id, current_state=None):
|
def check_user_was_in_room(self, room_id, user_id):
|
||||||
"""Check if the user was in the room at some point.
|
"""Check if the user was in the room at some point.
|
||||||
Args:
|
Args:
|
||||||
room_id(str): The room to check.
|
room_id(str): The room to check.
|
||||||
user_id(str): The user to check.
|
user_id(str): The user to check.
|
||||||
current_state(dict): Optional map of the current state of the room.
|
|
||||||
If provided then that map is used to check whether they are a
|
|
||||||
member of the room. Otherwise the current membership is
|
|
||||||
loaded from the database.
|
|
||||||
Raises:
|
Raises:
|
||||||
AuthError if the user was never in the room.
|
AuthError if the user was never in the room.
|
||||||
Returns:
|
Returns:
|
||||||
@@ -165,12 +196,6 @@ class Auth(object):
|
|||||||
room. This will be the join event if they are currently joined to
|
room. This will be the join event if they are currently joined to
|
||||||
the room. This will be the leave event if they have left the room.
|
the room. This will be the leave event if they have left the room.
|
||||||
"""
|
"""
|
||||||
if current_state:
|
|
||||||
member = current_state.get(
|
|
||||||
(EventTypes.Member, user_id),
|
|
||||||
None
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
member = yield self.state.get_current_state(
|
member = yield self.state.get_current_state(
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
event_type=EventTypes.Member,
|
event_type=EventTypes.Member,
|
||||||
@@ -183,6 +208,13 @@ class Auth(object):
|
|||||||
user_id, room_id
|
user_id, room_id
|
||||||
))
|
))
|
||||||
|
|
||||||
|
if membership == Membership.LEAVE:
|
||||||
|
forgot = yield self.store.did_forget(user_id, room_id)
|
||||||
|
if forgot:
|
||||||
|
raise AuthError(403, "User %s not in room %s" % (
|
||||||
|
user_id, room_id
|
||||||
|
))
|
||||||
|
|
||||||
defer.returnValue(member)
|
defer.returnValue(member)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@@ -219,6 +251,11 @@ class Auth(object):
|
|||||||
user_id, room_id, repr(member)
|
user_id, room_id, repr(member)
|
||||||
))
|
))
|
||||||
|
|
||||||
|
def can_federate(self, event, auth_events):
|
||||||
|
creation_event = auth_events.get((EventTypes.Create, ""))
|
||||||
|
|
||||||
|
return creation_event.content.get("m.federate", True) is True
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def is_membership_change_allowed(self, event, auth_events):
|
def is_membership_change_allowed(self, event, auth_events):
|
||||||
membership = event.content["membership"]
|
membership = event.content["membership"]
|
||||||
@@ -234,6 +271,15 @@ class Auth(object):
|
|||||||
|
|
||||||
target_user_id = event.state_key
|
target_user_id = event.state_key
|
||||||
|
|
||||||
|
creating_domain = RoomID.from_string(event.room_id).domain
|
||||||
|
target_domain = UserID.from_string(target_user_id).domain
|
||||||
|
if creating_domain != target_domain:
|
||||||
|
if not self.can_federate(event, auth_events):
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"This room has been marked as unfederatable."
|
||||||
|
)
|
||||||
|
|
||||||
# get info about the caller
|
# get info about the caller
|
||||||
key = (EventTypes.Member, event.user_id, )
|
key = (EventTypes.Member, event.user_id, )
|
||||||
caller = auth_events.get(key)
|
caller = auth_events.get(key)
|
||||||
@@ -279,8 +325,17 @@ class Auth(object):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if Membership.INVITE == membership and "third_party_invite" in event.content:
|
||||||
|
if not self._verify_third_party_invite(event, auth_events):
|
||||||
|
raise AuthError(403, "You are not invited to this room.")
|
||||||
|
return True
|
||||||
|
|
||||||
if Membership.JOIN != membership:
|
if Membership.JOIN != membership:
|
||||||
# JOIN is the only action you can perform if you're not in the room
|
if (caller_invited
|
||||||
|
and Membership.LEAVE == membership
|
||||||
|
and target_user_id == event.user_id):
|
||||||
|
return True
|
||||||
|
|
||||||
if not caller_in_room: # caller isn't joined
|
if not caller_in_room: # caller isn't joined
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
403,
|
403,
|
||||||
@@ -344,6 +399,66 @@ class Auth(object):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _verify_third_party_invite(self, event, auth_events):
|
||||||
|
"""
|
||||||
|
Validates that the invite event is authorized by a previous third-party invite.
|
||||||
|
|
||||||
|
Checks that the public key, and keyserver, match those in the third party invite,
|
||||||
|
and that the invite event has a signature issued using that public key.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: The m.room.member join event being validated.
|
||||||
|
auth_events: All relevant previous context events which may be used
|
||||||
|
for authorization decisions.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
True if the event fulfills the expectations of a previous third party
|
||||||
|
invite event.
|
||||||
|
"""
|
||||||
|
if "third_party_invite" not in event.content:
|
||||||
|
return False
|
||||||
|
if "signed" not in event.content["third_party_invite"]:
|
||||||
|
return False
|
||||||
|
signed = event.content["third_party_invite"]["signed"]
|
||||||
|
for key in {"mxid", "token"}:
|
||||||
|
if key not in signed:
|
||||||
|
return False
|
||||||
|
|
||||||
|
token = signed["token"]
|
||||||
|
|
||||||
|
invite_event = auth_events.get(
|
||||||
|
(EventTypes.ThirdPartyInvite, token,)
|
||||||
|
)
|
||||||
|
if not invite_event:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if event.user_id != invite_event.user_id:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
public_key = invite_event.content["public_key"]
|
||||||
|
if signed["mxid"] != event.state_key:
|
||||||
|
return False
|
||||||
|
if signed["token"] != token:
|
||||||
|
return False
|
||||||
|
for server, signature_block in signed["signatures"].items():
|
||||||
|
for key_name, encoded_signature in signature_block.items():
|
||||||
|
if not key_name.startswith("ed25519:"):
|
||||||
|
return False
|
||||||
|
verify_key = decode_verify_key_bytes(
|
||||||
|
key_name,
|
||||||
|
decode_base64(public_key)
|
||||||
|
)
|
||||||
|
verify_signed_json(signed, server, verify_key)
|
||||||
|
|
||||||
|
# We got the public key from the invite, so we know that the
|
||||||
|
# correct server signed the signed bundle.
|
||||||
|
# The caller is responsible for checking that the signing
|
||||||
|
# server has not revoked that public key.
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except (KeyError, SignatureVerifyException,):
|
||||||
|
return False
|
||||||
|
|
||||||
def _get_power_level_event(self, auth_events):
|
def _get_power_level_event(self, auth_events):
|
||||||
key = (EventTypes.PowerLevels, "", )
|
key = (EventTypes.PowerLevels, "", )
|
||||||
return auth_events.get(key)
|
return auth_events.get(key)
|
||||||
@@ -382,7 +497,7 @@ class Auth(object):
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_user_by_req(self, request):
|
def get_user_by_req(self, request, allow_guest=False):
|
||||||
""" Get a registered user's ID.
|
""" Get a registered user's ID.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -396,38 +511,18 @@ class Auth(object):
|
|||||||
"""
|
"""
|
||||||
# Can optionally look elsewhere in the request (e.g. headers)
|
# Can optionally look elsewhere in the request (e.g. headers)
|
||||||
try:
|
try:
|
||||||
access_token = request.args["access_token"][0]
|
user_id = yield self._get_appservice_user_id(request.args)
|
||||||
|
if user_id:
|
||||||
# Check for application service tokens with a user_id override
|
|
||||||
try:
|
|
||||||
app_service = yield self.store.get_app_service_by_token(
|
|
||||||
access_token
|
|
||||||
)
|
|
||||||
if not app_service:
|
|
||||||
raise KeyError
|
|
||||||
|
|
||||||
user_id = app_service.sender
|
|
||||||
if "user_id" in request.args:
|
|
||||||
user_id = request.args["user_id"][0]
|
|
||||||
if not app_service.is_interested_in_user(user_id):
|
|
||||||
raise AuthError(
|
|
||||||
403,
|
|
||||||
"Application service cannot masquerade as this user."
|
|
||||||
)
|
|
||||||
|
|
||||||
if not user_id:
|
|
||||||
raise KeyError
|
|
||||||
|
|
||||||
request.authenticated_entity = user_id
|
request.authenticated_entity = user_id
|
||||||
|
defer.returnValue(
|
||||||
|
Requester(UserID.from_string(user_id), "", False)
|
||||||
|
)
|
||||||
|
|
||||||
defer.returnValue((UserID.from_string(user_id), ""))
|
access_token = request.args["access_token"][0]
|
||||||
return
|
|
||||||
except KeyError:
|
|
||||||
pass # normal users won't have the user_id query parameter set.
|
|
||||||
|
|
||||||
user_info = yield self._get_user_by_access_token(access_token)
|
user_info = yield self._get_user_by_access_token(access_token)
|
||||||
user = user_info["user"]
|
user = user_info["user"]
|
||||||
token_id = user_info["token_id"]
|
token_id = user_info["token_id"]
|
||||||
|
is_guest = user_info["is_guest"]
|
||||||
|
|
||||||
ip_addr = self.hs.get_ip_from_request(request)
|
ip_addr = self.hs.get_ip_from_request(request)
|
||||||
user_agent = request.requestHeaders.getRawHeaders(
|
user_agent = request.requestHeaders.getRawHeaders(
|
||||||
@@ -435,22 +530,55 @@ class Auth(object):
|
|||||||
default=[""]
|
default=[""]
|
||||||
)[0]
|
)[0]
|
||||||
if user and access_token and ip_addr:
|
if user and access_token and ip_addr:
|
||||||
self.store.insert_client_ip(
|
preserve_context_over_fn(
|
||||||
|
self.store.insert_client_ip,
|
||||||
user=user,
|
user=user,
|
||||||
access_token=access_token,
|
access_token=access_token,
|
||||||
ip=ip_addr,
|
ip=ip_addr,
|
||||||
user_agent=user_agent
|
user_agent=user_agent
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if is_guest and not allow_guest:
|
||||||
|
raise AuthError(
|
||||||
|
403, "Guest access not allowed", errcode=Codes.GUEST_ACCESS_FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
request.authenticated_entity = user.to_string()
|
request.authenticated_entity = user.to_string()
|
||||||
|
|
||||||
defer.returnValue((user, token_id,))
|
defer.returnValue(Requester(user, token_id, is_guest))
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
self.TOKEN_NOT_FOUND_HTTP_STATUS, "Missing access token.",
|
self.TOKEN_NOT_FOUND_HTTP_STATUS, "Missing access token.",
|
||||||
errcode=Codes.MISSING_TOKEN
|
errcode=Codes.MISSING_TOKEN
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _get_appservice_user_id(self, request_args):
|
||||||
|
app_service = yield self.store.get_app_service_by_token(
|
||||||
|
request_args["access_token"][0]
|
||||||
|
)
|
||||||
|
if app_service is None:
|
||||||
|
defer.returnValue(None)
|
||||||
|
|
||||||
|
if "user_id" not in request_args:
|
||||||
|
defer.returnValue(app_service.sender)
|
||||||
|
|
||||||
|
user_id = request_args["user_id"][0]
|
||||||
|
if app_service.sender == user_id:
|
||||||
|
defer.returnValue(app_service.sender)
|
||||||
|
|
||||||
|
if not app_service.is_interested_in_user(user_id):
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"Application service cannot masquerade as this user."
|
||||||
|
)
|
||||||
|
if not (yield self.store.get_user_by_id(user_id)):
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"Application service has not registered this user"
|
||||||
|
)
|
||||||
|
defer.returnValue(user_id)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _get_user_by_access_token(self, token):
|
def _get_user_by_access_token(self, token):
|
||||||
""" Get a registered user's ID.
|
""" Get a registered user's ID.
|
||||||
@@ -463,7 +591,7 @@ class Auth(object):
|
|||||||
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.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
ret = yield self._get_user_from_macaroon(token)
|
ret = yield self.get_user_from_macaroon(token)
|
||||||
except AuthError:
|
except AuthError:
|
||||||
# TODO(daniel): Remove this fallback when all existing access tokens
|
# TODO(daniel): Remove this fallback when all existing access tokens
|
||||||
# have been re-issued as macaroons.
|
# have been re-issued as macaroons.
|
||||||
@@ -471,15 +599,33 @@ class Auth(object):
|
|||||||
defer.returnValue(ret)
|
defer.returnValue(ret)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _get_user_from_macaroon(self, macaroon_str):
|
def get_user_from_macaroon(self, macaroon_str):
|
||||||
try:
|
try:
|
||||||
macaroon = pymacaroons.Macaroon.deserialize(macaroon_str)
|
macaroon = pymacaroons.Macaroon.deserialize(macaroon_str)
|
||||||
self._validate_macaroon(macaroon)
|
self.validate_macaroon(macaroon, "access", False)
|
||||||
|
|
||||||
user_prefix = "user_id = "
|
user_prefix = "user_id = "
|
||||||
|
user = None
|
||||||
|
guest = False
|
||||||
for caveat in macaroon.caveats:
|
for caveat in macaroon.caveats:
|
||||||
if caveat.caveat_id.startswith(user_prefix):
|
if caveat.caveat_id.startswith(user_prefix):
|
||||||
user = UserID.from_string(caveat.caveat_id[len(user_prefix):])
|
user = UserID.from_string(caveat.caveat_id[len(user_prefix):])
|
||||||
|
elif caveat.caveat_id == "guest = true":
|
||||||
|
guest = True
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
raise AuthError(
|
||||||
|
self.TOKEN_NOT_FOUND_HTTP_STATUS, "No user caveat in macaroon",
|
||||||
|
errcode=Codes.UNKNOWN_TOKEN
|
||||||
|
)
|
||||||
|
|
||||||
|
if guest:
|
||||||
|
ret = {
|
||||||
|
"user": user,
|
||||||
|
"is_guest": True,
|
||||||
|
"token_id": None,
|
||||||
|
}
|
||||||
|
else:
|
||||||
# This codepath exists so that we can actually return a
|
# This codepath exists so that we can actually return a
|
||||||
# token ID, because we use token IDs in place of device
|
# token ID, because we use token IDs in place of device
|
||||||
# identifiers throughout the codebase.
|
# identifiers throughout the codebase.
|
||||||
@@ -498,22 +644,33 @@ class Auth(object):
|
|||||||
errcode=Codes.UNKNOWN_TOKEN
|
errcode=Codes.UNKNOWN_TOKEN
|
||||||
)
|
)
|
||||||
defer.returnValue(ret)
|
defer.returnValue(ret)
|
||||||
raise AuthError(
|
|
||||||
self.TOKEN_NOT_FOUND_HTTP_STATUS, "No user caveat in macaroon",
|
|
||||||
errcode=Codes.UNKNOWN_TOKEN
|
|
||||||
)
|
|
||||||
except (pymacaroons.exceptions.MacaroonException, TypeError, ValueError):
|
except (pymacaroons.exceptions.MacaroonException, TypeError, ValueError):
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
self.TOKEN_NOT_FOUND_HTTP_STATUS, "Invalid macaroon passed.",
|
self.TOKEN_NOT_FOUND_HTTP_STATUS, "Invalid macaroon passed.",
|
||||||
errcode=Codes.UNKNOWN_TOKEN
|
errcode=Codes.UNKNOWN_TOKEN
|
||||||
)
|
)
|
||||||
|
|
||||||
def _validate_macaroon(self, macaroon):
|
def validate_macaroon(self, macaroon, type_string, verify_expiry):
|
||||||
|
"""
|
||||||
|
validate that a Macaroon is understood by and was signed by this server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
macaroon(pymacaroons.Macaroon): The macaroon to validate
|
||||||
|
type_string(str): The kind of token this is (e.g. "access", "refresh")
|
||||||
|
verify_expiry(bool): Whether to verify whether the macaroon has expired.
|
||||||
|
This should really always be True, but no clients currently implement
|
||||||
|
token refresh, so we can't enforce expiry yet.
|
||||||
|
"""
|
||||||
v = pymacaroons.Verifier()
|
v = pymacaroons.Verifier()
|
||||||
v.satisfy_exact("gen = 1")
|
v.satisfy_exact("gen = 1")
|
||||||
v.satisfy_exact("type = access")
|
v.satisfy_exact("type = " + type_string)
|
||||||
v.satisfy_general(lambda c: c.startswith("user_id = "))
|
v.satisfy_general(lambda c: c.startswith("user_id = "))
|
||||||
|
v.satisfy_exact("guest = true")
|
||||||
|
if verify_expiry:
|
||||||
v.satisfy_general(self._verify_expiry)
|
v.satisfy_general(self._verify_expiry)
|
||||||
|
else:
|
||||||
|
v.satisfy_general(lambda c: c.startswith("time < "))
|
||||||
|
|
||||||
v.verify(macaroon, self.hs.config.macaroon_secret_key)
|
v.verify(macaroon, self.hs.config.macaroon_secret_key)
|
||||||
|
|
||||||
v = pymacaroons.Verifier()
|
v = pymacaroons.Verifier()
|
||||||
@@ -524,9 +681,6 @@ class Auth(object):
|
|||||||
prefix = "time < "
|
prefix = "time < "
|
||||||
if not caveat.startswith(prefix):
|
if not caveat.startswith(prefix):
|
||||||
return False
|
return False
|
||||||
# TODO(daniel): Enable expiry check when clients actually know how to
|
|
||||||
# refresh tokens. (And remember to enable the tests)
|
|
||||||
return True
|
|
||||||
expiry = int(caveat[len(prefix):])
|
expiry = int(caveat[len(prefix):])
|
||||||
now = self.hs.get_clock().time_msec()
|
now = self.hs.get_clock().time_msec()
|
||||||
return now < expiry
|
return now < expiry
|
||||||
@@ -544,6 +698,7 @@ class Auth(object):
|
|||||||
def _look_up_user_by_access_token(self, token):
|
def _look_up_user_by_access_token(self, token):
|
||||||
ret = yield self.store.get_user_by_access_token(token)
|
ret = yield self.store.get_user_by_access_token(token)
|
||||||
if not ret:
|
if not ret:
|
||||||
|
logger.warn("Unrecognised access token - not in store: %s" % (token,))
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
self.TOKEN_NOT_FOUND_HTTP_STATUS, "Unrecognised access token.",
|
self.TOKEN_NOT_FOUND_HTTP_STATUS, "Unrecognised access token.",
|
||||||
errcode=Codes.UNKNOWN_TOKEN
|
errcode=Codes.UNKNOWN_TOKEN
|
||||||
@@ -551,6 +706,7 @@ class Auth(object):
|
|||||||
user_info = {
|
user_info = {
|
||||||
"user": UserID.from_string(ret.get("name")),
|
"user": UserID.from_string(ret.get("name")),
|
||||||
"token_id": ret.get("token_id", None),
|
"token_id": ret.get("token_id", None),
|
||||||
|
"is_guest": False,
|
||||||
}
|
}
|
||||||
defer.returnValue(user_info)
|
defer.returnValue(user_info)
|
||||||
|
|
||||||
@@ -560,6 +716,7 @@ class Auth(object):
|
|||||||
token = request.args["access_token"][0]
|
token = request.args["access_token"][0]
|
||||||
service = yield self.store.get_app_service_by_token(token)
|
service = yield self.store.get_app_service_by_token(token)
|
||||||
if not service:
|
if not service:
|
||||||
|
logger.warn("Unrecognised appservice access token: %s" % (token,))
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
self.TOKEN_NOT_FOUND_HTTP_STATUS,
|
self.TOKEN_NOT_FOUND_HTTP_STATUS,
|
||||||
"Unrecognised access token.",
|
"Unrecognised access token.",
|
||||||
@@ -626,6 +783,16 @@ class Auth(object):
|
|||||||
else:
|
else:
|
||||||
if member_event:
|
if member_event:
|
||||||
auth_ids.append(member_event.event_id)
|
auth_ids.append(member_event.event_id)
|
||||||
|
|
||||||
|
if e_type == Membership.INVITE:
|
||||||
|
if "third_party_invite" in event.content:
|
||||||
|
key = (
|
||||||
|
EventTypes.ThirdPartyInvite,
|
||||||
|
event.content["third_party_invite"]["signed"]["token"]
|
||||||
|
)
|
||||||
|
third_party_invite = current_state.get(key)
|
||||||
|
if third_party_invite:
|
||||||
|
auth_ids.append(third_party_invite.event_id)
|
||||||
elif member_event:
|
elif member_event:
|
||||||
if member_event.content["membership"] == Membership.JOIN:
|
if member_event.content["membership"] == Membership.JOIN:
|
||||||
auth_ids.append(member_event.event_id)
|
auth_ids.append(member_event.event_id)
|
||||||
@@ -703,7 +870,7 @@ class Auth(object):
|
|||||||
|
|
||||||
redact_level = self._get_named_level(auth_events, "redact", 50)
|
redact_level = self._get_named_level(auth_events, "redact", 50)
|
||||||
|
|
||||||
if user_level > redact_level:
|
if user_level >= redact_level:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
redacter_domain = EventID.from_string(event.event_id).domain
|
redacter_domain = EventID.from_string(event.event_id).domain
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -63,10 +63,12 @@ class EventTypes(object):
|
|||||||
PowerLevels = "m.room.power_levels"
|
PowerLevels = "m.room.power_levels"
|
||||||
Aliases = "m.room.aliases"
|
Aliases = "m.room.aliases"
|
||||||
Redaction = "m.room.redaction"
|
Redaction = "m.room.redaction"
|
||||||
|
ThirdPartyInvite = "m.room.third_party_invite"
|
||||||
|
|
||||||
RoomHistoryVisibility = "m.room.history_visibility"
|
RoomHistoryVisibility = "m.room.history_visibility"
|
||||||
CanonicalAlias = "m.room.canonical_alias"
|
CanonicalAlias = "m.room.canonical_alias"
|
||||||
RoomAvatar = "m.room.avatar"
|
RoomAvatar = "m.room.avatar"
|
||||||
|
GuestAccess = "m.room.guest_access"
|
||||||
|
|
||||||
# These are used for validation
|
# These are used for validation
|
||||||
Message = "m.room.message"
|
Message = "m.room.message"
|
||||||
@@ -83,3 +85,4 @@ class RejectedReason(object):
|
|||||||
class RoomCreationPreset(object):
|
class RoomCreationPreset(object):
|
||||||
PRIVATE_CHAT = "private_chat"
|
PRIVATE_CHAT = "private_chat"
|
||||||
PUBLIC_CHAT = "public_chat"
|
PUBLIC_CHAT = "public_chat"
|
||||||
|
TRUSTED_PRIVATE_CHAT = "trusted_private_chat"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -29,10 +29,12 @@ class Codes(object):
|
|||||||
USER_IN_USE = "M_USER_IN_USE"
|
USER_IN_USE = "M_USER_IN_USE"
|
||||||
ROOM_IN_USE = "M_ROOM_IN_USE"
|
ROOM_IN_USE = "M_ROOM_IN_USE"
|
||||||
BAD_PAGINATION = "M_BAD_PAGINATION"
|
BAD_PAGINATION = "M_BAD_PAGINATION"
|
||||||
|
BAD_STATE = "M_BAD_STATE"
|
||||||
UNKNOWN = "M_UNKNOWN"
|
UNKNOWN = "M_UNKNOWN"
|
||||||
NOT_FOUND = "M_NOT_FOUND"
|
NOT_FOUND = "M_NOT_FOUND"
|
||||||
MISSING_TOKEN = "M_MISSING_TOKEN"
|
MISSING_TOKEN = "M_MISSING_TOKEN"
|
||||||
UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
|
UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
|
||||||
|
GUEST_ACCESS_FORBIDDEN = "M_GUEST_ACCESS_FORBIDDEN"
|
||||||
LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
|
LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
|
||||||
CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"
|
CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"
|
||||||
CAPTCHA_INVALID = "M_CAPTCHA_INVALID"
|
CAPTCHA_INVALID = "M_CAPTCHA_INVALID"
|
||||||
@@ -41,13 +43,13 @@ class Codes(object):
|
|||||||
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"
|
THREEPID_IN_USE = "THREEPID_IN_USE"
|
||||||
|
INVALID_USERNAME = "M_INVALID_USERNAME"
|
||||||
|
|
||||||
|
|
||||||
class CodeMessageException(RuntimeError):
|
class CodeMessageException(RuntimeError):
|
||||||
"""An exception with integer code and message string attributes."""
|
"""An exception with integer code and message string attributes."""
|
||||||
|
|
||||||
def __init__(self, code, msg):
|
def __init__(self, code, msg):
|
||||||
logger.info("%s: %s, %s", type(self).__name__, code, msg)
|
|
||||||
super(CodeMessageException, self).__init__("%d: %s" % (code, msg))
|
super(CodeMessageException, self).__init__("%d: %s" % (code, msg))
|
||||||
self.code = code
|
self.code = code
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
@@ -120,6 +122,15 @@ class AuthError(SynapseError):
|
|||||||
super(AuthError, self).__init__(*args, **kwargs)
|
super(AuthError, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class EventSizeError(SynapseError):
|
||||||
|
"""An error raised when an event is too big."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if "errcode" not in kwargs:
|
||||||
|
kwargs["errcode"] = Codes.TOO_LARGE
|
||||||
|
super(EventSizeError, self).__init__(413, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class EventStreamError(SynapseError):
|
class EventStreamError(SynapseError):
|
||||||
"""An error raised when there a problem with the event stream."""
|
"""An error raised when there a problem with the event stream."""
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -15,6 +15,8 @@
|
|||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
from synapse.types import UserID, RoomID
|
from synapse.types import UserID, RoomID
|
||||||
|
|
||||||
|
import ujson as json
|
||||||
|
|
||||||
|
|
||||||
class Filtering(object):
|
class Filtering(object):
|
||||||
|
|
||||||
@@ -24,18 +26,18 @@ class Filtering(object):
|
|||||||
|
|
||||||
def get_user_filter(self, user_localpart, filter_id):
|
def get_user_filter(self, user_localpart, filter_id):
|
||||||
result = self.store.get_user_filter(user_localpart, filter_id)
|
result = self.store.get_user_filter(user_localpart, filter_id)
|
||||||
result.addCallback(Filter)
|
result.addCallback(FilterCollection)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def add_user_filter(self, user_localpart, user_filter):
|
def add_user_filter(self, user_localpart, user_filter):
|
||||||
self._check_valid_filter(user_filter)
|
self.check_valid_filter(user_filter)
|
||||||
return self.store.add_user_filter(user_localpart, user_filter)
|
return self.store.add_user_filter(user_localpart, user_filter)
|
||||||
|
|
||||||
# TODO(paul): surely we should probably add a delete_user_filter or
|
# TODO(paul): surely we should probably add a delete_user_filter or
|
||||||
# replace_user_filter at some point? There's no REST API specified for
|
# replace_user_filter at some point? There's no REST API specified for
|
||||||
# them however
|
# them however
|
||||||
|
|
||||||
def _check_valid_filter(self, user_filter_json):
|
def check_valid_filter(self, user_filter_json):
|
||||||
"""Check if the provided filter is valid.
|
"""Check if the provided filter is valid.
|
||||||
|
|
||||||
This inspects all definitions contained within the filter.
|
This inspects all definitions contained within the filter.
|
||||||
@@ -50,11 +52,11 @@ class Filtering(object):
|
|||||||
# many definitions.
|
# many definitions.
|
||||||
|
|
||||||
top_level_definitions = [
|
top_level_definitions = [
|
||||||
"public_user_data", "private_user_data", "server_data"
|
"presence", "account_data"
|
||||||
]
|
]
|
||||||
|
|
||||||
room_level_definitions = [
|
room_level_definitions = [
|
||||||
"state", "events", "ephemeral"
|
"state", "timeline", "ephemeral", "account_data"
|
||||||
]
|
]
|
||||||
|
|
||||||
for key in top_level_definitions:
|
for key in top_level_definitions:
|
||||||
@@ -62,10 +64,29 @@ class Filtering(object):
|
|||||||
self._check_definition(user_filter_json[key])
|
self._check_definition(user_filter_json[key])
|
||||||
|
|
||||||
if "room" in user_filter_json:
|
if "room" in user_filter_json:
|
||||||
|
self._check_definition_room_lists(user_filter_json["room"])
|
||||||
for key in room_level_definitions:
|
for key in room_level_definitions:
|
||||||
if key in user_filter_json["room"]:
|
if key in user_filter_json["room"]:
|
||||||
self._check_definition(user_filter_json["room"][key])
|
self._check_definition(user_filter_json["room"][key])
|
||||||
|
|
||||||
|
def _check_definition_room_lists(self, definition):
|
||||||
|
"""Check that "rooms" and "not_rooms" are lists of room ids if they
|
||||||
|
are present
|
||||||
|
|
||||||
|
Args:
|
||||||
|
definition(dict): The filter definition
|
||||||
|
Raises:
|
||||||
|
SynapseError: If there was a problem with this definition.
|
||||||
|
"""
|
||||||
|
# check rooms are valid room IDs
|
||||||
|
room_id_keys = ["rooms", "not_rooms"]
|
||||||
|
for key in room_id_keys:
|
||||||
|
if key in definition:
|
||||||
|
if type(definition[key]) != list:
|
||||||
|
raise SynapseError(400, "Expected %s to be a list." % key)
|
||||||
|
for room_id in definition[key]:
|
||||||
|
RoomID.from_string(room_id)
|
||||||
|
|
||||||
def _check_definition(self, definition):
|
def _check_definition(self, definition):
|
||||||
"""Check if the provided definition is valid.
|
"""Check if the provided definition is valid.
|
||||||
|
|
||||||
@@ -85,14 +106,7 @@ class Filtering(object):
|
|||||||
400, "Expected JSON object, not %s" % (definition,)
|
400, "Expected JSON object, not %s" % (definition,)
|
||||||
)
|
)
|
||||||
|
|
||||||
# check rooms are valid room IDs
|
self._check_definition_room_lists(definition)
|
||||||
room_id_keys = ["rooms", "not_rooms"]
|
|
||||||
for key in room_id_keys:
|
|
||||||
if key in definition:
|
|
||||||
if type(definition[key]) != list:
|
|
||||||
raise SynapseError(400, "Expected %s to be a list." % key)
|
|
||||||
for room_id in definition[key]:
|
|
||||||
RoomID.from_string(room_id)
|
|
||||||
|
|
||||||
# check senders are valid user IDs
|
# check senders are valid user IDs
|
||||||
user_id_keys = ["senders", "not_senders"]
|
user_id_keys = ["senders", "not_senders"]
|
||||||
@@ -114,116 +128,142 @@ class Filtering(object):
|
|||||||
if not isinstance(event_type, basestring):
|
if not isinstance(event_type, basestring):
|
||||||
raise SynapseError(400, "Event type should be a string")
|
raise SynapseError(400, "Event type should be a string")
|
||||||
|
|
||||||
if "format" in definition:
|
|
||||||
event_format = definition["format"]
|
|
||||||
if event_format not in ["federation", "events"]:
|
|
||||||
raise SynapseError(400, "Invalid format: %s" % (event_format,))
|
|
||||||
|
|
||||||
if "select" in definition:
|
class FilterCollection(object):
|
||||||
event_select_list = definition["select"]
|
def __init__(self, filter_json):
|
||||||
for select_key in event_select_list:
|
self._filter_json = filter_json
|
||||||
if select_key not in ["event_id", "origin_server_ts",
|
|
||||||
"thread_id", "content", "content.body"]:
|
|
||||||
raise SynapseError(400, "Bad select: %s" % (select_key,))
|
|
||||||
|
|
||||||
if ("bundle_updates" in definition and
|
room_filter_json = self._filter_json.get("room", {})
|
||||||
type(definition["bundle_updates"]) != bool):
|
|
||||||
raise SynapseError(400, "Bad bundle_updates: expected bool.")
|
self._room_filter = Filter({
|
||||||
|
k: v for k, v in room_filter_json.items()
|
||||||
|
if k in ("rooms", "not_rooms")
|
||||||
|
})
|
||||||
|
|
||||||
|
self._room_timeline_filter = Filter(room_filter_json.get("timeline", {}))
|
||||||
|
self._room_state_filter = Filter(room_filter_json.get("state", {}))
|
||||||
|
self._room_ephemeral_filter = Filter(room_filter_json.get("ephemeral", {}))
|
||||||
|
self._room_account_data = Filter(room_filter_json.get("account_data", {}))
|
||||||
|
self._presence_filter = Filter(filter_json.get("presence", {}))
|
||||||
|
self._account_data = Filter(filter_json.get("account_data", {}))
|
||||||
|
|
||||||
|
self.include_leave = filter_json.get("room", {}).get(
|
||||||
|
"include_leave", False
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<FilterCollection %s>" % (json.dumps(self._filter_json),)
|
||||||
|
|
||||||
|
def get_filter_json(self):
|
||||||
|
return self._filter_json
|
||||||
|
|
||||||
|
def timeline_limit(self):
|
||||||
|
return self._room_timeline_filter.limit()
|
||||||
|
|
||||||
|
def presence_limit(self):
|
||||||
|
return self._presence_filter.limit()
|
||||||
|
|
||||||
|
def ephemeral_limit(self):
|
||||||
|
return self._room_ephemeral_filter.limit()
|
||||||
|
|
||||||
|
def filter_presence(self, events):
|
||||||
|
return self._presence_filter.filter(events)
|
||||||
|
|
||||||
|
def filter_account_data(self, events):
|
||||||
|
return self._account_data.filter(events)
|
||||||
|
|
||||||
|
def filter_room_state(self, events):
|
||||||
|
return self._room_state_filter.filter(self._room_filter.filter(events))
|
||||||
|
|
||||||
|
def filter_room_timeline(self, events):
|
||||||
|
return self._room_timeline_filter.filter(self._room_filter.filter(events))
|
||||||
|
|
||||||
|
def filter_room_ephemeral(self, events):
|
||||||
|
return self._room_ephemeral_filter.filter(self._room_filter.filter(events))
|
||||||
|
|
||||||
|
def filter_room_account_data(self, events):
|
||||||
|
return self._room_account_data.filter(self._room_filter.filter(events))
|
||||||
|
|
||||||
|
|
||||||
class Filter(object):
|
class Filter(object):
|
||||||
def __init__(self, filter_json):
|
def __init__(self, filter_json):
|
||||||
self.filter_json = filter_json
|
self.filter_json = filter_json
|
||||||
|
|
||||||
def filter_public_user_data(self, events):
|
def check(self, event):
|
||||||
return self._filter_on_key(events, ["public_user_data"])
|
"""Checks whether the filter matches the given event.
|
||||||
|
|
||||||
def filter_private_user_data(self, events):
|
|
||||||
return self._filter_on_key(events, ["private_user_data"])
|
|
||||||
|
|
||||||
def filter_room_state(self, events):
|
|
||||||
return self._filter_on_key(events, ["room", "state"])
|
|
||||||
|
|
||||||
def filter_room_events(self, events):
|
|
||||||
return self._filter_on_key(events, ["room", "events"])
|
|
||||||
|
|
||||||
def filter_room_ephemeral(self, events):
|
|
||||||
return self._filter_on_key(events, ["room", "ephemeral"])
|
|
||||||
|
|
||||||
def _filter_on_key(self, events, keys):
|
|
||||||
filter_json = self.filter_json
|
|
||||||
if not filter_json:
|
|
||||||
return events
|
|
||||||
|
|
||||||
try:
|
|
||||||
# extract the right definition from the filter
|
|
||||||
definition = filter_json
|
|
||||||
for key in keys:
|
|
||||||
definition = definition[key]
|
|
||||||
return self._filter_with_definition(events, definition)
|
|
||||||
except KeyError:
|
|
||||||
# return all events if definition isn't specified.
|
|
||||||
return events
|
|
||||||
|
|
||||||
def _filter_with_definition(self, events, definition):
|
|
||||||
return [e for e in events if self._passes_definition(definition, e)]
|
|
||||||
|
|
||||||
def _passes_definition(self, definition, event):
|
|
||||||
"""Check if the event passes through the given definition.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
definition(dict): The definition to check against.
|
|
||||||
event(Event): The event to check.
|
|
||||||
Returns:
|
Returns:
|
||||||
True if the event passes through the filter.
|
bool: True if the event matches
|
||||||
"""
|
"""
|
||||||
# Algorithm notes:
|
sender = event.get("sender", None)
|
||||||
# For each key in the definition, check the event meets the criteria:
|
if not sender:
|
||||||
# * For types: Literal match or prefix match (if ends with wildcard)
|
# Presence events have their 'sender' in content.user_id
|
||||||
# * For senders/rooms: Literal match only
|
sender = event.get("content", {}).get("user_id", None)
|
||||||
# * "not_" checks take presedence (e.g. if "m.*" is in both 'types'
|
|
||||||
# and 'not_types' then it is treated as only being in 'not_types')
|
|
||||||
|
|
||||||
# room checks
|
return self.check_fields(
|
||||||
if hasattr(event, "room_id"):
|
event.get("room_id", None),
|
||||||
room_id = event.room_id
|
sender,
|
||||||
allow_rooms = definition.get("rooms", None)
|
event.get("type", None),
|
||||||
reject_rooms = definition.get("not_rooms", None)
|
)
|
||||||
if reject_rooms and room_id in reject_rooms:
|
|
||||||
return False
|
def check_fields(self, room_id, sender, event_type):
|
||||||
if allow_rooms and room_id not in allow_rooms:
|
"""Checks whether the filter matches the given event fields.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the event fields match
|
||||||
|
"""
|
||||||
|
literal_keys = {
|
||||||
|
"rooms": lambda v: room_id == v,
|
||||||
|
"senders": lambda v: sender == v,
|
||||||
|
"types": lambda v: _matches_wildcard(event_type, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, match_func in literal_keys.items():
|
||||||
|
not_name = "not_%s" % (name,)
|
||||||
|
disallowed_values = self.filter_json.get(not_name, [])
|
||||||
|
if any(map(match_func, disallowed_values)):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# sender checks
|
allowed_values = self.filter_json.get(name, None)
|
||||||
if hasattr(event, "sender"):
|
if allowed_values is not None:
|
||||||
# Should we be including event.state_key for some event types?
|
if not any(map(match_func, allowed_values)):
|
||||||
sender = event.sender
|
|
||||||
allow_senders = definition.get("senders", None)
|
|
||||||
reject_senders = definition.get("not_senders", None)
|
|
||||||
if reject_senders and sender in reject_senders:
|
|
||||||
return False
|
|
||||||
if allow_senders and sender not in allow_senders:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# type checks
|
|
||||||
if "not_types" in definition:
|
|
||||||
for def_type in definition["not_types"]:
|
|
||||||
if self._event_matches_type(event, def_type):
|
|
||||||
return False
|
|
||||||
if "types" in definition:
|
|
||||||
included = False
|
|
||||||
for def_type in definition["types"]:
|
|
||||||
if self._event_matches_type(event, def_type):
|
|
||||||
included = True
|
|
||||||
break
|
|
||||||
if not included:
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _event_matches_type(self, event, def_type):
|
def filter_rooms(self, room_ids):
|
||||||
if def_type.endswith("*"):
|
"""Apply the 'rooms' filter to a given list of rooms.
|
||||||
type_prefix = def_type[:-1]
|
|
||||||
return event.type.startswith(type_prefix)
|
Args:
|
||||||
|
room_ids (list): A list of room_ids.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: A list of room_ids that match the filter
|
||||||
|
"""
|
||||||
|
room_ids = set(room_ids)
|
||||||
|
|
||||||
|
disallowed_rooms = set(self.filter_json.get("not_rooms", []))
|
||||||
|
room_ids -= disallowed_rooms
|
||||||
|
|
||||||
|
allowed_rooms = self.filter_json.get("rooms", None)
|
||||||
|
if allowed_rooms is not None:
|
||||||
|
room_ids &= set(allowed_rooms)
|
||||||
|
|
||||||
|
return room_ids
|
||||||
|
|
||||||
|
def filter(self, events):
|
||||||
|
return filter(self.check, events)
|
||||||
|
|
||||||
|
def limit(self):
|
||||||
|
return self.filter_json.get("limit", 10)
|
||||||
|
|
||||||
|
|
||||||
|
def _matches_wildcard(actual_value, filter_value):
|
||||||
|
if filter_value.endswith("*"):
|
||||||
|
type_prefix = filter_value[:-1]
|
||||||
|
return actual_value.startswith(type_prefix)
|
||||||
else:
|
else:
|
||||||
return event.type == def_type
|
return actual_value == filter_value
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_FILTER_COLLECTION = FilterCollection({})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -23,5 +23,6 @@ WEB_CLIENT_PREFIX = "/_matrix/client"
|
|||||||
CONTENT_REPO_PREFIX = "/_matrix/content"
|
CONTENT_REPO_PREFIX = "/_matrix/content"
|
||||||
SERVER_KEY_PREFIX = "/_matrix/key/v1"
|
SERVER_KEY_PREFIX = "/_matrix/key/v1"
|
||||||
SERVER_KEY_V2_PREFIX = "/_matrix/key/v2"
|
SERVER_KEY_V2_PREFIX = "/_matrix/key/v2"
|
||||||
MEDIA_PREFIX = "/_matrix/media/v1"
|
MEDIA_PREFIX = "/_matrix/media/r0"
|
||||||
|
LEGACY_MEDIA_PREFIX = "/_matrix/media/v1"
|
||||||
APP_SERVICE_PREFIX = "/_matrix/appservice/v1"
|
APP_SERVICE_PREFIX = "/_matrix/appservice/v1"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -12,3 +12,22 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# 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 sys
|
||||||
|
sys.dont_write_bytecode = True
|
||||||
|
|
||||||
|
from synapse.python_dependencies import (
|
||||||
|
check_requirements, MissingRequirementError
|
||||||
|
) # NOQA
|
||||||
|
|
||||||
|
try:
|
||||||
|
check_requirements()
|
||||||
|
except MissingRequirementError as e:
|
||||||
|
message = "\n".join([
|
||||||
|
"Missing Requirement: %s" % (e.message,),
|
||||||
|
"To install run:",
|
||||||
|
" pip install --upgrade --force \"%s\"" % (e.dependency,),
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
sys.stderr.writelines(message)
|
||||||
|
sys.exit(1)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -14,65 +14,6 @@
|
|||||||
# 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 sys
|
|
||||||
sys.dont_write_bytecode = True
|
|
||||||
from synapse.python_dependencies import (
|
|
||||||
check_requirements, DEPENDENCY_LINKS, MissingRequirementError
|
|
||||||
)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
try:
|
|
||||||
check_requirements()
|
|
||||||
except MissingRequirementError as e:
|
|
||||||
message = "\n".join([
|
|
||||||
"Missing Requirement: %s" % (e.message,),
|
|
||||||
"To install run:",
|
|
||||||
" pip install --upgrade --force \"%s\"" % (e.dependency,),
|
|
||||||
"",
|
|
||||||
])
|
|
||||||
sys.stderr.writelines(message)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
from synapse.storage.engines import create_engine, IncorrectDatabaseSetup
|
|
||||||
from synapse.storage import (
|
|
||||||
are_all_users_on_domain, UpgradeDatabaseException,
|
|
||||||
)
|
|
||||||
|
|
||||||
from synapse.server import HomeServer
|
|
||||||
|
|
||||||
|
|
||||||
from twisted.internet import reactor, task, defer
|
|
||||||
from twisted.application import service
|
|
||||||
from twisted.enterprise import adbapi
|
|
||||||
from twisted.web.resource import Resource, EncodingResourceWrapper
|
|
||||||
from twisted.web.static import File
|
|
||||||
from twisted.web.server import Site, GzipEncoderFactory, Request
|
|
||||||
from synapse.http.server import JsonResource, RootRedirect
|
|
||||||
from synapse.rest.media.v0.content_repository import ContentRepoResource
|
|
||||||
from synapse.rest.media.v1.media_repository import MediaRepositoryResource
|
|
||||||
from synapse.rest.key.v1.server_key_resource import LocalKey
|
|
||||||
from synapse.rest.key.v2 import KeyApiV2Resource
|
|
||||||
from synapse.http.matrixfederationclient import MatrixFederationHttpClient
|
|
||||||
from synapse.api.urls import (
|
|
||||||
CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX,
|
|
||||||
SERVER_KEY_PREFIX, MEDIA_PREFIX, CLIENT_V2_ALPHA_PREFIX, STATIC_PREFIX,
|
|
||||||
SERVER_KEY_V2_PREFIX,
|
|
||||||
)
|
|
||||||
from synapse.config.homeserver import HomeServerConfig
|
|
||||||
from synapse.crypto import context_factory
|
|
||||||
from synapse.util.logcontext import LoggingContext
|
|
||||||
from synapse.rest.client.v1 import ClientV1RestResource
|
|
||||||
from synapse.rest.client.v2_alpha import ClientV2AlphaRestResource
|
|
||||||
from synapse.metrics.resource import MetricsResource, METRICS_PREFIX
|
|
||||||
|
|
||||||
from synapse import events
|
|
||||||
|
|
||||||
from daemonize import Daemonize
|
|
||||||
import twisted.manhole.telnet
|
|
||||||
|
|
||||||
from multiprocessing import Process
|
|
||||||
|
|
||||||
import synapse
|
import synapse
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
@@ -80,34 +21,66 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import resource
|
import resource
|
||||||
import signal
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
from synapse.config._base import ConfigError
|
||||||
|
|
||||||
|
from synapse.python_dependencies import (
|
||||||
|
check_requirements, DEPENDENCY_LINKS
|
||||||
|
)
|
||||||
|
|
||||||
|
from synapse.rest import ClientRestResource
|
||||||
|
from synapse.storage.engines import create_engine, IncorrectDatabaseSetup
|
||||||
|
from synapse.storage import are_all_users_on_domain
|
||||||
|
from synapse.storage.prepare_database import UpgradeDatabaseException
|
||||||
|
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
|
||||||
|
|
||||||
|
from twisted.conch.manhole import ColoredManhole
|
||||||
|
from twisted.conch.insults import insults
|
||||||
|
from twisted.conch import manhole_ssh
|
||||||
|
from twisted.cred import checkers, portal
|
||||||
|
|
||||||
|
|
||||||
|
from twisted.internet import reactor, task, defer
|
||||||
|
from twisted.application import service
|
||||||
|
from twisted.web.resource import Resource, EncodingResourceWrapper
|
||||||
|
from twisted.web.static import File
|
||||||
|
from twisted.web.server import Site, GzipEncoderFactory, Request
|
||||||
|
from synapse.http.server import RootRedirect
|
||||||
|
from synapse.rest.media.v0.content_repository import ContentRepoResource
|
||||||
|
from synapse.rest.media.v1.media_repository import MediaRepositoryResource
|
||||||
|
from synapse.rest.key.v1.server_key_resource import LocalKey
|
||||||
|
from synapse.rest.key.v2 import KeyApiV2Resource
|
||||||
|
from synapse.api.urls import (
|
||||||
|
FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX,
|
||||||
|
SERVER_KEY_PREFIX, LEGACY_MEDIA_PREFIX, MEDIA_PREFIX, STATIC_PREFIX,
|
||||||
|
SERVER_KEY_V2_PREFIX,
|
||||||
|
)
|
||||||
|
from synapse.config.homeserver import HomeServerConfig
|
||||||
|
from synapse.crypto import context_factory
|
||||||
|
from synapse.util.logcontext import LoggingContext
|
||||||
|
from synapse.metrics.resource import MetricsResource, METRICS_PREFIX
|
||||||
|
from synapse.federation.transport.server import TransportLayerServer
|
||||||
|
|
||||||
|
from synapse import events
|
||||||
|
|
||||||
|
from daemonize import Daemonize
|
||||||
|
|
||||||
logger = logging.getLogger("synapse.app.homeserver")
|
logger = logging.getLogger("synapse.app.homeserver")
|
||||||
|
|
||||||
|
|
||||||
|
ACCESS_TOKEN_RE = re.compile(r'(\?.*access(_|%5[Ff])token=)[^&]*(.*)$')
|
||||||
|
|
||||||
|
|
||||||
def gz_wrap(r):
|
def gz_wrap(r):
|
||||||
return EncodingResourceWrapper(r, [GzipEncoderFactory()])
|
return EncodingResourceWrapper(r, [GzipEncoderFactory()])
|
||||||
|
|
||||||
|
|
||||||
class SynapseHomeServer(HomeServer):
|
def build_resource_for_web_client(hs):
|
||||||
|
webclient_path = hs.get_config().web_client_location
|
||||||
def build_http_client(self):
|
|
||||||
return MatrixFederationHttpClient(self)
|
|
||||||
|
|
||||||
def build_resource_for_client(self):
|
|
||||||
return ClientV1RestResource(self)
|
|
||||||
|
|
||||||
def build_resource_for_client_v2_alpha(self):
|
|
||||||
return ClientV2AlphaRestResource(self)
|
|
||||||
|
|
||||||
def build_resource_for_federation(self):
|
|
||||||
return JsonResource(self)
|
|
||||||
|
|
||||||
def build_resource_for_web_client(self):
|
|
||||||
webclient_path = self.get_config().web_client_location
|
|
||||||
if not webclient_path:
|
if not webclient_path:
|
||||||
try:
|
try:
|
||||||
import syweb
|
import syweb
|
||||||
@@ -135,38 +108,8 @@ class SynapseHomeServer(HomeServer):
|
|||||||
# return GzipFile(webclient_path) # TODO configurable?
|
# 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):
|
|
||||||
# This is old and should go away: not going to bother adding gzip
|
|
||||||
return File("static")
|
|
||||||
|
|
||||||
def build_resource_for_content_repo(self):
|
|
||||||
return ContentRepoResource(
|
|
||||||
self, self.config.uploads_path, self.auth, self.content_addr
|
|
||||||
)
|
|
||||||
|
|
||||||
def build_resource_for_media_repository(self):
|
|
||||||
return MediaRepositoryResource(self)
|
|
||||||
|
|
||||||
def build_resource_for_server_key(self):
|
|
||||||
return LocalKey(self)
|
|
||||||
|
|
||||||
def build_resource_for_server_key_v2(self):
|
|
||||||
return KeyApiV2Resource(self)
|
|
||||||
|
|
||||||
def build_resource_for_metrics(self):
|
|
||||||
if self.get_config().enable_metrics:
|
|
||||||
return MetricsResource(self)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def build_db_pool(self):
|
|
||||||
name = self.db_config["name"]
|
|
||||||
|
|
||||||
return adbapi.ConnectionPool(
|
|
||||||
name,
|
|
||||||
**self.db_config.get("args", {})
|
|
||||||
)
|
|
||||||
|
|
||||||
|
class SynapseHomeServer(HomeServer):
|
||||||
def _listener_http(self, config, listener_config):
|
def _listener_http(self, config, listener_config):
|
||||||
port = listener_config["port"]
|
port = listener_config["port"]
|
||||||
bind_address = listener_config.get("bind_address", "")
|
bind_address = listener_config.get("bind_address", "")
|
||||||
@@ -176,51 +119,55 @@ class SynapseHomeServer(HomeServer):
|
|||||||
if tls and config.no_tls:
|
if tls and config.no_tls:
|
||||||
return
|
return
|
||||||
|
|
||||||
metrics_resource = self.get_resource_for_metrics()
|
|
||||||
|
|
||||||
resources = {}
|
resources = {}
|
||||||
for res in listener_config["resources"]:
|
for res in listener_config["resources"]:
|
||||||
for name in res["names"]:
|
for name in res["names"]:
|
||||||
if name == "client":
|
if name == "client":
|
||||||
|
client_resource = ClientRestResource(self)
|
||||||
if res["compress"]:
|
if res["compress"]:
|
||||||
client_v1 = gz_wrap(self.get_resource_for_client())
|
client_resource = gz_wrap(client_resource)
|
||||||
client_v2 = gz_wrap(self.get_resource_for_client_v2_alpha())
|
|
||||||
else:
|
|
||||||
client_v1 = self.get_resource_for_client()
|
|
||||||
client_v2 = self.get_resource_for_client_v2_alpha()
|
|
||||||
|
|
||||||
resources.update({
|
resources.update({
|
||||||
CLIENT_PREFIX: client_v1,
|
"/_matrix/client/api/v1": client_resource,
|
||||||
CLIENT_V2_ALPHA_PREFIX: client_v2,
|
"/_matrix/client/r0": client_resource,
|
||||||
|
"/_matrix/client/unstable": client_resource,
|
||||||
|
"/_matrix/client/v2_alpha": client_resource,
|
||||||
|
"/_matrix/client/versions": client_resource,
|
||||||
})
|
})
|
||||||
|
|
||||||
if name == "federation":
|
if name == "federation":
|
||||||
resources.update({
|
resources.update({
|
||||||
FEDERATION_PREFIX: self.get_resource_for_federation(),
|
FEDERATION_PREFIX: TransportLayerServer(self),
|
||||||
})
|
})
|
||||||
|
|
||||||
if name in ["static", "client"]:
|
if name in ["static", "client"]:
|
||||||
resources.update({
|
resources.update({
|
||||||
STATIC_PREFIX: self.get_resource_for_static_content(),
|
STATIC_PREFIX: File(
|
||||||
|
os.path.join(os.path.dirname(synapse.__file__), "static")
|
||||||
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
if name in ["media", "federation", "client"]:
|
if name in ["media", "federation", "client"]:
|
||||||
|
media_repo = MediaRepositoryResource(self)
|
||||||
resources.update({
|
resources.update({
|
||||||
MEDIA_PREFIX: self.get_resource_for_media_repository(),
|
MEDIA_PREFIX: media_repo,
|
||||||
CONTENT_REPO_PREFIX: self.get_resource_for_content_repo(),
|
LEGACY_MEDIA_PREFIX: media_repo,
|
||||||
|
CONTENT_REPO_PREFIX: ContentRepoResource(
|
||||||
|
self, self.config.uploads_path, self.auth, self.content_addr
|
||||||
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
if name in ["keys", "federation"]:
|
if name in ["keys", "federation"]:
|
||||||
resources.update({
|
resources.update({
|
||||||
SERVER_KEY_PREFIX: self.get_resource_for_server_key(),
|
SERVER_KEY_PREFIX: LocalKey(self),
|
||||||
SERVER_KEY_V2_PREFIX: self.get_resource_for_server_key_v2(),
|
SERVER_KEY_V2_PREFIX: KeyApiV2Resource(self),
|
||||||
})
|
})
|
||||||
|
|
||||||
if name == "webclient":
|
if name == "webclient":
|
||||||
resources[WEB_CLIENT_PREFIX] = self.get_resource_for_web_client()
|
resources[WEB_CLIENT_PREFIX] = build_resource_for_web_client(self)
|
||||||
|
|
||||||
if name == "metrics" and metrics_resource:
|
if name == "metrics" and self.get_config().enable_metrics:
|
||||||
resources[METRICS_PREFIX] = metrics_resource
|
resources[METRICS_PREFIX] = MetricsResource(self)
|
||||||
|
|
||||||
root_resource = create_resource_tree(resources)
|
root_resource = create_resource_tree(resources)
|
||||||
if tls:
|
if tls:
|
||||||
@@ -255,10 +202,21 @@ class SynapseHomeServer(HomeServer):
|
|||||||
if listener["type"] == "http":
|
if listener["type"] == "http":
|
||||||
self._listener_http(config, listener)
|
self._listener_http(config, listener)
|
||||||
elif listener["type"] == "manhole":
|
elif listener["type"] == "manhole":
|
||||||
f = twisted.manhole.telnet.ShellFactory()
|
checker = checkers.InMemoryUsernamePasswordDatabaseDontUse(
|
||||||
f.username = "matrix"
|
matrix="rabbithole"
|
||||||
f.password = "rabbithole"
|
)
|
||||||
f.namespace['hs'] = self
|
|
||||||
|
rlm = manhole_ssh.TerminalRealm()
|
||||||
|
rlm.chainedProtocolFactory = lambda: insults.ServerProtocol(
|
||||||
|
ColoredManhole,
|
||||||
|
{
|
||||||
|
"__name__": "__console__",
|
||||||
|
"hs": self,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
f = manhole_ssh.ConchFactory(portal.Portal(rlm, [checker]))
|
||||||
|
|
||||||
reactor.listenTCP(
|
reactor.listenTCP(
|
||||||
listener["port"],
|
listener["port"],
|
||||||
f,
|
f,
|
||||||
@@ -283,6 +241,18 @@ class SynapseHomeServer(HomeServer):
|
|||||||
except IncorrectDatabaseSetup as e:
|
except IncorrectDatabaseSetup as e:
|
||||||
quit_with_error(e.message)
|
quit_with_error(e.message)
|
||||||
|
|
||||||
|
def get_db_conn(self):
|
||||||
|
# Any param beginning with cp_ is a parameter for adbapi, and should
|
||||||
|
# not be passed to the database engine.
|
||||||
|
db_params = {
|
||||||
|
k: v for k, v in self.db_config.get("args", {}).items()
|
||||||
|
if not k.startswith("cp_")
|
||||||
|
}
|
||||||
|
db_conn = self.database_engine.module.connect(**db_params)
|
||||||
|
|
||||||
|
self.database_engine.on_new_connection(db_conn)
|
||||||
|
return db_conn
|
||||||
|
|
||||||
|
|
||||||
def quit_with_error(error_string):
|
def quit_with_error(error_string):
|
||||||
message_lines = error_string.split("\n")
|
message_lines = error_string.split("\n")
|
||||||
@@ -365,41 +335,44 @@ def change_resource_limit(soft_file_no):
|
|||||||
soft_file_no = hard
|
soft_file_no = hard
|
||||||
|
|
||||||
resource.setrlimit(resource.RLIMIT_NOFILE, (soft_file_no, hard))
|
resource.setrlimit(resource.RLIMIT_NOFILE, (soft_file_no, hard))
|
||||||
|
|
||||||
logger.info("Set file limit to: %d", soft_file_no)
|
logger.info("Set file limit to: %d", soft_file_no)
|
||||||
|
|
||||||
|
resource.setrlimit(
|
||||||
|
resource.RLIMIT_CORE, (resource.RLIM_INFINITY, resource.RLIM_INFINITY)
|
||||||
|
)
|
||||||
except (ValueError, resource.error) as e:
|
except (ValueError, resource.error) as e:
|
||||||
logger.warn("Failed to set file limit: %s", e)
|
logger.warn("Failed to set file or core limit: %s", e)
|
||||||
|
|
||||||
|
|
||||||
def load_config(config_options):
|
def setup(config_options):
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
config_options_options: The options passed to Synapse. Usually
|
config_options_options: The options passed to Synapse. Usually
|
||||||
`sys.argv[1:]`.
|
`sys.argv[1:]`.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
HomeServerConfig
|
HomeServer
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
config = HomeServerConfig.load_config(
|
config = HomeServerConfig.load_config(
|
||||||
"Synapse Homeserver",
|
"Synapse Homeserver",
|
||||||
config_options,
|
config_options,
|
||||||
generate_section="Homeserver"
|
generate_section="Homeserver"
|
||||||
)
|
)
|
||||||
|
except ConfigError as e:
|
||||||
|
sys.stderr.write("\n" + e.message + "\n")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not config:
|
||||||
|
# If a config isn't returned, and an exception isn't raised, we're just
|
||||||
|
# generating config files and shouldn't try to continue.
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
config.setup_logging()
|
config.setup_logging()
|
||||||
|
|
||||||
return config
|
# check any extra requirements we have now we have a config
|
||||||
|
check_requirements(config)
|
||||||
|
|
||||||
|
|
||||||
def setup(config):
|
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
config (Homeserver)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
HomeServer
|
|
||||||
"""
|
|
||||||
version_string = get_version_string()
|
version_string = get_version_string()
|
||||||
|
|
||||||
logger.info("Server hostname: %s", config.server_name)
|
logger.info("Server hostname: %s", config.server_name)
|
||||||
@@ -425,13 +398,7 @@ def setup(config):
|
|||||||
logger.info("Preparing database: %s...", config.database_config['name'])
|
logger.info("Preparing database: %s...", config.database_config['name'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db_conn = database_engine.module.connect(
|
db_conn = hs.get_db_conn()
|
||||||
**{
|
|
||||||
k: v for k, v in config.database_config.get("args", {}).items()
|
|
||||||
if not k.startswith("cp_")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
database_engine.prepare_database(db_conn)
|
database_engine.prepare_database(db_conn)
|
||||||
hs.run_startup_checks(db_conn, database_engine)
|
hs.run_startup_checks(db_conn, database_engine)
|
||||||
|
|
||||||
@@ -446,48 +413,17 @@ def setup(config):
|
|||||||
|
|
||||||
logger.info("Database prepared in %s.", config.database_config['name'])
|
logger.info("Database prepared in %s.", config.database_config['name'])
|
||||||
|
|
||||||
|
hs.setup()
|
||||||
hs.start_listening()
|
hs.start_listening()
|
||||||
|
|
||||||
|
def start():
|
||||||
hs.get_pusherpool().start()
|
hs.get_pusherpool().start()
|
||||||
hs.get_state_handler().start_caching()
|
hs.get_state_handler().start_caching()
|
||||||
hs.get_datastore().start_profiling()
|
hs.get_datastore().start_profiling()
|
||||||
|
hs.get_datastore().start_doing_background_updates()
|
||||||
hs.get_replication_layer().start_get_pdu_cache()
|
hs.get_replication_layer().start_get_pdu_cache()
|
||||||
|
|
||||||
start_time = time.time()
|
reactor.callWhenRunning(start)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def phone_stats_home():
|
|
||||||
now = int(time.time())
|
|
||||||
uptime = int(now - start_time)
|
|
||||||
if uptime < 0:
|
|
||||||
uptime = 0
|
|
||||||
|
|
||||||
stats = {}
|
|
||||||
stats["homeserver"] = config.server_name
|
|
||||||
stats["timestamp"] = now
|
|
||||||
stats["uptime_seconds"] = uptime
|
|
||||||
stats["total_users"] = yield hs.get_datastore().count_all_users()
|
|
||||||
|
|
||||||
all_rooms = yield hs.get_datastore().get_rooms(False)
|
|
||||||
stats["total_room_count"] = len(all_rooms)
|
|
||||||
|
|
||||||
stats["daily_active_users"] = yield hs.get_datastore().count_daily_users()
|
|
||||||
daily_messages = yield hs.get_datastore().count_daily_messages()
|
|
||||||
if daily_messages is not None:
|
|
||||||
stats["daily_messages"] = daily_messages
|
|
||||||
|
|
||||||
logger.info("Reporting stats to matrix.org: %s" % (stats,))
|
|
||||||
try:
|
|
||||||
yield hs.get_simple_http_client().put_json(
|
|
||||||
"https://matrix.org/report-usage-stats/push",
|
|
||||||
stats
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warn("Error reporting stats: %s", e)
|
|
||||||
|
|
||||||
if hs.config.report_stats:
|
|
||||||
phone_home_task = task.LoopingCall(phone_stats_home)
|
|
||||||
phone_home_task.start(60 * 60 * 24, now=False)
|
|
||||||
|
|
||||||
return hs
|
return hs
|
||||||
|
|
||||||
@@ -526,9 +462,8 @@ class SynapseRequest(Request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_redacted_uri(self):
|
def get_redacted_uri(self):
|
||||||
return re.sub(
|
return ACCESS_TOKEN_RE.sub(
|
||||||
r'(\?.*access_token=)[^&]*(.*)$',
|
r'\1<redacted>\3',
|
||||||
r'\1<redacted>\2',
|
|
||||||
self.uri
|
self.uri
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -546,13 +481,28 @@ class SynapseRequest(Request):
|
|||||||
self.start_time = int(time.time() * 1000)
|
self.start_time = int(time.time() * 1000)
|
||||||
|
|
||||||
def finished_processing(self):
|
def finished_processing(self):
|
||||||
|
|
||||||
|
try:
|
||||||
|
context = LoggingContext.current_context()
|
||||||
|
ru_utime, ru_stime = context.get_resource_usage()
|
||||||
|
db_txn_count = context.db_txn_count
|
||||||
|
db_txn_duration = context.db_txn_duration
|
||||||
|
except:
|
||||||
|
ru_utime, ru_stime = (0, 0)
|
||||||
|
db_txn_count, db_txn_duration = (0, 0)
|
||||||
|
|
||||||
self.site.access_logger.info(
|
self.site.access_logger.info(
|
||||||
"%s - %s - {%s}"
|
"%s - %s - {%s}"
|
||||||
" Processed request: %dms %sB %s \"%s %s %s\" \"%s\"",
|
" Processed request: %dms (%dms, %dms) (%dms/%d)"
|
||||||
|
" %sB %s \"%s %s %s\" \"%s\"",
|
||||||
self.getClientIP(),
|
self.getClientIP(),
|
||||||
self.site.site_tag,
|
self.site.site_tag,
|
||||||
self.authenticated_entity,
|
self.authenticated_entity,
|
||||||
int(time.time() * 1000) - self.start_time,
|
int(time.time() * 1000) - self.start_time,
|
||||||
|
int(ru_utime * 1000),
|
||||||
|
int(ru_stime * 1000),
|
||||||
|
int(db_txn_duration * 1000),
|
||||||
|
int(db_txn_count),
|
||||||
self.sentLength,
|
self.sentLength,
|
||||||
self.code,
|
self.code,
|
||||||
self.method,
|
self.method,
|
||||||
@@ -689,7 +639,7 @@ def _resource_id(resource, path_seg):
|
|||||||
the mapping should looks like _resource_id(A,C) = B.
|
the mapping should looks like _resource_id(A,C) = B.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
resource (Resource): The *parent* Resource
|
resource (Resource): The *parent* Resourceb
|
||||||
path_seg (str): The name of the child Resource to be attached.
|
path_seg (str): The name of the child Resource to be attached.
|
||||||
Returns:
|
Returns:
|
||||||
str: A unique string which can be a key to the child Resource.
|
str: A unique string which can be a key to the child Resource.
|
||||||
@@ -697,7 +647,7 @@ def _resource_id(resource, path_seg):
|
|||||||
return "%s-%s" % (resource, path_seg)
|
return "%s-%s" % (resource, path_seg)
|
||||||
|
|
||||||
|
|
||||||
def run(config):
|
def run(hs):
|
||||||
PROFILE_SYNAPSE = False
|
PROFILE_SYNAPSE = False
|
||||||
if PROFILE_SYNAPSE:
|
if PROFILE_SYNAPSE:
|
||||||
def profile(func):
|
def profile(func):
|
||||||
@@ -711,7 +661,7 @@ def run(config):
|
|||||||
profile.disable()
|
profile.disable()
|
||||||
ident = current_thread().ident
|
ident = current_thread().ident
|
||||||
profile.dump_stats("/tmp/%s.%s.%i.pstat" % (
|
profile.dump_stats("/tmp/%s.%s.%i.pstat" % (
|
||||||
config.server_name, func.__name__, ident
|
hs.hostname, func.__name__, ident
|
||||||
))
|
))
|
||||||
|
|
||||||
return profiled
|
return profiled
|
||||||
@@ -720,52 +670,60 @@ def run(config):
|
|||||||
ThreadPool._worker = profile(ThreadPool._worker)
|
ThreadPool._worker = profile(ThreadPool._worker)
|
||||||
reactor.run = profile(reactor.run)
|
reactor.run = profile(reactor.run)
|
||||||
|
|
||||||
|
start_time = hs.get_clock().time()
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def phone_stats_home():
|
||||||
|
logger.info("Gathering stats for reporting")
|
||||||
|
now = int(hs.get_clock().time())
|
||||||
|
uptime = int(now - start_time)
|
||||||
|
if uptime < 0:
|
||||||
|
uptime = 0
|
||||||
|
|
||||||
|
stats = {}
|
||||||
|
stats["homeserver"] = hs.config.server_name
|
||||||
|
stats["timestamp"] = now
|
||||||
|
stats["uptime_seconds"] = uptime
|
||||||
|
stats["total_users"] = yield hs.get_datastore().count_all_users()
|
||||||
|
|
||||||
|
room_count = yield hs.get_datastore().get_room_count()
|
||||||
|
stats["total_room_count"] = room_count
|
||||||
|
|
||||||
|
stats["daily_active_users"] = yield hs.get_datastore().count_daily_users()
|
||||||
|
daily_messages = yield hs.get_datastore().count_daily_messages()
|
||||||
|
if daily_messages is not None:
|
||||||
|
stats["daily_messages"] = daily_messages
|
||||||
|
|
||||||
|
logger.info("Reporting stats to matrix.org: %s" % (stats,))
|
||||||
|
try:
|
||||||
|
yield hs.get_simple_http_client().put_json(
|
||||||
|
"https://matrix.org/report-usage-stats/push",
|
||||||
|
stats
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn("Error reporting stats: %s", e)
|
||||||
|
|
||||||
|
if hs.config.report_stats:
|
||||||
|
phone_home_task = task.LoopingCall(phone_stats_home)
|
||||||
|
logger.info("Scheduling stats reporting for 24 hour intervals")
|
||||||
|
phone_home_task.start(60 * 60 * 24, now=False)
|
||||||
|
|
||||||
def in_thread():
|
def in_thread():
|
||||||
hs = setup(config)
|
# Uncomment to enable tracing of log context changes.
|
||||||
|
# sys.settrace(logcontext_tracer)
|
||||||
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()
|
||||||
|
|
||||||
def start_in_process_checker():
|
if hs.config.daemonize:
|
||||||
p = None
|
|
||||||
should_restart = [True]
|
|
||||||
|
|
||||||
def proxy_signal(signum, stack):
|
if hs.config.print_pidfile:
|
||||||
logger.info("Got signal: %r", signum)
|
print hs.config.pid_file
|
||||||
if p is not None:
|
|
||||||
os.kill(p.pid, signum)
|
|
||||||
|
|
||||||
if signum == signal.SIGTERM:
|
|
||||||
should_restart[0] = False
|
|
||||||
|
|
||||||
if getattr(signal, "SIGHUP"):
|
|
||||||
signal.signal(signal.SIGHUP, proxy_signal)
|
|
||||||
signal.signal(signal.SIGTERM, proxy_signal)
|
|
||||||
|
|
||||||
last_start = 0
|
|
||||||
next_delay = 1
|
|
||||||
|
|
||||||
while should_restart[0]:
|
|
||||||
last_start = time.time()
|
|
||||||
p = Process(target=in_thread, args=())
|
|
||||||
p.start()
|
|
||||||
p.join()
|
|
||||||
|
|
||||||
if time.time() - last_start < 120:
|
|
||||||
next_delay = min(next_delay * 5, 5 * 60)
|
|
||||||
else:
|
|
||||||
next_delay = 1
|
|
||||||
|
|
||||||
time.sleep(next_delay)
|
|
||||||
|
|
||||||
if config.daemonize:
|
|
||||||
if config.print_pidfile:
|
|
||||||
print config.pid_file
|
|
||||||
|
|
||||||
daemon = Daemonize(
|
daemon = Daemonize(
|
||||||
app="synapse-homeserver",
|
app="synapse-homeserver",
|
||||||
pid=config.pid_file,
|
pid=hs.config.pid_file,
|
||||||
action=lambda: start_in_process_checker(),
|
action=lambda: in_thread(),
|
||||||
auto_close_fds=False,
|
auto_close_fds=False,
|
||||||
verbose=True,
|
verbose=True,
|
||||||
logger=logger,
|
logger=logger,
|
||||||
@@ -780,8 +738,8 @@ def main():
|
|||||||
with LoggingContext("main"):
|
with LoggingContext("main"):
|
||||||
# check base requirements
|
# check base requirements
|
||||||
check_requirements()
|
check_requirements()
|
||||||
config = load_config(sys.argv[1:])
|
hs = setup(sys.argv[1:])
|
||||||
run(config)
|
run(hs)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -32,9 +32,9 @@ def start(configfile):
|
|||||||
print "Starting ...",
|
print "Starting ...",
|
||||||
args = SYNAPSE
|
args = SYNAPSE
|
||||||
args.extend(["--daemonize", "-c", configfile])
|
args.extend(["--daemonize", "-c", configfile])
|
||||||
cwd = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
try:
|
try:
|
||||||
subprocess.check_call(args, cwd=cwd)
|
subprocess.check_call(args)
|
||||||
print GREEN + "started" + NORMAL
|
print GREEN + "started" + NORMAL
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
print (
|
print (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -224,8 +224,8 @@ class _Recoverer(object):
|
|||||||
self.clock.call_later((2 ** self.backoff_counter), self.retry)
|
self.clock.call_later((2 ** self.backoff_counter), self.retry)
|
||||||
|
|
||||||
def _backoff(self):
|
def _backoff(self):
|
||||||
# cap the backoff to be around 18h => (2^16) = 65536 secs
|
# cap the backoff to be around 8.5min => (2^9) = 512 secs
|
||||||
if self.backoff_counter < 16:
|
if self.backoff_counter < 9:
|
||||||
self.backoff_counter += 1
|
self.backoff_counter += 1
|
||||||
self.recover()
|
self.recover()
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# 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.
|
||||||
|
from synapse.config._base import ConfigError
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import sys
|
import sys
|
||||||
@@ -21,7 +22,11 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
if action == "read":
|
if action == "read":
|
||||||
key = sys.argv[2]
|
key = sys.argv[2]
|
||||||
|
try:
|
||||||
config = HomeServerConfig.load_config("", sys.argv[3:])
|
config = HomeServerConfig.load_config("", sys.argv[3:])
|
||||||
|
except ConfigError as e:
|
||||||
|
sys.stderr.write("\n" + e.message + "\n")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
print getattr(config, key)
|
print getattr(config, key)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -14,9 +14,9 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import errno
|
||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
import sys
|
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
|
||||||
|
|
||||||
@@ -24,18 +24,29 @@ class ConfigError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# We split these messages out to allow packages to override with package
|
||||||
|
# specific instructions.
|
||||||
|
MISSING_REPORT_STATS_CONFIG_INSTRUCTIONS = """\
|
||||||
|
Please opt in or out of reporting anonymized homeserver usage statistics, by
|
||||||
|
setting the `report_stats` key in your config file to either True or False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
MISSING_REPORT_STATS_SPIEL = """\
|
||||||
|
We would really appreciate it if you could help our project out by reporting
|
||||||
|
anonymized usage statistics from your homeserver. Only very basic aggregate
|
||||||
|
data (e.g. number of users) will be reported, but it helps us to track the
|
||||||
|
growth of the Matrix community, and helps us to make Matrix a success, as well
|
||||||
|
as to convince other networks that they should peer with us.
|
||||||
|
|
||||||
|
Thank you.
|
||||||
|
"""
|
||||||
|
|
||||||
|
MISSING_SERVER_NAME = """\
|
||||||
|
Missing mandatory `server_name` config option.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Config(object):
|
class Config(object):
|
||||||
|
|
||||||
stats_reporting_begging_spiel = (
|
|
||||||
"We would really appreciate it if you could help our project out by"
|
|
||||||
" reporting anonymized usage statistics from your homeserver. Only very"
|
|
||||||
" basic aggregate data (e.g. number of users) will be reported, but it"
|
|
||||||
" helps us to track the growth of the Matrix community, and helps us to"
|
|
||||||
" make Matrix a success, as well as to convince other networks that they"
|
|
||||||
" should peer with us."
|
|
||||||
"\nThank you."
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_size(value):
|
def parse_size(value):
|
||||||
if isinstance(value, int) or isinstance(value, long):
|
if isinstance(value, int) or isinstance(value, long):
|
||||||
@@ -91,8 +102,11 @@ class Config(object):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def ensure_directory(cls, dir_path):
|
def ensure_directory(cls, dir_path):
|
||||||
dir_path = cls.abspath(dir_path)
|
dir_path = cls.abspath(dir_path)
|
||||||
if not os.path.exists(dir_path):
|
try:
|
||||||
os.makedirs(dir_path)
|
os.makedirs(dir_path)
|
||||||
|
except OSError, e:
|
||||||
|
if e.errno != errno.EEXIST:
|
||||||
|
raise
|
||||||
if not os.path.isdir(dir_path):
|
if not os.path.isdir(dir_path):
|
||||||
raise ConfigError(
|
raise ConfigError(
|
||||||
"%s is not a directory" % (dir_path,)
|
"%s is not a directory" % (dir_path,)
|
||||||
@@ -121,13 +135,20 @@ class Config(object):
|
|||||||
results.append(getattr(cls, name)(self, *args, **kargs))
|
results.append(getattr(cls, name)(self, *args, **kargs))
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def generate_config(self, config_dir_path, server_name, report_stats=None):
|
def generate_config(
|
||||||
|
self,
|
||||||
|
config_dir_path,
|
||||||
|
server_name,
|
||||||
|
is_generating_file,
|
||||||
|
report_stats=None,
|
||||||
|
):
|
||||||
default_config = "# vim:ft=yaml\n"
|
default_config = "# vim:ft=yaml\n"
|
||||||
|
|
||||||
default_config += "\n\n".join(dedent(conf) for conf in self.invoke_all(
|
default_config += "\n\n".join(dedent(conf) for conf in self.invoke_all(
|
||||||
"default_config",
|
"default_config",
|
||||||
config_dir_path=config_dir_path,
|
config_dir_path=config_dir_path,
|
||||||
server_name=server_name,
|
server_name=server_name,
|
||||||
|
is_generating_file=is_generating_file,
|
||||||
report_stats=report_stats,
|
report_stats=report_stats,
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -211,7 +232,7 @@ class Config(object):
|
|||||||
if config_args.report_stats is None:
|
if config_args.report_stats is None:
|
||||||
config_parser.error(
|
config_parser.error(
|
||||||
"Please specify either --report-stats=yes or --report-stats=no\n\n" +
|
"Please specify either --report-stats=yes or --report-stats=no\n\n" +
|
||||||
cls.stats_reporting_begging_spiel
|
MISSING_REPORT_STATS_SPIEL
|
||||||
)
|
)
|
||||||
if not config_files:
|
if not config_files:
|
||||||
config_parser.error(
|
config_parser.error(
|
||||||
@@ -229,8 +250,10 @@ class Config(object):
|
|||||||
|
|
||||||
server_name = config_args.server_name
|
server_name = config_args.server_name
|
||||||
if not server_name:
|
if not server_name:
|
||||||
print "Must specify a server_name to a generate config for."
|
raise ConfigError(
|
||||||
sys.exit(1)
|
"Must specify a server_name to a generate config for."
|
||||||
|
" Pass -H server.name."
|
||||||
|
)
|
||||||
if not os.path.exists(config_dir_path):
|
if not os.path.exists(config_dir_path):
|
||||||
os.makedirs(config_dir_path)
|
os.makedirs(config_dir_path)
|
||||||
with open(config_path, "wb") as config_file:
|
with open(config_path, "wb") as config_file:
|
||||||
@@ -238,6 +261,7 @@ class Config(object):
|
|||||||
config_dir_path=config_dir_path,
|
config_dir_path=config_dir_path,
|
||||||
server_name=server_name,
|
server_name=server_name,
|
||||||
report_stats=(config_args.report_stats == "yes"),
|
report_stats=(config_args.report_stats == "yes"),
|
||||||
|
is_generating_file=True
|
||||||
)
|
)
|
||||||
obj.invoke_all("generate_files", config)
|
obj.invoke_all("generate_files", config)
|
||||||
config_file.write(config_bytes)
|
config_file.write(config_bytes)
|
||||||
@@ -251,7 +275,7 @@ class Config(object):
|
|||||||
"If this server name is incorrect, you will need to"
|
"If this server name is incorrect, you will need to"
|
||||||
" regenerate the SSL certificates"
|
" regenerate the SSL certificates"
|
||||||
)
|
)
|
||||||
sys.exit(0)
|
return
|
||||||
else:
|
else:
|
||||||
print (
|
print (
|
||||||
"Config file %r already exists. Generating any missing key"
|
"Config file %r already exists. Generating any missing key"
|
||||||
@@ -286,25 +310,26 @@ class Config(object):
|
|||||||
yaml_config = cls.read_config_file(config_file)
|
yaml_config = cls.read_config_file(config_file)
|
||||||
specified_config.update(yaml_config)
|
specified_config.update(yaml_config)
|
||||||
|
|
||||||
|
if "server_name" not in specified_config:
|
||||||
|
raise ConfigError(MISSING_SERVER_NAME)
|
||||||
|
|
||||||
server_name = specified_config["server_name"]
|
server_name = specified_config["server_name"]
|
||||||
_, config = obj.generate_config(
|
_, config = obj.generate_config(
|
||||||
config_dir_path=config_dir_path,
|
config_dir_path=config_dir_path,
|
||||||
server_name=server_name
|
server_name=server_name,
|
||||||
|
is_generating_file=False,
|
||||||
)
|
)
|
||||||
config.pop("log_config")
|
config.pop("log_config")
|
||||||
config.update(specified_config)
|
config.update(specified_config)
|
||||||
if "report_stats" not in config:
|
if "report_stats" not in config:
|
||||||
sys.stderr.write(
|
raise ConfigError(
|
||||||
"Please opt in or out of reporting anonymized homeserver usage "
|
MISSING_REPORT_STATS_CONFIG_INSTRUCTIONS + "\n" +
|
||||||
"statistics, by setting the report_stats key in your config file "
|
MISSING_REPORT_STATS_SPIEL
|
||||||
" ( " + config_path + " ) " +
|
)
|
||||||
"to either True or False.\n\n" +
|
|
||||||
Config.stats_reporting_begging_spiel + "\n")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if generate_keys:
|
if generate_keys:
|
||||||
obj.invoke_all("generate_files", config)
|
obj.invoke_all("generate_files", config)
|
||||||
sys.exit(0)
|
return
|
||||||
|
|
||||||
obj.invoke_all("read_config", config)
|
obj.invoke_all("read_config", config)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright 2015 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -29,10 +29,10 @@ class CaptchaConfig(Config):
|
|||||||
## Captcha ##
|
## Captcha ##
|
||||||
|
|
||||||
# This Home Server's ReCAPTCHA public key.
|
# This Home Server's ReCAPTCHA public key.
|
||||||
recaptcha_private_key: "YOUR_PRIVATE_KEY"
|
recaptcha_public_key: "YOUR_PUBLIC_KEY"
|
||||||
|
|
||||||
# This Home Server's ReCAPTCHA private key.
|
# This Home Server's ReCAPTCHA private key.
|
||||||
recaptcha_public_key: "YOUR_PUBLIC_KEY"
|
recaptcha_private_key: "YOUR_PRIVATE_KEY"
|
||||||
|
|
||||||
# Enables ReCaptcha checks when registering, preventing signup
|
# Enables ReCaptcha checks when registering, preventing signup
|
||||||
# unless a captcha is answered. Requires a valid ReCaptcha
|
# unless a captcha is answered. Requires a valid ReCaptcha
|
||||||
|
|||||||
47
synapse/config/cas.py
Normal file
47
synapse/config/cas.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015, 2016 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 Config
|
||||||
|
|
||||||
|
|
||||||
|
class CasConfig(Config):
|
||||||
|
"""Cas Configuration
|
||||||
|
|
||||||
|
cas_server_url: URL of CAS server
|
||||||
|
"""
|
||||||
|
|
||||||
|
def read_config(self, config):
|
||||||
|
cas_config = config.get("cas_config", None)
|
||||||
|
if cas_config:
|
||||||
|
self.cas_enabled = cas_config.get("enabled", True)
|
||||||
|
self.cas_server_url = cas_config["server_url"]
|
||||||
|
self.cas_service_url = cas_config["service_url"]
|
||||||
|
self.cas_required_attributes = cas_config.get("required_attributes", {})
|
||||||
|
else:
|
||||||
|
self.cas_enabled = False
|
||||||
|
self.cas_server_url = None
|
||||||
|
self.cas_service_url = None
|
||||||
|
self.cas_required_attributes = {}
|
||||||
|
|
||||||
|
def default_config(self, config_dir_path, server_name, **kwargs):
|
||||||
|
return """
|
||||||
|
# Enable CAS for registration and login.
|
||||||
|
#cas_config:
|
||||||
|
# enabled: true
|
||||||
|
# server_url: "https://cas-server.com"
|
||||||
|
# service_url: "https://homesever.domain.com:8448"
|
||||||
|
# #required_attributes:
|
||||||
|
# # name: value
|
||||||
|
"""
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -26,12 +26,15 @@ from .metrics import MetricsConfig
|
|||||||
from .appservice import AppServiceConfig
|
from .appservice import AppServiceConfig
|
||||||
from .key import KeyConfig
|
from .key import KeyConfig
|
||||||
from .saml2 import SAML2Config
|
from .saml2 import SAML2Config
|
||||||
|
from .cas import CasConfig
|
||||||
|
from .password import PasswordConfig
|
||||||
|
|
||||||
|
|
||||||
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
||||||
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
|
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
|
||||||
VoipConfig, RegistrationConfig, MetricsConfig,
|
VoipConfig, RegistrationConfig, MetricsConfig,
|
||||||
AppServiceConfig, KeyConfig, SAML2Config, ):
|
AppServiceConfig, KeyConfig, SAML2Config, CasConfig,
|
||||||
|
PasswordConfig,):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -22,8 +22,14 @@ from signedjson.key import (
|
|||||||
read_signing_keys, write_signing_keys, NACL_ED25519
|
read_signing_keys, write_signing_keys, NACL_ED25519
|
||||||
)
|
)
|
||||||
from unpaddedbase64 import decode_base64
|
from unpaddedbase64 import decode_base64
|
||||||
|
from synapse.util.stringutils import random_string_with_symbols
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class KeyConfig(Config):
|
class KeyConfig(Config):
|
||||||
@@ -40,9 +46,29 @@ class KeyConfig(Config):
|
|||||||
config["perspectives"]
|
config["perspectives"]
|
||||||
)
|
)
|
||||||
|
|
||||||
def default_config(self, config_dir_path, server_name, **kwargs):
|
self.macaroon_secret_key = config.get(
|
||||||
|
"macaroon_secret_key", self.registration_shared_secret
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.macaroon_secret_key:
|
||||||
|
# Unfortunately, there are people out there that don't have this
|
||||||
|
# set. Lets just be "nice" and derive one from their secret key.
|
||||||
|
logger.warn("Config is missing missing macaroon_secret_key")
|
||||||
|
seed = self.signing_key[0].seed
|
||||||
|
self.macaroon_secret_key = hashlib.sha256(seed)
|
||||||
|
|
||||||
|
def default_config(self, config_dir_path, server_name, is_generating_file=False,
|
||||||
|
**kwargs):
|
||||||
base_key_name = os.path.join(config_dir_path, server_name)
|
base_key_name = os.path.join(config_dir_path, server_name)
|
||||||
|
|
||||||
|
if is_generating_file:
|
||||||
|
macaroon_secret_key = random_string_with_symbols(50)
|
||||||
|
else:
|
||||||
|
macaroon_secret_key = None
|
||||||
|
|
||||||
return """\
|
return """\
|
||||||
|
macaroon_secret_key: "%(macaroon_secret_key)s"
|
||||||
|
|
||||||
## Signing Keys ##
|
## Signing Keys ##
|
||||||
|
|
||||||
# Path to the signing key to sign messages with
|
# Path to the signing key to sign messages with
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -22,6 +22,7 @@ import yaml
|
|||||||
from string import Template
|
from string import Template
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
|
from synapse.util.debug import debug_deferreds
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_LOG_CONFIG = Template("""
|
DEFAULT_LOG_CONFIG = Template("""
|
||||||
@@ -69,6 +70,8 @@ class LoggingConfig(Config):
|
|||||||
self.verbosity = config.get("verbose", 0)
|
self.verbosity = config.get("verbose", 0)
|
||||||
self.log_config = self.abspath(config.get("log_config"))
|
self.log_config = self.abspath(config.get("log_config"))
|
||||||
self.log_file = self.abspath(config.get("log_file"))
|
self.log_file = self.abspath(config.get("log_file"))
|
||||||
|
if config.get("full_twisted_stacktraces"):
|
||||||
|
debug_deferreds()
|
||||||
|
|
||||||
def default_config(self, config_dir_path, server_name, **kwargs):
|
def default_config(self, config_dir_path, server_name, **kwargs):
|
||||||
log_file = self.abspath("homeserver.log")
|
log_file = self.abspath("homeserver.log")
|
||||||
@@ -84,6 +87,11 @@ class LoggingConfig(Config):
|
|||||||
|
|
||||||
# A yaml python logging config file
|
# A yaml python logging config file
|
||||||
log_config: "%(log_config)s"
|
log_config: "%(log_config)s"
|
||||||
|
|
||||||
|
# Stop twisted from discarding the stack traces of exceptions in
|
||||||
|
# deferreds by waiting a reactor tick before running a deferred's
|
||||||
|
# callbacks.
|
||||||
|
# full_twisted_stacktraces: true
|
||||||
""" % locals()
|
""" % locals()
|
||||||
|
|
||||||
def read_arguments(self, args):
|
def read_arguments(self, args):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
32
synapse/config/password.py
Normal file
32
synapse/config/password.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015, 2016 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 Config
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordConfig(Config):
|
||||||
|
"""Password login configuration
|
||||||
|
"""
|
||||||
|
|
||||||
|
def read_config(self, config):
|
||||||
|
password_config = config.get("password_config", {})
|
||||||
|
self.password_enabled = password_config.get("enabled", True)
|
||||||
|
|
||||||
|
def default_config(self, config_dir_path, server_name, **kwargs):
|
||||||
|
return """
|
||||||
|
# Enable password for login.
|
||||||
|
password_config:
|
||||||
|
enabled: true
|
||||||
|
"""
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -23,20 +23,23 @@ from distutils.util import strtobool
|
|||||||
class RegistrationConfig(Config):
|
class RegistrationConfig(Config):
|
||||||
|
|
||||||
def read_config(self, config):
|
def read_config(self, config):
|
||||||
self.disable_registration = not bool(
|
self.enable_registration = bool(
|
||||||
strtobool(str(config["enable_registration"]))
|
strtobool(str(config["enable_registration"]))
|
||||||
)
|
)
|
||||||
if "disable_registration" in config:
|
if "disable_registration" in config:
|
||||||
self.disable_registration = bool(
|
self.enable_registration = not bool(
|
||||||
strtobool(str(config["disable_registration"]))
|
strtobool(str(config["disable_registration"]))
|
||||||
)
|
)
|
||||||
|
|
||||||
self.registration_shared_secret = config.get("registration_shared_secret")
|
self.registration_shared_secret = config.get("registration_shared_secret")
|
||||||
self.macaroon_secret_key = config.get("macaroon_secret_key")
|
|
||||||
|
self.bcrypt_rounds = config.get("bcrypt_rounds", 12)
|
||||||
|
self.trusted_third_party_id_servers = config["trusted_third_party_id_servers"]
|
||||||
|
self.allow_guest_access = config.get("allow_guest_access", False)
|
||||||
|
|
||||||
def default_config(self, **kwargs):
|
def default_config(self, **kwargs):
|
||||||
registration_shared_secret = random_string_with_symbols(50)
|
registration_shared_secret = random_string_with_symbols(50)
|
||||||
macaroon_secret_key = random_string_with_symbols(50)
|
|
||||||
return """\
|
return """\
|
||||||
## Registration ##
|
## Registration ##
|
||||||
|
|
||||||
@@ -47,7 +50,21 @@ class RegistrationConfig(Config):
|
|||||||
# secret, even if registration is otherwise disabled.
|
# secret, even if registration is otherwise disabled.
|
||||||
registration_shared_secret: "%(registration_shared_secret)s"
|
registration_shared_secret: "%(registration_shared_secret)s"
|
||||||
|
|
||||||
macaroon_secret_key: "%(macaroon_secret_key)s"
|
# Set the number of bcrypt rounds used to generate password hash.
|
||||||
|
# Larger numbers increase the work factor needed to generate the hash.
|
||||||
|
# The default number of rounds is 12.
|
||||||
|
bcrypt_rounds: 12
|
||||||
|
|
||||||
|
# Allows users to register as guests without a password/email/etc, and
|
||||||
|
# participate in rooms hosted on this server which have been made
|
||||||
|
# accessible to anonymous users.
|
||||||
|
allow_guest_access: False
|
||||||
|
|
||||||
|
# The list of identity servers trusted to verify third party
|
||||||
|
# identifiers by this server.
|
||||||
|
trusted_third_party_id_servers:
|
||||||
|
- matrix.org
|
||||||
|
- vector.im
|
||||||
""" % locals()
|
""" % locals()
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
@@ -59,6 +76,6 @@ class RegistrationConfig(Config):
|
|||||||
|
|
||||||
def read_arguments(self, args):
|
def read_arguments(self, args):
|
||||||
if args.enable_registration is not None:
|
if args.enable_registration is not None:
|
||||||
self.disable_registration = not bool(
|
self.enable_registration = bool(
|
||||||
strtobool(str(args.enable_registration))
|
strtobool(str(args.enable_registration))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class SAML2Config(Config):
|
|||||||
def read_config(self, config):
|
def read_config(self, config):
|
||||||
saml2_config = config.get("saml2_config", None)
|
saml2_config = config.get("saml2_config", None)
|
||||||
if saml2_config:
|
if saml2_config:
|
||||||
self.saml2_enabled = True
|
self.saml2_enabled = saml2_config.get("enabled", True)
|
||||||
self.saml2_config_path = saml2_config["config_path"]
|
self.saml2_config_path = saml2_config["config_path"]
|
||||||
self.saml2_idp_redirect_url = saml2_config["idp_redirect_url"]
|
self.saml2_idp_redirect_url = saml2_config["idp_redirect_url"]
|
||||||
else:
|
else:
|
||||||
@@ -49,6 +49,7 @@ class SAML2Config(Config):
|
|||||||
# the user back to /login/saml2 with proper info.
|
# the user back to /login/saml2 with proper info.
|
||||||
# See pysaml2 docs for format of config.
|
# See pysaml2 docs for format of config.
|
||||||
#saml2_config:
|
#saml2_config:
|
||||||
|
# enabled: true
|
||||||
# config_path: "%s/sp_conf.py"
|
# config_path: "%s/sp_conf.py"
|
||||||
# idp_redirect_url: "http://%s/idp"
|
# idp_redirect_url: "http://%s/idp"
|
||||||
""" % (config_dir_path, server_name)
|
""" % (config_dir_path, server_name)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -26,6 +26,7 @@ class ServerConfig(Config):
|
|||||||
self.soft_file_limit = config["soft_file_limit"]
|
self.soft_file_limit = config["soft_file_limit"]
|
||||||
self.daemonize = config.get("daemonize")
|
self.daemonize = config.get("daemonize")
|
||||||
self.print_pidfile = config.get("print_pidfile")
|
self.print_pidfile = config.get("print_pidfile")
|
||||||
|
self.user_agent_suffix = config.get("user_agent_suffix")
|
||||||
self.use_frozen_dicts = config.get("use_frozen_dicts", True)
|
self.use_frozen_dicts = config.get("use_frozen_dicts", True)
|
||||||
|
|
||||||
self.listeners = config.get("listeners", [])
|
self.listeners = config.get("listeners", [])
|
||||||
@@ -132,6 +133,7 @@ class ServerConfig(Config):
|
|||||||
# The domain name of the server, with optional explicit port.
|
# The domain name of the server, with optional explicit port.
|
||||||
# This is used by remote servers to connect to this server,
|
# This is used by remote servers to connect to this server,
|
||||||
# e.g. matrix.org, localhost:8080, etc.
|
# e.g. matrix.org, localhost:8080, etc.
|
||||||
|
# This is also the last part of your UserID.
|
||||||
server_name: "%(server_name)s"
|
server_name: "%(server_name)s"
|
||||||
|
|
||||||
# When running as a daemon, the file to store the pid in
|
# When running as a daemon, the file to store the pid in
|
||||||
@@ -198,7 +200,7 @@ class ServerConfig(Config):
|
|||||||
- names: [federation]
|
- names: [federation]
|
||||||
compress: false
|
compress: false
|
||||||
|
|
||||||
# Turn on the twisted telnet manhole service on localhost on the given
|
# Turn on the twisted ssh manhole service on localhost on the given
|
||||||
# port.
|
# port.
|
||||||
# - port: 9000
|
# - port: 9000
|
||||||
# bind_address: 127.0.0.1
|
# bind_address: 127.0.0.1
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -18,6 +18,10 @@ 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 import unwrapFirstError
|
||||||
from synapse.util.async import ObservableDeferred
|
from synapse.util.async import ObservableDeferred
|
||||||
|
from synapse.util.logcontext import (
|
||||||
|
preserve_context_over_deferred, preserve_context_over_fn, PreserveLoggingContext,
|
||||||
|
preserve_fn
|
||||||
|
)
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
@@ -142,6 +146,8 @@ class Keyring(object):
|
|||||||
for server_name, _ in server_and_json
|
for server_name, _ in server_and_json
|
||||||
}
|
}
|
||||||
|
|
||||||
|
with PreserveLoggingContext():
|
||||||
|
|
||||||
# We want to wait for any previous lookups to complete before
|
# We want to wait for any previous lookups to complete before
|
||||||
# proceeding.
|
# proceeding.
|
||||||
wait_on_deferred = self.wait_for_previous_lookups(
|
wait_on_deferred = self.wait_for_previous_lookups(
|
||||||
@@ -175,7 +181,8 @@ class Keyring(object):
|
|||||||
# Pass those keys to handle_key_deferred so that the json object
|
# Pass those keys to handle_key_deferred so that the json object
|
||||||
# signatures can be verified
|
# signatures can be verified
|
||||||
return [
|
return [
|
||||||
handle_key_deferred(
|
preserve_context_over_fn(
|
||||||
|
handle_key_deferred,
|
||||||
group_id_to_group[g_id],
|
group_id_to_group[g_id],
|
||||||
deferreds[g_id],
|
deferreds[g_id],
|
||||||
)
|
)
|
||||||
@@ -198,12 +205,13 @@ class Keyring(object):
|
|||||||
if server_name in self.key_downloads
|
if server_name in self.key_downloads
|
||||||
]
|
]
|
||||||
if wait_on:
|
if wait_on:
|
||||||
|
with PreserveLoggingContext():
|
||||||
yield defer.DeferredList(wait_on)
|
yield defer.DeferredList(wait_on)
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
for server_name, deferred in server_to_deferred.items():
|
for server_name, deferred in server_to_deferred.items():
|
||||||
d = ObservableDeferred(deferred)
|
d = ObservableDeferred(preserve_context_over_deferred(deferred))
|
||||||
self.key_downloads[server_name] = d
|
self.key_downloads[server_name] = d
|
||||||
|
|
||||||
def rm(r, server_name):
|
def rm(r, server_name):
|
||||||
@@ -230,7 +238,9 @@ class Keyring(object):
|
|||||||
|
|
||||||
missing_keys = {}
|
missing_keys = {}
|
||||||
for group in group_id_to_group.values():
|
for group in group_id_to_group.values():
|
||||||
missing_keys.setdefault(group.server_name, set()).union(group.key_ids)
|
missing_keys.setdefault(group.server_name, set()).update(
|
||||||
|
group.key_ids
|
||||||
|
)
|
||||||
|
|
||||||
for fn in key_fetch_fns:
|
for fn in key_fetch_fns:
|
||||||
results = yield fn(missing_keys.items())
|
results = yield fn(missing_keys.items())
|
||||||
@@ -242,6 +252,7 @@ class Keyring(object):
|
|||||||
for group in group_id_to_group.values():
|
for group in group_id_to_group.values():
|
||||||
for key_id in group.key_ids:
|
for key_id in group.key_ids:
|
||||||
if key_id in merged_results[group.server_name]:
|
if key_id in merged_results[group.server_name]:
|
||||||
|
with PreserveLoggingContext():
|
||||||
group_id_to_deferred[group.group_id].callback((
|
group_id_to_deferred[group.group_id].callback((
|
||||||
group.group_id,
|
group.group_id,
|
||||||
group.server_name,
|
group.server_name,
|
||||||
@@ -381,11 +392,6 @@ class Keyring(object):
|
|||||||
def get_server_verify_key_v2_indirect(self, server_names_and_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(
|
|
||||||
perspective_name, self.clock, self.store
|
|
||||||
)
|
|
||||||
|
|
||||||
with limiter:
|
|
||||||
# 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.
|
||||||
@@ -402,6 +408,7 @@ class Keyring(object):
|
|||||||
for server_name, key_ids in server_names_and_key_ids
|
for server_name, key_ids in server_names_and_key_ids
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
long_retries=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
keys = {}
|
keys = {}
|
||||||
@@ -506,7 +513,7 @@ class Keyring(object):
|
|||||||
|
|
||||||
yield defer.gatherResults(
|
yield defer.gatherResults(
|
||||||
[
|
[
|
||||||
self.store_keys(
|
preserve_fn(self.store_keys)(
|
||||||
server_name=key_server_name,
|
server_name=key_server_name,
|
||||||
from_server=server_name,
|
from_server=server_name,
|
||||||
verify_keys=verify_keys,
|
verify_keys=verify_keys,
|
||||||
@@ -575,7 +582,7 @@ class Keyring(object):
|
|||||||
|
|
||||||
yield defer.gatherResults(
|
yield defer.gatherResults(
|
||||||
[
|
[
|
||||||
self.store.store_server_keys_json(
|
preserve_fn(self.store.store_server_keys_json)(
|
||||||
server_name=server_name,
|
server_name=server_name,
|
||||||
key_id=key_id,
|
key_id=key_id,
|
||||||
from_server=server_name,
|
from_server=server_name,
|
||||||
@@ -677,7 +684,7 @@ class Keyring(object):
|
|||||||
# TODO(markjh): Store whether the keys have expired.
|
# TODO(markjh): Store whether the keys have expired.
|
||||||
yield defer.gatherResults(
|
yield defer.gatherResults(
|
||||||
[
|
[
|
||||||
self.store.store_server_verify_key(
|
preserve_fn(self.store.store_server_verify_key)(
|
||||||
server_name, server_name, key.time_added, key
|
server_name, server_name, key.time_added, key
|
||||||
)
|
)
|
||||||
for key_id, key in verify_keys.items()
|
for key_id, key in verify_keys.items()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -117,6 +117,15 @@ class EventBase(object):
|
|||||||
def __set__(self, instance, value):
|
def __set__(self, instance, value):
|
||||||
raise AttributeError("Unrecognized attribute %s" % (instance,))
|
raise AttributeError("Unrecognized attribute %s" % (instance,))
|
||||||
|
|
||||||
|
def __getitem__(self, field):
|
||||||
|
return self._event_dict[field]
|
||||||
|
|
||||||
|
def __contains__(self, field):
|
||||||
|
return field in self._event_dict
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return self._event_dict.items()
|
||||||
|
|
||||||
|
|
||||||
class FrozenEvent(EventBase):
|
class FrozenEvent(EventBase):
|
||||||
def __init__(self, event_dict, internal_metadata_dict={}, rejected_reason=None):
|
def __init__(self, event_dict, internal_metadata_dict={}, rejected_reason=None):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -20,3 +20,4 @@ class EventContext(object):
|
|||||||
self.current_state = current_state
|
self.current_state = current_state
|
||||||
self.state_group = None
|
self.state_group = None
|
||||||
self.rejected = False
|
self.rejected = False
|
||||||
|
self.push_actions = []
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -66,7 +66,6 @@ def prune_event(event):
|
|||||||
"users_default",
|
"users_default",
|
||||||
"events",
|
"events",
|
||||||
"events_default",
|
"events_default",
|
||||||
"events_default",
|
|
||||||
"state_default",
|
"state_default",
|
||||||
"ban",
|
"ban",
|
||||||
"kick",
|
"kick",
|
||||||
@@ -101,19 +100,20 @@ def format_event_raw(d):
|
|||||||
|
|
||||||
|
|
||||||
def format_event_for_client_v1(d):
|
def format_event_for_client_v1(d):
|
||||||
d["user_id"] = d.pop("sender", None)
|
d = format_event_for_client_v2(d)
|
||||||
|
|
||||||
move_keys = ("age", "redacted_because", "replaces_state", "prev_content")
|
sender = d.get("sender")
|
||||||
for key in move_keys:
|
if sender is not None:
|
||||||
|
d["user_id"] = sender
|
||||||
|
|
||||||
|
copy_keys = (
|
||||||
|
"age", "redacted_because", "replaces_state", "prev_content",
|
||||||
|
"invite_room_state",
|
||||||
|
)
|
||||||
|
for key in copy_keys:
|
||||||
if key in d["unsigned"]:
|
if key in d["unsigned"]:
|
||||||
d[key] = d["unsigned"][key]
|
d[key] = d["unsigned"][key]
|
||||||
|
|
||||||
drop_keys = (
|
|
||||||
"auth_events", "prev_events", "hashes", "signatures", "depth",
|
|
||||||
"unsigned", "origin", "prev_state"
|
|
||||||
)
|
|
||||||
for key in drop_keys:
|
|
||||||
d.pop(key, None)
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
@@ -127,10 +127,9 @@ def format_event_for_client_v2(d):
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
def format_event_for_client_v2_without_event_id(d):
|
def format_event_for_client_v2_without_room_id(d):
|
||||||
d = format_event_for_client_v2(d)
|
d = format_event_for_client_v2(d)
|
||||||
d.pop("room_id", None)
|
d.pop("room_id", None)
|
||||||
d.pop("event_id", None)
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
@@ -152,7 +151,8 @@ def serialize_event(e, time_now_ms, as_client_event=True,
|
|||||||
|
|
||||||
if "redacted_because" in e.unsigned:
|
if "redacted_because" in e.unsigned:
|
||||||
d["unsigned"]["redacted_because"] = serialize_event(
|
d["unsigned"]["redacted_because"] = serialize_event(
|
||||||
e.unsigned["redacted_because"], time_now_ms
|
e.unsigned["redacted_because"], time_now_ms,
|
||||||
|
event_format=event_format
|
||||||
)
|
)
|
||||||
|
|
||||||
if token_id is not None:
|
if token_id is not None:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -17,15 +17,10 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from .replication import ReplicationLayer
|
from .replication import ReplicationLayer
|
||||||
from .transport import TransportLayer
|
from .transport.client import TransportLayerClient
|
||||||
|
|
||||||
|
|
||||||
def initialize_http_replication(homeserver):
|
def initialize_http_replication(homeserver):
|
||||||
transport = TransportLayer(
|
transport = TransportLayerClient(homeserver)
|
||||||
homeserver,
|
|
||||||
homeserver.hostname,
|
|
||||||
server=homeserver.get_resource_for_federation(),
|
|
||||||
client=homeserver.get_http_client()
|
|
||||||
)
|
|
||||||
|
|
||||||
return ReplicationLayer(homeserver, transport)
|
return ReplicationLayer(homeserver, transport)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from .federation_base import FederationBase
|
from .federation_base import FederationBase
|
||||||
|
from synapse.api.constants import Membership
|
||||||
from .units import Edu
|
from .units import Edu
|
||||||
|
|
||||||
from synapse.api.errors import (
|
from synapse.api.errors import (
|
||||||
@@ -356,19 +357,55 @@ class FederationClient(FederationBase):
|
|||||||
defer.returnValue(signed_auth)
|
defer.returnValue(signed_auth)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def make_join(self, destinations, room_id, user_id):
|
def make_membership_event(self, destinations, room_id, user_id, membership,
|
||||||
|
content={},):
|
||||||
|
"""
|
||||||
|
Creates an m.room.member event, with context, without participating in the room.
|
||||||
|
|
||||||
|
Does so by asking one of the already participating servers to create an
|
||||||
|
event with proper context.
|
||||||
|
|
||||||
|
Note that this does not append any events to any graphs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
destinations (str): Candidate homeservers which are probably
|
||||||
|
participating in the room.
|
||||||
|
room_id (str): The room in which the event will happen.
|
||||||
|
user_id (str): The user whose membership is being evented.
|
||||||
|
membership (str): The "membership" property of the event. Must be
|
||||||
|
one of "join" or "leave".
|
||||||
|
content (object): Any additional data to put into the content field
|
||||||
|
of the event.
|
||||||
|
Return:
|
||||||
|
A tuple of (origin (str), event (object)) where origin is the remote
|
||||||
|
homeserver which generated the event.
|
||||||
|
"""
|
||||||
|
valid_memberships = {Membership.JOIN, Membership.LEAVE}
|
||||||
|
if membership not in valid_memberships:
|
||||||
|
raise RuntimeError(
|
||||||
|
"make_membership_event called with membership='%s', must be one of %s" %
|
||||||
|
(membership, ",".join(valid_memberships))
|
||||||
|
)
|
||||||
for destination in destinations:
|
for destination in destinations:
|
||||||
if destination == self.server_name:
|
if destination == self.server_name:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ret = yield self.transport_layer.make_join(
|
ret = yield self.transport_layer.make_membership_event(
|
||||||
destination, room_id, user_id
|
destination, room_id, user_id, membership
|
||||||
)
|
)
|
||||||
|
|
||||||
pdu_dict = ret["event"]
|
pdu_dict = ret["event"]
|
||||||
|
|
||||||
logger.debug("Got response to make_join: %s", pdu_dict)
|
logger.debug("Got response to make_%s: %s", membership, pdu_dict)
|
||||||
|
|
||||||
|
pdu_dict["content"].update(content)
|
||||||
|
|
||||||
|
# The protoevent received over the JSON wire may not have all
|
||||||
|
# the required fields. Lets just gloss over that because
|
||||||
|
# there's some we never care about
|
||||||
|
if "prev_state" not in pdu_dict:
|
||||||
|
pdu_dict["prev_state"] = []
|
||||||
|
|
||||||
defer.returnValue(
|
defer.returnValue(
|
||||||
(destination, self.event_from_pdu_json(pdu_dict))
|
(destination, self.event_from_pdu_json(pdu_dict))
|
||||||
@@ -378,8 +415,8 @@ class FederationClient(FederationBase):
|
|||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"Failed to make_join via %s: %s",
|
"Failed to make_%s via %s: %s",
|
||||||
destination, e.message
|
membership, destination, e.message
|
||||||
)
|
)
|
||||||
|
|
||||||
raise RuntimeError("Failed to send to any server.")
|
raise RuntimeError("Failed to send to any server.")
|
||||||
@@ -485,6 +522,33 @@ class FederationClient(FederationBase):
|
|||||||
|
|
||||||
defer.returnValue(pdu)
|
defer.returnValue(pdu)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def send_leave(self, destinations, pdu):
|
||||||
|
for destination in destinations:
|
||||||
|
if destination == self.server_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
time_now = self._clock.time_msec()
|
||||||
|
_, content = yield self.transport_layer.send_leave(
|
||||||
|
destination=destination,
|
||||||
|
room_id=pdu.room_id,
|
||||||
|
event_id=pdu.event_id,
|
||||||
|
content=pdu.get_pdu_json(time_now),
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug("Got content: %s", content)
|
||||||
|
defer.returnValue(None)
|
||||||
|
except CodeMessageException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(
|
||||||
|
"Failed to send_leave via %s: %s",
|
||||||
|
destination, e.message
|
||||||
|
)
|
||||||
|
|
||||||
|
raise RuntimeError("Failed to send to any server.")
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def query_auth(self, destination, room_id, event_id, local_auth):
|
def query_auth(self, destination, room_id, event_id, local_auth):
|
||||||
"""
|
"""
|
||||||
@@ -643,3 +707,26 @@ class FederationClient(FederationBase):
|
|||||||
event.internal_metadata.outlier = outlier
|
event.internal_metadata.outlier = outlier
|
||||||
|
|
||||||
return event
|
return event
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def forward_third_party_invite(self, destinations, room_id, event_dict):
|
||||||
|
for destination in destinations:
|
||||||
|
if destination == self.server_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield self.transport_layer.exchange_third_party_invite(
|
||||||
|
destination=destination,
|
||||||
|
room_id=room_id,
|
||||||
|
event_dict=event_dict,
|
||||||
|
)
|
||||||
|
defer.returnValue(None)
|
||||||
|
except CodeMessageException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(
|
||||||
|
"Failed to send_third_party_invite via %s: %s",
|
||||||
|
destination, e.message
|
||||||
|
)
|
||||||
|
|
||||||
|
raise RuntimeError("Failed to send to any server.")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -126,10 +126,8 @@ class FederationServer(FederationBase):
|
|||||||
results = []
|
results = []
|
||||||
|
|
||||||
for pdu in pdu_list:
|
for pdu in pdu_list:
|
||||||
d = self._handle_new_pdu(transaction.origin, pdu)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield d
|
yield self._handle_new_pdu(transaction.origin, pdu)
|
||||||
results.append({})
|
results.append({})
|
||||||
except FederationError as e:
|
except FederationError as e:
|
||||||
self.send_failure(e, transaction.origin)
|
self.send_failure(e, transaction.origin)
|
||||||
@@ -254,6 +252,20 @@ class FederationServer(FederationBase):
|
|||||||
],
|
],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_make_leave_request(self, room_id, user_id):
|
||||||
|
pdu = yield self.handler.on_make_leave_request(room_id, user_id)
|
||||||
|
time_now = self._clock.time_msec()
|
||||||
|
defer.returnValue({"event": pdu.get_pdu_json(time_now)})
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_send_leave_request(self, origin, content):
|
||||||
|
logger.debug("on_send_leave_request: content: %s", content)
|
||||||
|
pdu = self.event_from_pdu_json(content)
|
||||||
|
logger.debug("on_send_leave_request: pdu sigs: %s", pdu.signatures)
|
||||||
|
yield self.handler.on_send_leave_request(origin, pdu)
|
||||||
|
defer.returnValue((200, {}))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_event_auth(self, origin, room_id, event_id):
|
def on_event_auth(self, origin, room_id, event_id):
|
||||||
time_now = self._clock.time_msec()
|
time_now = self._clock.time_msec()
|
||||||
@@ -529,3 +541,15 @@ class FederationServer(FederationBase):
|
|||||||
event.internal_metadata.outlier = outlier
|
event.internal_metadata.outlier = outlier
|
||||||
|
|
||||||
return event
|
return event
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def exchange_third_party_invite(self, invite):
|
||||||
|
ret = yield self.handler.exchange_third_party_invite(invite)
|
||||||
|
defer.returnValue(ret)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_exchange_third_party_invite_request(self, origin, room_id, event_dict):
|
||||||
|
ret = yield self.handler.on_exchange_third_party_invite_request(
|
||||||
|
origin, room_id, event_dict
|
||||||
|
)
|
||||||
|
defer.returnValue(ret)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -54,8 +54,6 @@ class ReplicationLayer(FederationClient, FederationServer):
|
|||||||
self.keyring = hs.get_keyring()
|
self.keyring = hs.get_keyring()
|
||||||
|
|
||||||
self.transport_layer = transport_layer
|
self.transport_layer = transport_layer
|
||||||
self.transport_layer.register_received_handler(self)
|
|
||||||
self.transport_layer.register_request_handler(self)
|
|
||||||
|
|
||||||
self.federation_client = self
|
self.federation_client = self
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -103,7 +103,6 @@ class TransactionQueue(object):
|
|||||||
else:
|
else:
|
||||||
return not destination.startswith("localhost")
|
return not destination.startswith("localhost")
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
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
|
||||||
@@ -141,8 +140,6 @@ class TransactionQueue(object):
|
|||||||
|
|
||||||
deferreds.append(deferred)
|
deferreds.append(deferred)
|
||||||
|
|
||||||
yield defer.DeferredList(deferreds, consumeErrors=True)
|
|
||||||
|
|
||||||
# NO inlineCallbacks
|
# NO inlineCallbacks
|
||||||
def enqueue_edu(self, edu):
|
def enqueue_edu(self, edu):
|
||||||
destination = edu.destination
|
destination = edu.destination
|
||||||
@@ -202,6 +199,7 @@ class TransactionQueue(object):
|
|||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def _attempt_new_transaction(self, destination):
|
def _attempt_new_transaction(self, destination):
|
||||||
|
# list of (pending_pdu, deferred, order)
|
||||||
if destination in self.pending_transactions:
|
if destination in self.pending_transactions:
|
||||||
# XXX: pending_transactions can get stuck on by a never-ending
|
# XXX: pending_transactions can get stuck on by a never-ending
|
||||||
# request at which point pending_pdus_by_dest just keeps growing.
|
# request at which point pending_pdus_by_dest just keeps growing.
|
||||||
@@ -213,9 +211,6 @@ class TransactionQueue(object):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.debug("TX [%s] _attempt_new_transaction", destination)
|
|
||||||
|
|
||||||
# list of (pending_pdu, deferred, order)
|
|
||||||
pending_pdus = self.pending_pdus_by_dest.pop(destination, [])
|
pending_pdus = self.pending_pdus_by_dest.pop(destination, [])
|
||||||
pending_edus = self.pending_edus_by_dest.pop(destination, [])
|
pending_edus = self.pending_edus_by_dest.pop(destination, [])
|
||||||
pending_failures = self.pending_failures_by_dest.pop(destination, [])
|
pending_failures = self.pending_failures_by_dest.pop(destination, [])
|
||||||
@@ -228,6 +223,11 @@ class TransactionQueue(object):
|
|||||||
logger.debug("TX [%s] Nothing to send", destination)
|
logger.debug("TX [%s] Nothing to send", destination)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.pending_transactions[destination] = 1
|
||||||
|
|
||||||
|
logger.debug("TX [%s] _attempt_new_transaction", destination)
|
||||||
|
|
||||||
# Sort based on the order field
|
# Sort based on the order field
|
||||||
pending_pdus.sort(key=lambda t: t[2])
|
pending_pdus.sort(key=lambda t: t[2])
|
||||||
|
|
||||||
@@ -239,9 +239,6 @@ class TransactionQueue(object):
|
|||||||
for x in pending_pdus + pending_edus + pending_failures
|
for x in pending_pdus + pending_edus + pending_failures
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
|
||||||
self.pending_transactions[destination] = 1
|
|
||||||
|
|
||||||
txn_id = str(self._next_txn_id)
|
txn_id = str(self._next_txn_id)
|
||||||
|
|
||||||
limiter = yield get_retry_limiter(
|
limiter = yield get_retry_limiter(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -20,55 +20,3 @@ By default this is done over HTTPS (and all home servers are required to
|
|||||||
support HTTPS), however individual pairings of servers may decide to
|
support HTTPS), however individual pairings of servers may decide to
|
||||||
communicate over a different (albeit still reliable) protocol.
|
communicate over a different (albeit still reliable) protocol.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .server import TransportLayerServer
|
|
||||||
from .client import TransportLayerClient
|
|
||||||
|
|
||||||
from synapse.util.ratelimitutils import FederationRateLimiter
|
|
||||||
|
|
||||||
|
|
||||||
class TransportLayer(TransportLayerServer, TransportLayerClient):
|
|
||||||
"""This is a basic implementation of the transport layer that translates
|
|
||||||
transactions and other requests to/from HTTP.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
server_name (str): Local home server host
|
|
||||||
|
|
||||||
server (synapse.http.server.HttpServer): the http server to
|
|
||||||
register listeners on
|
|
||||||
|
|
||||||
client (synapse.http.client.HttpClient): the http client used to
|
|
||||||
send requests
|
|
||||||
|
|
||||||
request_handler (TransportRequestHandler): The handler to fire when we
|
|
||||||
receive requests for data.
|
|
||||||
|
|
||||||
received_handler (TransportReceivedHandler): The handler to fire when
|
|
||||||
we receive data.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, homeserver, server_name, server, client):
|
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
server_name (str): Local home server host
|
|
||||||
server (synapse.protocol.http.HttpServer): the http server to
|
|
||||||
register listeners on
|
|
||||||
client (synapse.protocol.http.HttpClient): the http client used to
|
|
||||||
send requests
|
|
||||||
"""
|
|
||||||
self.keyring = homeserver.get_keyring()
|
|
||||||
self.clock = homeserver.get_clock()
|
|
||||||
self.server_name = server_name
|
|
||||||
self.server = server
|
|
||||||
self.client = client
|
|
||||||
self.request_handler = None
|
|
||||||
self.received_handler = None
|
|
||||||
|
|
||||||
self.ratelimiter = FederationRateLimiter(
|
|
||||||
self.clock,
|
|
||||||
window_size=homeserver.config.federation_rc_window_size,
|
|
||||||
sleep_limit=homeserver.config.federation_rc_sleep_limit,
|
|
||||||
sleep_msec=homeserver.config.federation_rc_sleep_delay,
|
|
||||||
reject_limit=homeserver.config.federation_rc_reject_limit,
|
|
||||||
concurrent_requests=homeserver.config.federation_rc_concurrent,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
from synapse.api.constants import Membership
|
||||||
|
|
||||||
from synapse.api.urls import FEDERATION_PREFIX as PREFIX
|
from synapse.api.urls import FEDERATION_PREFIX as PREFIX
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
@@ -27,6 +28,10 @@ logger = logging.getLogger(__name__)
|
|||||||
class TransportLayerClient(object):
|
class TransportLayerClient(object):
|
||||||
"""Sends federation HTTP requests to other servers"""
|
"""Sends federation HTTP requests to other servers"""
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
self.server_name = hs.hostname
|
||||||
|
self.client = hs.get_http_client()
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def get_room_state(self, destination, room_id, event_id):
|
def get_room_state(self, destination, room_id, event_id):
|
||||||
""" Requests all state for a given room from the given server at the
|
""" Requests all state for a given room from the given server at the
|
||||||
@@ -135,6 +140,7 @@ class TransportLayerClient(object):
|
|||||||
path=PREFIX + "/send/%s/" % transaction.transaction_id,
|
path=PREFIX + "/send/%s/" % transaction.transaction_id,
|
||||||
data=json_data,
|
data=json_data,
|
||||||
json_data_callback=json_data_callback,
|
json_data_callback=json_data_callback,
|
||||||
|
long_retries=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -160,13 +166,19 @@ class TransportLayerClient(object):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def make_join(self, destination, room_id, user_id, retry_on_dns_fail=True):
|
def make_membership_event(self, destination, room_id, user_id, membership):
|
||||||
path = PREFIX + "/make_join/%s/%s" % (room_id, user_id)
|
valid_memberships = {Membership.JOIN, Membership.LEAVE}
|
||||||
|
if membership not in valid_memberships:
|
||||||
|
raise RuntimeError(
|
||||||
|
"make_membership_event called with membership='%s', must be one of %s" %
|
||||||
|
(membership, ",".join(valid_memberships))
|
||||||
|
)
|
||||||
|
path = PREFIX + "/make_%s/%s/%s" % (membership, room_id, user_id)
|
||||||
|
|
||||||
content = yield self.client.get_json(
|
content = yield self.client.get_json(
|
||||||
destination=destination,
|
destination=destination,
|
||||||
path=path,
|
path=path,
|
||||||
retry_on_dns_fail=retry_on_dns_fail,
|
retry_on_dns_fail=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
defer.returnValue(content)
|
defer.returnValue(content)
|
||||||
@@ -184,6 +196,19 @@ class TransportLayerClient(object):
|
|||||||
|
|
||||||
defer.returnValue(response)
|
defer.returnValue(response)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
@log_function
|
||||||
|
def send_leave(self, destination, room_id, event_id, content):
|
||||||
|
path = PREFIX + "/send_leave/%s/%s" % (room_id, event_id)
|
||||||
|
|
||||||
|
response = yield self.client.put_json(
|
||||||
|
destination=destination,
|
||||||
|
path=path,
|
||||||
|
data=content,
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue(response)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def send_invite(self, destination, room_id, event_id, content):
|
def send_invite(self, destination, room_id, event_id, content):
|
||||||
@@ -197,6 +222,19 @@ class TransportLayerClient(object):
|
|||||||
|
|
||||||
defer.returnValue(response)
|
defer.returnValue(response)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
@log_function
|
||||||
|
def exchange_third_party_invite(self, destination, room_id, event_dict):
|
||||||
|
path = PREFIX + "/exchange_third_party_invite/%s" % (room_id,)
|
||||||
|
|
||||||
|
response = yield self.client.put_json(
|
||||||
|
destination=destination,
|
||||||
|
path=path,
|
||||||
|
data=event_dict,
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue(response)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def get_event_auth(self, destination, room_id, event_id):
|
def get_event_auth(self, destination, room_id, event_id):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -17,7 +17,8 @@ from twisted.internet import defer
|
|||||||
|
|
||||||
from synapse.api.urls import FEDERATION_PREFIX as PREFIX
|
from synapse.api.urls import FEDERATION_PREFIX as PREFIX
|
||||||
from synapse.api.errors import Codes, SynapseError
|
from synapse.api.errors import Codes, SynapseError
|
||||||
from synapse.util.logutils import log_function
|
from synapse.http.server import JsonResource
|
||||||
|
from synapse.util.ratelimitutils import FederationRateLimiter
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
@@ -28,9 +29,41 @@ import re
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TransportLayerServer(object):
|
class TransportLayerServer(JsonResource):
|
||||||
"""Handles incoming federation HTTP requests"""
|
"""Handles incoming federation HTTP requests"""
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
self.hs = hs
|
||||||
|
self.clock = hs.get_clock()
|
||||||
|
|
||||||
|
super(TransportLayerServer, self).__init__(hs)
|
||||||
|
|
||||||
|
self.authenticator = Authenticator(hs)
|
||||||
|
self.ratelimiter = FederationRateLimiter(
|
||||||
|
self.clock,
|
||||||
|
window_size=hs.config.federation_rc_window_size,
|
||||||
|
sleep_limit=hs.config.federation_rc_sleep_limit,
|
||||||
|
sleep_msec=hs.config.federation_rc_sleep_delay,
|
||||||
|
reject_limit=hs.config.federation_rc_reject_limit,
|
||||||
|
concurrent_requests=hs.config.federation_rc_concurrent,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.register_servlets()
|
||||||
|
|
||||||
|
def register_servlets(self):
|
||||||
|
register_servlets(
|
||||||
|
self.hs,
|
||||||
|
resource=self,
|
||||||
|
ratelimiter=self.ratelimiter,
|
||||||
|
authenticator=self.authenticator,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Authenticator(object):
|
||||||
|
def __init__(self, hs):
|
||||||
|
self.keyring = hs.get_keyring()
|
||||||
|
self.server_name = hs.hostname
|
||||||
|
|
||||||
# A method just so we can pass 'self' as the authenticator to the Servlets
|
# A method just so we can pass 'self' as the authenticator to the Servlets
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def authenticate_request(self, request):
|
def authenticate_request(self, request):
|
||||||
@@ -98,37 +131,9 @@ class TransportLayerServer(object):
|
|||||||
|
|
||||||
defer.returnValue((origin, content))
|
defer.returnValue((origin, content))
|
||||||
|
|
||||||
@log_function
|
|
||||||
def register_received_handler(self, handler):
|
|
||||||
""" Register a handler that will be fired when we receive data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
handler (TransportReceivedHandler)
|
|
||||||
"""
|
|
||||||
FederationSendServlet(
|
|
||||||
handler,
|
|
||||||
authenticator=self,
|
|
||||||
ratelimiter=self.ratelimiter,
|
|
||||||
server_name=self.server_name,
|
|
||||||
).register(self.server)
|
|
||||||
|
|
||||||
@log_function
|
|
||||||
def register_request_handler(self, handler):
|
|
||||||
""" Register a handler that will be fired when we get asked for data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
handler (TransportRequestHandler)
|
|
||||||
"""
|
|
||||||
for servletclass in SERVLET_CLASSES:
|
|
||||||
servletclass(
|
|
||||||
handler,
|
|
||||||
authenticator=self,
|
|
||||||
ratelimiter=self.ratelimiter,
|
|
||||||
).register(self.server)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseFederationServlet(object):
|
class BaseFederationServlet(object):
|
||||||
def __init__(self, handler, authenticator, ratelimiter):
|
def __init__(self, handler, authenticator, ratelimiter, server_name):
|
||||||
self.handler = handler
|
self.handler = handler
|
||||||
self.authenticator = authenticator
|
self.authenticator = authenticator
|
||||||
self.ratelimiter = ratelimiter
|
self.ratelimiter = ratelimiter
|
||||||
@@ -165,14 +170,16 @@ class BaseFederationServlet(object):
|
|||||||
if code is None:
|
if code is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
server.register_path(method, pattern, self._wrap(code))
|
server.register_paths(method, (pattern,), self._wrap(code))
|
||||||
|
|
||||||
|
|
||||||
class FederationSendServlet(BaseFederationServlet):
|
class FederationSendServlet(BaseFederationServlet):
|
||||||
PATH = "/send/([^/]*)/"
|
PATH = "/send/([^/]*)/"
|
||||||
|
|
||||||
def __init__(self, handler, server_name, **kwargs):
|
def __init__(self, handler, server_name, **kwargs):
|
||||||
super(FederationSendServlet, self).__init__(handler, **kwargs)
|
super(FederationSendServlet, self).__init__(
|
||||||
|
handler, server_name=server_name, **kwargs
|
||||||
|
)
|
||||||
self.server_name = server_name
|
self.server_name = server_name
|
||||||
|
|
||||||
# This is when someone is trying to send us a bunch of data.
|
# This is when someone is trying to send us a bunch of data.
|
||||||
@@ -296,6 +303,24 @@ class FederationMakeJoinServlet(BaseFederationServlet):
|
|||||||
defer.returnValue((200, content))
|
defer.returnValue((200, content))
|
||||||
|
|
||||||
|
|
||||||
|
class FederationMakeLeaveServlet(BaseFederationServlet):
|
||||||
|
PATH = "/make_leave/([^/]*)/([^/]*)"
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_GET(self, origin, content, query, context, user_id):
|
||||||
|
content = yield self.handler.on_make_leave_request(context, user_id)
|
||||||
|
defer.returnValue((200, content))
|
||||||
|
|
||||||
|
|
||||||
|
class FederationSendLeaveServlet(BaseFederationServlet):
|
||||||
|
PATH = "/send_leave/([^/]*)/([^/]*)"
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_PUT(self, origin, content, query, room_id, txid):
|
||||||
|
content = yield self.handler.on_send_leave_request(origin, content)
|
||||||
|
defer.returnValue((200, content))
|
||||||
|
|
||||||
|
|
||||||
class FederationEventAuthServlet(BaseFederationServlet):
|
class FederationEventAuthServlet(BaseFederationServlet):
|
||||||
PATH = "/event_auth/([^/]*)/([^/]*)"
|
PATH = "/event_auth/([^/]*)/([^/]*)"
|
||||||
|
|
||||||
@@ -325,6 +350,17 @@ class FederationInviteServlet(BaseFederationServlet):
|
|||||||
defer.returnValue((200, content))
|
defer.returnValue((200, content))
|
||||||
|
|
||||||
|
|
||||||
|
class FederationThirdPartyInviteExchangeServlet(BaseFederationServlet):
|
||||||
|
PATH = "/exchange_third_party_invite/([^/]*)"
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_PUT(self, origin, content, query, room_id):
|
||||||
|
content = yield self.handler.on_exchange_third_party_invite_request(
|
||||||
|
origin, room_id, content
|
||||||
|
)
|
||||||
|
defer.returnValue((200, content))
|
||||||
|
|
||||||
|
|
||||||
class FederationClientKeysQueryServlet(BaseFederationServlet):
|
class FederationClientKeysQueryServlet(BaseFederationServlet):
|
||||||
PATH = "/user/keys/query"
|
PATH = "/user/keys/query"
|
||||||
|
|
||||||
@@ -378,19 +414,58 @@ class FederationGetMissingEventsServlet(BaseFederationServlet):
|
|||||||
defer.returnValue((200, content))
|
defer.returnValue((200, content))
|
||||||
|
|
||||||
|
|
||||||
|
class On3pidBindServlet(BaseFederationServlet):
|
||||||
|
PATH = "/3pid/onbind"
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_POST(self, request):
|
||||||
|
content_bytes = request.content.read()
|
||||||
|
content = json.loads(content_bytes)
|
||||||
|
if "invites" in content:
|
||||||
|
last_exception = None
|
||||||
|
for invite in content["invites"]:
|
||||||
|
try:
|
||||||
|
yield self.handler.exchange_third_party_invite(invite)
|
||||||
|
except Exception as e:
|
||||||
|
last_exception = e
|
||||||
|
if last_exception:
|
||||||
|
raise last_exception
|
||||||
|
defer.returnValue((200, {}))
|
||||||
|
|
||||||
|
# Avoid doing remote HS authorization checks which are done by default by
|
||||||
|
# BaseFederationServlet.
|
||||||
|
def _wrap(self, code):
|
||||||
|
return code
|
||||||
|
|
||||||
|
|
||||||
SERVLET_CLASSES = (
|
SERVLET_CLASSES = (
|
||||||
|
FederationSendServlet,
|
||||||
FederationPullServlet,
|
FederationPullServlet,
|
||||||
FederationEventServlet,
|
FederationEventServlet,
|
||||||
FederationStateServlet,
|
FederationStateServlet,
|
||||||
FederationBackfillServlet,
|
FederationBackfillServlet,
|
||||||
FederationQueryServlet,
|
FederationQueryServlet,
|
||||||
FederationMakeJoinServlet,
|
FederationMakeJoinServlet,
|
||||||
|
FederationMakeLeaveServlet,
|
||||||
FederationEventServlet,
|
FederationEventServlet,
|
||||||
FederationSendJoinServlet,
|
FederationSendJoinServlet,
|
||||||
|
FederationSendLeaveServlet,
|
||||||
FederationInviteServlet,
|
FederationInviteServlet,
|
||||||
FederationQueryAuthServlet,
|
FederationQueryAuthServlet,
|
||||||
FederationGetMissingEventsServlet,
|
FederationGetMissingEventsServlet,
|
||||||
FederationEventAuthServlet,
|
FederationEventAuthServlet,
|
||||||
FederationClientKeysQueryServlet,
|
FederationClientKeysQueryServlet,
|
||||||
FederationClientKeysClaimServlet,
|
FederationClientKeysClaimServlet,
|
||||||
|
FederationThirdPartyInviteExchangeServlet,
|
||||||
|
On3pidBindServlet,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register_servlets(hs, resource, authenticator, ratelimiter):
|
||||||
|
for servletclass in SERVLET_CLASSES:
|
||||||
|
servletclass(
|
||||||
|
handler=hs.get_replication_layer(),
|
||||||
|
authenticator=authenticator,
|
||||||
|
ratelimiter=ratelimiter,
|
||||||
|
server_name=hs.hostname,
|
||||||
|
).register(resource)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -17,7 +17,7 @@ from synapse.appservice.scheduler import AppServiceScheduler
|
|||||||
from synapse.appservice.api import ApplicationServiceApi
|
from synapse.appservice.api import ApplicationServiceApi
|
||||||
from .register import RegistrationHandler
|
from .register import RegistrationHandler
|
||||||
from .room import (
|
from .room import (
|
||||||
RoomCreationHandler, RoomMemberHandler, RoomListHandler
|
RoomCreationHandler, RoomMemberHandler, RoomListHandler, RoomContextHandler,
|
||||||
)
|
)
|
||||||
from .message import MessageHandler
|
from .message import MessageHandler
|
||||||
from .events import EventStreamHandler, EventHandler
|
from .events import EventStreamHandler, EventHandler
|
||||||
@@ -32,6 +32,7 @@ from .sync import SyncHandler
|
|||||||
from .auth import AuthHandler
|
from .auth import AuthHandler
|
||||||
from .identity import IdentityHandler
|
from .identity import IdentityHandler
|
||||||
from .receipts import ReceiptsHandler
|
from .receipts import ReceiptsHandler
|
||||||
|
from .search import SearchHandler
|
||||||
|
|
||||||
|
|
||||||
class Handlers(object):
|
class Handlers(object):
|
||||||
@@ -68,3 +69,5 @@ class Handlers(object):
|
|||||||
self.sync_handler = SyncHandler(hs)
|
self.sync_handler = SyncHandler(hs)
|
||||||
self.auth_handler = AuthHandler(hs)
|
self.auth_handler = AuthHandler(hs)
|
||||||
self.identity_handler = IdentityHandler(hs)
|
self.identity_handler = IdentityHandler(hs)
|
||||||
|
self.search_handler = SearchHandler(hs)
|
||||||
|
self.room_context_handler = RoomContextHandler(hs)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014 - 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -19,6 +19,7 @@ from synapse.api.errors import LimitExceededError, SynapseError, AuthError
|
|||||||
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, RoomAlias
|
from synapse.types import UserID, RoomAlias
|
||||||
|
from synapse.push.action_generator import ActionGenerator
|
||||||
|
|
||||||
from synapse.util.logcontext import PreserveLoggingContext
|
from synapse.util.logcontext import PreserveLoggingContext
|
||||||
|
|
||||||
@@ -29,6 +30,12 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class BaseHandler(object):
|
class BaseHandler(object):
|
||||||
|
"""
|
||||||
|
Common base class for the event handlers.
|
||||||
|
|
||||||
|
:type store: synapse.storage.events.StateStore
|
||||||
|
:type state_handler: synapse.state.StateHandler
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
@@ -45,6 +52,87 @@ class BaseHandler(object):
|
|||||||
|
|
||||||
self.event_builder_factory = hs.get_event_builder_factory()
|
self.event_builder_factory = hs.get_event_builder_factory()
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _filter_events_for_clients(self, user_tuples, events, event_id_to_state):
|
||||||
|
""" Returns dict of user_id -> list of events that user is allowed to
|
||||||
|
see.
|
||||||
|
"""
|
||||||
|
forgotten = yield defer.gatherResults([
|
||||||
|
self.store.who_forgot_in_room(
|
||||||
|
room_id,
|
||||||
|
)
|
||||||
|
for room_id in frozenset(e.room_id for e in events)
|
||||||
|
], consumeErrors=True)
|
||||||
|
|
||||||
|
# Set of membership event_ids that have been forgotten
|
||||||
|
event_id_forgotten = frozenset(
|
||||||
|
row["event_id"] for rows in forgotten for row in rows
|
||||||
|
)
|
||||||
|
|
||||||
|
def allowed(event, user_id, is_peeking):
|
||||||
|
state = event_id_to_state[event.event_id]
|
||||||
|
|
||||||
|
visibility_event = state.get((EventTypes.RoomHistoryVisibility, ""), None)
|
||||||
|
if visibility_event:
|
||||||
|
visibility = visibility_event.content.get("history_visibility", "shared")
|
||||||
|
else:
|
||||||
|
visibility = "shared"
|
||||||
|
|
||||||
|
if visibility == "world_readable":
|
||||||
|
return True
|
||||||
|
|
||||||
|
if is_peeking:
|
||||||
|
return False
|
||||||
|
|
||||||
|
membership_event = state.get((EventTypes.Member, user_id), None)
|
||||||
|
if membership_event:
|
||||||
|
if membership_event.event_id in event_id_forgotten:
|
||||||
|
membership = None
|
||||||
|
else:
|
||||||
|
membership = membership_event.membership
|
||||||
|
else:
|
||||||
|
membership = None
|
||||||
|
|
||||||
|
if membership == Membership.JOIN:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if event.type == EventTypes.RoomHistoryVisibility:
|
||||||
|
return not is_peeking
|
||||||
|
|
||||||
|
if visibility == "shared":
|
||||||
|
return True
|
||||||
|
elif visibility == "joined":
|
||||||
|
return membership == Membership.JOIN
|
||||||
|
elif visibility == "invited":
|
||||||
|
return membership == Membership.INVITE
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
defer.returnValue({
|
||||||
|
user_id: [
|
||||||
|
event
|
||||||
|
for event in events
|
||||||
|
if allowed(event, user_id, is_peeking)
|
||||||
|
]
|
||||||
|
for user_id, is_peeking in user_tuples
|
||||||
|
})
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _filter_events_for_client(self, user_id, events, is_peeking=False):
|
||||||
|
# Assumes that user has at some point joined the room if not is_guest.
|
||||||
|
types = (
|
||||||
|
(EventTypes.RoomHistoryVisibility, ""),
|
||||||
|
(EventTypes.Member, user_id),
|
||||||
|
)
|
||||||
|
event_id_to_state = yield self.store.get_state_for_events(
|
||||||
|
frozenset(e.event_id for e in events),
|
||||||
|
types=types
|
||||||
|
)
|
||||||
|
res = yield self._filter_events_for_clients(
|
||||||
|
[(user_id, is_peeking)], events, event_id_to_state
|
||||||
|
)
|
||||||
|
defer.returnValue(res.get(user_id, []))
|
||||||
|
|
||||||
def ratelimit(self, user_id):
|
def ratelimit(self, user_id):
|
||||||
time_now = self.clock.time()
|
time_now = self.clock.time()
|
||||||
allowed, time_allowed = self.ratelimiter.send_message(
|
allowed, time_allowed = self.ratelimiter.send_message(
|
||||||
@@ -100,13 +188,13 @@ class BaseHandler(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def handle_new_client_event(self, event, context, extra_destinations=[],
|
def handle_new_client_event(self, event, context, extra_users=[]):
|
||||||
extra_users=[], suppress_auth=False):
|
|
||||||
# We now need to go and hit out to wherever we need to hit out to.
|
# We now need to go and hit out to wherever we need to hit out to.
|
||||||
|
|
||||||
if not suppress_auth:
|
|
||||||
self.auth.check(event, auth_events=context.current_state)
|
self.auth.check(event, auth_events=context.current_state)
|
||||||
|
|
||||||
|
yield self.maybe_kick_guest_users(event, context.current_state.values())
|
||||||
|
|
||||||
if event.type == EventTypes.CanonicalAlias:
|
if event.type == EventTypes.CanonicalAlias:
|
||||||
# Check the alias is acually valid (at this time at least)
|
# Check the alias is acually valid (at this time at least)
|
||||||
room_alias_str = event.content.get("alias", None)
|
room_alias_str = event.content.get("alias", None)
|
||||||
@@ -123,24 +211,39 @@ class BaseHandler(object):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
(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
|
||||||
|
|
||||||
if event.type == EventTypes.Member:
|
if event.type == EventTypes.Member:
|
||||||
if event.content["membership"] == Membership.INVITE:
|
if event.content["membership"] == Membership.INVITE:
|
||||||
|
event.unsigned["invite_room_state"] = [
|
||||||
|
{
|
||||||
|
"type": e.type,
|
||||||
|
"state_key": e.state_key,
|
||||||
|
"content": e.content,
|
||||||
|
"sender": e.sender,
|
||||||
|
}
|
||||||
|
for k, e in context.current_state.items()
|
||||||
|
if e.type in (
|
||||||
|
EventTypes.JoinRules,
|
||||||
|
EventTypes.CanonicalAlias,
|
||||||
|
EventTypes.RoomAvatar,
|
||||||
|
EventTypes.Name,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
invitee = UserID.from_string(event.state_key)
|
invitee = UserID.from_string(event.state_key)
|
||||||
if not self.hs.is_mine(invitee):
|
if not self.hs.is_mine(invitee):
|
||||||
# TODO: Can we add signature from remote server in a nicer
|
# TODO: Can we add signature from remote server in a nicer
|
||||||
# way? If we have been invited by a remote server, we need
|
# way? If we have been invited by a remote server, we need
|
||||||
# to get them to sign the event.
|
# to get them to sign the event.
|
||||||
|
|
||||||
returned_invite = yield federation_handler.send_invite(
|
returned_invite = yield federation_handler.send_invite(
|
||||||
invitee.domain,
|
invitee.domain,
|
||||||
event,
|
event,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
event.unsigned.pop("room_state", None)
|
||||||
|
|
||||||
# TODO: Make sure the signatures actually are correct.
|
# TODO: Make sure the signatures actually are correct.
|
||||||
event.signatures.update(
|
event.signatures.update(
|
||||||
returned_invite.signatures
|
returned_invite.signatures
|
||||||
@@ -161,7 +264,16 @@ class BaseHandler(object):
|
|||||||
"You don't have permission to redact events"
|
"You don't have permission to redact events"
|
||||||
)
|
)
|
||||||
|
|
||||||
destinations = set(extra_destinations)
|
action_generator = ActionGenerator(self.hs)
|
||||||
|
yield action_generator.handle_push_actions_for_event(
|
||||||
|
event, context, self
|
||||||
|
)
|
||||||
|
|
||||||
|
(event_stream_id, max_stream_id) = yield self.store.persist_event(
|
||||||
|
event, context=context
|
||||||
|
)
|
||||||
|
|
||||||
|
destinations = set()
|
||||||
for k, s in context.current_state.items():
|
for k, s in context.current_state.items():
|
||||||
try:
|
try:
|
||||||
if k[0] == EventTypes.Member:
|
if k[0] == EventTypes.Member:
|
||||||
@@ -176,19 +288,69 @@ class BaseHandler(object):
|
|||||||
|
|
||||||
with PreserveLoggingContext():
|
with PreserveLoggingContext():
|
||||||
# Don't block waiting on waking up all the listeners.
|
# Don't block waiting on waking up all the listeners.
|
||||||
notify_d = self.notifier.on_new_room_event(
|
self.notifier.on_new_room_event(
|
||||||
event, event_stream_id, max_stream_id,
|
event, event_stream_id, max_stream_id,
|
||||||
extra_users=extra_users
|
extra_users=extra_users
|
||||||
)
|
)
|
||||||
|
|
||||||
def log_failure(f):
|
# If invite, remove room_state from unsigned before sending.
|
||||||
logger.warn(
|
event.unsigned.pop("invite_room_state", None)
|
||||||
"Failed to notify about %s: %s",
|
|
||||||
event.event_id, f.value
|
|
||||||
)
|
|
||||||
|
|
||||||
notify_d.addErrback(log_failure)
|
|
||||||
|
|
||||||
federation_handler.handle_new_event(
|
federation_handler.handle_new_event(
|
||||||
event, destinations=destinations,
|
event, destinations=destinations,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def maybe_kick_guest_users(self, event, current_state):
|
||||||
|
# Technically this function invalidates current_state by changing it.
|
||||||
|
# Hopefully this isn't that important to the caller.
|
||||||
|
if event.type == EventTypes.GuestAccess:
|
||||||
|
guest_access = event.content.get("guest_access", "forbidden")
|
||||||
|
if guest_access != "can_join":
|
||||||
|
yield self.kick_guest_users(current_state)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def kick_guest_users(self, current_state):
|
||||||
|
for member_event in current_state:
|
||||||
|
try:
|
||||||
|
if member_event.type != EventTypes.Member:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not self.hs.is_mine(UserID.from_string(member_event.state_key)):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if member_event.content["membership"] not in {
|
||||||
|
Membership.JOIN,
|
||||||
|
Membership.INVITE
|
||||||
|
}:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (
|
||||||
|
"kind" not in member_event.content
|
||||||
|
or member_event.content["kind"] != "guest"
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# We make the user choose to leave, rather than have the
|
||||||
|
# event-sender kick them. This is partially because we don't
|
||||||
|
# need to worry about power levels, and partially because guest
|
||||||
|
# users are a concept which doesn't hugely work over federation,
|
||||||
|
# and having homeservers have their own users leave keeps more
|
||||||
|
# of that decision-making and control local to the guest-having
|
||||||
|
# homeserver.
|
||||||
|
message_handler = self.hs.get_handlers().message_handler
|
||||||
|
yield message_handler.create_and_send_event(
|
||||||
|
{
|
||||||
|
"type": EventTypes.Member,
|
||||||
|
"state_key": member_event.state_key,
|
||||||
|
"content": {
|
||||||
|
"membership": Membership.LEAVE,
|
||||||
|
"kind": "guest"
|
||||||
|
},
|
||||||
|
"room_id": member_event.room_id,
|
||||||
|
"sender": member_event.state_key
|
||||||
|
},
|
||||||
|
ratelimit=False,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn("Error kicking guest user: %s" % (e,))
|
||||||
|
|||||||
65
synapse/handlers/account_data.py
Normal file
65
synapse/handlers/account_data.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015, 2016 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
|
||||||
|
|
||||||
|
|
||||||
|
class AccountDataEventSource(object):
|
||||||
|
def __init__(self, hs):
|
||||||
|
self.store = hs.get_datastore()
|
||||||
|
|
||||||
|
def get_current_key(self, direction='f'):
|
||||||
|
return self.store.get_max_account_data_stream_id()
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_new_events(self, user, from_key, **kwargs):
|
||||||
|
user_id = user.to_string()
|
||||||
|
last_stream_id = from_key
|
||||||
|
|
||||||
|
current_stream_id = yield self.store.get_max_account_data_stream_id()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
tags = yield self.store.get_updated_tags(user_id, last_stream_id)
|
||||||
|
|
||||||
|
for room_id, room_tags in tags.items():
|
||||||
|
results.append({
|
||||||
|
"type": "m.tag",
|
||||||
|
"content": {"tags": room_tags},
|
||||||
|
"room_id": room_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
account_data, room_account_data = (
|
||||||
|
yield self.store.get_updated_account_data_for_user(user_id, last_stream_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
for account_data_type, content in account_data.items():
|
||||||
|
results.append({
|
||||||
|
"type": account_data_type,
|
||||||
|
"content": content,
|
||||||
|
})
|
||||||
|
|
||||||
|
for room_id, account_data in room_account_data.items():
|
||||||
|
for account_data_type, content in account_data.items():
|
||||||
|
results.append({
|
||||||
|
"type": account_data_type,
|
||||||
|
"content": content,
|
||||||
|
"room_id": room_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
defer.returnValue((results, current_stream_id))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_pagination_rows(self, user, config, key):
|
||||||
|
defer.returnValue(([], config.to_id))
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -30,34 +30,27 @@ class AdminHandler(BaseHandler):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_whois(self, user):
|
def get_whois(self, user):
|
||||||
res = yield self.store.get_user_ip_and_agents(user)
|
connections = []
|
||||||
|
|
||||||
d = {}
|
sessions = yield self.store.get_user_ip_and_agents(user)
|
||||||
for r in res:
|
for session in sessions:
|
||||||
# Note that device_id is always None
|
connections.append({
|
||||||
device = d.setdefault(r["device_id"], {})
|
"ip": session["ip"],
|
||||||
session = device.setdefault(r["access_token"], [])
|
"last_seen": session["last_seen"],
|
||||||
session.append({
|
"user_agent": session["user_agent"],
|
||||||
"ip": r["ip"],
|
|
||||||
"user_agent": r["user_agent"],
|
|
||||||
"last_seen": r["last_seen"],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
ret = {
|
ret = {
|
||||||
"user_id": user.to_string(),
|
"user_id": user.to_string(),
|
||||||
"devices": [
|
"devices": {
|
||||||
{
|
"": {
|
||||||
"device_id": k,
|
|
||||||
"sessions": [
|
"sessions": [
|
||||||
{
|
{
|
||||||
# "access_token": x, TODO (erikj)
|
"connections": connections,
|
||||||
"connections": y,
|
|
||||||
}
|
}
|
||||||
for x, y in v.items()
|
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
for k, v in d.items()
|
},
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer.returnValue(ret)
|
defer.returnValue(ret)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014 - 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -18,7 +18,7 @@ from twisted.internet import defer
|
|||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
from synapse.api.constants import LoginType
|
from synapse.api.constants import LoginType
|
||||||
from synapse.types import UserID
|
from synapse.types import UserID
|
||||||
from synapse.api.errors import LoginError, Codes
|
from synapse.api.errors import AuthError, LoginError, Codes
|
||||||
from synapse.util.async import run_on_reactor
|
from synapse.util.async import run_on_reactor
|
||||||
|
|
||||||
from twisted.web.client import PartialDownloadError
|
from twisted.web.client import PartialDownloadError
|
||||||
@@ -44,7 +44,9 @@ class AuthHandler(BaseHandler):
|
|||||||
LoginType.EMAIL_IDENTITY: self._check_email_identity,
|
LoginType.EMAIL_IDENTITY: self._check_email_identity,
|
||||||
LoginType.DUMMY: self._check_dummy_auth,
|
LoginType.DUMMY: self._check_dummy_auth,
|
||||||
}
|
}
|
||||||
|
self.bcrypt_rounds = hs.config.bcrypt_rounds
|
||||||
self.sessions = {}
|
self.sessions = {}
|
||||||
|
self.INVALID_TOKEN_HTTP_STATUS = 401
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def check_auth(self, flows, clientdict, clientip):
|
def check_auth(self, flows, clientdict, clientip):
|
||||||
@@ -295,6 +297,39 @@ class AuthHandler(BaseHandler):
|
|||||||
refresh_token = yield self.issue_refresh_token(user_id)
|
refresh_token = yield self.issue_refresh_token(user_id)
|
||||||
defer.returnValue((user_id, access_token, refresh_token))
|
defer.returnValue((user_id, access_token, refresh_token))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_login_tuple_for_user_id(self, user_id):
|
||||||
|
"""
|
||||||
|
Gets login tuple for the user with the given user ID.
|
||||||
|
The user is assumed to have been authenticated by some other
|
||||||
|
machanism (e.g. CAS)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id (str): User ID
|
||||||
|
Returns:
|
||||||
|
A tuple of:
|
||||||
|
The user's ID.
|
||||||
|
The access token for the user's session.
|
||||||
|
The refresh 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, ignored = yield self._find_user_id_and_pwd_hash(user_id)
|
||||||
|
|
||||||
|
logger.info("Logging in user %s", user_id)
|
||||||
|
access_token = yield self.issue_access_token(user_id)
|
||||||
|
refresh_token = yield self.issue_refresh_token(user_id)
|
||||||
|
defer.returnValue((user_id, access_token, refresh_token))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def does_user_exist(self, user_id):
|
||||||
|
try:
|
||||||
|
yield self._find_user_id_and_pwd_hash(user_id)
|
||||||
|
defer.returnValue(True)
|
||||||
|
except LoginError:
|
||||||
|
defer.returnValue(False)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _find_user_id_and_pwd_hash(self, user_id):
|
def _find_user_id_and_pwd_hash(self, user_id):
|
||||||
"""Checks to see if a user with the given id exists. Will check case
|
"""Checks to see if a user with the given id exists. Will check case
|
||||||
@@ -339,12 +374,15 @@ class AuthHandler(BaseHandler):
|
|||||||
yield self.store.add_refresh_token_to_user(user_id, refresh_token)
|
yield self.store.add_refresh_token_to_user(user_id, refresh_token)
|
||||||
defer.returnValue(refresh_token)
|
defer.returnValue(refresh_token)
|
||||||
|
|
||||||
def generate_access_token(self, user_id):
|
def generate_access_token(self, user_id, extra_caveats=None):
|
||||||
|
extra_caveats = extra_caveats or []
|
||||||
macaroon = self._generate_base_macaroon(user_id)
|
macaroon = self._generate_base_macaroon(user_id)
|
||||||
macaroon.add_first_party_caveat("type = access")
|
macaroon.add_first_party_caveat("type = access")
|
||||||
now = self.hs.get_clock().time_msec()
|
now = self.hs.get_clock().time_msec()
|
||||||
expiry = now + (60 * 60 * 1000)
|
expiry = now + (60 * 60 * 1000)
|
||||||
macaroon.add_first_party_caveat("time < %d" % (expiry,))
|
macaroon.add_first_party_caveat("time < %d" % (expiry,))
|
||||||
|
for caveat in extra_caveats:
|
||||||
|
macaroon.add_first_party_caveat(caveat)
|
||||||
return macaroon.serialize()
|
return macaroon.serialize()
|
||||||
|
|
||||||
def generate_refresh_token(self, user_id):
|
def generate_refresh_token(self, user_id):
|
||||||
@@ -357,6 +395,23 @@ class AuthHandler(BaseHandler):
|
|||||||
))
|
))
|
||||||
return m.serialize()
|
return m.serialize()
|
||||||
|
|
||||||
|
def generate_short_term_login_token(self, user_id):
|
||||||
|
macaroon = self._generate_base_macaroon(user_id)
|
||||||
|
macaroon.add_first_party_caveat("type = login")
|
||||||
|
now = self.hs.get_clock().time_msec()
|
||||||
|
expiry = now + (2 * 60 * 1000)
|
||||||
|
macaroon.add_first_party_caveat("time < %d" % (expiry,))
|
||||||
|
return macaroon.serialize()
|
||||||
|
|
||||||
|
def validate_short_term_login_token_and_get_user_id(self, login_token):
|
||||||
|
try:
|
||||||
|
macaroon = pymacaroons.Macaroon.deserialize(login_token)
|
||||||
|
auth_api = self.hs.get_auth()
|
||||||
|
auth_api.validate_macaroon(macaroon, "login", True)
|
||||||
|
return self.get_user_from_macaroon(macaroon)
|
||||||
|
except (pymacaroons.exceptions.MacaroonException, TypeError, ValueError):
|
||||||
|
raise AuthError(401, "Invalid token", errcode=Codes.UNKNOWN_TOKEN)
|
||||||
|
|
||||||
def _generate_base_macaroon(self, user_id):
|
def _generate_base_macaroon(self, user_id):
|
||||||
macaroon = pymacaroons.Macaroon(
|
macaroon = pymacaroons.Macaroon(
|
||||||
location=self.hs.config.server_name,
|
location=self.hs.config.server_name,
|
||||||
@@ -366,6 +421,16 @@ class AuthHandler(BaseHandler):
|
|||||||
macaroon.add_first_party_caveat("user_id = %s" % (user_id,))
|
macaroon.add_first_party_caveat("user_id = %s" % (user_id,))
|
||||||
return macaroon
|
return macaroon
|
||||||
|
|
||||||
|
def get_user_from_macaroon(self, macaroon):
|
||||||
|
user_prefix = "user_id = "
|
||||||
|
for caveat in macaroon.caveats:
|
||||||
|
if caveat.caveat_id.startswith(user_prefix):
|
||||||
|
return caveat.caveat_id[len(user_prefix):]
|
||||||
|
raise AuthError(
|
||||||
|
self.INVALID_TOKEN_HTTP_STATUS, "No user_id found in token",
|
||||||
|
errcode=Codes.UNKNOWN_TOKEN
|
||||||
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def set_password(self, user_id, newpassword):
|
def set_password(self, user_id, newpassword):
|
||||||
password_hash = self.hash(newpassword)
|
password_hash = self.hash(newpassword)
|
||||||
@@ -400,7 +465,7 @@ class AuthHandler(BaseHandler):
|
|||||||
Returns:
|
Returns:
|
||||||
Hashed password (str).
|
Hashed password (str).
|
||||||
"""
|
"""
|
||||||
return bcrypt.hashpw(password, bcrypt.gensalt())
|
return bcrypt.hashpw(password, bcrypt.gensalt(self.bcrypt_rounds))
|
||||||
|
|
||||||
def validate_hash(self, password, stored_hash):
|
def validate_hash(self, password, stored_hash):
|
||||||
"""Validates that self.hash(password) == stored_hash.
|
"""Validates that self.hash(password) == stored_hash.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -175,8 +175,8 @@ class DirectoryHandler(BaseHandler):
|
|||||||
# If this server is in the list of servers, return it first.
|
# If this server is in the list of servers, return it first.
|
||||||
if self.server_name in servers:
|
if self.server_name in servers:
|
||||||
servers = (
|
servers = (
|
||||||
[self.server_name]
|
[self.server_name] +
|
||||||
+ [s for s in servers if s != self.server_name]
|
[s for s in servers if s != self.server_name]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
servers = list(servers)
|
servers = list(servers)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -18,6 +18,7 @@ from twisted.internet import defer
|
|||||||
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
|
||||||
|
from synapse.util.logcontext import preserve_context_over_fn
|
||||||
|
|
||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
|
|
||||||
@@ -28,6 +29,20 @@ import random
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def started_user_eventstream(distributor, user):
|
||||||
|
return preserve_context_over_fn(
|
||||||
|
distributor.fire,
|
||||||
|
"started_user_eventstream", user
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def stopped_user_eventstream(distributor, user):
|
||||||
|
return preserve_context_over_fn(
|
||||||
|
distributor.fire,
|
||||||
|
"stopped_user_eventstream", user
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EventStreamHandler(BaseHandler):
|
class EventStreamHandler(BaseHandler):
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
@@ -46,44 +61,75 @@ class EventStreamHandler(BaseHandler):
|
|||||||
|
|
||||||
self.notifier = hs.get_notifier()
|
self.notifier = hs.get_notifier()
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def started_stream(self, user):
|
||||||
|
"""Tells the presence handler that we have started an eventstream for
|
||||||
|
the user:
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user (User): The user who started a stream.
|
||||||
|
Returns:
|
||||||
|
A deferred that completes once their presence has been updated.
|
||||||
|
"""
|
||||||
|
if user not in self._streams_per_user:
|
||||||
|
# Make sure we set the streams per user to 1 here rather than
|
||||||
|
# setting it to zero and incrementing the value below.
|
||||||
|
# Otherwise this may race with stopped_stream causing the
|
||||||
|
# user to be erased from the map before we have a chance
|
||||||
|
# to increment it.
|
||||||
|
self._streams_per_user[user] = 1
|
||||||
|
if user in self._stop_timer_per_user:
|
||||||
|
try:
|
||||||
|
self.clock.cancel_call_later(
|
||||||
|
self._stop_timer_per_user.pop(user)
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
logger.exception("Failed to cancel event timer")
|
||||||
|
else:
|
||||||
|
yield started_user_eventstream(self.distributor, user)
|
||||||
|
else:
|
||||||
|
self._streams_per_user[user] += 1
|
||||||
|
|
||||||
|
def stopped_stream(self, user):
|
||||||
|
"""If there are no streams for a user this starts a timer that will
|
||||||
|
notify the presence handler that we haven't got an event stream for
|
||||||
|
the user unless the user starts a new stream in 30 seconds.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user (User): The user who stopped a stream.
|
||||||
|
"""
|
||||||
|
self._streams_per_user[user] -= 1
|
||||||
|
if not self._streams_per_user[user]:
|
||||||
|
del self._streams_per_user[user]
|
||||||
|
|
||||||
|
# 30 seconds of grace to allow the client to reconnect again
|
||||||
|
# before we think they're gone
|
||||||
|
def _later():
|
||||||
|
logger.debug("_later stopped_user_eventstream %s", user)
|
||||||
|
|
||||||
|
self._stop_timer_per_user.pop(user, None)
|
||||||
|
|
||||||
|
return stopped_user_eventstream(self.distributor, user)
|
||||||
|
|
||||||
|
logger.debug("Scheduling _later: for %s", user)
|
||||||
|
self._stop_timer_per_user[user] = (
|
||||||
|
self.clock.call_later(30, _later)
|
||||||
|
)
|
||||||
|
|
||||||
@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):
|
only_keys=None, room_id=None, is_guest=False):
|
||||||
"""Fetches the events stream for a given user.
|
"""Fetches the events stream for a given user.
|
||||||
|
|
||||||
If `only_room_events` is `True` only room events will be returned.
|
If `only_keys` is not None, events from keys will be sent down.
|
||||||
"""
|
"""
|
||||||
auth_user = UserID.from_string(auth_user_id)
|
auth_user = UserID.from_string(auth_user_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if affect_presence:
|
if affect_presence:
|
||||||
if auth_user not in self._streams_per_user:
|
yield self.started_stream(auth_user)
|
||||||
self._streams_per_user[auth_user] = 0
|
|
||||||
if auth_user in self._stop_timer_per_user:
|
|
||||||
try:
|
|
||||||
self.clock.cancel_call_later(
|
|
||||||
self._stop_timer_per_user.pop(auth_user)
|
|
||||||
)
|
|
||||||
except:
|
|
||||||
logger.exception("Failed to cancel event timer")
|
|
||||||
else:
|
|
||||||
yield self.distributor.fire(
|
|
||||||
"started_user_eventstream", auth_user
|
|
||||||
)
|
|
||||||
self._streams_per_user[auth_user] += 1
|
|
||||||
|
|
||||||
rm_handler = self.hs.get_handlers().room_member_handler
|
|
||||||
|
|
||||||
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.
|
||||||
@@ -94,8 +140,9 @@ class EventStreamHandler(BaseHandler):
|
|||||||
timeout = random.randint(int(timeout * 0.9), int(timeout * 1.1))
|
timeout = random.randint(int(timeout * 0.9), int(timeout * 1.1))
|
||||||
|
|
||||||
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, pagin_config, timeout,
|
||||||
only_room_events=only_room_events
|
only_keys=only_keys,
|
||||||
|
is_guest=is_guest, explicit_room_id=room_id
|
||||||
)
|
)
|
||||||
|
|
||||||
time_now = self.clock.time_msec()
|
time_now = self.clock.time_msec()
|
||||||
@@ -114,27 +161,7 @@ class EventStreamHandler(BaseHandler):
|
|||||||
|
|
||||||
finally:
|
finally:
|
||||||
if affect_presence:
|
if affect_presence:
|
||||||
self._streams_per_user[auth_user] -= 1
|
self.stopped_stream(auth_user)
|
||||||
if not self._streams_per_user[auth_user]:
|
|
||||||
del self._streams_per_user[auth_user]
|
|
||||||
|
|
||||||
# 10 seconds of grace to allow the client to reconnect again
|
|
||||||
# before we think they're gone
|
|
||||||
def _later():
|
|
||||||
logger.debug(
|
|
||||||
"_later stopped_user_eventstream %s", auth_user
|
|
||||||
)
|
|
||||||
|
|
||||||
self._stop_timer_per_user.pop(auth_user, None)
|
|
||||||
|
|
||||||
return self.distributor.fire(
|
|
||||||
"stopped_user_eventstream", auth_user
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.debug("Scheduling _later: for %s", auth_user)
|
|
||||||
self._stop_timer_per_user[auth_user] = (
|
|
||||||
self.clock.call_later(30, _later)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EventHandler(BaseHandler):
|
class EventHandler(BaseHandler):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -21,6 +21,7 @@ from synapse.api.errors import (
|
|||||||
AuthError, FederationError, StoreError, CodeMessageException, SynapseError,
|
AuthError, FederationError, StoreError, CodeMessageException, SynapseError,
|
||||||
)
|
)
|
||||||
from synapse.api.constants import EventTypes, Membership, RejectedReason
|
from synapse.api.constants import EventTypes, Membership, RejectedReason
|
||||||
|
from synapse.events.validator import EventValidator
|
||||||
from synapse.util import unwrapFirstError
|
from synapse.util import unwrapFirstError
|
||||||
from synapse.util.logcontext import PreserveLoggingContext
|
from synapse.util.logcontext import PreserveLoggingContext
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
@@ -35,15 +36,20 @@ from synapse.events.utils import prune_event
|
|||||||
|
|
||||||
from synapse.util.retryutils import NotRetryingDestination
|
from synapse.util.retryutils import NotRetryingDestination
|
||||||
|
|
||||||
|
from synapse.push.action_generator import ActionGenerator
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def user_joined_room(distributor, user, room_id):
|
||||||
|
return distributor.fire("user_joined_room", user, room_id)
|
||||||
|
|
||||||
|
|
||||||
class FederationHandler(BaseHandler):
|
class FederationHandler(BaseHandler):
|
||||||
"""Handles events that originated from federation.
|
"""Handles events that originated from federation.
|
||||||
Responsible for:
|
Responsible for:
|
||||||
@@ -58,22 +64,18 @@ class FederationHandler(BaseHandler):
|
|||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
super(FederationHandler, self).__init__(hs)
|
super(FederationHandler, self).__init__(hs)
|
||||||
|
|
||||||
self.distributor.observe(
|
self.hs = hs
|
||||||
"user_joined_room",
|
|
||||||
self._on_user_joined
|
self.distributor.observe("user_joined_room", self.user_joined_room)
|
||||||
)
|
|
||||||
|
|
||||||
self.waiting_for_join_list = {}
|
self.waiting_for_join_list = {}
|
||||||
|
|
||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
self.replication_layer = hs.get_replication_layer()
|
self.replication_layer = hs.get_replication_layer()
|
||||||
self.state_handler = hs.get_state_handler()
|
self.state_handler = hs.get_state_handler()
|
||||||
# self.auth_handler = gs.get_auth_handler()
|
|
||||||
self.server_name = hs.hostname
|
self.server_name = hs.hostname
|
||||||
self.keyring = hs.get_keyring()
|
self.keyring = hs.get_keyring()
|
||||||
|
|
||||||
self.lock_manager = hs.get_room_lock_manager()
|
|
||||||
|
|
||||||
self.replication_layer.set_handler(self)
|
self.replication_layer.set_handler(self)
|
||||||
|
|
||||||
# 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
|
||||||
@@ -125,8 +127,20 @@ class FederationHandler(BaseHandler):
|
|||||||
)
|
)
|
||||||
if not is_in_room and not event.internal_metadata.is_outlier():
|
if not is_in_room and not event.internal_metadata.is_outlier():
|
||||||
logger.debug("Got event for room we're not in.")
|
logger.debug("Got event for room we're not in.")
|
||||||
current_state = state
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
event_stream_id, max_stream_id = yield self._persist_auth_tree(
|
||||||
|
auth_chain, state, event
|
||||||
|
)
|
||||||
|
except AuthError as e:
|
||||||
|
raise FederationError(
|
||||||
|
"ERROR",
|
||||||
|
e.code,
|
||||||
|
e.msg,
|
||||||
|
affected=event.event_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
event_ids = set()
|
event_ids = set()
|
||||||
if state:
|
if state:
|
||||||
event_ids |= {e.event_id for e in state}
|
event_ids |= {e.event_id for e in state}
|
||||||
@@ -150,7 +164,7 @@ class FederationHandler(BaseHandler):
|
|||||||
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 or e.type == EventTypes.Create
|
||||||
}
|
}
|
||||||
event_infos.append({
|
event_infos.append({
|
||||||
"event": e,
|
"event": e,
|
||||||
@@ -165,7 +179,7 @@ class FederationHandler(BaseHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_, event_stream_id, max_stream_id = yield self._handle_new_event(
|
context, event_stream_id, max_stream_id = yield self._handle_new_event(
|
||||||
origin,
|
origin,
|
||||||
event,
|
event,
|
||||||
state=state,
|
state=state,
|
||||||
@@ -207,30 +221,25 @@ class FederationHandler(BaseHandler):
|
|||||||
extra_users.append(target_user)
|
extra_users.append(target_user)
|
||||||
|
|
||||||
with PreserveLoggingContext():
|
with PreserveLoggingContext():
|
||||||
d = self.notifier.on_new_room_event(
|
self.notifier.on_new_room_event(
|
||||||
event, event_stream_id, max_stream_id,
|
event, event_stream_id, max_stream_id,
|
||||||
extra_users=extra_users
|
extra_users=extra_users
|
||||||
)
|
)
|
||||||
|
|
||||||
def log_failure(f):
|
|
||||||
logger.warn(
|
|
||||||
"Failed to notify about %s: %s",
|
|
||||||
event.event_id, f.value
|
|
||||||
)
|
|
||||||
|
|
||||||
d.addErrback(log_failure)
|
|
||||||
|
|
||||||
if event.type == EventTypes.Member:
|
if event.type == EventTypes.Member:
|
||||||
if event.membership == Membership.JOIN:
|
if event.membership == Membership.JOIN:
|
||||||
|
prev_state = context.current_state.get((event.type, event.state_key))
|
||||||
|
if not prev_state or prev_state.membership != Membership.JOIN:
|
||||||
|
# Only fire user_joined_room if the user has acutally
|
||||||
|
# joined the room. Don't bother if the user is just
|
||||||
|
# changing their profile info.
|
||||||
user = UserID.from_string(event.state_key)
|
user = UserID.from_string(event.state_key)
|
||||||
yield self.distributor.fire(
|
yield user_joined_room(self.distributor, user, event.room_id)
|
||||||
"user_joined_room", user=user, room_id=event.room_id
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _filter_events_for_server(self, server_name, room_id, events):
|
def _filter_events_for_server(self, server_name, room_id, events):
|
||||||
event_to_state = yield self.store.get_state_for_events(
|
event_to_state = yield self.store.get_state_for_events(
|
||||||
room_id, frozenset(e.event_id for e in events),
|
frozenset(e.event_id for e in events),
|
||||||
types=(
|
types=(
|
||||||
(EventTypes.RoomHistoryVisibility, ""),
|
(EventTypes.RoomHistoryVisibility, ""),
|
||||||
(EventTypes.Member, None),
|
(EventTypes.Member, None),
|
||||||
@@ -553,7 +562,7 @@ class FederationHandler(BaseHandler):
|
|||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def do_invite_join(self, target_hosts, room_id, joinee, content, snapshot):
|
def do_invite_join(self, target_hosts, room_id, joinee, content):
|
||||||
""" Attempts to join the `joinee` to the room `room_id` via the
|
""" Attempts to join the `joinee` to the room `room_id` via the
|
||||||
server `target_host`.
|
server `target_host`.
|
||||||
|
|
||||||
@@ -569,49 +578,19 @@ class FederationHandler(BaseHandler):
|
|||||||
|
|
||||||
yield self.store.clean_room_for_join(room_id)
|
yield self.store.clean_room_for_join(room_id)
|
||||||
|
|
||||||
origin, pdu = yield self.replication_layer.make_join(
|
origin, event = yield self._make_and_verify_event(
|
||||||
target_hosts,
|
target_hosts,
|
||||||
room_id,
|
room_id,
|
||||||
joinee
|
joinee,
|
||||||
|
"join",
|
||||||
|
content,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug("Got response to make_join: %s", pdu)
|
|
||||||
|
|
||||||
event = pdu
|
|
||||||
|
|
||||||
# We should assert some things.
|
|
||||||
# FIXME: Do this in a nicer way
|
|
||||||
assert(event.type == EventTypes.Member)
|
|
||||||
assert(event.user_id == joinee)
|
|
||||||
assert(event.state_key == joinee)
|
|
||||||
assert(event.room_id == room_id)
|
|
||||||
|
|
||||||
event.internal_metadata.outlier = False
|
|
||||||
|
|
||||||
self.room_queues[room_id] = []
|
self.room_queues[room_id] = []
|
||||||
|
|
||||||
builder = self.event_builder_factory.new(
|
|
||||||
unfreeze(event.get_pdu_json())
|
|
||||||
)
|
|
||||||
|
|
||||||
handled_events = set()
|
handled_events = set()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
builder.event_id = self.event_builder_factory.create_event_id()
|
event = self._sign_event(event)
|
||||||
builder.origin = self.hs.hostname
|
|
||||||
builder.content = content
|
|
||||||
|
|
||||||
if not hasattr(event, "signatures"):
|
|
||||||
builder.signatures = {}
|
|
||||||
|
|
||||||
add_hashes_and_signatures(
|
|
||||||
builder,
|
|
||||||
self.hs.hostname,
|
|
||||||
self.hs.config.signing_key[0],
|
|
||||||
)
|
|
||||||
|
|
||||||
new_event = builder.build()
|
|
||||||
|
|
||||||
# Try the host we successfully got a response to /make_join/
|
# Try the host we successfully got a response to /make_join/
|
||||||
# request first.
|
# request first.
|
||||||
try:
|
try:
|
||||||
@@ -619,11 +598,7 @@ class FederationHandler(BaseHandler):
|
|||||||
target_hosts.insert(0, origin)
|
target_hosts.insert(0, origin)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
ret = yield self.replication_layer.send_join(target_hosts, event)
|
||||||
ret = yield self.replication_layer.send_join(
|
|
||||||
target_hosts,
|
|
||||||
new_event
|
|
||||||
)
|
|
||||||
|
|
||||||
origin = ret["origin"]
|
origin = ret["origin"]
|
||||||
state = ret["state"]
|
state = ret["state"]
|
||||||
@@ -632,12 +607,12 @@ class FederationHandler(BaseHandler):
|
|||||||
|
|
||||||
handled_events.update([s.event_id for s in state])
|
handled_events.update([s.event_id for s in state])
|
||||||
handled_events.update([a.event_id for a in auth_chain])
|
handled_events.update([a.event_id for a in auth_chain])
|
||||||
handled_events.add(new_event.event_id)
|
handled_events.add(event.event_id)
|
||||||
|
|
||||||
logger.debug("do_invite_join auth_chain: %s", auth_chain)
|
logger.debug("do_invite_join auth_chain: %s", auth_chain)
|
||||||
logger.debug("do_invite_join state: %s", state)
|
logger.debug("do_invite_join state: %s", state)
|
||||||
|
|
||||||
logger.debug("do_invite_join event: %s", new_event)
|
logger.debug("do_invite_join event: %s", event)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield self.store.store_room(
|
yield self.store.store_room(
|
||||||
@@ -649,51 +624,16 @@ class FederationHandler(BaseHandler):
|
|||||||
# FIXME
|
# FIXME
|
||||||
pass
|
pass
|
||||||
|
|
||||||
ev_infos = []
|
event_stream_id, max_stream_id = yield self._persist_auth_tree(
|
||||||
for e in itertools.chain(state, auth_chain):
|
auth_chain, state, event
|
||||||
if e.event_id == event.event_id:
|
|
||||||
continue
|
|
||||||
|
|
||||||
e.internal_metadata.outlier = True
|
|
||||||
auth_ids = [e_id for e_id, _ in e.auth_events]
|
|
||||||
ev_infos.append({
|
|
||||||
"event": e,
|
|
||||||
"auth_events": {
|
|
||||||
(e.type, e.state_key): e for e in auth_chain
|
|
||||||
if e.event_id in auth_ids
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
yield self._handle_new_events(origin, ev_infos, outliers=True)
|
|
||||||
|
|
||||||
auth_ids = [e_id for e_id, _ in event.auth_events]
|
|
||||||
auth_events = {
|
|
||||||
(e.type, e.state_key): e for e in auth_chain
|
|
||||||
if e.event_id in auth_ids
|
|
||||||
}
|
|
||||||
|
|
||||||
_, event_stream_id, max_stream_id = yield self._handle_new_event(
|
|
||||||
origin,
|
|
||||||
new_event,
|
|
||||||
state=state,
|
|
||||||
current_state=state,
|
|
||||||
auth_events=auth_events,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with PreserveLoggingContext():
|
with PreserveLoggingContext():
|
||||||
d = self.notifier.on_new_room_event(
|
self.notifier.on_new_room_event(
|
||||||
new_event, event_stream_id, max_stream_id,
|
event, event_stream_id, max_stream_id,
|
||||||
extra_users=[joinee]
|
extra_users=[joinee]
|
||||||
)
|
)
|
||||||
|
|
||||||
def log_failure(f):
|
|
||||||
logger.warn(
|
|
||||||
"Failed to notify about %s: %s",
|
|
||||||
new_event.event_id, f.value
|
|
||||||
)
|
|
||||||
|
|
||||||
d.addErrback(log_failure)
|
|
||||||
|
|
||||||
logger.debug("Finished joining %s to %s", joinee, room_id)
|
logger.debug("Finished joining %s to %s", joinee, room_id)
|
||||||
finally:
|
finally:
|
||||||
room_queue = self.room_queues[room_id]
|
room_queue = self.room_queues[room_id]
|
||||||
@@ -714,12 +654,14 @@ class FederationHandler(BaseHandler):
|
|||||||
@log_function
|
@log_function
|
||||||
def on_make_join_request(self, room_id, user_id):
|
def on_make_join_request(self, room_id, user_id):
|
||||||
""" We've received a /make_join/ request, so we create a partial
|
""" We've received a /make_join/ request, so we create a partial
|
||||||
join event for the room and return that. We don *not* persist or
|
join event for the room and return that. We do *not* persist or
|
||||||
process it until the other server has signed it and sent it back.
|
process it until the other server has signed it and sent it back.
|
||||||
"""
|
"""
|
||||||
|
event_content = {"membership": Membership.JOIN}
|
||||||
|
|
||||||
builder = self.event_builder_factory.new({
|
builder = self.event_builder_factory.new({
|
||||||
"type": EventTypes.Member,
|
"type": EventTypes.Member,
|
||||||
"content": {"membership": Membership.JOIN},
|
"content": event_content,
|
||||||
"room_id": room_id,
|
"room_id": room_id,
|
||||||
"sender": user_id,
|
"sender": user_id,
|
||||||
"state_key": user_id,
|
"state_key": user_id,
|
||||||
@@ -766,24 +708,14 @@ class FederationHandler(BaseHandler):
|
|||||||
extra_users.append(target_user)
|
extra_users.append(target_user)
|
||||||
|
|
||||||
with PreserveLoggingContext():
|
with PreserveLoggingContext():
|
||||||
d = self.notifier.on_new_room_event(
|
self.notifier.on_new_room_event(
|
||||||
event, event_stream_id, max_stream_id, extra_users=extra_users
|
event, event_stream_id, max_stream_id, extra_users=extra_users
|
||||||
)
|
)
|
||||||
|
|
||||||
def log_failure(f):
|
|
||||||
logger.warn(
|
|
||||||
"Failed to notify about %s: %s",
|
|
||||||
event.event_id, f.value
|
|
||||||
)
|
|
||||||
|
|
||||||
d.addErrback(log_failure)
|
|
||||||
|
|
||||||
if event.type == EventTypes.Member:
|
if event.type == EventTypes.Member:
|
||||||
if event.content["membership"] == Membership.JOIN:
|
if event.content["membership"] == Membership.JOIN:
|
||||||
user = UserID.from_string(event.state_key)
|
user = UserID.from_string(event.state_key)
|
||||||
yield self.distributor.fire(
|
yield user_joined_room(self.distributor, user, event.room_id)
|
||||||
"user_joined_room", user=user, room_id=event.room_id
|
|
||||||
)
|
|
||||||
|
|
||||||
new_pdu = event
|
new_pdu = event
|
||||||
|
|
||||||
@@ -849,21 +781,167 @@ class FederationHandler(BaseHandler):
|
|||||||
|
|
||||||
target_user = UserID.from_string(event.state_key)
|
target_user = UserID.from_string(event.state_key)
|
||||||
with PreserveLoggingContext():
|
with PreserveLoggingContext():
|
||||||
d = self.notifier.on_new_room_event(
|
self.notifier.on_new_room_event(
|
||||||
event, event_stream_id, max_stream_id,
|
event, event_stream_id, max_stream_id,
|
||||||
extra_users=[target_user],
|
extra_users=[target_user],
|
||||||
)
|
)
|
||||||
|
|
||||||
def log_failure(f):
|
defer.returnValue(event)
|
||||||
logger.warn(
|
|
||||||
"Failed to notify about %s: %s",
|
@defer.inlineCallbacks
|
||||||
event.event_id, f.value
|
def do_remotely_reject_invite(self, target_hosts, room_id, user_id):
|
||||||
|
origin, event = yield self._make_and_verify_event(
|
||||||
|
target_hosts,
|
||||||
|
room_id,
|
||||||
|
user_id,
|
||||||
|
"leave"
|
||||||
|
)
|
||||||
|
signed_event = self._sign_event(event)
|
||||||
|
|
||||||
|
# Try the host we successfully got a response to /make_join/
|
||||||
|
# request first.
|
||||||
|
try:
|
||||||
|
target_hosts.remove(origin)
|
||||||
|
target_hosts.insert(0, origin)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
yield self.replication_layer.send_leave(
|
||||||
|
target_hosts,
|
||||||
|
signed_event
|
||||||
|
)
|
||||||
|
defer.returnValue(None)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _make_and_verify_event(self, target_hosts, room_id, user_id, membership,
|
||||||
|
content={},):
|
||||||
|
origin, pdu = yield self.replication_layer.make_membership_event(
|
||||||
|
target_hosts,
|
||||||
|
room_id,
|
||||||
|
user_id,
|
||||||
|
membership,
|
||||||
|
content,
|
||||||
)
|
)
|
||||||
|
|
||||||
d.addErrback(log_failure)
|
logger.debug("Got response to make_%s: %s", membership, pdu)
|
||||||
|
|
||||||
|
event = pdu
|
||||||
|
|
||||||
|
# We should assert some things.
|
||||||
|
# FIXME: Do this in a nicer way
|
||||||
|
assert(event.type == EventTypes.Member)
|
||||||
|
assert(event.user_id == user_id)
|
||||||
|
assert(event.state_key == user_id)
|
||||||
|
assert(event.room_id == room_id)
|
||||||
|
defer.returnValue((origin, event))
|
||||||
|
|
||||||
|
def _sign_event(self, event):
|
||||||
|
event.internal_metadata.outlier = False
|
||||||
|
|
||||||
|
builder = self.event_builder_factory.new(
|
||||||
|
unfreeze(event.get_pdu_json())
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.event_id = self.event_builder_factory.create_event_id()
|
||||||
|
builder.origin = self.hs.hostname
|
||||||
|
|
||||||
|
if not hasattr(event, "signatures"):
|
||||||
|
builder.signatures = {}
|
||||||
|
|
||||||
|
add_hashes_and_signatures(
|
||||||
|
builder,
|
||||||
|
self.hs.hostname,
|
||||||
|
self.hs.config.signing_key[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
@log_function
|
||||||
|
def on_make_leave_request(self, room_id, user_id):
|
||||||
|
""" We've received a /make_leave/ request, so we create a partial
|
||||||
|
join event for the room and return that. We do *not* persist or
|
||||||
|
process it until the other server has signed it and sent it back.
|
||||||
|
"""
|
||||||
|
builder = self.event_builder_factory.new({
|
||||||
|
"type": EventTypes.Member,
|
||||||
|
"content": {"membership": Membership.LEAVE},
|
||||||
|
"room_id": room_id,
|
||||||
|
"sender": user_id,
|
||||||
|
"state_key": user_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
event, context = yield self._create_new_client_event(
|
||||||
|
builder=builder,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.auth.check(event, auth_events=context.current_state)
|
||||||
|
|
||||||
defer.returnValue(event)
|
defer.returnValue(event)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
@log_function
|
||||||
|
def on_send_leave_request(self, origin, pdu):
|
||||||
|
""" We have received a leave event for a room. Fully process it."""
|
||||||
|
event = pdu
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"on_send_leave_request: Got event: %s, signatures: %s",
|
||||||
|
event.event_id,
|
||||||
|
event.signatures,
|
||||||
|
)
|
||||||
|
|
||||||
|
event.internal_metadata.outlier = False
|
||||||
|
|
||||||
|
context, event_stream_id, max_stream_id = yield self._handle_new_event(
|
||||||
|
origin, event
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"on_send_leave_request: After _handle_new_event: %s, sigs: %s",
|
||||||
|
event.event_id,
|
||||||
|
event.signatures,
|
||||||
|
)
|
||||||
|
|
||||||
|
extra_users = []
|
||||||
|
if event.type == EventTypes.Member:
|
||||||
|
target_user_id = event.state_key
|
||||||
|
target_user = UserID.from_string(target_user_id)
|
||||||
|
extra_users.append(target_user)
|
||||||
|
|
||||||
|
with PreserveLoggingContext():
|
||||||
|
self.notifier.on_new_room_event(
|
||||||
|
event, event_stream_id, max_stream_id, extra_users=extra_users
|
||||||
|
)
|
||||||
|
|
||||||
|
new_pdu = event
|
||||||
|
|
||||||
|
destinations = set()
|
||||||
|
|
||||||
|
for k, s in context.current_state.items():
|
||||||
|
try:
|
||||||
|
if k[0] == EventTypes.Member:
|
||||||
|
if s.content["membership"] == Membership.LEAVE:
|
||||||
|
destinations.add(
|
||||||
|
UserID.from_string(s.state_key).domain
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
logger.warn(
|
||||||
|
"Failed to get destination from event %s", s.event_id
|
||||||
|
)
|
||||||
|
|
||||||
|
destinations.discard(origin)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"on_send_leave_request: Sending event: %s, signatures: %s",
|
||||||
|
event.event_id,
|
||||||
|
event.signatures,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.replication_layer.send_pdu(new_pdu, destinations)
|
||||||
|
|
||||||
|
defer.returnValue(None)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_state_for_pdu(self, origin, room_id, event_id, do_auth=True):
|
def get_state_for_pdu(self, origin, room_id, event_id, do_auth=True):
|
||||||
yield run_on_reactor()
|
yield run_on_reactor()
|
||||||
@@ -968,7 +1046,7 @@ class FederationHandler(BaseHandler):
|
|||||||
return self.store.get_min_depth(context)
|
return self.store.get_min_depth(context)
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def _on_user_joined(self, user, room_id):
|
def user_joined_room(self, user, room_id):
|
||||||
waiters = self.waiting_for_join_list.get(
|
waiters = self.waiting_for_join_list.get(
|
||||||
(user.to_string(), room_id),
|
(user.to_string(), room_id),
|
||||||
[]
|
[]
|
||||||
@@ -986,11 +1064,15 @@ class FederationHandler(BaseHandler):
|
|||||||
context = yield self._prep_event(
|
context = yield self._prep_event(
|
||||||
origin, event,
|
origin, event,
|
||||||
state=state,
|
state=state,
|
||||||
backfilled=backfilled,
|
|
||||||
current_state=current_state,
|
|
||||||
auth_events=auth_events,
|
auth_events=auth_events,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not backfilled and not event.internal_metadata.is_outlier():
|
||||||
|
action_generator = ActionGenerator(self.hs)
|
||||||
|
yield action_generator.handle_push_actions_for_event(
|
||||||
|
event, context, self
|
||||||
|
)
|
||||||
|
|
||||||
event_stream_id, max_stream_id = yield self.store.persist_event(
|
event_stream_id, max_stream_id = yield self.store.persist_event(
|
||||||
event,
|
event,
|
||||||
context=context,
|
context=context,
|
||||||
@@ -1010,7 +1092,6 @@ class FederationHandler(BaseHandler):
|
|||||||
origin,
|
origin,
|
||||||
ev_info["event"],
|
ev_info["event"],
|
||||||
state=ev_info.get("state"),
|
state=ev_info.get("state"),
|
||||||
backfilled=backfilled,
|
|
||||||
auth_events=ev_info.get("auth_events"),
|
auth_events=ev_info.get("auth_events"),
|
||||||
)
|
)
|
||||||
for ev_info in event_infos
|
for ev_info in event_infos
|
||||||
@@ -1027,8 +1108,83 @@ class FederationHandler(BaseHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _prep_event(self, origin, event, state=None, backfilled=False,
|
def _persist_auth_tree(self, auth_events, state, event):
|
||||||
current_state=None, auth_events=None):
|
"""Checks the auth chain is valid (and passes auth checks) for the
|
||||||
|
state and event. Then persists the auth chain and state atomically.
|
||||||
|
Persists the event seperately.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
2-tuple of (event_stream_id, max_stream_id) from the persist_event
|
||||||
|
call for `event`
|
||||||
|
"""
|
||||||
|
events_to_context = {}
|
||||||
|
for e in itertools.chain(auth_events, state):
|
||||||
|
ctx = yield self.state_handler.compute_event_context(
|
||||||
|
e, outlier=True,
|
||||||
|
)
|
||||||
|
events_to_context[e.event_id] = ctx
|
||||||
|
e.internal_metadata.outlier = True
|
||||||
|
|
||||||
|
event_map = {
|
||||||
|
e.event_id: e
|
||||||
|
for e in auth_events
|
||||||
|
}
|
||||||
|
|
||||||
|
create_event = None
|
||||||
|
for e in auth_events:
|
||||||
|
if (e.type, e.state_key) == (EventTypes.Create, ""):
|
||||||
|
create_event = e
|
||||||
|
break
|
||||||
|
|
||||||
|
for e in itertools.chain(auth_events, state, [event]):
|
||||||
|
auth_for_e = {
|
||||||
|
(event_map[e_id].type, event_map[e_id].state_key): event_map[e_id]
|
||||||
|
for e_id, _ in e.auth_events
|
||||||
|
}
|
||||||
|
if create_event:
|
||||||
|
auth_for_e[(EventTypes.Create, "")] = create_event
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.auth.check(e, auth_events=auth_for_e)
|
||||||
|
except SynapseError as err:
|
||||||
|
# we may get SynapseErrors here as well as AuthErrors. For
|
||||||
|
# instance, there are a couple of (ancient) events in some
|
||||||
|
# rooms whose senders do not have the correct sigil; these
|
||||||
|
# cause SynapseErrors in auth.check. We don't want to give up
|
||||||
|
# the attempt to federate altogether in such cases.
|
||||||
|
|
||||||
|
logger.warn(
|
||||||
|
"Rejecting %s because %s",
|
||||||
|
e.event_id, err.msg
|
||||||
|
)
|
||||||
|
|
||||||
|
if e == event:
|
||||||
|
raise
|
||||||
|
events_to_context[e.event_id].rejected = RejectedReason.AUTH_ERROR
|
||||||
|
|
||||||
|
yield self.store.persist_events(
|
||||||
|
[
|
||||||
|
(e, events_to_context[e.event_id])
|
||||||
|
for e in itertools.chain(auth_events, state)
|
||||||
|
],
|
||||||
|
is_new_state=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
new_event_context = yield self.state_handler.compute_event_context(
|
||||||
|
event, old_state=state, outlier=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
event_stream_id, max_stream_id = yield self.store.persist_event(
|
||||||
|
event, new_event_context,
|
||||||
|
backfilled=False,
|
||||||
|
is_new_state=True,
|
||||||
|
current_state=state,
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue((event_stream_id, max_stream_id))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _prep_event(self, origin, event, state=None, auth_events=None):
|
||||||
outlier = event.internal_metadata.is_outlier()
|
outlier = event.internal_metadata.is_outlier()
|
||||||
|
|
||||||
context = yield self.state_handler.compute_event_context(
|
context = yield self.state_handler.compute_event_context(
|
||||||
@@ -1061,6 +1217,10 @@ class FederationHandler(BaseHandler):
|
|||||||
|
|
||||||
context.rejected = RejectedReason.AUTH_ERROR
|
context.rejected = RejectedReason.AUTH_ERROR
|
||||||
|
|
||||||
|
if event.type == EventTypes.GuestAccess:
|
||||||
|
full_context = yield self.store.get_current_state(room_id=event.room_id)
|
||||||
|
yield self.maybe_kick_guest_users(event, full_context)
|
||||||
|
|
||||||
defer.returnValue(context)
|
defer.returnValue(context)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@@ -1166,7 +1326,7 @@ class FederationHandler(BaseHandler):
|
|||||||
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 remote_auth_chain
|
(e.type, e.state_key): e for e in remote_auth_chain
|
||||||
if e.event_id in auth_ids
|
if e.event_id in auth_ids or e.type == EventTypes.Create
|
||||||
}
|
}
|
||||||
e.internal_metadata.outlier = True
|
e.internal_metadata.outlier = True
|
||||||
|
|
||||||
@@ -1284,6 +1444,7 @@ class FederationHandler(BaseHandler):
|
|||||||
(e.type, e.state_key): e
|
(e.type, e.state_key): e
|
||||||
for e in result["auth_chain"]
|
for e in result["auth_chain"]
|
||||||
if e.event_id in auth_ids
|
if e.event_id in auth_ids
|
||||||
|
or event.type == EventTypes.Create
|
||||||
}
|
}
|
||||||
ev.internal_metadata.outlier = True
|
ev.internal_metadata.outlier = True
|
||||||
|
|
||||||
@@ -1456,3 +1617,116 @@ class FederationHandler(BaseHandler):
|
|||||||
},
|
},
|
||||||
"missing": [e.event_id for e in missing_locals],
|
"missing": [e.event_id for e in missing_locals],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
@log_function
|
||||||
|
def exchange_third_party_invite(self, invite):
|
||||||
|
sender = invite["sender"]
|
||||||
|
room_id = invite["room_id"]
|
||||||
|
|
||||||
|
if "signed" not in invite or "token" not in invite["signed"]:
|
||||||
|
logger.info(
|
||||||
|
"Discarding received notification of third party invite "
|
||||||
|
"without signed: %s" % (invite,)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
third_party_invite = {
|
||||||
|
"signed": invite["signed"],
|
||||||
|
}
|
||||||
|
|
||||||
|
event_dict = {
|
||||||
|
"type": EventTypes.Member,
|
||||||
|
"content": {
|
||||||
|
"membership": Membership.INVITE,
|
||||||
|
"third_party_invite": third_party_invite,
|
||||||
|
},
|
||||||
|
"room_id": room_id,
|
||||||
|
"sender": sender,
|
||||||
|
"state_key": invite["mxid"],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yield self.auth.check_host_in_room(room_id, self.hs.hostname)):
|
||||||
|
builder = self.event_builder_factory.new(event_dict)
|
||||||
|
EventValidator().validate_new(builder)
|
||||||
|
event, context = yield self._create_new_client_event(builder=builder)
|
||||||
|
|
||||||
|
event, context = yield self.add_display_name_to_third_party_invite(
|
||||||
|
event_dict, event, context
|
||||||
|
)
|
||||||
|
|
||||||
|
self.auth.check(event, context.current_state)
|
||||||
|
yield self._validate_keyserver(event, auth_events=context.current_state)
|
||||||
|
member_handler = self.hs.get_handlers().room_member_handler
|
||||||
|
yield member_handler.send_membership_event(event, context)
|
||||||
|
else:
|
||||||
|
destinations = set([x.split(":", 1)[-1] for x in (sender, room_id)])
|
||||||
|
yield self.replication_layer.forward_third_party_invite(
|
||||||
|
destinations,
|
||||||
|
room_id,
|
||||||
|
event_dict,
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
@log_function
|
||||||
|
def on_exchange_third_party_invite_request(self, origin, room_id, event_dict):
|
||||||
|
builder = self.event_builder_factory.new(event_dict)
|
||||||
|
|
||||||
|
event, context = yield self._create_new_client_event(
|
||||||
|
builder=builder,
|
||||||
|
)
|
||||||
|
|
||||||
|
event, context = yield self.add_display_name_to_third_party_invite(
|
||||||
|
event_dict, event, context
|
||||||
|
)
|
||||||
|
|
||||||
|
self.auth.check(event, auth_events=context.current_state)
|
||||||
|
yield self._validate_keyserver(event, auth_events=context.current_state)
|
||||||
|
|
||||||
|
returned_invite = yield self.send_invite(origin, event)
|
||||||
|
# TODO: Make sure the signatures actually are correct.
|
||||||
|
event.signatures.update(returned_invite.signatures)
|
||||||
|
member_handler = self.hs.get_handlers().room_member_handler
|
||||||
|
yield member_handler.send_membership_event(event, context)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def add_display_name_to_third_party_invite(self, event_dict, event, context):
|
||||||
|
key = (
|
||||||
|
EventTypes.ThirdPartyInvite,
|
||||||
|
event.content["third_party_invite"]["signed"]["token"]
|
||||||
|
)
|
||||||
|
original_invite = context.current_state.get(key)
|
||||||
|
if not original_invite:
|
||||||
|
logger.info(
|
||||||
|
"Could not find invite event for third_party_invite - "
|
||||||
|
"discarding: %s" % (event_dict,)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
display_name = original_invite.content["display_name"]
|
||||||
|
event_dict["content"]["third_party_invite"]["display_name"] = display_name
|
||||||
|
builder = self.event_builder_factory.new(event_dict)
|
||||||
|
EventValidator().validate_new(builder)
|
||||||
|
event, context = yield self._create_new_client_event(builder=builder)
|
||||||
|
defer.returnValue((event, context))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _validate_keyserver(self, event, auth_events):
|
||||||
|
token = event.content["third_party_invite"]["signed"]["token"]
|
||||||
|
|
||||||
|
invite_event = auth_events.get(
|
||||||
|
(EventTypes.ThirdPartyInvite, token,)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = yield self.hs.get_simple_http_client().get_json(
|
||||||
|
invite_event.content["key_validity_url"],
|
||||||
|
{"public_key": invite_event.content["public_key"]}
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
raise SynapseError(
|
||||||
|
502,
|
||||||
|
"Third party certificate could not be checked"
|
||||||
|
)
|
||||||
|
if "valid" not in response or not response["valid"]:
|
||||||
|
raise AuthError(403, "Third party certificate was invalid")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -20,7 +20,6 @@ from synapse.api.errors import (
|
|||||||
CodeMessageException
|
CodeMessageException
|
||||||
)
|
)
|
||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
from synapse.http.client import SimpleHttpClient
|
|
||||||
from synapse.util.async import run_on_reactor
|
from synapse.util.async import run_on_reactor
|
||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
|
|
||||||
@@ -35,17 +34,17 @@ class IdentityHandler(BaseHandler):
|
|||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
super(IdentityHandler, self).__init__(hs)
|
super(IdentityHandler, self).__init__(hs)
|
||||||
|
|
||||||
|
self.http_client = hs.get_simple_http_client()
|
||||||
|
|
||||||
|
self.trusted_id_servers = set(hs.config.trusted_third_party_id_servers)
|
||||||
|
self.trust_any_id_server_just_for_testing_do_not_use = (
|
||||||
|
hs.config.use_insecure_ssl_client_just_for_testing_do_not_use
|
||||||
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def threepid_from_creds(self, creds):
|
def threepid_from_creds(self, creds):
|
||||||
yield run_on_reactor()
|
yield run_on_reactor()
|
||||||
|
|
||||||
# TODO: get this from the homeserver rather than creating a new one for
|
|
||||||
# each request
|
|
||||||
http_client = SimpleHttpClient(self.hs)
|
|
||||||
# XXX: make this configurable!
|
|
||||||
# trustedIdServers = ['matrix.org', 'localhost:8090']
|
|
||||||
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']
|
||||||
elif 'idServer' in creds:
|
elif 'idServer' in creds:
|
||||||
@@ -60,14 +59,23 @@ class IdentityHandler(BaseHandler):
|
|||||||
else:
|
else:
|
||||||
raise SynapseError(400, "No client_secret in creds")
|
raise SynapseError(400, "No client_secret in creds")
|
||||||
|
|
||||||
if id_server not in trustedIdServers:
|
if id_server not in self.trusted_id_servers:
|
||||||
|
if self.trust_any_id_server_just_for_testing_do_not_use:
|
||||||
|
logger.warn(
|
||||||
|
"Trusting untrustworthy ID server %r even though it isn't"
|
||||||
|
" in the trusted id list for testing because"
|
||||||
|
" 'use_insecure_ssl_client_just_for_testing_do_not_use'"
|
||||||
|
" is set in the config",
|
||||||
|
id_server,
|
||||||
|
)
|
||||||
|
else:
|
||||||
logger.warn('%s is not a trusted ID server: rejecting 3pid ' +
|
logger.warn('%s is not a trusted ID server: rejecting 3pid ' +
|
||||||
'credentials', id_server)
|
'credentials', id_server)
|
||||||
defer.returnValue(None)
|
defer.returnValue(None)
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
try:
|
try:
|
||||||
data = yield http_client.get_json(
|
data = yield self.http_client.get_json(
|
||||||
"https://%s%s" % (
|
"https://%s%s" % (
|
||||||
id_server,
|
id_server,
|
||||||
"/_matrix/identity/api/v1/3pid/getValidated3pid"
|
"/_matrix/identity/api/v1/3pid/getValidated3pid"
|
||||||
@@ -85,7 +93,6 @@ class IdentityHandler(BaseHandler):
|
|||||||
def bind_threepid(self, creds, mxid):
|
def bind_threepid(self, creds, mxid):
|
||||||
yield run_on_reactor()
|
yield run_on_reactor()
|
||||||
logger.debug("binding threepid %r to %s", creds, mxid)
|
logger.debug("binding threepid %r to %s", creds, mxid)
|
||||||
http_client = SimpleHttpClient(self.hs)
|
|
||||||
data = None
|
data = None
|
||||||
|
|
||||||
if 'id_server' in creds:
|
if 'id_server' in creds:
|
||||||
@@ -103,7 +110,7 @@ class IdentityHandler(BaseHandler):
|
|||||||
raise SynapseError(400, "No client_secret in creds")
|
raise SynapseError(400, "No client_secret in creds")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = yield http_client.post_urlencoded_get_json(
|
data = yield self.http_client.post_urlencoded_get_json(
|
||||||
"https://%s%s" % (
|
"https://%s%s" % (
|
||||||
id_server, "/_matrix/identity/api/v1/3pid/bind"
|
id_server, "/_matrix/identity/api/v1/3pid/bind"
|
||||||
),
|
),
|
||||||
@@ -121,7 +128,6 @@ class IdentityHandler(BaseHandler):
|
|||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def requestEmailToken(self, id_server, email, client_secret, send_attempt, **kwargs):
|
def requestEmailToken(self, id_server, email, client_secret, send_attempt, **kwargs):
|
||||||
yield run_on_reactor()
|
yield run_on_reactor()
|
||||||
http_client = SimpleHttpClient(self.hs)
|
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
'email': email,
|
'email': email,
|
||||||
@@ -131,7 +137,7 @@ class IdentityHandler(BaseHandler):
|
|||||||
params.update(kwargs)
|
params.update(kwargs)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = yield http_client.post_urlencoded_get_json(
|
data = yield self.http_client.post_urlencoded_get_json(
|
||||||
"https://%s%s" % (
|
"https://%s%s" % (
|
||||||
id_server,
|
id_server,
|
||||||
"/_matrix/identity/api/v1/validate/email/requestToken"
|
"/_matrix/identity/api/v1/validate/email/requestToken"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014 - 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -16,21 +16,28 @@
|
|||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.constants import EventTypes, Membership
|
from synapse.api.constants import EventTypes, Membership
|
||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import AuthError, Codes
|
||||||
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 import unwrapFirstError
|
||||||
from synapse.util.logcontext import PreserveLoggingContext
|
from synapse.util.logcontext import PreserveLoggingContext
|
||||||
|
from synapse.util.caches.snapshot_cache import SnapshotCache
|
||||||
from synapse.types import UserID, RoomStreamToken, StreamToken
|
from synapse.types import UserID, RoomStreamToken, StreamToken
|
||||||
|
|
||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
|
|
||||||
|
from canonicaljson import encode_canonical_json
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_presencelike_data(distributor, user, content):
|
||||||
|
return distributor.fire("collect_presencelike_data", user, content)
|
||||||
|
|
||||||
|
|
||||||
class MessageHandler(BaseHandler):
|
class MessageHandler(BaseHandler):
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
@@ -39,6 +46,7 @@ class MessageHandler(BaseHandler):
|
|||||||
self.state = hs.get_state_handler()
|
self.state = hs.get_state_handler()
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
self.validator = EventValidator()
|
self.validator = EventValidator()
|
||||||
|
self.snapshot_cache = SnapshotCache()
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_message(self, msg_id=None, room_id=None, sender_id=None,
|
def get_message(self, msg_id=None, room_id=None, sender_id=None,
|
||||||
@@ -70,12 +78,12 @@ class MessageHandler(BaseHandler):
|
|||||||
defer.returnValue(None)
|
defer.returnValue(None)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_messages(self, user_id=None, room_id=None, pagin_config=None,
|
def get_messages(self, requester, room_id=None, pagin_config=None,
|
||||||
as_client_event=True):
|
as_client_event=True):
|
||||||
"""Get messages in a room.
|
"""Get messages in a room.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_id (str): The user requesting messages.
|
requester (Requester): The user requesting messages.
|
||||||
room_id (str): The room they want messages from.
|
room_id (str): The room they want messages from.
|
||||||
pagin_config (synapse.api.streams.PaginationConfig): The pagination
|
pagin_config (synapse.api.streams.PaginationConfig): The pagination
|
||||||
config rules to apply, if any.
|
config rules to apply, if any.
|
||||||
@@ -83,8 +91,7 @@ class MessageHandler(BaseHandler):
|
|||||||
Returns:
|
Returns:
|
||||||
dict: Pagination API results
|
dict: Pagination API results
|
||||||
"""
|
"""
|
||||||
member_event = yield self.auth.check_user_was_in_room(room_id, user_id)
|
user_id = requester.user.to_string()
|
||||||
|
|
||||||
data_source = self.hs.get_event_sources().sources["room"]
|
data_source = self.hs.get_event_sources().sources["room"]
|
||||||
|
|
||||||
if pagin_config.from_token:
|
if pagin_config.from_token:
|
||||||
@@ -98,8 +105,6 @@ class MessageHandler(BaseHandler):
|
|||||||
room_token = pagin_config.from_token.room_key
|
room_token = pagin_config.from_token.room_key
|
||||||
|
|
||||||
room_token = RoomStreamToken.parse(room_token)
|
room_token = RoomStreamToken.parse(room_token)
|
||||||
if room_token.topological is None:
|
|
||||||
raise SynapseError(400, "Invalid token")
|
|
||||||
|
|
||||||
pagin_config.from_token = pagin_config.from_token.copy_and_replace(
|
pagin_config.from_token = pagin_config.from_token.copy_and_replace(
|
||||||
"room_key", str(room_token)
|
"room_key", str(room_token)
|
||||||
@@ -107,32 +112,37 @@ class MessageHandler(BaseHandler):
|
|||||||
|
|
||||||
source_config = pagin_config.get_source_config("room")
|
source_config = pagin_config.get_source_config("room")
|
||||||
|
|
||||||
if member_event.membership == Membership.LEAVE:
|
membership, member_event_id = yield self._check_in_room_or_world_readable(
|
||||||
|
room_id, user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if source_config.direction == 'b':
|
||||||
|
# if we're going backwards, we might need to backfill. This
|
||||||
|
# requires that we have a topo token.
|
||||||
|
if room_token.topological:
|
||||||
|
max_topo = room_token.topological
|
||||||
|
else:
|
||||||
|
max_topo = yield self.store.get_max_topological_token_for_stream_and_room(
|
||||||
|
room_id, room_token.stream
|
||||||
|
)
|
||||||
|
|
||||||
|
if membership == Membership.LEAVE:
|
||||||
# If they have left the room then clamp the token to be before
|
# If they have left the room then clamp the token to be before
|
||||||
# they left the room
|
# they left the room, to save the effort of loading from the
|
||||||
|
# database.
|
||||||
leave_token = yield self.store.get_topological_token_for_event(
|
leave_token = yield self.store.get_topological_token_for_event(
|
||||||
member_event.event_id
|
member_event_id
|
||||||
)
|
)
|
||||||
leave_token = RoomStreamToken.parse(leave_token)
|
leave_token = RoomStreamToken.parse(leave_token)
|
||||||
if leave_token.topological < room_token.topological:
|
if leave_token.topological < max_topo:
|
||||||
source_config.from_key = str(leave_token)
|
source_config.from_key = str(leave_token)
|
||||||
|
|
||||||
if source_config.direction == "f":
|
|
||||||
if source_config.to_key is None:
|
|
||||||
source_config.to_key = str(leave_token)
|
|
||||||
else:
|
|
||||||
to_token = RoomStreamToken.parse(source_config.to_key)
|
|
||||||
if leave_token.topological < to_token.topological:
|
|
||||||
source_config.to_key = str(leave_token)
|
|
||||||
|
|
||||||
yield self.hs.get_handlers().federation_handler.maybe_backfill(
|
yield self.hs.get_handlers().federation_handler.maybe_backfill(
|
||||||
room_id, room_token.topological
|
room_id, max_topo
|
||||||
)
|
)
|
||||||
|
|
||||||
user = UserID.from_string(user_id)
|
|
||||||
|
|
||||||
events, next_key = yield data_source.get_pagination_rows(
|
events, next_key = yield data_source.get_pagination_rows(
|
||||||
user, source_config, room_id
|
requester.user, source_config, room_id
|
||||||
)
|
)
|
||||||
|
|
||||||
next_token = pagin_config.from_token.copy_and_replace(
|
next_token = pagin_config.from_token.copy_and_replace(
|
||||||
@@ -146,7 +156,11 @@ class MessageHandler(BaseHandler):
|
|||||||
"end": next_token.to_string(),
|
"end": next_token.to_string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
events = yield self._filter_events_for_client(user_id, room_id, events)
|
events = yield self._filter_events_for_client(
|
||||||
|
user_id,
|
||||||
|
events,
|
||||||
|
is_peeking=(member_event_id is None),
|
||||||
|
)
|
||||||
|
|
||||||
time_now = self.clock.time_msec()
|
time_now = self.clock.time_msec()
|
||||||
|
|
||||||
@@ -162,85 +176,32 @@ class MessageHandler(BaseHandler):
|
|||||||
defer.returnValue(chunk)
|
defer.returnValue(chunk)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _filter_events_for_client(self, user_id, room_id, events):
|
def create_event(self, event_dict, token_id=None, txn_id=None):
|
||||||
event_id_to_state = yield self.store.get_state_for_events(
|
"""
|
||||||
room_id, frozenset(e.event_id for e in events),
|
Given a dict from a client, create a new event.
|
||||||
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
|
|
||||||
def create_and_send_event(self, event_dict, ratelimit=True,
|
|
||||||
token_id=None, txn_id=None):
|
|
||||||
""" Given a dict from a client, create and handle a new event.
|
|
||||||
|
|
||||||
Creates an FrozenEvent object, filling out auth_events, prev_events,
|
Creates an FrozenEvent object, filling out auth_events, prev_events,
|
||||||
etc.
|
etc.
|
||||||
|
|
||||||
Adds display names to Join membership events.
|
Adds display names to Join membership events.
|
||||||
|
|
||||||
Persists and notifies local clients and federation.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
event_dict (dict): An entire event
|
event_dict (dict): An entire event
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of created event (FrozenEvent), Context
|
||||||
"""
|
"""
|
||||||
builder = self.event_builder_factory.new(event_dict)
|
builder = self.event_builder_factory.new(event_dict)
|
||||||
|
|
||||||
self.validator.validate_new(builder)
|
self.validator.validate_new(builder)
|
||||||
|
|
||||||
if ratelimit:
|
|
||||||
self.ratelimit(builder.user_id)
|
|
||||||
# TODO(paul): Why does 'event' not have a 'user' object?
|
|
||||||
user = UserID.from_string(builder.user_id)
|
|
||||||
assert self.hs.is_mine(user), "User must be our own: %s" % (user,)
|
|
||||||
|
|
||||||
if builder.type == EventTypes.Member:
|
if builder.type == EventTypes.Member:
|
||||||
membership = builder.content.get("membership", None)
|
membership = builder.content.get("membership", None)
|
||||||
if membership == Membership.JOIN:
|
if membership == Membership.JOIN:
|
||||||
joinee = UserID.from_string(builder.state_key)
|
joinee = UserID.from_string(builder.state_key)
|
||||||
# If event doesn't include a display name, add one.
|
# If event doesn't include a display name, add one.
|
||||||
yield self.distributor.fire(
|
yield collect_presencelike_data(
|
||||||
"collect_presencelike_data",
|
self.distributor, joinee, builder.content
|
||||||
joinee,
|
|
||||||
builder.content
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if token_id is not None:
|
if token_id is not None:
|
||||||
@@ -252,10 +213,39 @@ class MessageHandler(BaseHandler):
|
|||||||
event, context = yield self._create_new_client_event(
|
event, context = yield self._create_new_client_event(
|
||||||
builder=builder,
|
builder=builder,
|
||||||
)
|
)
|
||||||
|
defer.returnValue((event, context))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def send_event(self, event, context, ratelimit=True, is_guest=False):
|
||||||
|
"""
|
||||||
|
Persists and notifies local clients and federation of an event.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event (FrozenEvent) the event to send.
|
||||||
|
context (Context) the context of the event.
|
||||||
|
ratelimit (bool): Whether to rate limit this send.
|
||||||
|
is_guest (bool): Whether the sender is a guest.
|
||||||
|
"""
|
||||||
|
user = UserID.from_string(event.sender)
|
||||||
|
|
||||||
|
assert self.hs.is_mine(user), "User must be our own: %s" % (user,)
|
||||||
|
|
||||||
|
if ratelimit:
|
||||||
|
self.ratelimit(event.sender)
|
||||||
|
|
||||||
|
if event.is_state():
|
||||||
|
prev_state = context.current_state.get((event.type, event.state_key))
|
||||||
|
if prev_state and event.user_id == prev_state.user_id:
|
||||||
|
prev_content = encode_canonical_json(prev_state.content)
|
||||||
|
next_content = encode_canonical_json(event.content)
|
||||||
|
if prev_content == next_content:
|
||||||
|
# Duplicate suppression for state updates with same sender
|
||||||
|
# and content.
|
||||||
|
defer.returnValue(prev_state)
|
||||||
|
|
||||||
if event.type == EventTypes.Member:
|
if event.type == EventTypes.Member:
|
||||||
member_handler = self.hs.get_handlers().room_member_handler
|
member_handler = self.hs.get_handlers().room_member_handler
|
||||||
yield member_handler.change_membership(event, context)
|
yield member_handler.send_membership_event(event, context, is_guest=is_guest)
|
||||||
else:
|
else:
|
||||||
yield self.handle_new_client_event(
|
yield self.handle_new_client_event(
|
||||||
event=event,
|
event=event,
|
||||||
@@ -267,11 +257,30 @@ class MessageHandler(BaseHandler):
|
|||||||
with PreserveLoggingContext():
|
with PreserveLoggingContext():
|
||||||
presence.bump_presence_active_time(user)
|
presence.bump_presence_active_time(user)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def create_and_send_event(self, event_dict, ratelimit=True,
|
||||||
|
token_id=None, txn_id=None, is_guest=False):
|
||||||
|
"""
|
||||||
|
Creates an event, then sends it.
|
||||||
|
|
||||||
|
See self.create_event and self.send_event.
|
||||||
|
"""
|
||||||
|
event, context = yield self.create_event(
|
||||||
|
event_dict,
|
||||||
|
token_id=token_id,
|
||||||
|
txn_id=txn_id
|
||||||
|
)
|
||||||
|
yield self.send_event(
|
||||||
|
event,
|
||||||
|
context,
|
||||||
|
ratelimit=ratelimit,
|
||||||
|
is_guest=is_guest
|
||||||
|
)
|
||||||
defer.returnValue(event)
|
defer.returnValue(event)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_room_data(self, user_id=None, room_id=None,
|
def get_room_data(self, user_id=None, room_id=None,
|
||||||
event_type=None, state_key=""):
|
event_type=None, state_key="", is_guest=False):
|
||||||
""" Get data from a room.
|
""" Get data from a room.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -281,23 +290,50 @@ class MessageHandler(BaseHandler):
|
|||||||
Raises:
|
Raises:
|
||||||
SynapseError if something went wrong.
|
SynapseError if something went wrong.
|
||||||
"""
|
"""
|
||||||
member_event = yield self.auth.check_user_was_in_room(room_id, user_id)
|
membership, membership_event_id = yield self._check_in_room_or_world_readable(
|
||||||
|
room_id, user_id
|
||||||
|
)
|
||||||
|
|
||||||
if member_event.membership == Membership.JOIN:
|
if membership == Membership.JOIN:
|
||||||
data = yield self.state_handler.get_current_state(
|
data = yield self.state_handler.get_current_state(
|
||||||
room_id, event_type, state_key
|
room_id, event_type, state_key
|
||||||
)
|
)
|
||||||
elif member_event.membership == Membership.LEAVE:
|
elif membership == Membership.LEAVE:
|
||||||
key = (event_type, state_key)
|
key = (event_type, state_key)
|
||||||
room_state = yield self.store.get_state_for_events(
|
room_state = yield self.store.get_state_for_events(
|
||||||
room_id, [member_event.event_id], [key]
|
[membership_event_id], [key]
|
||||||
)
|
)
|
||||||
data = room_state[member_event.event_id].get(key)
|
data = room_state[membership_event_id].get(key)
|
||||||
|
|
||||||
defer.returnValue(data)
|
defer.returnValue(data)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_state_events(self, user_id, room_id):
|
def _check_in_room_or_world_readable(self, room_id, user_id):
|
||||||
|
try:
|
||||||
|
# check_user_was_in_room will return the most recent membership
|
||||||
|
# event for the user if:
|
||||||
|
# * The user is a non-guest user, and was ever in the room
|
||||||
|
# * The user is a guest user, and has joined the room
|
||||||
|
# else it will throw.
|
||||||
|
member_event = yield self.auth.check_user_was_in_room(room_id, user_id)
|
||||||
|
defer.returnValue((member_event.membership, member_event.event_id))
|
||||||
|
return
|
||||||
|
except AuthError:
|
||||||
|
visibility = yield self.state_handler.get_current_state(
|
||||||
|
room_id, EventTypes.RoomHistoryVisibility, ""
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
visibility and
|
||||||
|
visibility.content["history_visibility"] == "world_readable"
|
||||||
|
):
|
||||||
|
defer.returnValue((Membership.JOIN, None))
|
||||||
|
return
|
||||||
|
raise AuthError(
|
||||||
|
403, "Guest access not allowed", errcode=Codes.GUEST_ACCESS_FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_state_events(self, user_id, room_id, is_guest=False):
|
||||||
"""Retrieve all state events for a given room. If the user is
|
"""Retrieve all state events for a given room. If the user is
|
||||||
joined to the room then return the current state. If the user has
|
joined to the room then return the current state. If the user has
|
||||||
left the room return the state events from when they left.
|
left the room return the state events from when they left.
|
||||||
@@ -308,23 +344,25 @@ class MessageHandler(BaseHandler):
|
|||||||
Returns:
|
Returns:
|
||||||
A list of dicts representing state events. [{}, {}, {}]
|
A list of dicts representing state events. [{}, {}, {}]
|
||||||
"""
|
"""
|
||||||
member_event = yield self.auth.check_user_was_in_room(room_id, user_id)
|
membership, membership_event_id = yield self._check_in_room_or_world_readable(
|
||||||
|
room_id, user_id
|
||||||
if member_event.membership == Membership.JOIN:
|
|
||||||
room_state = yield self.state_handler.get_current_state(room_id)
|
|
||||||
elif member_event.membership == Membership.LEAVE:
|
|
||||||
room_state = yield self.store.get_state_for_events(
|
|
||||||
room_id, [member_event.event_id], None
|
|
||||||
)
|
)
|
||||||
room_state = room_state[member_event.event_id]
|
|
||||||
|
if membership == Membership.JOIN:
|
||||||
|
room_state = yield self.state_handler.get_current_state(room_id)
|
||||||
|
elif membership == Membership.LEAVE:
|
||||||
|
room_state = yield self.store.get_state_for_events(
|
||||||
|
[membership_event_id], None
|
||||||
|
)
|
||||||
|
room_state = room_state[membership_event_id]
|
||||||
|
|
||||||
now = self.clock.time_msec()
|
now = self.clock.time_msec()
|
||||||
defer.returnValue(
|
defer.returnValue(
|
||||||
[serialize_event(c, now) for c in room_state.values()]
|
[serialize_event(c, now) for c in room_state.values()]
|
||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
def snapshot_all_rooms(self, user_id=None, pagin_config=None,
|
||||||
def snapshot_all_rooms(self, user_id=None, pagin_config=None, as_client_event=True):
|
as_client_event=True, include_archived=False):
|
||||||
"""Retrieve a snapshot of all rooms the user is invited or has joined.
|
"""Retrieve a snapshot of all rooms the user is invited or has joined.
|
||||||
|
|
||||||
This snapshot may include messages for all rooms where the user is
|
This snapshot may include messages for all rooms where the user is
|
||||||
@@ -335,17 +373,41 @@ class MessageHandler(BaseHandler):
|
|||||||
pagin_config (synapse.api.streams.PaginationConfig): The pagination
|
pagin_config (synapse.api.streams.PaginationConfig): The pagination
|
||||||
config used to determine how many messages *PER ROOM* to return.
|
config used to determine how many messages *PER ROOM* to return.
|
||||||
as_client_event (bool): True to get events in client-server format.
|
as_client_event (bool): True to get events in client-server format.
|
||||||
|
include_archived (bool): True to get rooms that the user has left
|
||||||
Returns:
|
Returns:
|
||||||
A list of dicts with "room_id" and "membership" keys for all rooms
|
A list of dicts with "room_id" and "membership" keys for all rooms
|
||||||
the user is currently invited or joined in on. Rooms where the user
|
the user is currently invited or joined in on. Rooms where the user
|
||||||
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.
|
||||||
"""
|
"""
|
||||||
|
key = (
|
||||||
|
user_id,
|
||||||
|
pagin_config.from_token,
|
||||||
|
pagin_config.to_token,
|
||||||
|
pagin_config.direction,
|
||||||
|
pagin_config.limit,
|
||||||
|
as_client_event,
|
||||||
|
include_archived,
|
||||||
|
)
|
||||||
|
now_ms = self.clock.time_msec()
|
||||||
|
result = self.snapshot_cache.get(now_ms, key)
|
||||||
|
if result is not None:
|
||||||
|
return result
|
||||||
|
|
||||||
|
return self.snapshot_cache.set(now_ms, key, self._snapshot_all_rooms(
|
||||||
|
user_id, pagin_config, as_client_event, include_archived
|
||||||
|
))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _snapshot_all_rooms(self, user_id=None, pagin_config=None,
|
||||||
|
as_client_event=True, include_archived=False):
|
||||||
|
|
||||||
|
memberships = [Membership.INVITE, Membership.JOIN]
|
||||||
|
if include_archived:
|
||||||
|
memberships.append(Membership.LEAVE)
|
||||||
|
|
||||||
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=memberships
|
||||||
membership_list=[
|
|
||||||
Membership.INVITE, Membership.JOIN, Membership.LEAVE
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
user = UserID.from_string(user_id)
|
user = UserID.from_string(user_id)
|
||||||
@@ -365,6 +427,12 @@ class MessageHandler(BaseHandler):
|
|||||||
user, pagination_config.get_source_config("receipt"), None
|
user, pagination_config.get_source_config("receipt"), None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
tags_by_room = yield self.store.get_tags_for_user(user_id)
|
||||||
|
|
||||||
|
account_data, account_data_by_room = (
|
||||||
|
yield self.store.get_account_data_for_user(user_id)
|
||||||
|
)
|
||||||
|
|
||||||
public_room_ids = yield self.store.get_public_room_ids()
|
public_room_ids = yield self.store.get_public_room_ids()
|
||||||
|
|
||||||
limit = pagin_config.limit
|
limit = pagin_config.limit
|
||||||
@@ -383,8 +451,12 @@ class MessageHandler(BaseHandler):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if event.membership == Membership.INVITE:
|
if event.membership == Membership.INVITE:
|
||||||
|
time_now = self.clock.time_msec()
|
||||||
d["inviter"] = event.sender
|
d["inviter"] = event.sender
|
||||||
|
|
||||||
|
invite_event = yield self.store.get_event(event.event_id)
|
||||||
|
d["invite"] = serialize_event(invite_event, time_now, as_client_event)
|
||||||
|
|
||||||
rooms_ret.append(d)
|
rooms_ret.append(d)
|
||||||
|
|
||||||
if event.membership not in (Membership.JOIN, Membership.LEAVE):
|
if event.membership not in (Membership.JOIN, Membership.LEAVE):
|
||||||
@@ -399,7 +471,7 @@ class MessageHandler(BaseHandler):
|
|||||||
elif event.membership == Membership.LEAVE:
|
elif event.membership == Membership.LEAVE:
|
||||||
room_end_token = "s%d" % (event.stream_ordering,)
|
room_end_token = "s%d" % (event.stream_ordering,)
|
||||||
deferred_room_state = self.store.get_state_for_events(
|
deferred_room_state = self.store.get_state_for_events(
|
||||||
event.room_id, [event.event_id], None
|
[event.event_id], None
|
||||||
)
|
)
|
||||||
deferred_room_state.addCallback(
|
deferred_room_state.addCallback(
|
||||||
lambda states: states[event.event_id]
|
lambda states: states[event.event_id]
|
||||||
@@ -417,7 +489,7 @@ class MessageHandler(BaseHandler):
|
|||||||
).addErrback(unwrapFirstError)
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
messages = yield self._filter_events_for_client(
|
messages = yield self._filter_events_for_client(
|
||||||
user_id, event.room_id, messages
|
user_id, messages
|
||||||
)
|
)
|
||||||
|
|
||||||
start_token = now_token.copy_and_replace("room_key", token[0])
|
start_token = now_token.copy_and_replace("room_key", token[0])
|
||||||
@@ -437,6 +509,23 @@ class MessageHandler(BaseHandler):
|
|||||||
serialize_event(c, time_now, as_client_event)
|
serialize_event(c, time_now, as_client_event)
|
||||||
for c in current_state.values()
|
for c in current_state.values()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
account_data_events = []
|
||||||
|
tags = tags_by_room.get(event.room_id)
|
||||||
|
if tags:
|
||||||
|
account_data_events.append({
|
||||||
|
"type": "m.tag",
|
||||||
|
"content": {"tags": tags},
|
||||||
|
})
|
||||||
|
|
||||||
|
account_data = account_data_by_room.get(event.room_id, {})
|
||||||
|
for account_data_type, content in account_data.items():
|
||||||
|
account_data_events.append({
|
||||||
|
"type": account_data_type,
|
||||||
|
"content": content,
|
||||||
|
})
|
||||||
|
|
||||||
|
d["account_data"] = account_data_events
|
||||||
except:
|
except:
|
||||||
logger.exception("Failed to get snapshot")
|
logger.exception("Failed to get snapshot")
|
||||||
|
|
||||||
@@ -449,9 +538,17 @@ class MessageHandler(BaseHandler):
|
|||||||
consumeErrors=True
|
consumeErrors=True
|
||||||
).addErrback(unwrapFirstError)
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
|
account_data_events = []
|
||||||
|
for account_data_type, content in account_data.items():
|
||||||
|
account_data_events.append({
|
||||||
|
"type": account_data_type,
|
||||||
|
"content": content,
|
||||||
|
})
|
||||||
|
|
||||||
ret = {
|
ret = {
|
||||||
"rooms": rooms_ret,
|
"rooms": rooms_ret,
|
||||||
"presence": presence,
|
"presence": presence,
|
||||||
|
"account_data": account_data_events,
|
||||||
"receipts": receipt,
|
"receipts": receipt,
|
||||||
"end": now_token.to_string(),
|
"end": now_token.to_string(),
|
||||||
}
|
}
|
||||||
@@ -459,13 +556,13 @@ class MessageHandler(BaseHandler):
|
|||||||
defer.returnValue(ret)
|
defer.returnValue(ret)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def room_initial_sync(self, user_id, room_id, pagin_config=None):
|
def room_initial_sync(self, requester, room_id, pagin_config=None):
|
||||||
"""Capture the a snapshot of a room. If user is currently a member of
|
"""Capture the a snapshot of a room. If user is currently a member of
|
||||||
the room this will be what is currently in the room. If the user left
|
the room this will be what is currently in the room. If the user left
|
||||||
the room this will be what was in the room when they left.
|
the room this will be what was in the room when they left.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_id(str): The user to get a snapshot for.
|
requester(Requester): The user to get a snapshot for.
|
||||||
room_id(str): The room to get a snapshot of.
|
room_id(str): The room to get a snapshot of.
|
||||||
pagin_config(synapse.streams.config.PaginationConfig):
|
pagin_config(synapse.streams.config.PaginationConfig):
|
||||||
The pagination config used to determine how many messages to
|
The pagination config used to determine how many messages to
|
||||||
@@ -476,33 +573,56 @@ class MessageHandler(BaseHandler):
|
|||||||
A JSON serialisable dict with the snapshot of the room.
|
A JSON serialisable dict with the snapshot of the room.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
member_event = yield self.auth.check_user_was_in_room(room_id, user_id)
|
user_id = requester.user.to_string()
|
||||||
|
|
||||||
if member_event.membership == Membership.JOIN:
|
membership, member_event_id = yield self._check_in_room_or_world_readable(
|
||||||
|
room_id, user_id,
|
||||||
|
)
|
||||||
|
is_peeking = member_event_id is None
|
||||||
|
|
||||||
|
if membership == Membership.JOIN:
|
||||||
result = yield self._room_initial_sync_joined(
|
result = yield self._room_initial_sync_joined(
|
||||||
user_id, room_id, pagin_config, member_event
|
user_id, room_id, pagin_config, membership, is_peeking
|
||||||
)
|
)
|
||||||
elif member_event.membership == Membership.LEAVE:
|
elif membership == Membership.LEAVE:
|
||||||
result = yield self._room_initial_sync_parted(
|
result = yield self._room_initial_sync_parted(
|
||||||
user_id, room_id, pagin_config, member_event
|
user_id, room_id, pagin_config, membership, member_event_id, is_peeking
|
||||||
)
|
)
|
||||||
|
|
||||||
|
account_data_events = []
|
||||||
|
tags = yield self.store.get_tags_for_room(user_id, room_id)
|
||||||
|
if tags:
|
||||||
|
account_data_events.append({
|
||||||
|
"type": "m.tag",
|
||||||
|
"content": {"tags": tags},
|
||||||
|
})
|
||||||
|
|
||||||
|
account_data = yield self.store.get_account_data_for_room(user_id, room_id)
|
||||||
|
for account_data_type, content in account_data.items():
|
||||||
|
account_data_events.append({
|
||||||
|
"type": account_data_type,
|
||||||
|
"content": content,
|
||||||
|
})
|
||||||
|
|
||||||
|
result["account_data"] = account_data_events
|
||||||
|
|
||||||
defer.returnValue(result)
|
defer.returnValue(result)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _room_initial_sync_parted(self, user_id, room_id, pagin_config,
|
def _room_initial_sync_parted(self, user_id, room_id, pagin_config,
|
||||||
member_event):
|
membership, member_event_id, is_peeking):
|
||||||
room_state = yield self.store.get_state_for_events(
|
room_state = yield self.store.get_state_for_events(
|
||||||
member_event.room_id, [member_event.event_id], None
|
[member_event_id], None
|
||||||
)
|
)
|
||||||
|
|
||||||
room_state = room_state[member_event.event_id]
|
room_state = room_state[member_event_id]
|
||||||
|
|
||||||
limit = pagin_config.limit if pagin_config else None
|
limit = pagin_config.limit if pagin_config else None
|
||||||
if limit is None:
|
if limit is None:
|
||||||
limit = 10
|
limit = 10
|
||||||
|
|
||||||
stream_token = yield self.store.get_stream_token_for_event(
|
stream_token = yield self.store.get_stream_token_for_event(
|
||||||
member_event.event_id
|
member_event_id
|
||||||
)
|
)
|
||||||
|
|
||||||
messages, token = yield self.store.get_recent_events_for_room(
|
messages, token = yield self.store.get_recent_events_for_room(
|
||||||
@@ -512,16 +632,16 @@ class MessageHandler(BaseHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
messages = yield self._filter_events_for_client(
|
messages = yield self._filter_events_for_client(
|
||||||
user_id, room_id, messages
|
user_id, messages, is_peeking=is_peeking
|
||||||
)
|
)
|
||||||
|
|
||||||
start_token = StreamToken(token[0], 0, 0, 0)
|
start_token = StreamToken(token[0], 0, 0, 0, 0)
|
||||||
end_token = StreamToken(token[1], 0, 0, 0)
|
end_token = StreamToken(token[1], 0, 0, 0, 0)
|
||||||
|
|
||||||
time_now = self.clock.time_msec()
|
time_now = self.clock.time_msec()
|
||||||
|
|
||||||
defer.returnValue({
|
defer.returnValue({
|
||||||
"membership": member_event.membership,
|
"membership": membership,
|
||||||
"room_id": room_id,
|
"room_id": room_id,
|
||||||
"messages": {
|
"messages": {
|
||||||
"chunk": [serialize_event(m, time_now) for m in messages],
|
"chunk": [serialize_event(m, time_now) for m in messages],
|
||||||
@@ -535,7 +655,7 @@ class MessageHandler(BaseHandler):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _room_initial_sync_joined(self, user_id, room_id, pagin_config,
|
def _room_initial_sync_joined(self, user_id, room_id, pagin_config,
|
||||||
member_event):
|
membership, is_peeking):
|
||||||
current_state = yield self.state.get_current_state(
|
current_state = yield self.state.get_current_state(
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
)
|
)
|
||||||
@@ -576,12 +696,19 @@ class MessageHandler(BaseHandler):
|
|||||||
|
|
||||||
defer.returnValue(states.values())
|
defer.returnValue(states.values())
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_receipts():
|
||||||
receipts_handler = self.hs.get_handlers().receipts_handler
|
receipts_handler = self.hs.get_handlers().receipts_handler
|
||||||
|
receipts = yield receipts_handler.get_receipts_for_room(
|
||||||
|
room_id,
|
||||||
|
now_token.receipt_key
|
||||||
|
)
|
||||||
|
defer.returnValue(receipts)
|
||||||
|
|
||||||
presence, receipts, (messages, token) = yield defer.gatherResults(
|
presence, receipts, (messages, token) = yield defer.gatherResults(
|
||||||
[
|
[
|
||||||
get_presence(),
|
get_presence(),
|
||||||
receipts_handler.get_receipts_for_room(room_id, now_token.receipt_key),
|
get_receipts(),
|
||||||
self.store.get_recent_events_for_room(
|
self.store.get_recent_events_for_room(
|
||||||
room_id,
|
room_id,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
@@ -592,7 +719,7 @@ class MessageHandler(BaseHandler):
|
|||||||
).addErrback(unwrapFirstError)
|
).addErrback(unwrapFirstError)
|
||||||
|
|
||||||
messages = yield self._filter_events_for_client(
|
messages = yield self._filter_events_for_client(
|
||||||
user_id, room_id, messages
|
user_id, messages, is_peeking=is_peeking,
|
||||||
)
|
)
|
||||||
|
|
||||||
start_token = now_token.copy_and_replace("room_key", token[0])
|
start_token = now_token.copy_and_replace("room_key", token[0])
|
||||||
@@ -600,8 +727,7 @@ class MessageHandler(BaseHandler):
|
|||||||
|
|
||||||
time_now = self.clock.time_msec()
|
time_now = self.clock.time_msec()
|
||||||
|
|
||||||
defer.returnValue({
|
ret = {
|
||||||
"membership": member_event.membership,
|
|
||||||
"room_id": room_id,
|
"room_id": room_id,
|
||||||
"messages": {
|
"messages": {
|
||||||
"chunk": [serialize_event(m, time_now) for m in messages],
|
"chunk": [serialize_event(m, time_now) for m in messages],
|
||||||
@@ -611,4 +737,8 @@ class MessageHandler(BaseHandler):
|
|||||||
"state": state,
|
"state": state,
|
||||||
"presence": presence,
|
"presence": presence,
|
||||||
"receipts": receipts,
|
"receipts": receipts,
|
||||||
})
|
}
|
||||||
|
if not is_peeking:
|
||||||
|
ret["membership"] = membership
|
||||||
|
|
||||||
|
defer.returnValue(ret)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -62,6 +62,14 @@ def partitionbool(l, func):
|
|||||||
return ret.get(True, []), ret.get(False, [])
|
return ret.get(True, []), ret.get(False, [])
|
||||||
|
|
||||||
|
|
||||||
|
def user_presence_changed(distributor, user, statuscache):
|
||||||
|
return distributor.fire("user_presence_changed", user, statuscache)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_presencelike_data(distributor, user, content):
|
||||||
|
return distributor.fire("collect_presencelike_data", user, content)
|
||||||
|
|
||||||
|
|
||||||
class PresenceHandler(BaseHandler):
|
class PresenceHandler(BaseHandler):
|
||||||
|
|
||||||
STATE_LEVELS = {
|
STATE_LEVELS = {
|
||||||
@@ -361,9 +369,7 @@ class PresenceHandler(BaseHandler):
|
|||||||
yield self.store.set_presence_state(
|
yield self.store.set_presence_state(
|
||||||
target_user.localpart, state_to_store
|
target_user.localpart, state_to_store
|
||||||
)
|
)
|
||||||
yield self.distributor.fire(
|
yield collect_presencelike_data(self.distributor, target_user, state)
|
||||||
"collect_presencelike_data", target_user, state
|
|
||||||
)
|
|
||||||
|
|
||||||
if now_level > was_level:
|
if now_level > was_level:
|
||||||
state["last_active"] = self.clock.time_msec()
|
state["last_active"] = self.clock.time_msec()
|
||||||
@@ -372,13 +378,13 @@ class PresenceHandler(BaseHandler):
|
|||||||
was_polling = target_user in self._user_cachemap
|
was_polling = target_user in self._user_cachemap
|
||||||
|
|
||||||
if now_online and not was_polling:
|
if now_online and not was_polling:
|
||||||
self.start_polling_presence(target_user, state=state)
|
yield 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)
|
yield 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)
|
yield 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:
|
||||||
@@ -388,6 +394,7 @@ class PresenceHandler(BaseHandler):
|
|||||||
if now - prev_state.state.get("last_active", 0) < LAST_ACTIVE_GRANULARITY:
|
if now - prev_state.state.get("last_active", 0) < LAST_ACTIVE_GRANULARITY:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
with PreserveLoggingContext():
|
||||||
self.changed_presencelike_data(user, {"last_active": now})
|
self.changed_presencelike_data(user, {"last_active": now})
|
||||||
|
|
||||||
def get_joined_rooms_for_user(self, user):
|
def get_joined_rooms_for_user(self, user):
|
||||||
@@ -422,12 +429,12 @@ class PresenceHandler(BaseHandler):
|
|||||||
@log_function
|
@log_function
|
||||||
def started_user_eventstream(self, user):
|
def started_user_eventstream(self, user):
|
||||||
# TODO(paul): Use "last online" state
|
# TODO(paul): Use "last online" state
|
||||||
self.set_state(user, user, {"presence": PresenceState.ONLINE})
|
return self.set_state(user, user, {"presence": PresenceState.ONLINE})
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def stopped_user_eventstream(self, user):
|
def stopped_user_eventstream(self, user):
|
||||||
# TODO(paul): Save current state as "last online" state
|
# TODO(paul): Save current state as "last online" state
|
||||||
self.set_state(user, user, {"presence": PresenceState.OFFLINE})
|
return self.set_state(user, user, {"presence": PresenceState.OFFLINE})
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def user_joined_room(self, user, room_id):
|
def user_joined_room(self, user, room_id):
|
||||||
@@ -460,6 +467,7 @@ class PresenceHandler(BaseHandler):
|
|||||||
local_user, room_ids=[room_id], add_to_cache=False
|
local_user, room_ids=[room_id], add_to_cache=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with PreserveLoggingContext():
|
||||||
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],
|
||||||
@@ -467,7 +475,7 @@ class PresenceHandler(BaseHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def send_invite(self, observer_user, observed_user):
|
def send_presence_invite(self, observer_user, observed_user):
|
||||||
"""Request the presence of a local or remote user for a local 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")
|
||||||
@@ -550,7 +558,7 @@ class PresenceHandler(BaseHandler):
|
|||||||
observer_user.localpart, observed_user.to_string()
|
observer_user.localpart, observed_user.to_string()
|
||||||
)
|
)
|
||||||
|
|
||||||
self.start_polling_presence(
|
yield self.start_polling_presence(
|
||||||
observer_user, target_user=observed_user
|
observer_user, target_user=observed_user
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -878,7 +886,7 @@ class PresenceHandler(BaseHandler):
|
|||||||
room_ids=room_ids,
|
room_ids=room_ids,
|
||||||
statuscache=statuscache,
|
statuscache=statuscache,
|
||||||
)
|
)
|
||||||
yield self.distributor.fire("user_presence_changed", user, statuscache)
|
yield user_presence_changed(self.distributor, user, statuscache)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def incoming_presence(self, origin, content):
|
def incoming_presence(self, origin, content):
|
||||||
@@ -950,6 +958,7 @@ class PresenceHandler(BaseHandler):
|
|||||||
)
|
)
|
||||||
while len(self._remote_offline_serials) > MAX_OFFLINE_SERIALS:
|
while len(self._remote_offline_serials) > MAX_OFFLINE_SERIALS:
|
||||||
self._remote_offline_serials.pop() # remove the oldest
|
self._remote_offline_serials.pop() # remove the oldest
|
||||||
|
if user in self._user_cachemap:
|
||||||
del self._user_cachemap[user]
|
del self._user_cachemap[user]
|
||||||
else:
|
else:
|
||||||
# Remove the user from remote_offline_serials now that they're
|
# Remove the user from remote_offline_serials now that they're
|
||||||
@@ -1115,9 +1124,7 @@ class PresenceHandler(BaseHandler):
|
|||||||
self._user_cachemap[user].get_state()["last_active"]
|
self._user_cachemap[user].get_state()["last_active"]
|
||||||
)
|
)
|
||||||
|
|
||||||
yield self.distributor.fire(
|
yield collect_presencelike_data(self.distributor, user, state)
|
||||||
"collect_presencelike_data", user, state
|
|
||||||
)
|
|
||||||
|
|
||||||
if "last_active" in state:
|
if "last_active" in state:
|
||||||
state = dict(state)
|
state = dict(state)
|
||||||
@@ -1142,8 +1149,9 @@ class PresenceEventSource(object):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def get_new_events_for_user(self, user, from_key, limit):
|
def get_new_events(self, user, from_key, room_ids=None, **kwargs):
|
||||||
from_key = int(from_key)
|
from_key = int(from_key)
|
||||||
|
room_ids = room_ids or []
|
||||||
|
|
||||||
presence = self.hs.get_handlers().presence_handler
|
presence = self.hs.get_handlers().presence_handler
|
||||||
cachemap = presence._user_cachemap
|
cachemap = presence._user_cachemap
|
||||||
@@ -1161,7 +1169,6 @@ class PresenceEventSource(object):
|
|||||||
user_ids_to_check |= set(
|
user_ids_to_check |= set(
|
||||||
UserID.from_string(p["observed_user_id"]) for p in presence_list
|
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):
|
for room_id in set(room_ids) & set(presence._room_serials):
|
||||||
if presence._room_serials[room_id] > from_key:
|
if presence._room_serials[room_id] > from_key:
|
||||||
joined = yield presence.get_joined_users_for_room_id(room_id)
|
joined = yield presence.get_joined_users_for_room_id(room_id)
|
||||||
@@ -1263,6 +1270,11 @@ class UserPresenceCache(object):
|
|||||||
self.state = {"presence": PresenceState.OFFLINE}
|
self.state = {"presence": PresenceState.OFFLINE}
|
||||||
self.serial = None
|
self.serial = None
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "UserPresenceCache(state=%r, serial=%r)" % (
|
||||||
|
self.state, self.serial
|
||||||
|
)
|
||||||
|
|
||||||
def update(self, state, serial):
|
def update(self, state, serial):
|
||||||
assert("mtime_age" not in state)
|
assert("mtime_age" not in state)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -28,6 +28,14 @@ import logging
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def changed_presencelike_data(distributor, user, state):
|
||||||
|
return distributor.fire("changed_presencelike_data", user, state)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_presencelike_data(distributor, user, content):
|
||||||
|
return distributor.fire("collect_presencelike_data", user, content)
|
||||||
|
|
||||||
|
|
||||||
class ProfileHandler(BaseHandler):
|
class ProfileHandler(BaseHandler):
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
@@ -95,11 +103,9 @@ class ProfileHandler(BaseHandler):
|
|||||||
target_user.localpart, new_displayname
|
target_user.localpart, new_displayname
|
||||||
)
|
)
|
||||||
|
|
||||||
yield self.distributor.fire(
|
yield changed_presencelike_data(self.distributor, target_user, {
|
||||||
"changed_presencelike_data", target_user, {
|
|
||||||
"displayname": new_displayname,
|
"displayname": new_displayname,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
yield self._update_join_states(target_user)
|
yield self._update_join_states(target_user)
|
||||||
|
|
||||||
@@ -144,11 +150,9 @@ class ProfileHandler(BaseHandler):
|
|||||||
target_user.localpart, new_avatar_url
|
target_user.localpart, new_avatar_url
|
||||||
)
|
)
|
||||||
|
|
||||||
yield self.distributor.fire(
|
yield changed_presencelike_data(self.distributor, target_user, {
|
||||||
"changed_presencelike_data", target_user, {
|
|
||||||
"avatar_url": new_avatar_url,
|
"avatar_url": new_avatar_url,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
yield self._update_join_states(target_user)
|
yield self._update_join_states(target_user)
|
||||||
|
|
||||||
@@ -208,9 +212,7 @@ class ProfileHandler(BaseHandler):
|
|||||||
"membership": Membership.JOIN,
|
"membership": Membership.JOIN,
|
||||||
}
|
}
|
||||||
|
|
||||||
yield self.distributor.fire(
|
yield collect_presencelike_data(self.distributor, user, content)
|
||||||
"collect_presencelike_data", user, content
|
|
||||||
)
|
|
||||||
|
|
||||||
msg_handler = self.hs.get_handlers().message_handler
|
msg_handler = self.hs.get_handlers().message_handler
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2015 OpenMarket Ltd
|
# Copyright 2015, 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -156,13 +156,7 @@ class ReceiptsHandler(BaseHandler):
|
|||||||
if not result:
|
if not result:
|
||||||
defer.returnValue([])
|
defer.returnValue([])
|
||||||
|
|
||||||
event = {
|
defer.returnValue(result)
|
||||||
"type": "m.receipt",
|
|
||||||
"room_id": room_id,
|
|
||||||
"content": result,
|
|
||||||
}
|
|
||||||
|
|
||||||
defer.returnValue([event])
|
|
||||||
|
|
||||||
|
|
||||||
class ReceiptEventSource(object):
|
class ReceiptEventSource(object):
|
||||||
@@ -170,17 +164,15 @@ class ReceiptEventSource(object):
|
|||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_new_events_for_user(self, user, from_key, limit):
|
def get_new_events(self, from_key, room_ids, **kwargs):
|
||||||
from_key = int(from_key)
|
from_key = int(from_key)
|
||||||
to_key = yield self.get_current_key()
|
to_key = yield self.get_current_key()
|
||||||
|
|
||||||
if from_key == to_key:
|
if from_key == to_key:
|
||||||
defer.returnValue(([], 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(
|
events = yield self.store.get_linearized_receipts_for_rooms(
|
||||||
rooms,
|
room_ids,
|
||||||
from_key=from_key,
|
from_key=from_key,
|
||||||
to_key=to_key,
|
to_key=to_key,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014 - 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -21,7 +21,6 @@ from synapse.api.errors import (
|
|||||||
AuthError, Codes, SynapseError, RegistrationError, InvalidCaptchaError
|
AuthError, Codes, SynapseError, RegistrationError, InvalidCaptchaError
|
||||||
)
|
)
|
||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
import synapse.util.stringutils as stringutils
|
|
||||||
from synapse.util.async import run_on_reactor
|
from synapse.util.async import run_on_reactor
|
||||||
from synapse.http.client import CaptchaServerHttpClient
|
from synapse.http.client import CaptchaServerHttpClient
|
||||||
|
|
||||||
@@ -31,23 +30,31 @@ import urllib
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def registered_user(distributor, user):
|
||||||
|
return distributor.fire("registered_user", user)
|
||||||
|
|
||||||
|
|
||||||
class RegistrationHandler(BaseHandler):
|
class RegistrationHandler(BaseHandler):
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
super(RegistrationHandler, self).__init__(hs)
|
super(RegistrationHandler, self).__init__(hs)
|
||||||
|
|
||||||
|
self.auth = hs.get_auth()
|
||||||
self.distributor = hs.get_distributor()
|
self.distributor = hs.get_distributor()
|
||||||
self.distributor.declare("registered_user")
|
self.distributor.declare("registered_user")
|
||||||
|
self.captcha_client = CaptchaServerHttpClient(hs)
|
||||||
|
|
||||||
|
self._next_generated_user_id = None
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def check_username(self, localpart):
|
def check_username(self, localpart, guest_access_token=None):
|
||||||
yield run_on_reactor()
|
yield run_on_reactor()
|
||||||
|
|
||||||
if urllib.quote(localpart) != localpart:
|
if urllib.quote(localpart.encode('utf-8')) != localpart:
|
||||||
raise SynapseError(
|
raise SynapseError(
|
||||||
400,
|
400,
|
||||||
"User ID must only contain characters which do not"
|
"User ID can only contain characters a-z, 0-9, or '_-./'",
|
||||||
" require URL encoding."
|
Codes.INVALID_USERNAME
|
||||||
)
|
)
|
||||||
|
|
||||||
user = UserID(localpart, self.hs.hostname)
|
user = UserID(localpart, self.hs.hostname)
|
||||||
@@ -57,19 +64,35 @@ class RegistrationHandler(BaseHandler):
|
|||||||
|
|
||||||
users = yield self.store.get_users_by_id_case_insensitive(user_id)
|
users = yield self.store.get_users_by_id_case_insensitive(user_id)
|
||||||
if users:
|
if users:
|
||||||
|
if not guest_access_token:
|
||||||
raise SynapseError(
|
raise SynapseError(
|
||||||
400,
|
400,
|
||||||
"User ID already taken.",
|
"User ID already taken.",
|
||||||
errcode=Codes.USER_IN_USE,
|
errcode=Codes.USER_IN_USE,
|
||||||
)
|
)
|
||||||
|
user_data = yield self.auth.get_user_from_macaroon(guest_access_token)
|
||||||
|
if not user_data["is_guest"] or user_data["user"].localpart != localpart:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"Cannot register taken user ID without valid guest "
|
||||||
|
"credentials for that user.",
|
||||||
|
errcode=Codes.FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def register(self, localpart=None, password=None):
|
def register(
|
||||||
|
self,
|
||||||
|
localpart=None,
|
||||||
|
password=None,
|
||||||
|
generate_token=True,
|
||||||
|
guest_access_token=None,
|
||||||
|
make_guest=False
|
||||||
|
):
|
||||||
"""Registers a new client on the server.
|
"""Registers a new client on the server.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
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 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. This can be None which means they cannot 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).
|
via a password (e.g. the user is an application service user).
|
||||||
@@ -84,66 +107,64 @@ class RegistrationHandler(BaseHandler):
|
|||||||
password_hash = self.auth_handler().hash(password)
|
password_hash = self.auth_handler().hash(password)
|
||||||
|
|
||||||
if localpart:
|
if localpart:
|
||||||
yield self.check_username(localpart)
|
yield self.check_username(localpart, guest_access_token=guest_access_token)
|
||||||
|
|
||||||
|
was_guest = guest_access_token is not None
|
||||||
|
|
||||||
|
if not was_guest:
|
||||||
|
try:
|
||||||
|
int(localpart)
|
||||||
|
raise RegistrationError(
|
||||||
|
400,
|
||||||
|
"Numeric user IDs are reserved for guest users."
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
user = UserID(localpart, self.hs.hostname)
|
user = UserID(localpart, self.hs.hostname)
|
||||||
user_id = user.to_string()
|
user_id = user.to_string()
|
||||||
|
|
||||||
|
token = None
|
||||||
|
if generate_token:
|
||||||
token = self.auth_handler().generate_access_token(user_id)
|
token = self.auth_handler().generate_access_token(user_id)
|
||||||
yield self.store.register(
|
yield self.store.register(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
token=token,
|
token=token,
|
||||||
password_hash=password_hash
|
password_hash=password_hash,
|
||||||
|
was_guest=was_guest,
|
||||||
|
make_guest=make_guest,
|
||||||
)
|
)
|
||||||
|
|
||||||
yield self.distributor.fire("registered_user", user)
|
yield registered_user(self.distributor, user)
|
||||||
else:
|
else:
|
||||||
# autogen a random user ID
|
# autogen a sequential user ID
|
||||||
attempts = 0
|
attempts = 0
|
||||||
user_id = None
|
|
||||||
token = None
|
token = None
|
||||||
while not user_id and not token:
|
user = None
|
||||||
try:
|
while not user:
|
||||||
localpart = self._generate_user_id()
|
localpart = yield self._generate_user_id(attempts > 0)
|
||||||
user = UserID(localpart, self.hs.hostname)
|
user = UserID(localpart, self.hs.hostname)
|
||||||
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)
|
||||||
|
if generate_token:
|
||||||
token = self.auth_handler().generate_access_token(user_id)
|
token = self.auth_handler().generate_access_token(user_id)
|
||||||
|
try:
|
||||||
yield self.store.register(
|
yield self.store.register(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
token=token,
|
token=token,
|
||||||
password_hash=password_hash)
|
password_hash=password_hash,
|
||||||
|
make_guest=make_guest
|
||||||
self.distributor.fire("registered_user", user)
|
)
|
||||||
except SynapseError:
|
except SynapseError:
|
||||||
# if user id is taken, just generate another
|
# if user id is taken, just generate another
|
||||||
user_id = None
|
user_id = None
|
||||||
token = None
|
token = None
|
||||||
attempts += 1
|
attempts += 1
|
||||||
if attempts > 5:
|
yield registered_user(self.distributor, user)
|
||||||
raise RegistrationError(
|
|
||||||
500, "Cannot generate user ID.")
|
|
||||||
|
|
||||||
# create a default avatar for the user
|
# We used to generate default identicons here, but nowadays
|
||||||
# XXX: ideally clients would explicitly specify one, but given they don't
|
# we want clients to generate their own as part of their branding
|
||||||
# and we want consistent and pretty identicons for random users, we'll
|
# rather than there being consistent matrix-wide ones, so we don't.
|
||||||
# do it here.
|
|
||||||
try:
|
|
||||||
auth_user = UserID.from_string(user_id)
|
|
||||||
media_repository = self.hs.get_resource_for_media_repository()
|
|
||||||
identicon_resource = media_repository.getChildWithDefault("identicon", None)
|
|
||||||
upload_resource = media_repository.getChildWithDefault("upload", None)
|
|
||||||
identicon_bytes = identicon_resource.generate_identicon(user_id, 320, 320)
|
|
||||||
content_uri = yield upload_resource.create_content(
|
|
||||||
"image/png", None, identicon_bytes, len(identicon_bytes), auth_user
|
|
||||||
)
|
|
||||||
profile_handler = self.hs.get_handlers().profile_handler
|
|
||||||
profile_handler.set_avatar_url(
|
|
||||||
auth_user, auth_user, ("%s#auto" % (content_uri,))
|
|
||||||
)
|
|
||||||
except NotImplementedError:
|
|
||||||
pass # make tests pass without messing around creating default avatars
|
|
||||||
|
|
||||||
defer.returnValue((user_id, token))
|
defer.returnValue((user_id, token))
|
||||||
|
|
||||||
@@ -165,7 +186,7 @@ class RegistrationHandler(BaseHandler):
|
|||||||
token=token,
|
token=token,
|
||||||
password_hash=""
|
password_hash=""
|
||||||
)
|
)
|
||||||
self.distributor.fire("registered_user", user)
|
yield registered_user(self.distributor, user)
|
||||||
defer.returnValue((user_id, token))
|
defer.returnValue((user_id, token))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@@ -213,7 +234,7 @@ class RegistrationHandler(BaseHandler):
|
|||||||
token=token,
|
token=token,
|
||||||
password_hash=None
|
password_hash=None
|
||||||
)
|
)
|
||||||
yield self.distributor.fire("registered_user", user)
|
yield registered_user(self.distributor, user)
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
yield self.store.add_access_token_to_user(user_id, token)
|
yield self.store.add_access_token_to_user(user_id, token)
|
||||||
# Ignore Registration errors
|
# Ignore Registration errors
|
||||||
@@ -271,8 +292,16 @@ class RegistrationHandler(BaseHandler):
|
|||||||
errcode=Codes.EXCLUSIVE
|
errcode=Codes.EXCLUSIVE
|
||||||
)
|
)
|
||||||
|
|
||||||
def _generate_user_id(self):
|
@defer.inlineCallbacks
|
||||||
return "-" + stringutils.random_string(18)
|
def _generate_user_id(self, reseed=False):
|
||||||
|
if reseed or self._next_generated_user_id is None:
|
||||||
|
self._next_generated_user_id = (
|
||||||
|
yield self.store.find_next_generated_user_id_localpart()
|
||||||
|
)
|
||||||
|
|
||||||
|
id = self._next_generated_user_id
|
||||||
|
self._next_generated_user_id += 1
|
||||||
|
defer.returnValue(str(id))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _validate_captcha(self, ip_addr, private_key, challenge, response):
|
def _validate_captcha(self, ip_addr, private_key, challenge, response):
|
||||||
@@ -300,10 +329,7 @@ class RegistrationHandler(BaseHandler):
|
|||||||
"""
|
"""
|
||||||
Used only by c/s api v1
|
Used only by c/s api v1
|
||||||
"""
|
"""
|
||||||
# TODO: get this from the homeserver rather than creating a new one for
|
data = yield self.captcha_client.post_urlencoded_get_raw(
|
||||||
# each request
|
|
||||||
client = CaptchaServerHttpClient(self.hs)
|
|
||||||
data = yield client.post_urlencoded_get_raw(
|
|
||||||
"http://www.google.com:80/recaptcha/api/verify",
|
"http://www.google.com:80/recaptcha/api/verify",
|
||||||
args={
|
args={
|
||||||
'privatekey': private_key,
|
'privatekey': private_key,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014 - 2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -18,20 +18,47 @@ 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, RoomStreamToken
|
||||||
from synapse.api.constants import (
|
from synapse.api.constants import (
|
||||||
EventTypes, Membership, JoinRules, RoomCreationPreset,
|
EventTypes, Membership, JoinRules, RoomCreationPreset,
|
||||||
)
|
)
|
||||||
from synapse.api.errors import StoreError, SynapseError
|
from synapse.api.errors import AuthError, StoreError, SynapseError, Codes
|
||||||
from synapse.util import stringutils, unwrapFirstError
|
from synapse.util import stringutils, unwrapFirstError
|
||||||
from synapse.util.async import run_on_reactor
|
from synapse.util.async import run_on_reactor
|
||||||
|
from synapse.util.logcontext import preserve_context_over_fn
|
||||||
|
|
||||||
|
from signedjson.sign import verify_signed_json
|
||||||
|
from signedjson.key import decode_verify_key_bytes
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from unpaddedbase64 import decode_base64
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
import string
|
import string
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
id_server_scheme = "https://"
|
||||||
|
|
||||||
|
|
||||||
|
def collect_presencelike_data(distributor, user, content):
|
||||||
|
return distributor.fire("collect_presencelike_data", user, content)
|
||||||
|
|
||||||
|
|
||||||
|
def user_left_room(distributor, user, room_id):
|
||||||
|
return preserve_context_over_fn(
|
||||||
|
distributor.fire,
|
||||||
|
"user_left_room", user=user, room_id=room_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def user_joined_room(distributor, user, room_id):
|
||||||
|
return preserve_context_over_fn(
|
||||||
|
distributor.fire,
|
||||||
|
"user_joined_room", user=user, room_id=room_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RoomCreationHandler(BaseHandler):
|
class RoomCreationHandler(BaseHandler):
|
||||||
|
|
||||||
@@ -41,6 +68,11 @@ class RoomCreationHandler(BaseHandler):
|
|||||||
"history_visibility": "shared",
|
"history_visibility": "shared",
|
||||||
"original_invitees_have_ops": False,
|
"original_invitees_have_ops": False,
|
||||||
},
|
},
|
||||||
|
RoomCreationPreset.TRUSTED_PRIVATE_CHAT: {
|
||||||
|
"join_rules": JoinRules.INVITE,
|
||||||
|
"history_visibility": "shared",
|
||||||
|
"original_invitees_have_ops": True,
|
||||||
|
},
|
||||||
RoomCreationPreset.PUBLIC_CHAT: {
|
RoomCreationPreset.PUBLIC_CHAT: {
|
||||||
"join_rules": JoinRules.PUBLIC,
|
"join_rules": JoinRules.PUBLIC,
|
||||||
"history_visibility": "shared",
|
"history_visibility": "shared",
|
||||||
@@ -90,6 +122,8 @@ class RoomCreationHandler(BaseHandler):
|
|||||||
except:
|
except:
|
||||||
raise SynapseError(400, "Invalid user_id: %s" % (i,))
|
raise SynapseError(400, "Invalid user_id: %s" % (i,))
|
||||||
|
|
||||||
|
invite_3pid_list = config.get("invite_3pid", [])
|
||||||
|
|
||||||
is_public = config.get("visibility", None) == "public"
|
is_public = config.get("visibility", None) == "public"
|
||||||
|
|
||||||
if room_id:
|
if room_id:
|
||||||
@@ -149,12 +183,16 @@ class RoomCreationHandler(BaseHandler):
|
|||||||
for val in raw_initial_state:
|
for val in raw_initial_state:
|
||||||
initial_state[(val["type"], val.get("state_key", ""))] = val["content"]
|
initial_state[(val["type"], val.get("state_key", ""))] = val["content"]
|
||||||
|
|
||||||
|
creation_content = config.get("creation_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,
|
user, room_id,
|
||||||
preset_config=preset_config,
|
preset_config=preset_config,
|
||||||
invite_list=invite_list,
|
invite_list=invite_list,
|
||||||
initial_state=initial_state,
|
initial_state=initial_state,
|
||||||
|
creation_content=creation_content,
|
||||||
|
room_alias=room_alias,
|
||||||
)
|
)
|
||||||
|
|
||||||
msg_handler = self.hs.get_handlers().message_handler
|
msg_handler = self.hs.get_handlers().message_handler
|
||||||
@@ -191,6 +229,20 @@ class RoomCreationHandler(BaseHandler):
|
|||||||
"content": {"membership": Membership.INVITE},
|
"content": {"membership": Membership.INVITE},
|
||||||
}, ratelimit=False)
|
}, ratelimit=False)
|
||||||
|
|
||||||
|
for invite_3pid in invite_3pid_list:
|
||||||
|
id_server = invite_3pid["id_server"]
|
||||||
|
address = invite_3pid["address"]
|
||||||
|
medium = invite_3pid["medium"]
|
||||||
|
yield self.hs.get_handlers().room_member_handler.do_3pid_invite(
|
||||||
|
room_id,
|
||||||
|
user,
|
||||||
|
medium,
|
||||||
|
address,
|
||||||
|
id_server,
|
||||||
|
token_id=None,
|
||||||
|
txn_id=None,
|
||||||
|
)
|
||||||
|
|
||||||
result = {"room_id": room_id}
|
result = {"room_id": room_id}
|
||||||
|
|
||||||
if room_alias:
|
if room_alias:
|
||||||
@@ -202,7 +254,8 @@ class RoomCreationHandler(BaseHandler):
|
|||||||
defer.returnValue(result)
|
defer.returnValue(result)
|
||||||
|
|
||||||
def _create_events_for_new_room(self, creator, room_id, preset_config,
|
def _create_events_for_new_room(self, creator, room_id, preset_config,
|
||||||
invite_list, initial_state):
|
invite_list, initial_state, creation_content,
|
||||||
|
room_alias):
|
||||||
config = RoomCreationHandler.PRESETS_DICT[preset_config]
|
config = RoomCreationHandler.PRESETS_DICT[preset_config]
|
||||||
|
|
||||||
creator_id = creator.to_string()
|
creator_id = creator.to_string()
|
||||||
@@ -224,9 +277,10 @@ class RoomCreationHandler(BaseHandler):
|
|||||||
|
|
||||||
return e
|
return e
|
||||||
|
|
||||||
|
creation_content.update({"creator": creator.to_string()})
|
||||||
creation_event = create(
|
creation_event = create(
|
||||||
etype=EventTypes.Create,
|
etype=EventTypes.Create,
|
||||||
content={"creator": creator.to_string()},
|
content=creation_content,
|
||||||
)
|
)
|
||||||
|
|
||||||
join_event = create(
|
join_event = create(
|
||||||
@@ -271,6 +325,14 @@ class RoomCreationHandler(BaseHandler):
|
|||||||
|
|
||||||
returned_events.append(power_levels_event)
|
returned_events.append(power_levels_event)
|
||||||
|
|
||||||
|
if room_alias and (EventTypes.CanonicalAlias, '') not in initial_state:
|
||||||
|
room_alias_event = create(
|
||||||
|
etype=EventTypes.CanonicalAlias,
|
||||||
|
content={"alias": room_alias.to_string()},
|
||||||
|
)
|
||||||
|
|
||||||
|
returned_events.append(room_alias_event)
|
||||||
|
|
||||||
if (EventTypes.JoinRules, '') not in initial_state:
|
if (EventTypes.JoinRules, '') not in initial_state:
|
||||||
join_rules_event = create(
|
join_rules_event = create(
|
||||||
etype=EventTypes.JoinRules,
|
etype=EventTypes.JoinRules,
|
||||||
@@ -342,7 +404,58 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
remotedomains.add(member.domain)
|
remotedomains.add(member.domain)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def change_membership(self, event, context, do_auth=True):
|
def update_membership(self, requester, target, room_id, action, txn_id=None):
|
||||||
|
effective_membership_state = action
|
||||||
|
if action in ["kick", "unban"]:
|
||||||
|
effective_membership_state = "leave"
|
||||||
|
elif action == "forget":
|
||||||
|
effective_membership_state = "leave"
|
||||||
|
|
||||||
|
msg_handler = self.hs.get_handlers().message_handler
|
||||||
|
|
||||||
|
content = {"membership": unicode(effective_membership_state)}
|
||||||
|
if requester.is_guest:
|
||||||
|
content["kind"] = "guest"
|
||||||
|
|
||||||
|
event, context = yield msg_handler.create_event(
|
||||||
|
{
|
||||||
|
"type": EventTypes.Member,
|
||||||
|
"content": content,
|
||||||
|
"room_id": room_id,
|
||||||
|
"sender": requester.user.to_string(),
|
||||||
|
"state_key": target.to_string(),
|
||||||
|
},
|
||||||
|
token_id=requester.access_token_id,
|
||||||
|
txn_id=txn_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
old_state = context.current_state.get((EventTypes.Member, event.state_key))
|
||||||
|
old_membership = old_state.content.get("membership") if old_state else None
|
||||||
|
if action == "unban" and old_membership != "ban":
|
||||||
|
raise SynapseError(
|
||||||
|
403,
|
||||||
|
"Cannot unban user who was not banned (membership=%s)" % old_membership,
|
||||||
|
errcode=Codes.BAD_STATE
|
||||||
|
)
|
||||||
|
if old_membership == "ban" and action != "unban":
|
||||||
|
raise SynapseError(
|
||||||
|
403,
|
||||||
|
"Cannot %s user who was is banned" % (action,),
|
||||||
|
errcode=Codes.BAD_STATE
|
||||||
|
)
|
||||||
|
|
||||||
|
yield msg_handler.send_event(
|
||||||
|
event,
|
||||||
|
context,
|
||||||
|
ratelimit=True,
|
||||||
|
is_guest=requester.is_guest
|
||||||
|
)
|
||||||
|
|
||||||
|
if action == "forget":
|
||||||
|
yield self.forget(requester.user, room_id)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def send_membership_event(self, event, context, is_guest=False):
|
||||||
""" Change the membership status of a user in a room.
|
""" Change the membership status of a user in a room.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -363,9 +476,38 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
# if this HS is not currently in the room, i.e. we have to do the
|
# if this HS is not currently in the room, i.e. we have to do the
|
||||||
# invite/join dance.
|
# invite/join dance.
|
||||||
if event.membership == Membership.JOIN:
|
if event.membership == Membership.JOIN:
|
||||||
yield self._do_join(event, context, do_auth=do_auth)
|
if is_guest:
|
||||||
|
guest_access = context.current_state.get(
|
||||||
|
(EventTypes.GuestAccess, ""),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
is_guest_access_allowed = (
|
||||||
|
guest_access
|
||||||
|
and guest_access.content
|
||||||
|
and "guest_access" in guest_access.content
|
||||||
|
and guest_access.content["guest_access"] == "can_join"
|
||||||
|
)
|
||||||
|
if not is_guest_access_allowed:
|
||||||
|
raise AuthError(403, "Guest access not allowed")
|
||||||
|
|
||||||
|
yield self._do_join(event, context)
|
||||||
else:
|
else:
|
||||||
# This is not a JOIN, so we can handle it normally.
|
if event.membership == Membership.LEAVE:
|
||||||
|
is_host_in_room = yield self.is_host_in_room(room_id, context)
|
||||||
|
if not is_host_in_room:
|
||||||
|
# Rejecting an invite, rather than leaving a joined room
|
||||||
|
handler = self.hs.get_handlers().federation_handler
|
||||||
|
inviter = yield self.get_inviter(event)
|
||||||
|
if not inviter:
|
||||||
|
# return the same error as join_room_alias does
|
||||||
|
raise SynapseError(404, "No known servers")
|
||||||
|
yield handler.do_remotely_reject_invite(
|
||||||
|
[inviter.domain],
|
||||||
|
room_id,
|
||||||
|
event.user_id
|
||||||
|
)
|
||||||
|
defer.returnValue({"room_id": room_id})
|
||||||
|
return
|
||||||
|
|
||||||
# FIXME: This isn't idempotency.
|
# FIXME: This isn't idempotency.
|
||||||
if prev_state and prev_state.membership == event.membership:
|
if prev_state and prev_state.membership == event.membership:
|
||||||
@@ -375,21 +517,17 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
|
|
||||||
yield self._do_local_membership_update(
|
yield self._do_local_membership_update(
|
||||||
event,
|
event,
|
||||||
membership=event.content["membership"],
|
|
||||||
context=context,
|
context=context,
|
||||||
do_auth=do_auth,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if prev_state and prev_state.membership == Membership.JOIN:
|
if prev_state and prev_state.membership == Membership.JOIN:
|
||||||
user = UserID.from_string(event.user_id)
|
user = UserID.from_string(event.user_id)
|
||||||
self.distributor.fire(
|
user_left_room(self.distributor, user, event.room_id)
|
||||||
"user_left_room", user=user, room_id=event.room_id
|
|
||||||
)
|
|
||||||
|
|
||||||
defer.returnValue({"room_id": room_id})
|
defer.returnValue({"room_id": room_id})
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def join_room_alias(self, joinee, room_alias, do_auth=True, content={}):
|
def join_room_alias(self, joinee, room_alias, content={}):
|
||||||
directory_handler = self.hs.get_handlers().directory_handler
|
directory_handler = self.hs.get_handlers().directory_handler
|
||||||
mapping = yield directory_handler.get_association(room_alias)
|
mapping = yield directory_handler.get_association(room_alias)
|
||||||
|
|
||||||
@@ -402,9 +540,7 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
raise SynapseError(404, "No known servers")
|
raise SynapseError(404, "No known servers")
|
||||||
|
|
||||||
# If event doesn't include a display name, add one.
|
# If event doesn't include a display name, add one.
|
||||||
yield self.distributor.fire(
|
yield collect_presencelike_data(self.distributor, joinee, content)
|
||||||
"collect_presencelike_data", joinee, content
|
|
||||||
)
|
|
||||||
|
|
||||||
content.update({"membership": Membership.JOIN})
|
content.update({"membership": Membership.JOIN})
|
||||||
builder = self.event_builder_factory.new({
|
builder = self.event_builder_factory.new({
|
||||||
@@ -417,14 +553,12 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
})
|
})
|
||||||
event, context = yield self._create_new_client_event(builder)
|
event, context = yield self._create_new_client_event(builder)
|
||||||
|
|
||||||
yield self._do_join(event, context, room_hosts=hosts, do_auth=True)
|
yield self._do_join(event, context, room_hosts=hosts)
|
||||||
|
|
||||||
defer.returnValue({"room_id": room_id})
|
defer.returnValue({"room_id": room_id})
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _do_join(self, event, context, room_hosts=None, do_auth=True):
|
def _do_join(self, event, context, room_hosts=None):
|
||||||
joinee = UserID.from_string(event.state_key)
|
|
||||||
# room_id = RoomID.from_string(event.room_id, self.hs)
|
|
||||||
room_id = event.room_id
|
room_id = event.room_id
|
||||||
|
|
||||||
# XXX: We don't do an auth check if we are doing an invite
|
# XXX: We don't do an auth check if we are doing an invite
|
||||||
@@ -432,8 +566,65 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
# that we are allowed to join when we decide whether or not we
|
# that we are allowed to join when we decide whether or not we
|
||||||
# need to do the invite/join dance.
|
# need to do the invite/join dance.
|
||||||
|
|
||||||
|
is_host_in_room = yield self.is_host_in_room(room_id, context)
|
||||||
|
if is_host_in_room:
|
||||||
|
should_do_dance = False
|
||||||
|
elif room_hosts: # TODO: Shouldn't this be remote_room_host?
|
||||||
|
should_do_dance = True
|
||||||
|
else:
|
||||||
|
inviter = yield self.get_inviter(event)
|
||||||
|
if not inviter:
|
||||||
|
# return the same error as join_room_alias does
|
||||||
|
raise SynapseError(404, "No known servers")
|
||||||
|
should_do_dance = not self.hs.is_mine(inviter)
|
||||||
|
room_hosts = [inviter.domain]
|
||||||
|
|
||||||
|
if should_do_dance:
|
||||||
|
handler = self.hs.get_handlers().federation_handler
|
||||||
|
yield handler.do_invite_join(
|
||||||
|
room_hosts,
|
||||||
|
room_id,
|
||||||
|
event.user_id,
|
||||||
|
event.content,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.debug("Doing normal join")
|
||||||
|
|
||||||
|
yield self._do_local_membership_update(
|
||||||
|
event,
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
|
|
||||||
|
prev_state = context.current_state.get((event.type, event.state_key))
|
||||||
|
if not prev_state or prev_state.membership != Membership.JOIN:
|
||||||
|
# Only fire user_joined_room if the user has acutally joined the
|
||||||
|
# room. Don't bother if the user is just changing their profile
|
||||||
|
# info.
|
||||||
|
user = UserID.from_string(event.user_id)
|
||||||
|
yield user_joined_room(self.distributor, user, room_id)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_inviter(self, event):
|
||||||
|
# TODO(markjh): get prev_state from snapshot
|
||||||
|
prev_state = yield self.store.get_room_member(
|
||||||
|
event.user_id, event.room_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if prev_state and prev_state.membership == Membership.INVITE:
|
||||||
|
defer.returnValue(UserID.from_string(prev_state.user_id))
|
||||||
|
return
|
||||||
|
elif "third_party_invite" in event.content:
|
||||||
|
if "sender" in event.content["third_party_invite"]:
|
||||||
|
inviter = UserID.from_string(
|
||||||
|
event.content["third_party_invite"]["sender"]
|
||||||
|
)
|
||||||
|
defer.returnValue(inviter)
|
||||||
|
defer.returnValue(None)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def is_host_in_room(self, room_id, context):
|
||||||
is_host_in_room = yield self.auth.check_host_in_room(
|
is_host_in_room = yield self.auth.check_host_in_room(
|
||||||
event.room_id,
|
room_id,
|
||||||
self.hs.hostname
|
self.hs.hostname
|
||||||
)
|
)
|
||||||
if not is_host_in_room:
|
if not is_host_in_room:
|
||||||
@@ -448,49 +639,7 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
create_event = context.current_state.get(("m.room.create", ""))
|
create_event = context.current_state.get(("m.room.create", ""))
|
||||||
if create_event:
|
if create_event:
|
||||||
is_host_in_room = True
|
is_host_in_room = True
|
||||||
|
defer.returnValue(is_host_in_room)
|
||||||
if is_host_in_room:
|
|
||||||
should_do_dance = False
|
|
||||||
elif room_hosts: # TODO: Shouldn't this be remote_room_host?
|
|
||||||
should_do_dance = True
|
|
||||||
else:
|
|
||||||
# TODO(markjh): get prev_state from snapshot
|
|
||||||
prev_state = yield self.store.get_room_member(
|
|
||||||
joinee.to_string(), room_id
|
|
||||||
)
|
|
||||||
|
|
||||||
if prev_state and prev_state.membership == Membership.INVITE:
|
|
||||||
inviter = UserID.from_string(prev_state.user_id)
|
|
||||||
|
|
||||||
should_do_dance = not self.hs.is_mine(inviter)
|
|
||||||
room_hosts = [inviter.domain]
|
|
||||||
else:
|
|
||||||
# return the same error as join_room_alias does
|
|
||||||
raise SynapseError(404, "No known servers")
|
|
||||||
|
|
||||||
if should_do_dance:
|
|
||||||
handler = self.hs.get_handlers().federation_handler
|
|
||||||
yield handler.do_invite_join(
|
|
||||||
room_hosts,
|
|
||||||
room_id,
|
|
||||||
event.user_id,
|
|
||||||
event.content, # FIXME To get a non-frozen dict
|
|
||||||
context
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.debug("Doing normal join")
|
|
||||||
|
|
||||||
yield self._do_local_membership_update(
|
|
||||||
event,
|
|
||||||
membership=event.content["membership"],
|
|
||||||
context=context,
|
|
||||||
do_auth=do_auth,
|
|
||||||
)
|
|
||||||
|
|
||||||
user = UserID.from_string(event.user_id)
|
|
||||||
yield self.distributor.fire(
|
|
||||||
"user_joined_room", user=user, room_id=room_id
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_joined_rooms_for_user(self, user):
|
def get_joined_rooms_for_user(self, user):
|
||||||
@@ -508,8 +657,7 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
defer.returnValue(room_ids)
|
defer.returnValue(room_ids)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _do_local_membership_update(self, event, membership, context,
|
def _do_local_membership_update(self, event, context):
|
||||||
do_auth):
|
|
||||||
yield run_on_reactor()
|
yield run_on_reactor()
|
||||||
|
|
||||||
target_user = UserID.from_string(event.state_key)
|
target_user = UserID.from_string(event.state_key)
|
||||||
@@ -518,28 +666,357 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
event,
|
event,
|
||||||
context,
|
context,
|
||||||
extra_users=[target_user],
|
extra_users=[target_user],
|
||||||
suppress_auth=(not do_auth),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def do_3pid_invite(
|
||||||
|
self,
|
||||||
|
room_id,
|
||||||
|
inviter,
|
||||||
|
medium,
|
||||||
|
address,
|
||||||
|
id_server,
|
||||||
|
token_id,
|
||||||
|
txn_id
|
||||||
|
):
|
||||||
|
invitee = yield self._lookup_3pid(
|
||||||
|
id_server, medium, address
|
||||||
|
)
|
||||||
|
|
||||||
|
if invitee:
|
||||||
|
# make sure it looks like a user ID; it'll throw if it's invalid.
|
||||||
|
UserID.from_string(invitee)
|
||||||
|
yield self.hs.get_handlers().message_handler.create_and_send_event(
|
||||||
|
{
|
||||||
|
"type": EventTypes.Member,
|
||||||
|
"content": {
|
||||||
|
"membership": unicode("invite")
|
||||||
|
},
|
||||||
|
"room_id": room_id,
|
||||||
|
"sender": inviter.to_string(),
|
||||||
|
"state_key": invitee,
|
||||||
|
},
|
||||||
|
token_id=token_id,
|
||||||
|
txn_id=txn_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
yield self._make_and_store_3pid_invite(
|
||||||
|
id_server,
|
||||||
|
medium,
|
||||||
|
address,
|
||||||
|
room_id,
|
||||||
|
inviter,
|
||||||
|
token_id,
|
||||||
|
txn_id=txn_id
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _lookup_3pid(self, id_server, medium, address):
|
||||||
|
"""Looks up a 3pid in the passed identity server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id_server (str): The server name (including port, if required)
|
||||||
|
of the identity server to use.
|
||||||
|
medium (str): The type of the third party identifier (e.g. "email").
|
||||||
|
address (str): The third party identifier (e.g. "foo@example.com").
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(str) the matrix ID of the 3pid, or None if it is not recognized.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = yield self.hs.get_simple_http_client().get_json(
|
||||||
|
"%s%s/_matrix/identity/api/v1/lookup" % (id_server_scheme, id_server,),
|
||||||
|
{
|
||||||
|
"medium": medium,
|
||||||
|
"address": address,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if "mxid" in data:
|
||||||
|
if "signatures" not in data:
|
||||||
|
raise AuthError(401, "No signatures on 3pid binding")
|
||||||
|
self.verify_any_signature(data, id_server)
|
||||||
|
defer.returnValue(data["mxid"])
|
||||||
|
|
||||||
|
except IOError as e:
|
||||||
|
logger.warn("Error from identity server lookup: %s" % (e,))
|
||||||
|
defer.returnValue(None)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def verify_any_signature(self, data, server_hostname):
|
||||||
|
if server_hostname not in data["signatures"]:
|
||||||
|
raise AuthError(401, "No signature from server %s" % (server_hostname,))
|
||||||
|
for key_name, signature in data["signatures"][server_hostname].items():
|
||||||
|
key_data = yield self.hs.get_simple_http_client().get_json(
|
||||||
|
"%s%s/_matrix/identity/api/v1/pubkey/%s" %
|
||||||
|
(id_server_scheme, server_hostname, key_name,),
|
||||||
|
)
|
||||||
|
if "public_key" not in key_data:
|
||||||
|
raise AuthError(401, "No public key named %s from %s" %
|
||||||
|
(key_name, server_hostname,))
|
||||||
|
verify_signed_json(
|
||||||
|
data,
|
||||||
|
server_hostname,
|
||||||
|
decode_verify_key_bytes(key_name, decode_base64(key_data["public_key"]))
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _make_and_store_3pid_invite(
|
||||||
|
self,
|
||||||
|
id_server,
|
||||||
|
medium,
|
||||||
|
address,
|
||||||
|
room_id,
|
||||||
|
user,
|
||||||
|
token_id,
|
||||||
|
txn_id
|
||||||
|
):
|
||||||
|
room_state = yield self.hs.get_state_handler().get_current_state(room_id)
|
||||||
|
|
||||||
|
inviter_display_name = ""
|
||||||
|
inviter_avatar_url = ""
|
||||||
|
member_event = room_state.get((EventTypes.Member, user.to_string()))
|
||||||
|
if member_event:
|
||||||
|
inviter_display_name = member_event.content.get("displayname", "")
|
||||||
|
inviter_avatar_url = member_event.content.get("avatar_url", "")
|
||||||
|
|
||||||
|
canonical_room_alias = ""
|
||||||
|
canonical_alias_event = room_state.get((EventTypes.CanonicalAlias, ""))
|
||||||
|
if canonical_alias_event:
|
||||||
|
canonical_room_alias = canonical_alias_event.content.get("alias", "")
|
||||||
|
|
||||||
|
room_name = ""
|
||||||
|
room_name_event = room_state.get((EventTypes.Name, ""))
|
||||||
|
if room_name_event:
|
||||||
|
room_name = room_name_event.content.get("name", "")
|
||||||
|
|
||||||
|
room_join_rules = ""
|
||||||
|
join_rules_event = room_state.get((EventTypes.JoinRules, ""))
|
||||||
|
if join_rules_event:
|
||||||
|
room_join_rules = join_rules_event.content.get("join_rule", "")
|
||||||
|
|
||||||
|
room_avatar_url = ""
|
||||||
|
room_avatar_event = room_state.get((EventTypes.RoomAvatar, ""))
|
||||||
|
if room_avatar_event:
|
||||||
|
room_avatar_url = room_avatar_event.content.get("url", "")
|
||||||
|
|
||||||
|
token, public_key, key_validity_url, display_name = (
|
||||||
|
yield self._ask_id_server_for_third_party_invite(
|
||||||
|
id_server=id_server,
|
||||||
|
medium=medium,
|
||||||
|
address=address,
|
||||||
|
room_id=room_id,
|
||||||
|
inviter_user_id=user.to_string(),
|
||||||
|
room_alias=canonical_room_alias,
|
||||||
|
room_avatar_url=room_avatar_url,
|
||||||
|
room_join_rules=room_join_rules,
|
||||||
|
room_name=room_name,
|
||||||
|
inviter_display_name=inviter_display_name,
|
||||||
|
inviter_avatar_url=inviter_avatar_url
|
||||||
|
)
|
||||||
|
)
|
||||||
|
msg_handler = self.hs.get_handlers().message_handler
|
||||||
|
yield msg_handler.create_and_send_event(
|
||||||
|
{
|
||||||
|
"type": EventTypes.ThirdPartyInvite,
|
||||||
|
"content": {
|
||||||
|
"display_name": display_name,
|
||||||
|
"key_validity_url": key_validity_url,
|
||||||
|
"public_key": public_key,
|
||||||
|
},
|
||||||
|
"room_id": room_id,
|
||||||
|
"sender": user.to_string(),
|
||||||
|
"state_key": token,
|
||||||
|
},
|
||||||
|
token_id=token_id,
|
||||||
|
txn_id=txn_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _ask_id_server_for_third_party_invite(
|
||||||
|
self,
|
||||||
|
id_server,
|
||||||
|
medium,
|
||||||
|
address,
|
||||||
|
room_id,
|
||||||
|
inviter_user_id,
|
||||||
|
room_alias,
|
||||||
|
room_avatar_url,
|
||||||
|
room_join_rules,
|
||||||
|
room_name,
|
||||||
|
inviter_display_name,
|
||||||
|
inviter_avatar_url
|
||||||
|
):
|
||||||
|
is_url = "%s%s/_matrix/identity/api/v1/store-invite" % (
|
||||||
|
id_server_scheme, id_server,
|
||||||
|
)
|
||||||
|
data = yield self.hs.get_simple_http_client().post_urlencoded_get_json(
|
||||||
|
is_url,
|
||||||
|
{
|
||||||
|
"medium": medium,
|
||||||
|
"address": address,
|
||||||
|
"room_id": room_id,
|
||||||
|
"room_alias": room_alias,
|
||||||
|
"room_avatar_url": room_avatar_url,
|
||||||
|
"room_join_rules": room_join_rules,
|
||||||
|
"room_name": room_name,
|
||||||
|
"sender": inviter_user_id,
|
||||||
|
"sender_display_name": inviter_display_name,
|
||||||
|
"sender_avatar_url": inviter_avatar_url,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# TODO: Check for success
|
||||||
|
token = data["token"]
|
||||||
|
public_key = data["public_key"]
|
||||||
|
display_name = data["display_name"]
|
||||||
|
key_validity_url = "%s%s/_matrix/identity/api/v1/pubkey/isvalid" % (
|
||||||
|
id_server_scheme, id_server,
|
||||||
|
)
|
||||||
|
defer.returnValue((token, public_key, key_validity_url, display_name))
|
||||||
|
|
||||||
|
def forget(self, user, room_id):
|
||||||
|
return self.store.forget(user.to_string(), room_id)
|
||||||
|
|
||||||
|
|
||||||
class RoomListHandler(BaseHandler):
|
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)
|
room_ids = yield self.store.get_public_room_ids()
|
||||||
results = yield defer.gatherResults(
|
|
||||||
[
|
|
||||||
self.store.get_users_in_room(room["room_id"])
|
|
||||||
for room in chunk
|
|
||||||
],
|
|
||||||
consumeErrors=True,
|
|
||||||
).addErrback(unwrapFirstError)
|
|
||||||
|
|
||||||
for i, room in enumerate(chunk):
|
@defer.inlineCallbacks
|
||||||
room["num_joined_members"] = len(results[i])
|
def handle_room(room_id):
|
||||||
|
aliases = yield self.store.get_aliases_for_room(room_id)
|
||||||
|
if not aliases:
|
||||||
|
defer.returnValue(None)
|
||||||
|
|
||||||
|
state = yield self.state_handler.get_current_state(room_id)
|
||||||
|
|
||||||
|
result = {"aliases": aliases, "room_id": room_id}
|
||||||
|
|
||||||
|
name_event = state.get((EventTypes.Name, ""), None)
|
||||||
|
if name_event:
|
||||||
|
name = name_event.content.get("name", None)
|
||||||
|
if name:
|
||||||
|
result["name"] = name
|
||||||
|
|
||||||
|
topic_event = state.get((EventTypes.Topic, ""), None)
|
||||||
|
if topic_event:
|
||||||
|
topic = topic_event.content.get("topic", None)
|
||||||
|
if topic:
|
||||||
|
result["topic"] = topic
|
||||||
|
|
||||||
|
canonical_event = state.get((EventTypes.CanonicalAlias, ""), None)
|
||||||
|
if canonical_event:
|
||||||
|
canonical_alias = canonical_event.content.get("alias", None)
|
||||||
|
if canonical_alias:
|
||||||
|
result["canonical_alias"] = canonical_alias
|
||||||
|
|
||||||
|
visibility_event = state.get((EventTypes.RoomHistoryVisibility, ""), None)
|
||||||
|
visibility = None
|
||||||
|
if visibility_event:
|
||||||
|
visibility = visibility_event.content.get("history_visibility", None)
|
||||||
|
result["world_readable"] = visibility == "world_readable"
|
||||||
|
|
||||||
|
guest_event = state.get((EventTypes.GuestAccess, ""), None)
|
||||||
|
guest = None
|
||||||
|
if guest_event:
|
||||||
|
guest = guest_event.content.get("guest_access", None)
|
||||||
|
result["guest_can_join"] = guest == "can_join"
|
||||||
|
|
||||||
|
avatar_event = state.get(("m.room.avatar", ""), None)
|
||||||
|
if avatar_event:
|
||||||
|
avatar_url = avatar_event.content.get("url", None)
|
||||||
|
if avatar_url:
|
||||||
|
result["avatar_url"] = avatar_url
|
||||||
|
|
||||||
|
result["num_joined_members"] = sum(
|
||||||
|
1 for (event_type, _), ev in state.items()
|
||||||
|
if event_type == EventTypes.Member and ev.membership == Membership.JOIN
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue(result)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for chunk in (room_ids[i:i + 10] for i in xrange(0, len(room_ids), 10)):
|
||||||
|
chunk_result = yield defer.gatherResults([
|
||||||
|
handle_room(room_id)
|
||||||
|
for room_id in chunk
|
||||||
|
], consumeErrors=True).addErrback(unwrapFirstError)
|
||||||
|
result.extend(v for v in chunk_result if v)
|
||||||
|
|
||||||
# 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": result})
|
||||||
|
|
||||||
|
|
||||||
|
class RoomContextHandler(BaseHandler):
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_event_context(self, user, room_id, event_id, limit, is_guest):
|
||||||
|
"""Retrieves events, pagination tokens and state around a given event
|
||||||
|
in a room.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user (UserID)
|
||||||
|
room_id (str)
|
||||||
|
event_id (str)
|
||||||
|
limit (int): The maximum number of events to return in total
|
||||||
|
(excluding state).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict, or None if the event isn't found
|
||||||
|
"""
|
||||||
|
before_limit = math.floor(limit / 2.)
|
||||||
|
after_limit = limit - before_limit
|
||||||
|
|
||||||
|
now_token = yield self.hs.get_event_sources().get_current_token()
|
||||||
|
|
||||||
|
def filter_evts(events):
|
||||||
|
return self._filter_events_for_client(
|
||||||
|
user.to_string(),
|
||||||
|
events,
|
||||||
|
is_peeking=is_guest)
|
||||||
|
|
||||||
|
event = yield self.store.get_event(event_id, get_prev_content=True,
|
||||||
|
allow_none=True)
|
||||||
|
if not event:
|
||||||
|
defer.returnValue(None)
|
||||||
|
return
|
||||||
|
|
||||||
|
filtered = yield(filter_evts([event]))
|
||||||
|
if not filtered:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to access that event."
|
||||||
|
)
|
||||||
|
|
||||||
|
results = yield self.store.get_events_around(
|
||||||
|
room_id, event_id, before_limit, after_limit
|
||||||
|
)
|
||||||
|
|
||||||
|
results["events_before"] = yield filter_evts(results["events_before"])
|
||||||
|
results["events_after"] = yield filter_evts(results["events_after"])
|
||||||
|
results["event"] = event
|
||||||
|
|
||||||
|
if results["events_after"]:
|
||||||
|
last_event_id = results["events_after"][-1].event_id
|
||||||
|
else:
|
||||||
|
last_event_id = event_id
|
||||||
|
|
||||||
|
state = yield self.store.get_state_for_events(
|
||||||
|
[last_event_id], None
|
||||||
|
)
|
||||||
|
results["state"] = state[last_event_id].values()
|
||||||
|
|
||||||
|
results["start"] = now_token.copy_and_replace(
|
||||||
|
"room_key", results["start"]
|
||||||
|
).to_string()
|
||||||
|
|
||||||
|
results["end"] = now_token.copy_and_replace(
|
||||||
|
"room_key", results["end"]
|
||||||
|
).to_string()
|
||||||
|
|
||||||
|
defer.returnValue(results)
|
||||||
|
|
||||||
|
|
||||||
class RoomEventSource(object):
|
class RoomEventSource(object):
|
||||||
@@ -547,11 +1024,23 @@ class RoomEventSource(object):
|
|||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_new_events_for_user(self, user, from_key, limit):
|
def get_new_events(
|
||||||
|
self,
|
||||||
|
user,
|
||||||
|
from_key,
|
||||||
|
limit,
|
||||||
|
room_ids,
|
||||||
|
is_guest,
|
||||||
|
):
|
||||||
# We just ignore the key for now.
|
# We just ignore the key for now.
|
||||||
|
|
||||||
to_key = yield self.get_current_key()
|
to_key = yield self.get_current_key()
|
||||||
|
|
||||||
|
from_token = RoomStreamToken.parse(from_key)
|
||||||
|
if from_token.topological:
|
||||||
|
logger.warn("Stream has topological part!!!! %r", from_key)
|
||||||
|
from_key = "s%s" % (from_token.stream,)
|
||||||
|
|
||||||
app_service = yield self.store.get_app_service_by_user_id(
|
app_service = yield self.store.get_app_service_by_user_id(
|
||||||
user.to_string()
|
user.to_string()
|
||||||
)
|
)
|
||||||
@@ -563,14 +1052,30 @@ class RoomEventSource(object):
|
|||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
events, end_key = yield self.store.get_room_events_stream(
|
room_events = yield self.store.get_membership_changes_for_user(
|
||||||
user_id=user.to_string(),
|
user.to_string(), from_key, to_key
|
||||||
|
)
|
||||||
|
|
||||||
|
room_to_events = yield self.store.get_room_events_stream_for_rooms(
|
||||||
|
room_ids=room_ids,
|
||||||
from_key=from_key,
|
from_key=from_key,
|
||||||
to_key=to_key,
|
to_key=to_key,
|
||||||
room_id=None,
|
limit=limit or 10,
|
||||||
limit=limit,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
events = list(room_events)
|
||||||
|
events.extend(e for evs, _ in room_to_events.values() for e in evs)
|
||||||
|
|
||||||
|
events.sort(key=lambda e: e.internal_metadata.order)
|
||||||
|
|
||||||
|
if limit:
|
||||||
|
events[:] = events[:limit]
|
||||||
|
|
||||||
|
if events:
|
||||||
|
end_key = events[-1].internal_metadata.after
|
||||||
|
else:
|
||||||
|
end_key = to_key
|
||||||
|
|
||||||
defer.returnValue((events, end_key))
|
defer.returnValue((events, end_key))
|
||||||
|
|
||||||
def get_current_key(self, direction='f'):
|
def get_current_key(self, direction='f'):
|
||||||
|
|||||||
391
synapse/handlers/search.py
Normal file
391
synapse/handlers/search.py
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015, 2016 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.constants import Membership, EventTypes
|
||||||
|
from synapse.api.filtering import Filter
|
||||||
|
from synapse.api.errors import SynapseError
|
||||||
|
from synapse.events.utils import serialize_event
|
||||||
|
|
||||||
|
from unpaddedbase64 import decode_base64, encode_base64
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SearchHandler(BaseHandler):
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
super(SearchHandler, self).__init__(hs)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def search(self, user, content, batch=None):
|
||||||
|
"""Performs a full text search for a user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user (UserID)
|
||||||
|
content (dict): Search parameters
|
||||||
|
batch (str): The next_batch parameter. Used for pagination.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict to be returned to the client with results of search
|
||||||
|
"""
|
||||||
|
|
||||||
|
batch_group = None
|
||||||
|
batch_group_key = None
|
||||||
|
batch_token = None
|
||||||
|
if batch:
|
||||||
|
try:
|
||||||
|
b = decode_base64(batch)
|
||||||
|
batch_group, batch_group_key, batch_token = b.split("\n")
|
||||||
|
|
||||||
|
assert batch_group is not None
|
||||||
|
assert batch_group_key is not None
|
||||||
|
assert batch_token is not None
|
||||||
|
except:
|
||||||
|
raise SynapseError(400, "Invalid batch")
|
||||||
|
|
||||||
|
try:
|
||||||
|
room_cat = content["search_categories"]["room_events"]
|
||||||
|
|
||||||
|
# The actual thing to query in FTS
|
||||||
|
search_term = room_cat["search_term"]
|
||||||
|
|
||||||
|
# Which "keys" to search over in FTS query
|
||||||
|
keys = room_cat.get("keys", [
|
||||||
|
"content.body", "content.name", "content.topic",
|
||||||
|
])
|
||||||
|
|
||||||
|
# Filter to apply to results
|
||||||
|
filter_dict = room_cat.get("filter", {})
|
||||||
|
|
||||||
|
# What to order results by (impacts whether pagination can be doen)
|
||||||
|
order_by = room_cat.get("order_by", "rank")
|
||||||
|
|
||||||
|
# Return the current state of the rooms?
|
||||||
|
include_state = room_cat.get("include_state", False)
|
||||||
|
|
||||||
|
# Include context around each event?
|
||||||
|
event_context = room_cat.get(
|
||||||
|
"event_context", None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Group results together? May allow clients to paginate within a
|
||||||
|
# group
|
||||||
|
group_by = room_cat.get("groupings", {}).get("group_by", {})
|
||||||
|
group_keys = [g["key"] for g in group_by]
|
||||||
|
|
||||||
|
if event_context is not None:
|
||||||
|
before_limit = int(event_context.get(
|
||||||
|
"before_limit", 5
|
||||||
|
))
|
||||||
|
after_limit = int(event_context.get(
|
||||||
|
"after_limit", 5
|
||||||
|
))
|
||||||
|
|
||||||
|
# Return the historic display name and avatar for the senders
|
||||||
|
# of the events?
|
||||||
|
include_profile = bool(event_context.get("include_profile", False))
|
||||||
|
except KeyError:
|
||||||
|
raise SynapseError(400, "Invalid search query")
|
||||||
|
|
||||||
|
if order_by not in ("rank", "recent"):
|
||||||
|
raise SynapseError(400, "Invalid order by: %r" % (order_by,))
|
||||||
|
|
||||||
|
if set(group_keys) - {"room_id", "sender"}:
|
||||||
|
raise SynapseError(
|
||||||
|
400,
|
||||||
|
"Invalid group by keys: %r" % (set(group_keys) - {"room_id", "sender"},)
|
||||||
|
)
|
||||||
|
|
||||||
|
search_filter = Filter(filter_dict)
|
||||||
|
|
||||||
|
# TODO: Search through left rooms too
|
||||||
|
rooms = yield self.store.get_rooms_for_user_where_membership_is(
|
||||||
|
user.to_string(),
|
||||||
|
membership_list=[Membership.JOIN],
|
||||||
|
# membership_list=[Membership.JOIN, Membership.LEAVE, Membership.Ban],
|
||||||
|
)
|
||||||
|
room_ids = set(r.room_id for r in rooms)
|
||||||
|
|
||||||
|
room_ids = search_filter.filter_rooms(room_ids)
|
||||||
|
|
||||||
|
if batch_group == "room_id":
|
||||||
|
room_ids.intersection_update({batch_group_key})
|
||||||
|
|
||||||
|
if not room_ids:
|
||||||
|
defer.returnValue({
|
||||||
|
"search_categories": {
|
||||||
|
"room_events": {
|
||||||
|
"results": [],
|
||||||
|
"count": 0,
|
||||||
|
"highlights": [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
rank_map = {} # event_id -> rank of event
|
||||||
|
allowed_events = []
|
||||||
|
room_groups = {} # Holds result of grouping by room, if applicable
|
||||||
|
sender_group = {} # Holds result of grouping by sender, if applicable
|
||||||
|
|
||||||
|
# Holds the next_batch for the entire result set if one of those exists
|
||||||
|
global_next_batch = None
|
||||||
|
|
||||||
|
highlights = set()
|
||||||
|
|
||||||
|
count = None
|
||||||
|
|
||||||
|
if order_by == "rank":
|
||||||
|
search_result = yield self.store.search_msgs(
|
||||||
|
room_ids, search_term, keys
|
||||||
|
)
|
||||||
|
|
||||||
|
count = search_result["count"]
|
||||||
|
|
||||||
|
if search_result["highlights"]:
|
||||||
|
highlights.update(search_result["highlights"])
|
||||||
|
|
||||||
|
results = search_result["results"]
|
||||||
|
|
||||||
|
results_map = {r["event"].event_id: r for r in results}
|
||||||
|
|
||||||
|
rank_map.update({r["event"].event_id: r["rank"] for r in results})
|
||||||
|
|
||||||
|
filtered_events = search_filter.filter([r["event"] for r in results])
|
||||||
|
|
||||||
|
events = yield self._filter_events_for_client(
|
||||||
|
user.to_string(), filtered_events
|
||||||
|
)
|
||||||
|
|
||||||
|
events.sort(key=lambda e: -rank_map[e.event_id])
|
||||||
|
allowed_events = events[:search_filter.limit()]
|
||||||
|
|
||||||
|
for e in allowed_events:
|
||||||
|
rm = room_groups.setdefault(e.room_id, {
|
||||||
|
"results": [],
|
||||||
|
"order": rank_map[e.event_id],
|
||||||
|
})
|
||||||
|
rm["results"].append(e.event_id)
|
||||||
|
|
||||||
|
s = sender_group.setdefault(e.sender, {
|
||||||
|
"results": [],
|
||||||
|
"order": rank_map[e.event_id],
|
||||||
|
})
|
||||||
|
s["results"].append(e.event_id)
|
||||||
|
|
||||||
|
elif order_by == "recent":
|
||||||
|
room_events = []
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
pagination_token = batch_token
|
||||||
|
|
||||||
|
# We keep looping and we keep filtering until we reach the limit
|
||||||
|
# or we run out of things.
|
||||||
|
# But only go around 5 times since otherwise synapse will be sad.
|
||||||
|
while len(room_events) < search_filter.limit() and i < 5:
|
||||||
|
i += 1
|
||||||
|
search_result = yield self.store.search_rooms(
|
||||||
|
room_ids, search_term, keys, search_filter.limit() * 2,
|
||||||
|
pagination_token=pagination_token,
|
||||||
|
)
|
||||||
|
|
||||||
|
if search_result["highlights"]:
|
||||||
|
highlights.update(search_result["highlights"])
|
||||||
|
|
||||||
|
count = search_result["count"]
|
||||||
|
|
||||||
|
results = search_result["results"]
|
||||||
|
|
||||||
|
results_map = {r["event"].event_id: r for r in results}
|
||||||
|
|
||||||
|
rank_map.update({r["event"].event_id: r["rank"] for r in results})
|
||||||
|
|
||||||
|
filtered_events = search_filter.filter([
|
||||||
|
r["event"] for r in results
|
||||||
|
])
|
||||||
|
|
||||||
|
events = yield self._filter_events_for_client(
|
||||||
|
user.to_string(), filtered_events
|
||||||
|
)
|
||||||
|
|
||||||
|
room_events.extend(events)
|
||||||
|
room_events = room_events[:search_filter.limit()]
|
||||||
|
|
||||||
|
if len(results) < search_filter.limit() * 2:
|
||||||
|
pagination_token = None
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
pagination_token = results[-1]["pagination_token"]
|
||||||
|
|
||||||
|
for event in room_events:
|
||||||
|
group = room_groups.setdefault(event.room_id, {
|
||||||
|
"results": [],
|
||||||
|
})
|
||||||
|
group["results"].append(event.event_id)
|
||||||
|
|
||||||
|
if room_events and len(room_events) >= search_filter.limit():
|
||||||
|
last_event_id = room_events[-1].event_id
|
||||||
|
pagination_token = results_map[last_event_id]["pagination_token"]
|
||||||
|
|
||||||
|
# We want to respect the given batch group and group keys so
|
||||||
|
# that if people blindly use the top level `next_batch` token
|
||||||
|
# it returns more from the same group (if applicable) rather
|
||||||
|
# than reverting to searching all results again.
|
||||||
|
if batch_group and batch_group_key:
|
||||||
|
global_next_batch = encode_base64("%s\n%s\n%s" % (
|
||||||
|
batch_group, batch_group_key, pagination_token
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
global_next_batch = encode_base64("%s\n%s\n%s" % (
|
||||||
|
"all", "", pagination_token
|
||||||
|
))
|
||||||
|
|
||||||
|
for room_id, group in room_groups.items():
|
||||||
|
group["next_batch"] = encode_base64("%s\n%s\n%s" % (
|
||||||
|
"room_id", room_id, pagination_token
|
||||||
|
))
|
||||||
|
|
||||||
|
allowed_events.extend(room_events)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# We should never get here due to the guard earlier.
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
# If client has asked for "context" for each event (i.e. some surrounding
|
||||||
|
# events and state), fetch that
|
||||||
|
if event_context is not None:
|
||||||
|
now_token = yield self.hs.get_event_sources().get_current_token()
|
||||||
|
|
||||||
|
contexts = {}
|
||||||
|
for event in allowed_events:
|
||||||
|
res = yield self.store.get_events_around(
|
||||||
|
event.room_id, event.event_id, before_limit, after_limit
|
||||||
|
)
|
||||||
|
|
||||||
|
res["events_before"] = yield self._filter_events_for_client(
|
||||||
|
user.to_string(), res["events_before"]
|
||||||
|
)
|
||||||
|
|
||||||
|
res["events_after"] = yield self._filter_events_for_client(
|
||||||
|
user.to_string(), res["events_after"]
|
||||||
|
)
|
||||||
|
|
||||||
|
res["start"] = now_token.copy_and_replace(
|
||||||
|
"room_key", res["start"]
|
||||||
|
).to_string()
|
||||||
|
|
||||||
|
res["end"] = now_token.copy_and_replace(
|
||||||
|
"room_key", res["end"]
|
||||||
|
).to_string()
|
||||||
|
|
||||||
|
if include_profile:
|
||||||
|
senders = set(
|
||||||
|
ev.sender
|
||||||
|
for ev in itertools.chain(
|
||||||
|
res["events_before"], [event], res["events_after"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if res["events_after"]:
|
||||||
|
last_event_id = res["events_after"][-1].event_id
|
||||||
|
else:
|
||||||
|
last_event_id = event.event_id
|
||||||
|
|
||||||
|
state = yield self.store.get_state_for_event(
|
||||||
|
last_event_id,
|
||||||
|
types=[(EventTypes.Member, sender) for sender in senders]
|
||||||
|
)
|
||||||
|
|
||||||
|
res["profile_info"] = {
|
||||||
|
s.state_key: {
|
||||||
|
"displayname": s.content.get("displayname", None),
|
||||||
|
"avatar_url": s.content.get("avatar_url", None),
|
||||||
|
}
|
||||||
|
for s in state.values()
|
||||||
|
if s.type == EventTypes.Member and s.state_key in senders
|
||||||
|
}
|
||||||
|
|
||||||
|
contexts[event.event_id] = res
|
||||||
|
else:
|
||||||
|
contexts = {}
|
||||||
|
|
||||||
|
# TODO: Add a limit
|
||||||
|
|
||||||
|
time_now = self.clock.time_msec()
|
||||||
|
|
||||||
|
for context in contexts.values():
|
||||||
|
context["events_before"] = [
|
||||||
|
serialize_event(e, time_now)
|
||||||
|
for e in context["events_before"]
|
||||||
|
]
|
||||||
|
context["events_after"] = [
|
||||||
|
serialize_event(e, time_now)
|
||||||
|
for e in context["events_after"]
|
||||||
|
]
|
||||||
|
|
||||||
|
state_results = {}
|
||||||
|
if include_state:
|
||||||
|
rooms = set(e.room_id for e in allowed_events)
|
||||||
|
for room_id in rooms:
|
||||||
|
state = yield self.state_handler.get_current_state(room_id)
|
||||||
|
state_results[room_id] = state.values()
|
||||||
|
|
||||||
|
state_results.values()
|
||||||
|
|
||||||
|
# We're now about to serialize the events. We should not make any
|
||||||
|
# blocking calls after this. Otherwise the 'age' will be wrong
|
||||||
|
|
||||||
|
results = [
|
||||||
|
{
|
||||||
|
"rank": rank_map[e.event_id],
|
||||||
|
"result": serialize_event(e, time_now),
|
||||||
|
"context": contexts.get(e.event_id, {}),
|
||||||
|
}
|
||||||
|
for e in allowed_events
|
||||||
|
]
|
||||||
|
|
||||||
|
rooms_cat_res = {
|
||||||
|
"results": results,
|
||||||
|
"count": count,
|
||||||
|
"highlights": list(highlights),
|
||||||
|
}
|
||||||
|
|
||||||
|
if state_results:
|
||||||
|
rooms_cat_res["state"] = {
|
||||||
|
room_id: [serialize_event(e, time_now) for e in state]
|
||||||
|
for room_id, state in state_results.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
if room_groups and "room_id" in group_keys:
|
||||||
|
rooms_cat_res.setdefault("groups", {})["room_id"] = room_groups
|
||||||
|
|
||||||
|
if sender_group and "sender" in group_keys:
|
||||||
|
rooms_cat_res.setdefault("groups", {})["sender"] = sender_group
|
||||||
|
|
||||||
|
if global_next_batch:
|
||||||
|
rooms_cat_res["next_batch"] = global_next_batch
|
||||||
|
|
||||||
|
defer.returnValue({
|
||||||
|
"search_categories": {
|
||||||
|
"room_events": rooms_cat_res
|
||||||
|
}
|
||||||
|
})
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -19,6 +19,7 @@ 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.util.logcontext import PreserveLoggingContext
|
||||||
|
from synapse.util.metrics import Measure
|
||||||
from synapse.types import UserID
|
from synapse.types import UserID
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -222,6 +223,7 @@ class TypingNotificationHandler(BaseHandler):
|
|||||||
class TypingNotificationEventSource(object):
|
class TypingNotificationEventSource(object):
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
self.hs = hs
|
self.hs = hs
|
||||||
|
self.clock = hs.get_clock()
|
||||||
self._handler = None
|
self._handler = None
|
||||||
self._room_member_handler = None
|
self._room_member_handler = None
|
||||||
|
|
||||||
@@ -246,17 +248,13 @@ class TypingNotificationEventSource(object):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
def get_new_events(self, from_key, room_ids, **kwargs):
|
||||||
def get_new_events_for_user(self, user, from_key, limit):
|
with Measure(self.clock, "typing.get_new_events"):
|
||||||
from_key = int(from_key)
|
from_key = int(from_key)
|
||||||
handler = self.handler()
|
handler = self.handler()
|
||||||
|
|
||||||
joined_room_ids = (
|
|
||||||
yield self.room_member_handler().get_joined_rooms_for_user(user)
|
|
||||||
)
|
|
||||||
|
|
||||||
events = []
|
events = []
|
||||||
for room_id in joined_room_ids:
|
for room_id in room_ids:
|
||||||
if room_id not in handler._room_serials:
|
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:
|
||||||
@@ -264,7 +262,7 @@ class TypingNotificationEventSource(object):
|
|||||||
|
|
||||||
events.append(self._make_event_for(room_id))
|
events.append(self._make_event_for(room_id))
|
||||||
|
|
||||||
defer.returnValue((events, handler._latest_room_serial))
|
return events, handler._latest_room_serial
|
||||||
|
|
||||||
def get_current_key(self):
|
def get_current_key(self):
|
||||||
return self.handler()._latest_room_serial
|
return self.handler()._latest_room_serial
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -24,7 +24,6 @@ from canonicaljson import encode_canonical_json
|
|||||||
from twisted.internet import defer, reactor, ssl
|
from twisted.internet import defer, reactor, ssl
|
||||||
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
|
||||||
|
|
||||||
@@ -59,15 +58,14 @@ 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'
|
||||||
pool = HTTPConnectionPool(reactor)
|
|
||||||
pool.maxPersistentPerHost = 10
|
|
||||||
self.agent = Agent(
|
self.agent = Agent(
|
||||||
reactor,
|
reactor,
|
||||||
pool=pool,
|
|
||||||
connectTimeout=15,
|
connectTimeout=15,
|
||||||
contextFactory=hs.get_http_client_context_factory()
|
contextFactory=hs.get_http_client_context_factory()
|
||||||
)
|
)
|
||||||
self.version_string = hs.version_string
|
self.user_agent = hs.version_string
|
||||||
|
if hs.config.user_agent_suffix:
|
||||||
|
self.user_agent = "%s %s" % (self.user_agent, hs.config.user_agent_suffix,)
|
||||||
|
|
||||||
def request(self, method, uri, *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
|
||||||
@@ -112,7 +110,7 @@ class SimpleHttpClient(object):
|
|||||||
uri.encode("ascii"),
|
uri.encode("ascii"),
|
||||||
headers=Headers({
|
headers=Headers({
|
||||||
b"Content-Type": [b"application/x-www-form-urlencoded"],
|
b"Content-Type": [b"application/x-www-form-urlencoded"],
|
||||||
b"User-Agent": [self.version_string],
|
b"User-Agent": [self.user_agent],
|
||||||
}),
|
}),
|
||||||
bodyProducer=FileBodyProducer(StringIO(query_bytes))
|
bodyProducer=FileBodyProducer(StringIO(query_bytes))
|
||||||
)
|
)
|
||||||
@@ -131,7 +129,8 @@ class SimpleHttpClient(object):
|
|||||||
"POST",
|
"POST",
|
||||||
uri.encode("ascii"),
|
uri.encode("ascii"),
|
||||||
headers=Headers({
|
headers=Headers({
|
||||||
"Content-Type": ["application/json"]
|
b"Content-Type": [b"application/json"],
|
||||||
|
b"User-Agent": [self.user_agent],
|
||||||
}),
|
}),
|
||||||
bodyProducer=FileBodyProducer(StringIO(json_str))
|
bodyProducer=FileBodyProducer(StringIO(json_str))
|
||||||
)
|
)
|
||||||
@@ -157,27 +156,8 @@ class SimpleHttpClient(object):
|
|||||||
On a non-2xx HTTP response. The response body will be used as the
|
On a non-2xx HTTP response. The response body will be used as the
|
||||||
error message.
|
error message.
|
||||||
"""
|
"""
|
||||||
if len(args):
|
body = yield self.get_raw(uri, args)
|
||||||
query_bytes = urllib.urlencode(args, True)
|
|
||||||
uri = "%s?%s" % (uri, query_bytes)
|
|
||||||
|
|
||||||
response = yield self.request(
|
|
||||||
"GET",
|
|
||||||
uri.encode("ascii"),
|
|
||||||
headers=Headers({
|
|
||||||
b"User-Agent": [self.version_string],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
body = yield preserve_context_over_fn(readBody, response)
|
|
||||||
|
|
||||||
if 200 <= response.code < 300:
|
|
||||||
defer.returnValue(json.loads(body))
|
defer.returnValue(json.loads(body))
|
||||||
else:
|
|
||||||
# NB: This is explicitly not json.loads(body)'d because the contract
|
|
||||||
# of CodeMessageException is a *string* message. Callers can always
|
|
||||||
# load it into JSON if they want.
|
|
||||||
raise CodeMessageException(response.code, body)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def put_json(self, uri, json_body, args={}):
|
def put_json(self, uri, json_body, args={}):
|
||||||
@@ -206,7 +186,7 @@ class SimpleHttpClient(object):
|
|||||||
"PUT",
|
"PUT",
|
||||||
uri.encode("ascii"),
|
uri.encode("ascii"),
|
||||||
headers=Headers({
|
headers=Headers({
|
||||||
b"User-Agent": [self.version_string],
|
b"User-Agent": [self.user_agent],
|
||||||
"Content-Type": ["application/json"]
|
"Content-Type": ["application/json"]
|
||||||
}),
|
}),
|
||||||
bodyProducer=FileBodyProducer(StringIO(json_str))
|
bodyProducer=FileBodyProducer(StringIO(json_str))
|
||||||
@@ -222,6 +202,42 @@ class SimpleHttpClient(object):
|
|||||||
# load it into JSON if they want.
|
# load it into JSON if they want.
|
||||||
raise CodeMessageException(response.code, body)
|
raise CodeMessageException(response.code, body)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_raw(self, uri, args={}):
|
||||||
|
""" Gets raw text from the given URI.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
uri (str): The URI to request, not including query parameters
|
||||||
|
args (dict): A dictionary used to create query strings, defaults to
|
||||||
|
None.
|
||||||
|
**Note**: The value of each key is assumed to be an iterable
|
||||||
|
and *not* a string.
|
||||||
|
Returns:
|
||||||
|
Deferred: Succeeds when we get *any* 2xx HTTP response, with the
|
||||||
|
HTTP body at text.
|
||||||
|
Raises:
|
||||||
|
On a non-2xx HTTP response. The response body will be used as the
|
||||||
|
error message.
|
||||||
|
"""
|
||||||
|
if len(args):
|
||||||
|
query_bytes = urllib.urlencode(args, True)
|
||||||
|
uri = "%s?%s" % (uri, query_bytes)
|
||||||
|
|
||||||
|
response = yield self.request(
|
||||||
|
"GET",
|
||||||
|
uri.encode("ascii"),
|
||||||
|
headers=Headers({
|
||||||
|
b"User-Agent": [self.user_agent],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
body = yield preserve_context_over_fn(readBody, response)
|
||||||
|
|
||||||
|
if 200 <= response.code < 300:
|
||||||
|
defer.returnValue(body)
|
||||||
|
else:
|
||||||
|
raise CodeMessageException(response.code, body)
|
||||||
|
|
||||||
|
|
||||||
class CaptchaServerHttpClient(SimpleHttpClient):
|
class CaptchaServerHttpClient(SimpleHttpClient):
|
||||||
"""
|
"""
|
||||||
@@ -241,7 +257,7 @@ class CaptchaServerHttpClient(SimpleHttpClient):
|
|||||||
bodyProducer=FileBodyProducer(StringIO(query_bytes)),
|
bodyProducer=FileBodyProducer(StringIO(query_bytes)),
|
||||||
headers=Headers({
|
headers=Headers({
|
||||||
b"Content-Type": [b"application/x-www-form-urlencoded"],
|
b"Content-Type": [b"application/x-www-form-urlencoded"],
|
||||||
b"User-Agent": [self.version_string],
|
b"User-Agent": [self.user_agent],
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014, 2015 OpenMarket Ltd
|
# Copyright 2014-2016 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -17,7 +17,7 @@ from twisted.internet.endpoints import SSL4ClientEndpoint, TCP4ClientEndpoint
|
|||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
from twisted.internet.error import ConnectError
|
from twisted.internet.error import ConnectError
|
||||||
from twisted.names import client, dns
|
from twisted.names import client, dns
|
||||||
from twisted.names.error import DNSNameError
|
from twisted.names.error import DNSNameError, DomainError
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
import logging
|
import logging
|
||||||
@@ -27,6 +27,14 @@ import random
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
SERVER_CACHE = {}
|
||||||
|
|
||||||
|
|
||||||
|
_Server = collections.namedtuple(
|
||||||
|
"_Server", "priority weight host port"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def matrix_federation_endpoint(reactor, destination, ssl_context_factory=None,
|
def matrix_federation_endpoint(reactor, destination, ssl_context_factory=None,
|
||||||
timeout=None):
|
timeout=None):
|
||||||
"""Construct an endpoint for the given matrix destination.
|
"""Construct an endpoint for the given matrix destination.
|
||||||
@@ -73,10 +81,6 @@ class SRVClientEndpoint(object):
|
|||||||
Implements twisted.internet.interfaces.IStreamClientEndpoint.
|
Implements twisted.internet.interfaces.IStreamClientEndpoint.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_Server = collections.namedtuple(
|
|
||||||
"_Server", "priority weight host port"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, reactor, service, domain, protocol="tcp",
|
def __init__(self, reactor, service, domain, protocol="tcp",
|
||||||
default_port=None, endpoint=TCP4ClientEndpoint,
|
default_port=None, endpoint=TCP4ClientEndpoint,
|
||||||
endpoint_kw_args={}):
|
endpoint_kw_args={}):
|
||||||
@@ -84,7 +88,7 @@ class SRVClientEndpoint(object):
|
|||||||
self.service_name = "_%s._%s.%s" % (service, protocol, domain)
|
self.service_name = "_%s._%s.%s" % (service, protocol, domain)
|
||||||
|
|
||||||
if default_port is not None:
|
if default_port is not None:
|
||||||
self.default_server = self._Server(
|
self.default_server = _Server(
|
||||||
host=domain,
|
host=domain,
|
||||||
port=default_port,
|
port=default_port,
|
||||||
priority=0,
|
priority=0,
|
||||||
@@ -101,32 +105,8 @@ class SRVClientEndpoint(object):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def fetch_servers(self):
|
def fetch_servers(self):
|
||||||
try:
|
|
||||||
answers, auth, add = yield client.lookupService(self.service_name)
|
|
||||||
except DNSNameError:
|
|
||||||
answers = []
|
|
||||||
|
|
||||||
if (len(answers) == 1
|
|
||||||
and answers[0].type == dns.SRV
|
|
||||||
and answers[0].payload
|
|
||||||
and answers[0].payload.target == dns.Name('.')):
|
|
||||||
raise ConnectError("Service %s unavailable", self.service_name)
|
|
||||||
|
|
||||||
self.servers = []
|
|
||||||
self.used_servers = []
|
self.used_servers = []
|
||||||
|
self.servers = yield resolve_service(self.service_name)
|
||||||
for answer in answers:
|
|
||||||
if answer.type != dns.SRV or not answer.payload:
|
|
||||||
continue
|
|
||||||
payload = answer.payload
|
|
||||||
self.servers.append(self._Server(
|
|
||||||
host=str(payload.target),
|
|
||||||
port=int(payload.port),
|
|
||||||
priority=int(payload.priority),
|
|
||||||
weight=int(payload.weight)
|
|
||||||
))
|
|
||||||
|
|
||||||
self.servers.sort()
|
|
||||||
|
|
||||||
def pick_server(self):
|
def pick_server(self):
|
||||||
if not self.servers:
|
if not self.servers:
|
||||||
@@ -170,3 +150,64 @@ class SRVClientEndpoint(object):
|
|||||||
)
|
)
|
||||||
connection = yield endpoint.connect(protocolFactory)
|
connection = yield endpoint.connect(protocolFactory)
|
||||||
defer.returnValue(connection)
|
defer.returnValue(connection)
|
||||||
|
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def resolve_service(service_name, dns_client=client, cache=SERVER_CACHE):
|
||||||
|
servers = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
answers, _, _ = yield dns_client.lookupService(service_name)
|
||||||
|
except DNSNameError:
|
||||||
|
defer.returnValue([])
|
||||||
|
|
||||||
|
if (len(answers) == 1
|
||||||
|
and answers[0].type == dns.SRV
|
||||||
|
and answers[0].payload
|
||||||
|
and answers[0].payload.target == dns.Name('.')):
|
||||||
|
raise ConnectError("Service %s unavailable", service_name)
|
||||||
|
|
||||||
|
for answer in answers:
|
||||||
|
if answer.type != dns.SRV or not answer.payload:
|
||||||
|
continue
|
||||||
|
|
||||||
|
payload = answer.payload
|
||||||
|
|
||||||
|
host = str(payload.target)
|
||||||
|
|
||||||
|
try:
|
||||||
|
answers, _, _ = yield dns_client.lookupAddress(host)
|
||||||
|
except DNSNameError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ips = [
|
||||||
|
answer.payload.dottedQuad()
|
||||||
|
for answer in answers
|
||||||
|
if answer.type == dns.A and answer.payload
|
||||||
|
]
|
||||||
|
|
||||||
|
for ip in ips:
|
||||||
|
servers.append(_Server(
|
||||||
|
host=ip,
|
||||||
|
port=int(payload.port),
|
||||||
|
priority=int(payload.priority),
|
||||||
|
weight=int(payload.weight)
|
||||||
|
))
|
||||||
|
|
||||||
|
servers.sort()
|
||||||
|
cache[service_name] = list(servers)
|
||||||
|
except DomainError as e:
|
||||||
|
# We failed to resolve the name (other than a NameError)
|
||||||
|
# Try something in the cache, else rereaise
|
||||||
|
cache_entry = cache.get(service_name, None)
|
||||||
|
if cache_entry:
|
||||||
|
logger.warn(
|
||||||
|
"Failed to resolve %r, falling back to cache. %r",
|
||||||
|
service_name, e
|
||||||
|
)
|
||||||
|
servers = list(cache_entry)
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
defer.returnValue(servers)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user